├── .editorconfig
├── .github
└── workflows
│ └── build.yml
├── 2021-03-12-incubation-update.md
├── 2021-06-28-incubation-update.md
├── 2021-11-04-incubation-update.md
├── CONTRIBUTING.md
├── Makefile
├── README.md
├── security-privacy-questionnaire.md
├── spec.html
└── w3c.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 | charset = utf-8
7 | indent_size = 2
8 | indent_style = space
9 | trim_trailing_whitespace = true
10 |
11 | [Makefile]
12 | indent_style = tab
13 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 | on:
3 | pull_request:
4 | branches:
5 | - main
6 | push:
7 | branches:
8 | - main
9 | jobs:
10 | build:
11 | name: Build
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 | - name: Assemble out/ directory
16 | run: |
17 | mkdir out
18 | mv spec.html out/index.html
19 | - name: Deploy
20 | if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
21 | uses: peaceiris/actions-gh-pages@v3
22 | with:
23 | github_token: ${{ secrets.GITHUB_TOKEN }}
24 | publish_dir: ./out
25 |
--------------------------------------------------------------------------------
/2021-03-12-incubation-update.md:
--------------------------------------------------------------------------------
1 | # Incubation Update: March 12, 2021
2 |
3 | A lot of progress has been on app history since it's initial publication and move to WICG! Here's an update.
4 |
5 | ## Prototype implementation
6 |
7 | [@natechapin](https://github.com/natechapin) has been hard at work prototyping app history in Chromium, behind the Experimental Web Platform features flag. You can try it yourself by using the latest build of [Chrome Canary](https://www.google.com/chrome/canary/) and flipping the flag in `chrome://flags`. There's a [demo site](https://gigantic-honored-octagon.glitch.me/) which we intend to keep updating.
8 |
9 | So far the implementation has focused on the `navigate` event and its `respondWith()` function, and especially on the complex [restrictions](./README.md#restrictions-on-firing-canceling-and-responding) on when the event fires, when it's cancelable, and when `respondWith()` can be called. Our rough plan for implementation (very subject to change!) is:
10 |
11 | * Continue filling out properties of the `AppHistoryNavigateEvent` object (e.g. today Nate is [working on `userInitiated`](https://chromium-review.googlesource.com/c/chromium/src/+/2757310)).
12 | * Implement accessibility technology integration.
13 | * Implement `appHistory.entries`.
14 | * Implement `event.destination` for `AppHistoryNavigateEvent`.
15 | * Implement `appHistory.push()` and `appHistory.update()`.
16 | * ... more ...
17 |
18 | You can watch our progress by starring [Chromium bug 1183545](https://bugs.chromium.org/p/chromium/issues/detail?id=1183545). Feel free to build your own demos to experiment, although keep in mind it's very early days and some basic things like `event.destination` don't work yet!
19 |
20 | ## Specification
21 |
22 | We have [an initial specification](https://wicg.github.io/app-history/)! It's roughly focused on the same area as the implementation, so that we can have the implementation serve as a check on the spec, and the spec serve as a check on the implementation. We expect this pattern to continue, with sometimes the spec being a bit ahead, and sometimes a bit behind.
23 |
24 | ## Design issues
25 |
26 | We've been resolving a lot of interesting design issues by updating the explainer. Here I'll provide links to the pull requests; if you want to see the deliberations that led to those conclusions, follow the links to the issues that each PR closes.
27 |
28 | * We realized that making all navigations async was not going to work, and instead made all `navigate`-intercepted navigations synchronous: [#46](https://github.com/WICG/app-history/pull/46).
29 | * We further pushed toward a `navigate`-centric model by making `appHistory.push()` and `appHistory.update()` do a full-page navigation, unless `navigate` intercepts them: [#54](https://github.com/WICG/app-history/pull/54).
30 | * A series of pull requests settled on the latest semantics for when navigations can be intercepted, and when they can be canceled: [#26](https://github.com/WICG/app-history/pull/26), [#56](https://github.com/WICG/app-history/pull/56), [#65](https://github.com/WICG/app-history/pull/65).
31 | * The API has gotten cleaner and easier to use through some renames and tweaks: [#35](https://github.com/WICG/app-history/pull/35), [#49](https://github.com/WICG/app-history/pull/49), [#55](https://github.com/WICG/app-history/pull/55).
32 | * We changed how state is retrieved: [#54](https://github.com/WICG/app-history/pull/54) (which was partially rolled back in [#61](https://github.com/WICG/app-history/pull/61)).
33 | * We resolved how app history interacts with the joint session history: it is purely a layer on top of it. See [#29](https://github.com/WICG/app-history/pull/29) and especially [#29 (comment)](https://github.com/WICG/app-history/pull/29#issuecomment-777773026).
34 |
35 | Next up is trying to resolve the following:
36 |
37 | * [#68](https://github.com/WICG/app-history/pull/68) attempts to solve a constellation of issues around interrupted and aborted navigations.
38 | * [#5](https://github.com/WICG/app-history/issues/5) contains good discussion about how to support the URL-rewriting case, although not yet a firm conclusion.
39 | * [#32](https://github.com/WICG/app-history/issues/32) discusses how we can allow back button interception, specifically for the "are you sure you want to abandon this filled-out form?" case. We have [a tentative idea](https://github.com/WICG/app-history/issues/32#issuecomment-789944257) that might work.
40 | * [#7](https://github.com/WICG/app-history/issues/7) is about the semantics of what updating or replacing an app history entry means, and how we should model that in a way that fits well with what apps need and are doing today.
41 | * [#33](https://github.com/WICG/app-history/issues/33) and [#59](https://github.com/WICG/app-history/issues/59) note that our current design for reporting the time a single-page navigation takes does not make sense; likely we will work with [the folks maintaining other performance measurement APIs](https://www.w3.org/webperf/) to come up with a more principled alternative.
42 |
43 | There are plenty of other open issues which have good discussion on them too, so feel free to check out [the issue tracker](https://github.com/WICG/app-history/issues) to get a more complete view.
44 |
45 | ## Thank you!
46 |
47 | I want to close with a thank you to the community that has been so engaged and helpful, on the issue tracker and elsewhere! It's exciting to know that we've hit a chord, and are solving a problem web developers are passionate about. Keep the great feedback coming!
48 |
--------------------------------------------------------------------------------
/2021-06-28-incubation-update.md:
--------------------------------------------------------------------------------
1 | # Incubation Update: June 28, 2021
2 |
3 | App history is happening! 🥳🥳🥳 Read on to find out what's new since [last time](./2021-03-12-incubation-update.md).
4 |
5 | ## Prototype implementation
6 |
7 | We continue to prototype app history in Chromium, behind the Experimental Web Platform features flag. You can try it yourself by using the latest build of [Chrome Canary](https://www.google.com/chrome/canary/) and flipping the flag in `chrome://flags`.
8 |
9 | And as of today, we feel that we've reached a milestone where people can seriously experiment with app history, and try to prototype real apps and libraries! We've implemented the core features of the proposal: introspection into the app history list, conversion of cross-document navigations into same-document navigations, and the `appHistory.navigate()` API. You can check out the following demos to see these in action:
10 |
11 | * [Basic SPA nav demo](https://gigantic-honored-octagon.glitch.me/)
12 | * [Form data handling demo](https://selective-heliotrope-dumpling.glitch.me/)
13 |
14 | (Note that unlike the last time you saw those demos, now [the back button works](https://bugs.chromium.org/p/chromium/issues/detail?id=1186299).)
15 |
16 | At this point it's easier to list what we haven't implemented, than what we have. The following APIs from the explainer are not yet in Chromium:
17 |
18 | * `appHistory.reload()`
19 | * `appHistory.transition`
20 | * `appHistoryNavigateEvent.navigationType`
21 | * `appHistoryNavigateEvent.signal`, and stop button integration
22 | * Integration of `navigate` events with accessibility technology
23 | * `appHistoryEntry` events
24 | * [Performance timeline integration](./README.md##performance-timeline-api-integration)
25 |
26 | Additionally, we have a number of open issues about updating to the exact spec semantics, especially in edge cases. Follow our progress in [Chromium bug 1183545](https://bugs.chromium.org/p/chromium/issues/detail?id=1183545) and its BlockedOn issues!
27 |
28 | ## Specification and tests
29 |
30 | The [specification](https://wicg.github.io/app-history/) continues to mostly track the prototype implementation. However, [specifying `goTo()`/`back()`/`forward()`](https://github.com/WICG/app-history/pull/109) is proving tricky, due to [shaky spec foundations](https://github.com/whatwg/html/issues/5767) around history traversal in general. We're taking care to stabilize the foundations beforehand, before building new features on top of them.
31 |
32 | We also [fixed an issue with the base navigation spec](https://github.com/whatwg/html/pull/6714) which was preventing us from confidently upstreaming our tests, since until recently the tests technically did not match the HTML spec. Now our tests are [headed to the web platform tests repository](https://chromium-review.googlesource.com/c/chromium/src/+/2991902), to better enable sharing with other browser vendors and with polyfill authors.
33 |
34 | ## Design issues
35 |
36 | Since [last time](./2021-03-12-incubation-update.md#design-issues), the substantial design changes worth noting include:
37 |
38 | * Specifying that any new navigation will interrupt an ongoing one, including firing `abort` on its `event.signal` property: [#68](https://github.com/WICG/app-history/pull/68)
39 | * Adding `appHistoryEntry.id` alongside `appHistoryEntry.key`: [#88](https://github.com/WICG/app-history/pull/88)
40 | * Renaming `appHistory.navigateTo()` to `appHistory.goTo()`, and combined `appHistory.push()` and `appHistory.update()` into `appHistory.navigate()`, both based on feedback from Mozilla: [#84](https://github.com/WICG/app-history/pull/84)
41 | * Subsequently splitting out `appHistory.reload()` from `appHistory.navigate()`: [#118](https://github.com/WICG/app-history/pull/118)
42 | * Introducing performance timeline API integration to replace the `currentchange` event: [#125](https://github.com/WICG/app-history/pull/125)
43 | * Allowing multiple calls to `appHistoryNavigateEvent.respondWith()`: [#126](https://github.com/WICG/app-history/pull/126)
44 |
45 | We still haven't settled on solutions for [URL-rewriting use cases](https://github.com/WICG/app-history/issues/5) or [back-button prevention](https://github.com/WICG/app-history/issues/32), but they remain on our radar. And the [naming discussion](https://github.com/WICG/app-history/issues/83) for the whole API continues; we were [considering](https://github.com/WICG/app-history/issues/83#issuecomment-839901780) renaming the API to `window.navigation`—Mozilla is especially enthusiastic—but haven't pulled the trigger yet due to concerns about it being confusing with `window.navigator`.
46 |
47 | New design issues which have cropped up include:
48 |
49 | * Since `appHistoryNavigateEvent.respondWith()`'s semantics have changed, the `respondWith()` name is no longer very good: [#94](https://github.com/WICG/app-history/issues/94#issuecomment-854929003)
50 | * How should `someOtherWindow.appHistory.navigate()`'s returned promise behave? [#95](https://github.com/WICG/app-history/issues/95)
51 | * `appHistoryNavigateEvent.destination` currently contains only `url`, `sameDocument`, and `getState()`. Are there use cases for more? [#97](https://github.com/WICG/app-history/issues/97)
52 | * How should `appHistory.navigate(currentURL, { replace: false })` behave? [#111](https://github.com/WICG/app-history/issues/111)
53 | * Should we expand the definition of `appHistoryNavigateEvent.userInitiated`? [#127](https://github.com/WICG/app-history/issues/127)
54 |
55 | Finally, we're contemplating the following additions based on what we've seen so far:
56 |
57 | * [#101](https://github.com/WICG/app-history/issues/101) is about allowing more powerful `` navigations, e.g. `` or maybe even ``.
58 | * [#115](https://github.com/WICG/app-history/issues/115) claims that we probably need to introduce the ability to modify an `AppHistoryEntry`'s state, even without performing an intercepted navigation.
59 | * [#124](https://github.com/WICG/app-history/issues/124) discusses introducing a more first-class API for single-page app "redirects".
60 |
61 | Note that these days we're maintaining a [feedback wanted](https://github.com/WICG/app-history/labels/feedback%20wanted) label, which lists all the issues where community feedback would be especially helpful.
62 |
63 | ## Go forth and prototype
64 |
65 | We've reached a new stage in the app history proposal, where we've got the core API implemented and now need to validate that it works for real apps and libraries. If you work on a router or history library, or a framework that manages those aspects, consider spinning up a branch to test out whether app history will help your users. Even if you don't want to fully buy into the [`navigate` event lifestyle](./README.md#using-navigate-handlers), consider sprinkling in a few uses of `appHistory.entries()`, using app history state instead of `history.state`, or enabling new experiences like [making your in-page back button sync with the session history](./README.md#sample-code).
66 |
67 | Similarly, if you have a hobby application, consider trying to use app history API directly so you can give us feedback. What works well? What works poorly? What's missing? Did you gain a new perspective on any of the [feedback wanted](https://github.com/WICG/app-history/labels/feedback%20wanted) issues?
68 |
69 | In all such cases, the [polyfill](https://github.com/frehner/appHistory) might be helpful. The author, [@frehner](https://github.com/frehner), has been heavily involved in the app history repository, and although at this stage we can't guarantee it always matches the spec or Chromium behavior, as the spec and implementation firm up we do plan to collaborate more closely with @frehner on the polyfill to make it production-ready.
70 |
--------------------------------------------------------------------------------
/2021-11-04-incubation-update.md:
--------------------------------------------------------------------------------
1 | # Incubation Update: November 4, 2021
2 |
3 | App history is starting to achieve its final form! Read on to find out what's new since [last time](./2021-06-28-incubation-update.md).
4 |
5 | ## Prototype implementation and origin trial
6 |
7 | The app history implementation in Chromium recently hit an important milestone. In addition to being available behind the Experimental Web Platform features flag, it is now available as an [origin trial](https://github.com/GoogleChrome/OriginTrials/blob/gh-pages/developer-guide.md)! From Chromium versions 96–99, you can add a HTTP header or `` tag to your site to try out app history in production, against real users. [Sign up and get a token](https://developer.chrome.com/origintrials/#/view_trial/2347501305766871041) if you're ready to start experimenting!
8 |
9 | At this point we believe all of the core API is ready, giving parity with `window.history` and, due to the `dispose` event, even exceeding it. You can see an update-to-date account of our progress in the [implementation plan document](https://docs.google.com/document/d/1vmxjUzn7qccn9xwVoIKRKVEPrIstrPzztg0chlgLAMY/edit#), but to summarize the stuff that's missing as of the time of this writing:
10 |
11 | * Stop/reload/loading spinner integration with the promises passed to `event.transitionWhile()` (in progress, but not yet landed)
12 | * Integration with accessibility technology
13 | * `appHistory.transition.finished` and `appHistory.transition.rollback()`
14 | * `AppHistoryEntry` events besides `dispose`, i.e. `navigateto`, `navigatefrom`, and `finish`
15 | * The `dispose` event for cases besides forward-pruning, e.g. when the user clears their history
16 | * [Performance timeline integration](./README.md#performance-timeline-api-integration)
17 |
18 | We look forward to hearing about what you can build with app history. If you're running a site that might use app history, or maintaining a router library or framework, now is a great time to play around with the API, and get in touch with any feedback.
19 |
20 | (By the way, if you use TypeScript, we now host [TypeScript definitions for the API](https://github.com/WICG/app-history/blob/main/app_history.d.ts) in this repository!)
21 |
22 | ## Design updates
23 |
24 | Since last time, we've made the following substantial changes:
25 |
26 | * Added `appHistory.updateCurrent()`, for specific use cases where you need to update app history state in response to a user action: [#146](https://github.com/WICG/app-history/pull/146)
27 | * Added back the `currentchange` event, for when `appHistory.current` changes: [#171](https://github.com/WICG/app-history/pull/171)
28 | * Added `key`, `id`, and `index` to `navigateEvent.destination` for `"traverse"` navigations: [#131](https://github.com/WICG/app-history/pull/131)
29 | * Changed the return values of all the navigating methods from just a promise, to a pair of `{ committed, finished }` promises: [#164](https://github.com/WICG/app-history/pull/164)
30 | * Renamed `navigateEvent.respondWith()` and `navigateEvent.canRespond` to `transitionWhile()` and `canTransition`: [#151](https://github.com/WICG/app-history/pull/151)
31 | * Renamed the `navigateInfo` option to just `info`: [#145](https://github.com/WICG/app-history/pull/145)
32 | * Started firing the `navigate` event for all traversals, but making it non-cancelable for now: [#182](https://github.com/WICG/app-history/pull/182)
33 | * Disabled most of the API for opaque-origin pages: [#169](https://github.com/WICG/app-history/pull/169)
34 |
35 | ## The road toward shipping?
36 |
37 | As mentioned above, at this point we believe the core API is ready to experiment with, including in production evironments using origin trials or similar time-limited measures. What remains for us to do, before we could consider shipping the API?
38 |
39 | We've collated a list of ["Might block v1"](https://github.com/WICG/app-history/milestone/1) issues on the issue tracker. Most of them are related to things that, if we changed them after shipping, could cause compatibility problems: so, we need to figure them out, and finalize the spec/implementation/tests. This includes the still-ongoing [API naming discussion](https://github.com/WICG/app-history/issues/83) 😅. If all goes well, including reviews with other browsers, it's conceivable we could solve these issues in time for Chromium 100 in March 2022.
40 |
41 | That list is just the bare minimum, however. Based on feedback from developers and other browser vendors, a few other things are on our radar as high priority, which we might try to finish up before the initial release or at least shortly afterward:
42 |
43 | * Updating, deleting, and rearranging non-current entries is a common developer pain point, with solid use cases: [#9](https://github.com/WICG/app-history/issues/9)
44 | * Canceling browser UI-initiated back/forward navigations, to implement "are you sure you want to leave this page?", is technically difficult but definitely planned: see [#32](https://github.com/WICG/app-history/issues/32) and also some discussion in [#178](https://github.com/WICG/app-history/issues/178)
45 | * Ensuring `appHistory.navigate()` has parity with `` by adding download, form data, and referrer policy options, would not be hard and would round out the API nicely: [#82](https://github.com/WICG/app-history/issues/82)
46 | * Adding an easy way to do "client-side redirects" would solve a sharp edge; you can do these with the current API but it's trickier than it should be: [#124](https://github.com/WICG/app-history/issues/124)
47 |
48 | These and many other such ideas are under the ["addition"](https://github.com/WICG/app-history/labels/addition) label in the issue tracker. If any such additions would be especially helpful to your project, please let us know with either a thumbs-up or, better yet, a comment describing your use case.
49 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contribution Details
2 |
3 | ## Joining WICG
4 |
5 | This repository is being used for work in the W3C [Web Platform Incubator Community Group](https://www.w3.org/community/wicg/) (WICG), governed by the [W3C Community License Agreement (CLA)](http://www.w3.org/community/about/agreements/cla/). To make substantive contributions, you must join the Community Group, thus signing the CLA.
6 |
7 | ## For maintainers: identifying contributors to a pull request
8 |
9 | If the author is not the sole contributor to a pull request, please identify all contributors in the pull request comment.
10 |
11 | To add a contributor (other than the author, which is automatic), mark them one per line as follows:
12 |
13 | ```
14 | +@github_username
15 | ```
16 |
17 | If you added a contributor by mistake, you can remove them in a comment with:
18 |
19 | ```
20 | -@github_username
21 | ```
22 |
23 | If the author is making a pull request on behalf of someone else but they had no part in designing the feature, you can remove them with the above syntax.
24 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | SHELL=/bin/bash
2 |
3 | local: spec.bs
4 | bikeshed --die-on=warning spec spec.bs spec.html
5 |
6 | spec.html: spec.bs
7 | @ (HTTP_STATUS=$$(curl https://api.csswg.org/bikeshed/ \
8 | --output spec.html \
9 | --write-out "%{http_code}" \
10 | --header "Accept: text/plain, text/html" \
11 | -F die-on=warning \
12 | -F file=@spec.bs) && \
13 | [[ "$$HTTP_STATUS" -eq "200" ]]) || ( \
14 | echo ""; cat spec.html; echo ""; \
15 | rm -f spec.html; \
16 | exit 22 \
17 | );
18 |
19 | remote: spec.html
20 |
21 | ci: spec.bs
22 | mkdir -p out
23 | make remote
24 | mv spec.html out/index.html
25 |
26 | clean:
27 | rm spec.html
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Navigation API
2 |
3 | _[Formerly known as](https://github.com/WICG/navigation-api/issues/83) the app history API_
4 |
5 | The web's existing [history API](https://developer.mozilla.org/en-US/docs/Web/API/History) is problematic for a number of reasons, which makes it hard to use for web applications. This proposal introduces a new API encompassing navigation and history traversal, which is more directly usable by web application developers. Its scope is: initiating navigations, intercepting navigations, and history introspection and mutation.
6 |
7 | This new `window.navigation` API [layers](#integration-with-the-existing-history-api-and-spec) on top of the existing API and specification infrastructure, with well-defined interaction points. The main differences are that it is scoped to the current origin and frame, and it is designed to be pleasant to use instead of being a historical accident with many sharp edges.
8 |
9 | ## Summary
10 |
11 | The existing [history API](https://developer.mozilla.org/en-US/docs/Web/API/History), including its ability to cause same-document navigations via `history.pushState()` and `history.replaceState()`, is hard to deal with in practice, especially for single-page applications. In the best case, developers can work around this with various hacks. In the worst case, it causes user-facing pain in the form of lost state and broken back buttons, or the inability to achieve the desired navigation flow for a web app.
12 |
13 | The main problems are:
14 |
15 | - Managing and introspecting your application's history list, and associated application state, is fragile. State can be lost sometimes (e.g. due to fragment navigations); the browser will spontaneously insert entries due to iframe navigations; and the existing `popstate` and `hashchange` events are [unreliable](https://github.com/whatwg/html/issues/5562). We solve this by providing a view only onto the history entries created directly by the application, and the ability to look at all previous entries for your app so that no state is ever lost.
16 |
17 | - It's hard to figure out all the ways that navigations can occur, so that an application can synchronize its state or convert those navigations into single-page navigations. We solve this by exposing events that allow the application to observe all navigation actions, and substitute their own behavior in place of the default.
18 |
19 | - Various parts of the platform, e.g. accessibility technology, the browser's UI, and performance APIs, do not have good visibility into single-page navigations. We solve this by providing a standardized API for telling the browser when a single-page navigation starts and finishes.
20 |
21 | - Some of the current navigation and history APIs are clunky and hard to understand. We solve this by providing a new interface that is easy for developers to use and understand.
22 |
23 | ## Sample code
24 |
25 | An application or framework's centralized router can use the `navigate` event to implement single-page app routing:
26 |
27 | ```js
28 | navigation.addEventListener("navigate", e => {
29 | if (!e.canIntercept || e.hashChange || e.downloadRequest !== null) {
30 | return;
31 | }
32 |
33 | if (routesTable.has(e.destination.url)) {
34 | const routeHandler = routesTable.get(e.destination.url);
35 | e.intercept({ handler: routeHandler });
36 | }
37 | });
38 | ```
39 |
40 | A page-supplied "back" button can actually take you back, even after reload, by inspecting the previous history entries:
41 |
42 | ```js
43 | backButtonEl.addEventListener("click", () => {
44 | if (navigation.entries()[navigation.currentEntry.index - 1]?.url === "/product-listing") {
45 | navigation.back();
46 | } else {
47 | // If the user arrived here by typing the URL directly:
48 | navigation.navigate("/product-listing", { history: "replace" });
49 | }
50 | });
51 | ```
52 |
53 |
54 |
55 | ## Table of contents
56 |
57 | - [Problem statement](#problem-statement)
58 | - [Goals](#goals)
59 | - [Proposal](#proposal)
60 | - [The current entry](#the-current-entry)
61 | - [Inspection of the history entry list](#inspection-of-the-history-entry-list)
62 | - [Navigation through the history entry list](#navigation-through-the-history-entry-list)
63 | - [Keys and IDs](#keys-and-ids)
64 | - [Navigation monitoring and interception](#navigation-monitoring-and-interception)
65 | - [Example: replacing navigations with single-page app navigations](#example-replacing-navigations-with-single-page-app-navigations)
66 | - [Example: async transitions with special back/forward handling](#example-async-transitions-with-special-backforward-handling)
67 | - [Example: progressively enhancing form submissions](#example-progressively-enhancing-form-submissions)
68 | - [Restrictions on firing, canceling, and responding](#restrictions-on-firing-canceling-and-responding)
69 | - [Measuring standardized single-page navigations](#measuring-standardized-single-page-navigations)
70 | - [Aborted navigations](#aborted-navigations)
71 | - [Customizations and consequences of navigation interception](#customizations-and-consequences-of-navigation-interception)
72 | - [Accessibility technology announcements](#accessibility-technology-announcements)
73 | - [Loading spinners and stop buttons](#loading-spinners-and-stop-buttons)
74 | - [Focus management](#focus-management)
75 | - [Scrolling to fragments and scroll resetting](#scrolling-to-fragments-and-scroll-resetting)
76 | - [Scroll position restoration](#scroll-position-restoration)
77 | - [Precommit handlers](#precommit-handlers)
78 | - [Redirects during deferred commit](#redirects-during-deferred-commit)
79 | - [Transitional time after navigation interception](#transitional-time-after-navigation-interception)
80 | - [Example: handling failed navigations](#example-handling-failed-navigations)
81 | - [The `navigate()` and `reload()` methods](#the-navigate-and-reload-methods)
82 | - [Example: using `info`](#example-using-info)
83 | - [Example: next/previous buttons](#example-nextprevious-buttons)
84 | - [Setting the current entry's state without navigating](#setting-the-current-entrys-state-without-navigating)
85 | - [Notifications on entry disposal](#notifications-on-entry-disposal)
86 | - [Current entry change monitoring](#current-entry-change-monitoring)
87 | - [Complete event sequence](#complete-event-sequence)
88 | - [Guide for migrating from the existing history API](#guide-for-migrating-from-the-existing-history-api)
89 | - [Performing navigations](#performing-navigations)
90 | - [Warning: back/forward are not always opposites](#warning-backforward-are-not-always-opposites)
91 | - [Using `navigate` handlers](#using-navigate-handlers)
92 | - [Attaching and using history state](#attaching-and-using-history-state)
93 | - [Introspecting the history list](#introspecting-the-history-list)
94 | - [Watching for navigations](#watching-for-navigations)
95 | - [Integration with the existing history API and spec](#integration-with-the-existing-history-api-and-spec)
96 | - [Correspondence with session history entries](#correspondence-with-session-history-entries)
97 | - [Correspondence with the joint session history](#correspondence-with-the-joint-session-history)
98 | - [Integration with navigation](#integration-with-navigation)
99 | - [Impact on the back button and user agent UI](#impact-on-the-back-button-and-user-agent-ui)
100 | - [Security and privacy considerations](#security-and-privacy-considerations)
101 | - [Future extensions](#future-extensions)
102 | - [More per-entry events](#more-per-entry-events)
103 | - [Performance timeline API integration](#performance-timeline-api-integration)
104 | - [More](#more)
105 | - [Stakeholder feedback](#stakeholder-feedback)
106 | - [Acknowledgments](#acknowledgments)
107 | - [Appendix: types of navigations](#appendix-types-of-navigations)
108 |
109 |
110 |
111 | ## Problem statement
112 |
113 | Web application developers, as well as the developers of router libraries for single-page applications, want to accomplish a number of use cases related to history:
114 |
115 | - Intercepting cross-document navigations, replacing them with single-page navigations (i.e. loading content into the appropriate part of the existing document), and then updating the URL bar.
116 |
117 | - Performing single-page navigations that create and push a new entry onto the history list, to represent a new conceptual history entry.
118 |
119 | - Navigating backward or forward through the history list via application-provided UI.
120 |
121 | - Synchronizing application or UI state with the current position in the history list, so that user- or application-initiated navigations through the history list appropriately restore application/UI state.
122 |
123 | The existing [history API](https://developer.mozilla.org/en-US/docs/Web/API/History) is difficult to use for these purposes. The fundamental problem is that `window.history` surfaces the joint session history of a browsing session, and so gets updated in response to navigations in nested frames, or cross-origin navigations. Although this view is important for the user, especially in terms of how it impacts their back button, it doesn't map well to web application development. A web application cares about its own, same-origin, current-frame history entries, and having to deal with the entire joint session history makes this very painful. Even in a carefully-crafted web app, a single iframe can completely mess up the application's history.
124 |
125 | The existing history API also has a number of less-fundamental, but still very painful, problems around how its API shape has grown organically, with only very slight considerations for single-page app architectures. For example, it provides no mechanism for intercepting navigations; to do this, developers have to intercept all `click` events, cancel them, and perform the appropriate `history.pushState()` call. The `history.state` property is a very bad storage mechanism for application and UI state, as it disappears and reappears as you transition throughout the history list, instead of allowing access to earlier entries in the list. And the ability to navigate throughout the list is limited to numeric offsets, with `history.go(-2)` or similar; thus, navigating back to an actual specific state requires keeping a side table mapping history indices to application states.
126 |
127 | To hear more detail about these problems, in the words of a web developer, see [@dvoytenko](https://github.com/dvoytenko)'s ["The case for the new Web History API"](https://github.com/dvoytenko/web-history-api/blob/master/problem.md). See also [@housseindjirdeh](https://github.com/housseindjirdeh)'s ["History API and JavaScript frameworks"](https://docs.google.com/document/d/1gLW_FlR_wD93ZWXWmH14q0UssBaR0eGMk8njyr6p3cE/edit).
128 |
129 | ## Goals
130 |
131 | Overall, our guiding principle is to make it easy for web application developers to write applications which give good user experiences in terms of the history list, back button, and other navigation UI (such as open-in-new-tab). We believe this is too hard today with the `window.history` API.
132 |
133 | From an API perspective, our primary goals are as follows:
134 |
135 | - Allow easy conversion of cross-document navigations into single-page app same-document navigations, without fragile hacks like a global `click` handler.
136 |
137 | - Improve the accessibility of single-page app navigations ([1](https://github.com/w3c/aria/issues/1353), [2](https://docs.google.com/document/d/1MYClmO3FkjhSuSYKlVPVDnXvtOm-yzG15SY9jopJIxQ/edit#), [3](https://www.gatsbyjs.com/blog/2019-07-11-user-testing-accessible-client-routing/)), ideally to be on par with cross-document navigations, when they are implemented using this API.
138 |
139 | - Provide a uniform way to signal single-page app navigations, including their duration.
140 |
141 | - Provide a reliable system to tie application and UI state to history entries.
142 |
143 | - Continue to support the pattern of allowing the history list to contain state that is not serialized to the URL. (This is possible with `history.pushState()` today.)
144 |
145 | - Provide events for notifying the application about navigations and traversals, which they can use to synchronize application or UI state.
146 |
147 | - Allow metrics code to watch for navigations, including gathering timing information about how long they took, without interfering with the rest of the application.
148 |
149 | - Provide a way for an application to reliably navigate through its own history list.
150 |
151 | - Provide a reasonable layering onto and integration with the existing `window.history` API, in terms of spec primitives and ensuring non-terrible behavior when both are used.
152 |
153 | Non-goals:
154 |
155 | - Allow web applications to intercept user-initiated navigations in a way that would trap the user (e.g., disabling the URL bar or back button).
156 |
157 | - Provide applications knowledge of cross-origin history entries or state.
158 |
159 | - Provide applications knowledge of other frames' entries or state.
160 |
161 | - Provide platform support for the coordination problem of multiple routers (e.g., per-UI-component routers) on a single page. We plan to leave this coordination to frameworks for now (with the frameworks using the new API).
162 |
163 | - Handle the case where the Android back button is being used as a "close signal"; instead, we believe that's best handled by [a separate API](https://github.com/domenic/close-watcher).
164 |
165 | - Provide any handling for preventing navigations that might lose data: this is already handled orthogonally by the platform's `beforeunload` event.
166 |
167 | - Provide an _elegant_ layering onto or integration with the existing `window.history` API. That API is quite problematic, and we can't be tied down by a need to make every operation in the new API isomorphic to one in the old API.
168 |
169 | A goal that might not be possible, but we'd like to try:
170 |
171 | - It would be ideal if this API were polyfillable, especially in its mainline usage scenarios.
172 |
173 | Finally, although it's really a goal for all web APIs, we want to call out a strong focus on interoperability, backstopped by [web platform tests](http://web-platform-tests.org/). The existing history API and its interactions with navigation have terrible interoperability (see [this vivid example](https://docs.google.com/document/d/1Pdve-DJ1JCGilj9Yqf5HxRJyBKSel5owgOvUJqTauwU/edit#)). We hope to have solid and well-tested specifications for:
174 |
175 | - Every aspect and self-interaction of the new API
176 |
177 | - Every aspect of how the new API integrates and interacts with the `window.history` API (including things like relative timing of events)
178 |
179 | Additionally, we hope to drive interoperability through tests, spec updates, and browser bugfixes for the existing `window.history` API while we're in the area, to the extent that is possible; some of this work is being done in [whatwg/html#5767](https://github.com/whatwg/html/issues/5767).
180 |
181 | ## Proposal
182 |
183 | ### The current entry
184 |
185 | The entry point for the new navigation API is `window.navigation`. Let's start with `navigation.currentEntry`, which is an instance of the new `NavigationHistoryEntry` class. This class has the following readonly properties:
186 |
187 | - `id`: a user-agent-generated UUID identifying this particular `NavigationHistoryEntry`. This will be changed upon any mutation of the current history entry, such as replacing its state or updating the current URL.
188 |
189 | - `key`: a user-agent-generated UUID identifying this history entry "slot". This will stay the same even if the entry is replaced.
190 |
191 | - `index`: the index of this `NavigationHistoryEntry` within the (`Window`- and origin-specific) history entry list. (Or, `-1` if the entry is no longer in the list, or not yet in the list.)
192 |
193 | - `url`: the URL of this history entry (as a string).
194 |
195 | - `sameDocument`: a boolean indicating whether this entry is for the current document, or whether navigating to it will require a full navigation (either from the network, or from the browser's back/forward cache). Note: for `navigation.currentEntry`, this will always be `true`.
196 |
197 | It also has a method `getState()`, which retrieve the navigation API state for the entry. This is somewhat similar to `history.state`, but it will survive fragment navigations, and `getState()` always returns a fresh clone of the state to avoid the [misleading nature of `history.state`](https://github.com/WICG/navigation-api/issues/36):
198 |
199 | ```js
200 | navigation.reload({ state: { test: 2 } });
201 |
202 | // Don't do this: it won't be saved to the stored state.
203 | navigation.currentEntry.getState().test = 3;
204 |
205 | console.assert(navigation.currentEntry.getState().test === 2);
206 |
207 | // Instead do this, combined with a `navigate` event handler:
208 | navigation.reload({ state: { ...navigation.currentEntry.getState(), test: 3 } });
209 | ```
210 |
211 | Crucially, `navigation.currentEntry` stays the same regardless of what iframe navigations happen. It only reflects the current entry for the current frame. The complete list of ways the current navigation API history entry can change to a new entry (with a new `NavigationHistoryEntry` object, and a new `key` value) are:
212 |
213 | - A fragment navigation, which will copy over the navigation API state to the new entry.
214 |
215 | - Via `history.pushState()`. (Not `history.replaceState()`.)
216 |
217 | - A full-page navigation to a different document. This could be an existing document in the browser's back/forward cache, or a new document. In the latter case, this will generate a new entry on the new page's `window.navigation.entries()` list, somewhat similar to `navigation.navigate(navigatedToURL, { state: undefined })`. Note that if the navigation is cross-origin, then we'll end up in a separate navigation API history entries list for that other origin.
218 |
219 | - When using the `navigate` event to [convert a cross-document non-replace navigation into a same-document navigation](#navigation-monitoring-and-interception).
220 |
221 | The current entry can be replaced with a new entry, with a new `NavigationHistoryEntry` object and a new `id` (but usually the same `key`), in the following ways:
222 |
223 | - Via `history.replaceState()`.
224 |
225 | - 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.
226 |
227 | - When using the `navigate` event to [convert a cross-document replace navigation into a same-document navigation](#navigation-monitoring-and-interception).
228 |
229 | For any same-document navigation, traversal, or replacement, the `currententrychange` event will fire on `navigation`:
230 |
231 | ```js
232 | navigation.addEventListener("currententrychange", () => {
233 | // navigation.currentEntry has changed: either to a completely new entry (with a new key),
234 | // or it has been replaced (keeping the same key but with a new id).
235 | });
236 | ```
237 |
238 | ### Inspection of the history entry list
239 |
240 | In addition to the current entry, the entire list of history entries can be inspected, using `navigation.entries()`, which returns an array of `NavigationHistoryEntry` instances. (Recall that all navigation API history entries are same-origin contiguous entries for the current frame, so this is not a security issue.)
241 |
242 | This solves the problem of allowing applications to reliably store state in a `NavigationHistoryEntry`'s state: because they can inspect the values stored in previous entries at any time, it can be used as real application state storage, without needing to keep a side table like one has to do when using `history.state`.
243 |
244 | Note that we have a method, `navigation.entries()`, instead of a static array, `navigation.entries`, to emphasize that retrieving the entries gives you a snapshot at a given point in time. That is, the current set of history entries could change at any point due to manipulations of the history list, including by the user.
245 |
246 | In combination with the following section, the `entries()` API also allows applications to display a UI allowing navigation through the entry list.
247 |
248 | ### Navigation through the history entry list
249 |
250 | The way for an application to navigate through the history entry list is using `navigation.traverseTo(key)`. For example:
251 |
252 | ```js
253 | function renderHomepage() {
254 | const homepageKey = navigation.currentEntry.key;
255 |
256 | // ... set up some UI ...
257 |
258 | document.querySelector("#home-button").addEventListener("click", async e => {
259 | try {
260 | await navigation.traverseTo(homepageKey).finished;
261 | } catch {
262 | // Fall back to a normal push navigation
263 | navigation.navigate("/");
264 | }
265 | });
266 | }
267 | ```
268 |
269 | Unlike the existing history API's `history.go()` method, which navigates by offset, traversing by key allows the application to not care about intermediate history entries; it just specifies its desired destination entry. There are also convenience methods, `navigation.back()` and `navigation.forward()`, and convenience booleans, `navigation.canGoBack` and `navigation.canGoForward`.
270 |
271 | All of these methods return `{ committed, finished }` pairs, where both values are promises. This because navigations can be intercepted and made asynchronous by the `navigate` event handlers that we're about to describe in the next section. There are then several possible outcomes:
272 |
273 | - A `navigate` event handler calls `event.preventDefault()`, in which case both promises reject with an `"AbortError"` `DOMException`, and `location.href` and `navigation.currentEntry` stay on their original value.
274 |
275 | - It's not possible to navigate to the given entry, e.g. `navigation.traverseTo(key)` was given a non-existant `key`, or `navigation.back()` was called when there's no previous entries in the list of accessible history entries. In this case, both promises reject with an `"InvalidStateError"` `DOMException`, and `location.href` and `navigation.currentEntry` stay on their original value.
276 |
277 | - The `navigate` event responds to the navigation using `event.intercept()` with a `commit` option of `"immediate"` (the default). In this case the `committed` promise immediately fulfills, while the `finished` promise fulfills or rejects according to any promise(s) returned by handlers passed to `intercept()`. (However, even if the `finished` promise rejects, `location.href` and `navigation.currentEntry` will change.)
278 |
279 | - The `navigate` event listener responds to the navigation using `event.intercept()` with a `commit` option of `"after-transition"`. In this case the `committed` promise fulfills and `location.href` and `navigation.currentEntry` change when `event.commit()` is called. The `finished` promise fulfills or rejects according to any promise(s) returned by handlers passed to `intercept()`. If a promise returned by a handler rejects before `event.commit()` is called, then both the `committed` and `finished` promises reject and `location.href` and `navigation.currentEntry` do not update. If all promise(s) returned by handlers fulfill, but the `committed` promise has not yet fulfilled, the `committed` promise will be fulfilled and and `location.href` and `navigation.currentEntry` will be updated first, then `finished` will fulfill.
280 |
281 | - The navigation succeeds, and was a same-document navigation (but not intercepted using `event.intercept()`). Then both promises immediately fulfill, and `location.href` and `navigation.currentEntry` will have been set to their new value.
282 |
283 | - The navigation succeeds, and it was a different-document navigation. Then the promise will never settle, because the entire document and all its promises will disappear.
284 |
285 | In all cases, the fulfillment value for the promises is the `NavigationHistoryEntry` being navigated to. This can be useful for setting up [per-entry event](#per-entry-events) handlers.
286 |
287 | As discussed in more detail in the section on [integration with the existing history API and spec](#integration-with-the-existing-history-api-and-spec), navigating through the navigation API history list does navigate through the joint session history. This means it _can_ impact other frames on the page. It's just that, unlike `history.back()` and friends, such other-frame navigations always happen as a side effect of navigating your own frame; they are never the sole result of a navigation API traversal. (An interesting consequence of this is that [`navigation.back()` and `navigation.forward()` are not always opposites](#warning-backforward-are-not-always-opposites).)
288 |
289 | ### Keys and IDs
290 |
291 | As noted [above](#the-current-entry), `key` stays stable to represent the "slot" in the history list, whereas `id` gets updated whenever the history entry is updated. This allows them to serve distinct purposes:
292 |
293 | - `key` provides a stable identifier for a given slot in the history entry list, for use by the `navigation.traverseTo()` method which allows navigating to specific waypoints within the history list.
294 |
295 | - `id` provides an identifier for the specific URL and navigation API state currently in the entry, which can be used to correlate a history entry with an out-of-band resource such as a cache.
296 |
297 | With the `window.history` API, web applications have tried to use the URL for such purposes, but the URL is not guaranteed to be unique within a given history list.
298 |
299 | Note that both `key` and `id` are user-agent-generated random UUIDs. This is done, instead of e.g. using a numeric index, to encourage treating them as opaque identifiers.
300 |
301 | Both `key` and `id` are stored in the browser's session history, and as such are stable across session restores.
302 |
303 | Note that `key` is not a stable identifier for a slot in the _joint session history list_, but instead in the _navigation API history entry list_. In particular, this means that if a given history entry is replaced with a cross-origin one, which lives in a different navigation API history list, it will get a new key. (This replacement prevents cross-site tracking.)
304 |
305 | ### Navigation monitoring and interception
306 |
307 | The most interesting event on `window.navigation` is the one which allows monitoring and interception of navigations: the `navigate` event. It fires on almost any navigation, either user-initiated or application-initiated, which would update the value of `navigation.currentEntry`. This includes cross-origin navigations (which will take us out of the current navigation API history entry list). **We expect this to be the main event used by application- or framework-level routers.**
308 |
309 | The event object has several useful properties:
310 |
311 | - `cancelable` (inherited from `Event`): indicates whether `preventDefault()` is allowed to cancel this navigation.
312 |
313 | - `canIntercept`: indicates whether `intercept()`, discussed below, is allowed for this navigation.
314 |
315 | - `navigationType`: either `"reload"`, `"push"`, `"replace"`, or `"traverse"`.
316 |
317 | - `userInitiated`: a boolean indicating whether the navigation is user-initiated (i.e., a click on an ``, or a form submission) or application-initiated (e.g. `location.href = ...`, `navigation.navigate(...)`, etc.). Note that this will _not_ be `true` when you use mechanisms such as `button.onclick = () => navigation.navigate(...)`; 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.
318 |
319 | - `destination`: an object containing the information about the destination of the navigation. It has many of the same properties as a `NavigationHistoryEntry`: namely `url`, `sameDocument`, and `getState()` for all navigations, and `id`, `key`, and `index` for same-origin `"traverse"` navigations. (See [#97](https://github.com/WICG/navigation-api/issues/97) for discussion as to whether we should add the latter to non-`"traverse"` same-origin navigations as well.)
320 |
321 | - `hashChange`: a boolean, indicating whether or not this is a same-document [fragment navigation](https://html.spec.whatwg.org/#scroll-to-fragid).
322 |
323 | - `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.
324 |
325 | - `downloadRequest`: a string or null, indicating whether this navigation was initiated by a `` link. If it was, then this will contain the value of the attribute (which could be the empty string).
326 |
327 | - `info`: any value passed by `navigation.navigate(url, { state, info })`, `navigation.back({ info })`, or similar, if the navigation was initiated by one of those methods and the `info` option was supplied. Otherwise, undefined. See [the example below](#example-using-info) for more.
328 |
329 | - `sourceElement`: an `Element` or null, indicating what element (if any) initiated this navigation. If the navigation was triggered by a link click, the `sourceElement` will be the ``. If the navigation was triggered by a form submission, the `sourceElement` will be the [the element that sent the `submit` event to the form](https://developer.mozilla.org/en-US/docs/Web/API/SubmitEvent/submitter), or if that is null, the `
1163 |
1164 |
1178 | ```
1179 |
1180 | Note how in this case we don't need to use `navigation.navigate()`, even though the original code used `history.pushState()`.
1181 |
1182 | ### Attaching and using history state
1183 |
1184 | To update the current entry's state, instead of using `history.replaceState(newState)`, either:
1185 |
1186 | - Use `navigation.reload({ state: newState })`, combined with a `navigate` handler to convert the cross-document navigation into a same-document one and update the document appropriately, if your state update is meant to drive a page update.
1187 |
1188 | - Use `navigation.updateCurrentEntry({ state: newState })`, if your state update is meant to capture something that's already happened to the page.
1189 |
1190 | To create a new entry with the same URL but a new state value, instead of using `history.pushState(newState)`, use `navigation.navigate(navigation.currentEntry.url, { state: newState })`, again combined with a `navigate` handler.
1191 |
1192 | To read the current entry's state, instead of using `history.state`, use `navigation.currentEntry.getState()`. Note that this will give a clone of the state, so you cannot set properties on it: to update state, see above.
1193 |
1194 | In general, state in the `window.navigation` API is expected to be more useful than state in the `window.history` API, because:
1195 |
1196 | - It can be introspected even for the non-current entry, e.g. using `navigation.entries()[i].getState()`.
1197 | - It is not erased by navigations that are not under the developer's control, such as fragment navigations (for which the state is copied over) and iframe navigations (which don't affect the navigation API history entry list).
1198 |
1199 | This means that the patterns that are often necessary to reliably store application and UI state with `window.history`, such as maintaining a side-table or using `sessionStorage`, should not be necessary with `window.navigation`.
1200 |
1201 | ### Introspecting the history list
1202 |
1203 | To see how many history entries are in the navigation API history entry list, use `navigation.entries().length`, instead of `history.length`. However, note that the semantics are different: navigation API history entries only include same-origin contiguous entries for the current frame, and so that this doesn't reflect the history before the user arrived at the current origin, or the history of iframes. We believe this will be more useful for the patterns that people want in practice.
1204 |
1205 | The navigation API allows introspecting all entries in its history entry list, using `navigation.entries()`. This should replace some of the workarounds people use today with the `window.history` API for getting a sense of the history list, e.g. as described in [whatwg/html#2710](https://github.com/whatwg/html/issues/2710).
1206 |
1207 | Finally, note that `history.length` is highly non-interoperable today, in part due to the complexity of the joint session history model, and in part due to historical baggage. `navigation`'s less complex model, and the fact that it will be developed in the modern era when there's a high focus on ensuring interoperability through web platform tests, means that using it should allow developers to avoid cross-browser issues with `history.length`.
1208 |
1209 | ### Watching for navigations
1210 |
1211 | Today there are two events related to navigations, `hashchange` and `popstate`, both on `Window`. These events are quite problematic and hard to use; see, for example, [whatwg/html#5562](https://github.com/whatwg/html/issues/5562) or other [open issues](https://github.com/whatwg/html/issues?q=is%3Aissue+is%3Aopen+popstate) for some discussion. MDN's fourteen-step guide to ["When popstate is sent"](https://developer.mozilla.org/en-us/docs/Web/API/Window/popstate_event#when_popstate_is_sent), which [doesn't even match any browsers](https://docs.google.com/document/d/1Pdve-DJ1JCGilj9Yqf5HxRJyBKSel5owgOvUJqTauwU/edit#), is also indicative of the problem.
1212 |
1213 | The new navigation API provides several replacements that subsume these events:
1214 |
1215 | - To react to and potentially intercept navigations before they complete, use the `navigate` event on `navigation`. See the [Navigation monitoring and interception](#navigation-monitoring-and-interception) section for more details, including how the event object provides useful information that can be used to distinguish different types of navigations.
1216 |
1217 | - To react to navigations that have finished, including any asynchronous work, use the `navigatesuccess` or `navigateerror` events on `navigation`. Note that these will only be fired after any promises returned by handlers passed to the `navigate` event's `event.intercept()` method have settled.
1218 |
1219 | - To react to navigations that have committed (but not necessarily yet finished), use the [`currententrychange` event](#current-entry-change-monitoring) on `navigation`. This is the most direct counterpart to `popstate` and `hashchange`, so might be easiest to use as part of an initial migration while your app is adapting to a `navigate` event-centric paradigm.
1220 |
1221 | - To watch a particular entry to see when it becomes unreachable, use that `NavigationHistoryEntry`'s [`dispose` event](#notifications-on-entry-disposal).
1222 |
1223 | ## Integration with the existing history API and spec
1224 |
1225 | At a high level, the new navigation API is meant to be a layer on top of the HTML Standard's existing concepts. It does not require a novel model for session history, either in implementations or specifications. (Although, it will only be possible to specify it rigorously once the existing specification gets cleaned up, per the work we're doing in [whatwg/html#5767](https://github.com/whatwg/html/issues/5767).)
1226 |
1227 | This is done through:
1228 |
1229 | - Ensuring that navigation API `NavigationHistoryEntry`s map directly to the specification's existing history entries. (See the next section.)
1230 |
1231 | - Ensuring that traversal through the history via the new navigation API always maps to a traversal through the joint session history, i.e. a traversal which is already possible today.
1232 |
1233 | ### Correspondence with session history entries
1234 |
1235 | A `NavigationHistoryEntry` corresponds directly to a [session history entry](https://html.spec.whatwg.org/#session-history-entry) from the existing HTML specification. However, not every session history entry would have a corresponding `NavigationHistoryEntry` in a given `Window`: `NavigationHistoryEntry` objects only exist for session history entries which are same-origin to the current one, and contiguous within that frame.
1236 |
1237 | Example: if a browsing session contains session history entries with the URLs
1238 |
1239 | ```
1240 | 1. https://example.com/foo
1241 | 2. https://example.com/bar
1242 | 3. https://other.example.com/whatever
1243 | 4. https://example.com/baz
1244 | ```
1245 |
1246 | then, if the current entry is 4, there would only be one `NavigationHistoryEntry` in `navigation.entries()`, corresponding to 4 itself. If the current entry is 2, then there would be two `NavigationHistoryEntry`s in `navigation.entries()`, corresponding to 1 and 2.
1247 |
1248 | To make this correspondence work, every spec-level session history entry would gain three new fields:
1249 |
1250 | - key, containing a browser-generated UUID. This is what backs `historyEntry.key`.
1251 | - id, containing a browser-generated UUID. This is what backs `historyEntry.id`.
1252 | - navigation API state, containing a JavaScript value. This is what backs `historyEntry.getState()`.
1253 |
1254 | Note that the "navigation API state" field has no interaction with the existing "serialized state" field, which is what backs `history.state`. This route was chosen for a few reasons:
1255 |
1256 | - The desired semantics of navigation API state is that it be carried over on fragment navigations, whereas `history.state` is not carried over. (This is a hard blocker.)
1257 | - A clean separation can help when a page contains code that uses both `window.history` and `window.navigation`. That is, it's convenient that existing code using `window.history` does not inadvertently mess with new code that does state management using `window.navigation`.
1258 | - Today, the serialized state of a session history entry is only exposed when that entry is the current one. The navigation API exposes `historyEntry.getState()` for all entries in `navigation.entries()`. This is not a security issue since all navigation API history entries are same-origin contiguous, but if we exposed the serialized state value even for non-current entries, it might break some assumptions of existing code.
1259 | - Switching to a separate field, accessible only via the `getState()` method, avoids the mutability problems discussed in [#36](https://github.com/WICG/navigation-api/issues/36). If the object was shared with `history.state`, those problems would be carried over.
1260 |
1261 | Apart from these new fields, the session history entries which correspond to `NavigationHistoryEntry` objects will continue to manage other fields like document, scroll restoration mode, scroll position data, and persisted user state behind the scenes, in the usual way. The serialized state and browsing context name fields would continue to work if they were set or accessed via the usual APIs, but they don't have any manifestation inside the navigation APIs, and will be left as null by applications that avoid `window.history` and `window.name`.
1262 |
1263 | _TODO: should we allow global control over the default scroll restoration mode, like `history.scrollRestoration` gives? That API has legitimate use cases, and we'd like to allow people to never touch `window.history`... Discuss in [#67](https://github.com/WICG/navigation-api/issues/67)._
1264 |
1265 | ### Correspondence with the joint session history
1266 |
1267 | The view of history which the user sees, and which is traversable with existing APIs like `history.go()`, is the joint session history.
1268 |
1269 | Unlike the view of history presented by `window.history`, `window.navigation` only gives a view onto session history entries for the current browsing session. This view does not present the joint session history, i.e. it is not impacted by frames. Notably, this means `navigation.entries().length` is likely to be quite different from `history.length`.
1270 |
1271 | Example: consider the following setup.
1272 |
1273 | 1. `https://example.com/start` loads.
1274 | 1. The user navigates to `https://example.com/outer` by clicking a link. This page contains an iframe with `https://example.com/inner-start`.
1275 | 1. Code on `https://example.com/outer` calls `history.pushState(null, "", "/outer-pushed")`.
1276 | 1. The iframe navigates to `https://example.com/inner-end`.
1277 |
1278 | The joint session session history contains four entries:
1279 |
1280 | ```
1281 | A. https://example.com/start
1282 | B. https://example.com/outer
1283 | ┗ https://example.com/inner-start
1284 | C. https://example.com/outer-pushed
1285 | ┗ https://example.com/inner-start
1286 | D. https://example.com/outer-pushed
1287 | ┗ https://example.com/inner-end
1288 | ```
1289 |
1290 | The navigation API history entry list (which also matches the existing spec's frame-specific "session history") for the outer frame looks like:
1291 |
1292 | ```
1293 | O1. https://example.com/start (associated to A)
1294 | O2. https://example.com/outer (associated to B)
1295 | O3. https://example.com/outer-pushed (associated to C and D)
1296 | ```
1297 |
1298 | The navigation API history entry list for the inner frame looks like:
1299 |
1300 | ```
1301 | I1. https://example.com/inner-start (associated to B and C)
1302 | I2. https://example.com/inner-end (associated to D)
1303 | ```
1304 |
1305 | Traversal operates on the joint session history, which means that it's possible to impact other frames. Continuing with our previous setup, and assuming the current entry in the joint session history is D, then:
1306 |
1307 | - If code in the outer frame calls `navigation.back()`, this will take us back to O2, and thus take the joint session history back to B. This means the inner frame will be navigated from `/inner-end` to `/inner-start`, changing its current navigation API `NavigationHistoryEntry` from I2 to I1.
1308 |
1309 | - If code in the inner frame calls `navigation.back()`, this will take us back to I1, and take the joint session history back to C. (This does not impact the outer frame.) The rule here for choosing C, instead of B, is that it moves the joint session history the fewest number of steps necessary to make I1 active.
1310 |
1311 | - If code in either the inner frame or the outer frame calls `history.back()`, this will take the joint session history back to C, and thus update the inner frame's current navigation API `NavigationHistoryEntry` from I2 to I1. (There is no impact on the outer frame.)
1312 |
1313 | ### Integration with navigation
1314 |
1315 | To understand when navigation interception interacts with the existing navigation spec, see [the navigation types appendix](#appendix-types-of-navigations). In cases where interception is allowed and takes place, it is essentially equivalent to preventing the normal navigation and instead synchronously performing the [URL and history update steps](https://html.spec.whatwg.org/#url-and-history-update-steps).
1316 |
1317 | The way in which navigation interacts with session history entries generally is not meant to change; the correspondence of a session history entry to a `NavigationHistoryEntry` does not introduce anything novel there.
1318 |
1319 | ## Impact on the back button and user agent UI
1320 |
1321 | The navigation API doesn't change anything about how user agents implement their UI: it's really about developer-facing affordances. Users still care about the joint session history, and so that will continue to be presented in UI surfaces like holding down the back button. Similarly, pressing the back button will continue to navigate through the joint session history, potentially across origins and out of the current navigation API history list (into a new navigation API history list, on the new origin). The design discussed in [the previous section](#correspondence-with-the-joint-session-history) ensures that the navigation API cannot get the browser into a strange novel state that has not previously been seen in the joint session history.
1322 |
1323 | One consequence of this is that when iframes are involved, the back button may navigate through the joint session history, without changing the current navigation API `NavigationHistoryEntry`. This is because, for the most part, the behavior of the back button is the same as that of `history.back()`, which as the previous section showed, only impacts one frame (and thus one navigation API history entry list) at a time.
1324 |
1325 | Finally, note that user agents can continue to refine their mapping of UI to joint session history to give a better experience. For example, in some cases user agents today have the back button skip joint session history entries which were created without user interaction. We expect this heuristic would continue to be applied for same-document entries generated by intercepting the `navigate` event, just like it is for today's `history.pushState()`.
1326 |
1327 | ## Security and privacy considerations
1328 |
1329 | Privacy-wise, this feature is neutral, due to its strict same-origin contiguous entry scoping. That is, it only exposes information which the application already has access to, just in a more convenient form. The storage of navigation API state in the `NavigationHistoryEntry`s is a convenience with no new privacy concerns, since that state is only accessible same-origin; that is, it provides the same power as something like `sessionStorage` or `history.state`.
1330 |
1331 | One particular point of interest is the user-agent generated `historyEntry.key` and `historyEntry.id` fields, which are a user-agent-generated random UUIDs. Here again the strict same-origin contiguous entry scoping prevents them from being used for cross-site tracking or similar. Specifically:
1332 |
1333 | - These UUIDs lives only for the duration of that navigation API history entry, which is at most for the lifetime of the browsing session. For example, opening a new tab (or iframe) to the same URL will generate different `key` and `id` values. So it is not a stable user-specific identifier.
1334 |
1335 | - This information is not accessible across sites, as a given navigation API history entry is specific to a frame and origin. That is, cross-site pages will always have different `key` and `id` values for all `NavigationHistoryEntry`s they can examine; there is no way to use history entry keys and IDs to correlate users.
1336 |
1337 | (Collaborating cross-origin same-site pages can inspect each other's `NavigationHistoryEntry`s using `document.domain`, but they can also inspect every other aspect of each others' global objects.)
1338 |
1339 | Security-wise, this feature has been carefully designed to give no new abilities that might be disruptive to the user or to delicate parts of browser code. See, for example, the restrictions on [navigation monitoring and interception](#navigation-monitoring-and-interception) to ensure that it does not allow trapping the user, or the discussion of how this proposal [does not impact how browser UI presents session history](#impact-on-the-back-button-and-user-agent-ui).
1340 |
1341 | In particular, note that navigation interception can only update the URL bar to perform single-page app navigations to the same extent as `history.pushState()` does: the destination URL must only differ from the page's current URL in path, query, or fragment components. Thus, the `navigate` event does not allow URL spoofing by updating the URL bar to a cross-origin destination while providing your own origin's content.
1342 |
1343 | See also the [W3C TAG security and privacy questionnaire answers](./security-privacy-questionnaire.md). We also have a [corresponding specification section](https://wicg.github.io/navigation-api/#security-privacy), which largely restates the points here but with links to specification concepts instead of explainer sections.
1344 |
1345 | ## Future extensions
1346 |
1347 | ### More per-entry events
1348 |
1349 | We've heard some use cases for additional events on `NavigationHistoryEntry` objects, in addition to the [`dispose` event](#notifications-on-entry-disposal). Currently we're thinking of adding `navigateto` and `navigatefrom` events.
1350 |
1351 | We expect these would mostly be used by decentralized parts of the application's codebase, such as components, to synchronize their state with the history list. Unlike the `navigate` event, these events are not cancelable. They are used only for reacting to changes, not intercepting or preventing navigations.
1352 |
1353 | For example, consider a photo gallery application. One way of implementing this would be to store metadata about the photo in the corresponding `NavigationHistoryEntry`'s state. This might look something like this:
1354 |
1355 | ```js
1356 | async function showPhoto(photoId) {
1357 | // In our app, the `navigate` handler will take care of actually showing the photo and updating the content area.
1358 | const entry = await navigation.navigate(`/photos/${photoId}`, { state: {
1359 | dateTaken: null,
1360 | caption: null
1361 | } }).committed;
1362 |
1363 | // When we navigate away from this photo, save any changes the user made.
1364 | entry.addEventListener("navigatefrom", e => {
1365 | navigation.updateCurrentEntry({
1366 | state: {
1367 | dateTaken: document.querySelector("#photo-container > .date-taken").value,
1368 | caption: document.querySelector("#photo-container > .caption").value
1369 | }
1370 | });
1371 | });
1372 |
1373 | // If we ever navigate back to this photo, e.g. using the browser back button or
1374 | // navigation.traverseTo(), restore the input values.
1375 | entry.addEventListener("navigateto", e => {
1376 | const { dateTaken, caption } = entry.getState();
1377 | document.querySelector("#photo-container > .date-taken").value = dateTaken;
1378 | document.querySelector("#photo-container > .caption").value = caption;
1379 | });
1380 | }
1381 | ```
1382 |
1383 | Note how we use the fulfillment value of the `committed` promise to get a handle to the entry. This is more robust than assuming `navigation.currentEntry` is correct, in edge cases where one navigation can interrupt another.
1384 |
1385 | ### Performance timeline API integration
1386 |
1387 | The [performance timeline API](https://w3c.github.io/performance-timeline/) provides a generic framework for the browser to signal about interesting events, their durations, and their associated data via `PerformanceEntry` objects. For example, cross-document navigations are done with the [navigation timing API](https://w3c.github.io/navigation-timing/), which uses a subclass of `PerformanceEntry` called `PerformanceNavigationTiming`.
1388 |
1389 | It is not currently possible to measure such data for same-document navigations. This is somewhat understandable, as such navigations have always been "zero duration": they occur instantaneously when the application calls `history.pushState()` or `history.replaceState()`. So measuring them isn't that interesting. But with the new navigation API, [browsers know about the start time, end time, and duration of the navigation](#measuring-standardized-single-page-navigations), so we can give useful performance entries.
1390 |
1391 | We propose adding new `PerformanceEntry` instances for such same-document navigations. They would be instances of a new subclass, `SameDocumentNavigationEntry`, with the following properties:
1392 |
1393 | - `name`: the URL being navigated to. (The use of `name` instead of `url` is strange, but matches all the other `PerformanceEntry`s on the platform.)
1394 |
1395 | - `entryType`: always `"same-document-navigation"`.
1396 |
1397 | - `startTime`: the time at which the navigation was initiated, i.e. when the corresponding API was called (like `location.href` or `navigation.navigate()`), or when the user activated the corresponding `` element, or submitted the corresponding `