Skip to content

Commit eb76072

Browse files
authored
Change { replace: true } to { history: "replace" }
We can then introduce { history: "push" } which explicitly errors if we're in a convert-to-replace situation. Closes #111. This also fixes a preexisting omission where we were not properly doing replace navigations before the document is loaded.
1 parent c302264 commit eb76072

File tree

3 files changed

+39
-14
lines changed

3 files changed

+39
-14
lines changed

README.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ backButtonEl.addEventListener("click", () => {
4545
navigation.back();
4646
} else {
4747
// If the user arrived here by typing the URL directly:
48-
navigation.navigate("/product-listing", { replace: true });
48+
navigation.navigate("/product-listing", { history: "replace" });
4949
}
5050
});
5151
```
@@ -225,7 +225,7 @@ The current entry can be replaced with a new entry, with a new `NavigationHistor
225225
226226
- Via `history.replaceState()`.
227227
228-
- Via cross-document replace navigations generated by `location.replace()` or `navigation.navigate(url, { replace: true, ... })`. Note that if the navigation is cross-origin, then we'll end up in a separate navigation API history entry list for that other origin, where `key` will not be preserved.
228+
- Via cross-document replace navigations generated by `location.replace()` or `navigation.navigate(url, { history: "replace", ... })`. Note that if the navigation is cross-origin, then we'll end up in a separate navigation API history entry list for that other origin, where `key` will not be preserved.
229229
230230
- When using the `navigate` event to [convert a cross-document replace navigation into a same-document navigation](#navigation-monitoring-and-interception).
231231
@@ -445,7 +445,7 @@ navigation.addEventListener("navigate", e => {
445445
await fetch("/form-submit", { body: e.formData });
446446

447447
// Perform a client-side "redirect" to /destination.
448-
await navigation.navigate("/destination", { replace: true }).finished;
448+
await navigation.navigate("/destination", { history: "replace" }).finished;
449449
}()));
450450
break;
451451
}
@@ -746,22 +746,24 @@ Note how unlike `history.pushState()`, `navigation.navigate()` will by default p
746746

747747
Regardless of whether the navigation gets converted or not, calling `navigation.navigate()` in this form will clear any future entries in the joint session history. (This includes entries coming from frame navigations, or cross-origin entries: so, it can have an impact beyond just the `navigation.entries()` list.)
748748

749-
`navigation.navigate()` also takes a `replace` option, which indicates that it will replace the current history entry in a similar manner to `location.replace()`. It is used as follows:
749+
`navigation.navigate()` also takes a `history` option, which controls whether the navigation will replace the current history entry in a similar manner to `location.replace()`. It is used as follows:
750750

751751
```js
752752
// Performs a navigation to the given URL, but replace the current history entry
753753
// instead of pushing a new one.
754754
// (equivalent to `location.replace(url)`)
755-
navigation.navigate(url, { replace: true });
755+
navigation.navigate(url, { history: "replace" });
756756
757757
// Replace the URL and state at the same time.
758-
navigation.navigate(url, { replace: true, state });
758+
navigation.navigate(url, { history: "replace", state });
759759
760760
// You can still pass along info:
761-
navigation.navigate(url, { replace: true, state, info });
761+
navigation.navigate(url, { history: "replace", state, info });
762762
```
763763

764-
Again, unlike `history.replaceState()`, `navigation.navigate(url, { replace: true })` will by default perform a full navigation. And again, single-page apps will usually intercept these using `navigate`.
764+
Again, unlike `history.replaceState()`, `navigation.navigate(url, { history: "replace" })` will by default perform a full navigation. And again, single-page apps will usually intercept these using `navigate`.
765+
766+
There are two other values the `history` option can take: `"auto"`, which will usually perform a push navigation but will perform a replace navigation under special circumstances (such as when on the initial `about:blank` document or when navigating to the current URL); and `"push"`, which will always either perform a push navigation or fail if called under those special circumstances. Most developers will be fine either omitting the `history` option (which has `"auto"` behavior) or using `"replace"`.
765767

