You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Revamp when navigate fires, is cancelable, and is respond-able
* Closes#53 by allowing respondWith() for same-document back/forward navigations (but not yet allowing preventDefault(); that's #32).
* Closes#51 by transitioning from event.sameOrigin to event.canRespond, which has updated semantics.
* Does not fire navigate events for document.open(), as per some initial implementation investigations that's more trouble than it's worth.
* Adds an example for special back/forward handling in the navigate event, per #53 (comment).
* Makes the appendix table a bit more precise in various ways, and expands it to cover cancelable and canRespond.
@@ -243,12 +245,14 @@ The most interesting event on `window.appHistory` is the one which allows monito
243
245
244
246
The event object has several useful properties:
245
247
248
+
- `cancelable` (inherited from `Event`): indicates whether `preventDefault()` is allowed to cancel this navigation.
249
+
250
+
- `canRespond`: indicates whether `respondWith()`, discussed below, is allowed for this navigation.
251
+
246
252
- `userInitiated`: a boolean indicating whether the navigation is user-initiated (i.e., a click on an `<a>`, or a form submission) or application-initiated (e.g. `location.href=...`, `appHistory.push(...)`, etc.). Note that this will _not_ be `true` when you use mechanisms such as `button.onclick= () =>appHistory.push(...)`; the user interaction needs to be with a real link or form. See the table in the [appendix](#appendix-types-of-navigations) for more details.
247
253
248
254
- `destination`: an `AppHistoryEntry` containing the information about the destination of the navigation. Note that this entry might or might not yet be in `window.appHistory.entries`; if it is not, then its `state` will be `null`.
249
255
250
-
- `sameOrigin`: a convenience boolean indicating whether the navigation is same-origin, and thus will stay in the same app history or not. (I.e., this is `(newURL(e.destination.url)).origin===self.origin`.)
251
-
252
256
- `hashChange`: a boolean, indicating whether or not this is a same-document [fragment navigation](https://html.spec.whatwg.org/#scroll-to-fragid).
253
257
254
258
- `formData`: a [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object containing form submission data, or `null` if the navigation is not a form submission.
@@ -257,26 +261,9 @@ The event object has several useful properties:
257
261
258
262
- `signal`: an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) which can be monitored for when the navigation gets aborted.
259
263
260
-
Note that you can check if the navigation will be [same-document or cross-document](#appendix-types-of-navigations) via `event.destination.sameDocument`.
261
-
262
-
The event is not fired in the following cases:
263
-
264
-
- User-initiated cross-document navigations via browser UI, such as the URL bar, back/forward button, or bookmarks.
265
-
- User-initiated same-document navigations via the browser back/forward buttons. (See discussion in [#32](https://github.com/WICG/app-history/issues/32).)
266
-
267
-
Whenever it is fired, the event is cancelable via `event.preventDefault()`, which prevents the navigation from going through. To name a few notable examples of when the event is fired, i.e. when you can intercept the navigation:
268
-
269
-
- User-initiated navigations via `<a>` and `<form>` elements, including both same-document fragment navigations and cross-document navigations.
270
-
- Programmatically-initiated navigations, via mechanisms such as `location.href=...` or `aElement.click()`, including both same-document fragment navigations and cross-document navigations.
271
-
- Programmatically-initiated same-document navigations initiated via `appHistory.push()`, `appHistory.update()`, or their old counterparts `history.pushState()` and `history.replaceState()`.
272
-
273
-
(This list is not comprehensive; for the complete list of possible navigations on the web platform, see [this appendix](#appendix-types-of-navigations).)
274
-
275
-
Although the ability to intercept cross-document navigations, especially cross-origin ones, might be surprising, in general it doesn't grant additional power. That is, web developers can already intercept `<a>``click` events, or modify their code that would set `location.href`, even if the destination URL is cross-origin.
264
+
Note that you can check if the navigation will be [same-document or cross-document](#appendix-types-of-navigations) via `event.destination.sameDocument`, and you can check whether the navigation is to an already-existing app history entry (i.e. is a back/forward navigation) via `appHistory.entries.includes(event.destination)`.
276
265
277
-
On the other hand, cases that are not interceptable today, where the user is the instigator of the navigation through browser UI, are not interceptable. This ensures that web applications cannot trap the user on a given document by intercepting cross-document URL bar navigations, or disable the user's back/forward buttons. Note that, in the case of the back/forward buttons, even same-document interception isn't allowed. This is because it's easy to generate same-document app history entries (e.g., using `appHistory.push()` or `history.pushState()`); if we allowed intercepting traversal to them, this would allow sites to disable the back/forward buttons. We realize that this limits the utility of the `navigate` event in some cases, and are open to exploring other ways of combating abuse: see the discussion in [#32](https://github.com/WICG/app-history/issues/32).
278
-
279
-
Additionally, the event has a special method `event.respondWith(promise)`. If called for a same-origin navigation, this will:
266
+
The event object has a special method `event.respondWith(promise)`. This works only under certain circumstances, e.g. it cannot be used on cross-origin navigations. ([See below](#restrictions-on-firing-canceling-and-responding) for full details.) It will:
280
267
281
268
- Cancel any fragment navigation or cross-document navigation.
282
269
- Immediately update the URL bar, `location.href`, and `appHistory.current`, but with `appHistory.current.finished` set to false.
@@ -295,8 +282,8 @@ The following is the kind of code you might see in an application or framework's
295
282
296
283
```js
297
284
appHistory.addEventListener("navigate", e=> {
298
-
//Don't intercept cross-origin navigations; let the browser handle those normally.
299
-
if (!e.sameOrigin) {
285
+
//Some navigations, e.g. cross-origin navigations, we cannot intercept. Let the browser handle those normally.
286
+
if (!e.canRespond) {
300
287
return;
301
288
}
302
289
@@ -326,10 +313,10 @@ Note how this example responds to various types of navigations:
326
313
327
314
- Cross-origin navigations: let the browser handle it as usual.
328
315
- Same-document fragment navigations: let the browser handle it as usual.
- Same-document URL or state updates (via `history.pushState()` or `history.replaceState()`):
330
317
1. Send the information about the URL/state update to `doSinglePageAppNav()`, which will use it to modify the current document.
331
318
1. After that UI update is done, potentially asynchronously, notify the app and the browser about the navigation's success or failure.
332
-
- Cross-document normal navigations:
319
+
- Cross-document normal navigations (including those via `appHistory.push()` or `appHistory.update()`):
333
320
1. Prevent the browser handling, which would unload the document and create a new one from the network.
334
321
1. Instead, send the information about the navigation to `doSinglePageAppNav()`, which will use it to modify the current document.
335
322
1. After that UI update is done, potentially asynchronously, notify the app and the browser about the navigation's success or failure.
@@ -340,6 +327,63 @@ Note how this example responds to various types of navigations:
340
327
341
328
Notice also how by passing through the `AbortSignal` found in `e.signal`, we ensure that any aborted navigations abort the associated fetch as well.
342
329
330
+
#### Example: async transitions with special back/forward handling
331
+
332
+
Sometimes it's desirable to handle back/forward navigations specially, e.g. reusing cached views by transitioning them onto the screen. This can be done by branching as follows:
// This will probably result in myFramework storing the rendered page in myFramework.previousPages.
353
+
awaitmyFramework.renderPage(event.destination);
354
+
}
355
+
})());
356
+
});
357
+
```
358
+
359
+
#### Restrictions on firing, canceling, and responding
360
+
361
+
There are many types of navigations a given page can experience; see [this appendix](#appendix-types-of-navigations) for a full breakdown. Some of these need to be treated specially for the purposes of the navigate event.
362
+
363
+
First, the following navigations **will not fire `navigate`** at all:
364
+
365
+
- User-initiated [cross-document](#appendix-types-of-navigations) navigations via browser UI, such as the URL bar, back/forward button, or bookmarks.
366
+
- [`document.open()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/open), which can strip off the fragment from the current document's URL.
367
+
368
+
Navigations of the first sort are outside the scope of the webpage, and can never be intercepted or prevented. This is true even if they are to same-origin documents, e.g. if the browser is currently displaying `https://example.com/foo` and the user edits the URL bar to read `https://example.com/bar` and presses enter. On the other hand, we do allow the page to intercept user-initiated _same_-document navigations via browser UI, e.g. if the the browser is currently displaying `https://example.com/foo` and the user edits the URL bar to read `https://example.com/foo#fragment` and presses enter.
369
+
370
+
As for`document.open()`, it is a terrible legacy APIwith lots of strange side effects, which makes supporting it not worth the implementation cost. Modern sites which use the app history API should never be using `document.open()`.
371
+
372
+
Second, the following navigations **cannot be canceled** using `event.preventDefault()`, and as such will have `event.cancelable` equal to false:
373
+
374
+
- User-initiated same-document navigations via the browser's back/forward buttons.
375
+
376
+
This is important to avoid abusive pages trapping the user by disabling their back button. Note that adding a same-origin restriction would not help here: imagine a user which navigates to `https://evil-in-disguise.example/`, and then clicks a link to `https://evil-in-disguise.example/2`. If `https://evil-in-disguise.example/2` were allowed to cancel same-origin browser back button navigations, they have effectively disabled the user's back button.
377
+
378
+
We're discussing this restriction in [#32](https://github.com/WICG/app-history/issues/32), as it does hurt some use cases, and we'd like to soften it in some way.
379
+
380
+
Finally, the following navigations **cannot be replaced with same-document navigations** by using `event.respondWith()`, and as such will have `event.canRespond` equal to false:
381
+
382
+
- Any navigation to a URL which differs in scheme, username, password, host, or port. (I.e., you can only intercept URLs which differ in path, query, or fragment.)
383
+
- Any [cross-document](#appendix-types-of-navigations) back/forward navigations. Transitioning two adjacent history entries from cross-document to same-document has unpleasant ripple effects on web application and browser implementation architecture.
384
+
385
+
We'll note that these restrictions still allow canceling cross-origin non-back/forward navigations. Although this might be surprising, in general it doesn't grant additional power. That is, web developers can already intercept `<a>``click` events, or modify their code that would set `location.href`, even if the destination URL is cross-origin.
386
+
343
387
#### Accessibility benefits of standardized single-page navigations
344
388
345
389
The `navigate`event's `event.respondWith()` method provides a helpful convenience for implementing single-page navigations, as discussed above. But beyond that, providing a direct signal to the browser as to the duration and outcome of a single-page navigation has benefits for accessibility technology users.
@@ -1035,28 +1079,31 @@ Most navigations are cross-document navigations. Same-document navigations can h
1035
1079
1036
1080
Here's a summary table:
1037
1081
1038
-
|Trigger|Cross- vs. same-document|Fires `navigate`?|`event.userInitiated`|
- Regarding the "No" values for the "Fires `navigate`?" column: recall that we need to disallow abusive pages from trapping the user by intercepting the back button. To get notified of such non-interceptable cases after the fact, you can use `currentchange`.
1058
-
1059
-
- Today it is not possible to intercept cases where other frames or windows programatically navigate your frame, e.g. via `window.open(url, name)`, or `history.back()` happening in a subframe. So, firing the `navigate` event and allowing interception in such cases represents a new capability. We believe this is OK, but will report back after some implementation experience. See also [#32](https://github.com/WICG/app-history/issues/32).
1082
+
|Trigger|Cross- vs. same-document|Fires `navigate`?|`e.userInitiated`|`e.cancelable`|`e.canRespond`|
- ‡ = No if triggered via, e.g., `element.click()`
1101
+
- \* = No if the URL differs in components besides path/fragment/query
1102
+
- ◊ = fragment navigations initiated by `<meta http-equiv="refresh">` or the `Refresh` header are only same-document in some browsers: [whatwg/html#6451](https://github.com/whatwg/html/issues/6451)
1103
+
1104
+
See the discussion on [restrictions](#restrictions-on-firing-canceling-and-responding) to understand the reasons why the last few columns are filled out in the way they are.
1105
+
1106
+
Note that today it is not possible to intercept cases where other frames or windows programatically navigate your frame, e.g. via `window.open(url, name)`, or `history.back()` happening in a subframe. So, firing the `navigate` event and allowing interception in such cases represents a new capability. We believe this is OK, but will report back after some implementation experience.
1060
1107
1061
1108
_Spec details: the above comprehensive list does not fully match when the HTML Standard's [navigate](https://html.spec.whatwg.org/#navigate) algorithm is called. In particular, HTML does not handle non-fragment-related same-document navigations through the navigate algorithm; instead it uses the [URL and history update steps](https://html.spec.whatwg.org/#url-and-history-update-steps) for those. Also, HTML calls the navigate algorithm for the initial loads of new browsing contexts as they transition from the initial `about:blank`; our current thinking is that `appHistory` should just not work on the initial `about:blank` so we can avoid that edge case._
0 commit comments