766768
Finally, we have `navigation.reload()`. This can be used as a replacement for `location.reload()`, but it also allows passing `info` and `state`, which are useful when a single-page app intercepts the reload using the `navigate` event:
767769

@@ -993,7 +995,7 @@ This can be useful for cleaning up any information in secondary stores, such as
993995
994996
### Current entry change monitoring
995997
996-
The `window.navigation` object has an event, `currententrychange`, which allows the application to react to any updates to the `navigation.currentEntry` property. This includes both navigations that change its value to a new `NavigationHistoryEntry`, and calls to APIs like `history.replaceState()`, `navigation.updateCurrentEntry()`, or intercepted `navigation.navigate(url, { replace: true, state: newState })` calls that change its state or URL.
998+
The `window.navigation` object has an event, `currententrychange`, which allows the application to react to any updates to the `navigation.currentEntry` property. This includes both navigations that change its value to a new `NavigationHistoryEntry`, and calls to APIs like `history.replaceState()`, `navigation.updateCurrentEntry()`, or intercepted `navigation.navigate(url, { history: "replace", state: newState })` calls that change its state or URL.
997999
9981000
Unlike `navigate`, this event occurs *after* the navigation is committed. As such, it cannot be intercepted or canceled; it's just an after-the-fact notification.
9991001

@@ -1094,7 +1096,7 @@ For web developers using the API, here's a guide to explain how you would replac
10941096
10951097
Instead of using `history.pushState(state, "", url)`, use `navigation.navigate(url, { state })` and combine it with a `navigate` handler to convert the default cross-document navigation into a same-document navigation.
10961098
1097-
Instead of using `history.replaceState(state, "", url)`, use `navigation.navigate(url, { replace: true, state })`, again combined with a `navigate` handler. Note that if you omit the state value, i.e. if you say `navigation.navigate(url, { replace: true })`, then this will overwrite the entry's state with `undefined`.
1099+
Instead of using `history.replaceState(state, "", url)`, use `navigation.navigate(url, { history: "replace", state })`, again combined with a `navigate` handler. Note that if you omit the state value, i.e. if you say `navigation.navigate(url, { history: "replace" })`, then this will overwrite the entry's state with `undefined`.
10981100

10991101
Instead of using `history.back()` and `history.forward()`, use `navigation.back()` and `navigation.forward()`. Note that unlike the `history` APIs, the `navigation` APIs will ignore other frames when determining where to navigate to. This means it might move through multiple entries in the joint session history, skipping over any entries that were generated purely by other-frame navigations.
11001102

navigation_api.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ interface NavigationOptions {
8989

9090
interface NavigationNavigateOptions extends NavigationOptions {
9191
state?: unknown;
92-
replace?: boolean;
92+
history?: "auto"|"push"|"replace";
9393
}
9494

9595
interface NavigationReloadOptions extends NavigationOptions {

spec.bs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ dictionary NavigationOptions {
189189

190190
dictionary NavigationNavigateOptions : NavigationOptions {
191191
any state;
192-
boolean replace = false;
192+
NavigationHistoryBehavior history = "auto";
193193
};
194194

195195
dictionary NavigationReloadOptions : NavigationOptions {
@@ -200,6 +200,12 @@ dictionary NavigationResult {
200200
Promise<NavigationHistoryEntry> committed;
201201
Promise<NavigationHistoryEntry> finished;
202202
};
203+
204+
enum NavigationHistoryBehavior {
205+
"auto",
206+
"push",
207+
"replace"
208+
};
203209
</xmp>
204210

205211
Each {{Navigation}} object has an associated <dfn for="Navigation">entry list</dfn>, a [=list=] of {{NavigationHistoryEntry}} objects, initially empty.
@@ -750,7 +756,7 @@ An <dfn>navigation API method navigation</dfn> is a [=struct=] with the followin
750756
<dd>
751757
<p>Navigates the current page to the given <var ignore>url</var>. <var ignore>options</var> can contain the following values:
752758

753-
* {{NavigationNavigateOptions/replace}} can be set to true to replace the current session history entry, instead of pushing a new one.
759+
* {{NavigationNavigateOptions/history}} can be set to "{{NavigationHistoryBehavior/replace}}" to replace the current session history entry, instead of pushing a new one.
754760
* {{NavigationOptions/info}} can be set to any value; it will populate the {{NavigateEvent/info}} property of the corresponding {{Navigation/navigate}} event.
755761
* {{NavigationNavigateOptions/state}} can be set to any [=serializable object|serializable=] value; it will populate the state retrieved by {{NavigationHistoryEntry/getState()|navigation.currentEntry.getState()}} once the navigation completes, for same-document navigations. (It will be ignored for navigations that end up cross-document.)
756762

@@ -787,6 +793,23 @@ An <dfn>navigation API method navigation</dfn> is a [=struct=] with the followin
787793

788794
1. <a spec="HTML" lt="parse a URL">Parse</a> |url| relative to [=this=]'s [=relevant settings object=]. If that returns failure, then return [=an early error result=] for a "{{SyntaxError}}" {{DOMException}}. Otherwise, let |urlRecord| be the <a spec="HTML">resulting URL record</a>.
789795

796+
1. Let |document| be [=this=]'s [=relevant global object=]'s [=associated document=].
797+
798+
1. If |options|["{{NavigationNavigateOptions/history}}"] is "{{NavigationHistoryBehavior/push}}", and any of the following are true:
799+
800+
* |document|'s <a spec="HTML">is initial about:blank</a> is true;
801+
* |document| is not <a spec="HTML">completely loaded</a>;
802+
* |url| equals |document|'s [=Document/URL=]; or
803+
* |url|'s [=url/scheme=] is "`javascript`"
804+
805+
then return [=an early error result=] for a "{{NotSupportedError}}" {{DOMException}}.
806+
807+
<div class="note">
808+
<p>These are the conditions under which a push navigation will be converted into a replace navigation by the <a spec="HTML">navigate</a> algorithm or by the below step. If the developer explicitly requested a push, we fail to let them know it won't happen.
809+
810+
<p>In the future, we could consider loosening some of these conditions, e.g., allowing explicitly-requested push navigations to the current URL or before the document is completely loaded.
811+
</div>
812+
790813
1. Let |serializedState| be null.
791814

792815
1. If |options|["{{NavigationNavigateOptions/state}}"] [=map/exists=], then set |serializedState| to [$StructuredSerializeForStorage$](|options|["{{NavigationNavigateOptions/state}}"]). If this throws an exception, then return [=an early error result=] for that exception.
@@ -795,7 +818,7 @@ An <dfn>navigation API method navigation</dfn> is a [=struct=] with the followin
795818

796819
1. Let |info| be |options|["{{NavigationOptions/info}}"] if it exists; otherwise, undefined.
797820

798-
1. Let |historyHandling| be "<a for="history handling behavior">`replace`</a>" if |options|["{{NavigationNavigateOptions/replace}}"] is true; otherwise, "<a for="history handling behavior">`default`</a>".
821+
1. Let |historyHandling| be "<a for="history handling behavior">`replace`</a>" if |options|["{{NavigationNavigateOptions/history}}"] is "{{NavigationHistoryBehavior/replace}}" or if |document| is not <a spec="HTML">completely loaded</a>; otherwise, "<a for="history handling behavior">`default`</a>".
799822

800823
1. Return the result of [=performing a non-traverse navigation API navigation=] given [=this=], |urlRecord|, |serializedState|, |info|, and |historyHandling|.
801824
</div>

0 commit comments

Comments
 (0)