├── .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 `
` being submitted. The `sourceElement` will also be null when a targeting a different `window` (e.g., ``). 330 | 331 | - `signal`: an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) which can be monitored for when the navigation gets aborted. 332 | 333 | 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 history entry (i.e. is a back/forward navigation) via `event.navigationType`. 334 | 335 | The event object has a special method `event.intercept(options)`. 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: 336 | 337 | - Cancel any fragment navigation or cross-document navigation. 338 | - Immediately update the URL bar, `location.href`, and `navigation.currentEntry` unless the `event.intercept()` was called with a `commit` option of `"after-transition"`. 339 | - Create the [`navigation.transition`](#transitional-time-after-navigation-interception) object. 340 | - If `options.handler` is given, it can be a function that returns a promise. That function will be then be called, and the browser will wait for the returned promise to settle. Once it does, the browser will: 341 | - If the promise rejects, fire `navigateerror` on `navigation` and reject `navigation.transition.finished`. 342 | - If the promise fulfills, fire `navigatesuccess` on `navigation` and fulfill `navigation.transition.finished`. 343 | - Set `navigation.transition` to null. 344 | - For the duration of any such promise settling, any browser loading UI such as a spinner will behave as if it were doing a cross-document navigation. 345 | 346 | Note that the browser does not wait for any returned promises to settle in order to update its URL/history-displaying UI (such as URL bar or back button), or to update `location.href` and `navigation.currentEntry`, unless a `commit` option of `"after-transition"` is provided to `event.intercept()`. [See below](#deferred-commit) for more details. 347 | 348 | If `intercept()` is called multiple times (e.g., by multiple different listeners to the `navigate` event), then all of the promises returned by any handlers will be combined together using the equivalent of `Promise.all()`, so that the navigation only counts as a success once they have all fulfilled, or the navigation counts as an error at the point where any of them reject. 349 | 350 | _In [#66](https://github.com/WICG/navigation-api/issues/66), we are discussing adding the capability to delay URL/current entry updates to not happen immediately, as a future extension._ 351 | 352 | #### Example: replacing navigations with single-page app navigations 353 | 354 | The following is the kind of code you might see in an application or framework's router: 355 | 356 | ```js 357 | navigation.addEventListener("navigate", e => { 358 | // Some navigations, e.g. cross-origin navigations, we cannot intercept. Let the browser handle those normally. 359 | if (!e.canIntercept) { 360 | return; 361 | } 362 | 363 | // Don't intercept fragment navigations or downloads. 364 | if (e.hashChange || e.downloadRequest !== null) { 365 | return; 366 | } 367 | 368 | e.intercept({ 369 | handler() { 370 | if (e.formData) { 371 | processFormDataAndUpdateUI(e.formData, e.sourceElement, e.signal); 372 | } else { 373 | doSinglePageAppNav(e.destination, e.signal); 374 | } 375 | } 376 | }); 377 | }); 378 | ``` 379 | 380 | Here, `doSinglePageAppNav` and `processFormDataAndUpdateUI` are functions that can return a promise. For example: 381 | 382 | ```js 383 | async function doSinglePageAppNav(destination, signal) { 384 | const htmlFromTheServer = await (await fetch(destination.url, { signal })).text(); 385 | document.querySelector("main").innerHTML = htmlFromTheServer; 386 | } 387 | ``` 388 | 389 | Note how this example responds to various types of navigations: 390 | 391 | - Cross-origin navigations: let the browser handle it as usual. 392 | - Same-document fragment navigations: let the browser handle it as usual. 393 | - Same-document URL or state updates (via `history.pushState()` or `history.replaceState()`): 394 | 1. Send the information about the URL/state update to `doSinglePageAppNav()`, which will use it to modify the current document. 395 | 1. After that UI update is done, potentially asynchronously, notify the app and the browser about the navigation's success or failure. 396 | - Cross-document normal navigations (including those via `navigation.navigate()`): 397 | 1. Prevent the browser handling, which would unload the document and create a new one from the network. Instead, immediately change the URL bar/`location.href`/`navigation.currentEntry`, while staying on the same document. 398 | 1. Send the information about the navigation to `doSinglePageAppNav()`, which will use it to modify the current document. 399 | 1. After that UI update is done, potentially asynchronously, notify the app and the browser about the navigation's success or failure. 400 | - Cross-document form submissions: 401 | 1. Prevent the browser handling, which would unload the document and create a new one from the network. Instead, immediately change the URL bar/`location.href`/`navigation.currentEntry`, while staying on the same document. 402 | 1. Send the form data to `processFormDataAndUpdateUI()`, which will use it to modify the current document. 403 | 1. After that UI update is done, potentially asynchronously, notify the app and the browser about the navigation's success or failure. 404 | 405 | Notice also how by passing through the `AbortSignal` found in `e.signal`, we ensure that any aborted navigations abort the associated fetch as well. 406 | 407 | #### Example: async transitions with special back/forward handling 408 | 409 | 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: 410 | 411 | ```js 412 | navigation.addEventListener("navigate", e => { 413 | // As before. 414 | if (!e.canIntercept || e.hashChange || e.downloadRequest !== null) { 415 | return; 416 | } 417 | 418 | e.intercept({ async handler() { 419 | if (myFramework.currentPage) { 420 | await myFramework.currentPage.transitionOut(); 421 | } 422 | 423 | let { key } = e.destination; 424 | 425 | if (e.navigationType === "traverse" && myFramework.previousPages.has(key)) { 426 | await myFramework.previousPages.get(key).transitionIn(); 427 | } else { 428 | // This will probably result in myFramework storing the rendered page in myFramework.previousPages. 429 | await myFramework.renderPage(e.destination); 430 | } 431 | } }); 432 | }); 433 | ``` 434 | 435 | #### Example: progressively enhancing form submissions 436 | 437 | A common pattern for multi-page web apps is [post/redirect/get](https://en.wikipedia.org/wiki/Post/Redirect/Get), which handles POST form submissions by performing a server-side redirect to a page retrieved with GET. This avoids a POST-derived page from ever entering the session history, since this can lead to confusing "Do you want to resubmit the form?" popups and [interop problems](https://github.com/whatwg/html/issues/6600). 438 | 439 | The navigation API's `navigate` event allows emulating this pattern on the client side using code such as the following: 440 | 441 | ```js 442 | navigation.addEventListener("navigate", e => { 443 | const url = new URL(e.destination.url); 444 | 445 | switch (url.pathname) { 446 | case "/form-submit": { 447 | e.intercept({ async handler() { 448 | // Do not navigate to form-submit; instead send the data to that endpoint using fetch(). 449 | await fetch("/form-submit", { body: e.formData }); 450 | 451 | // Perform a client-side "redirect" to /destination. 452 | await navigation.navigate("/destination", { history: "replace" }).finished; 453 | } }); 454 | break; 455 | } 456 | case "/destination": { 457 | e.intercept({ async handler() { 458 | document.body.innerHTML = "Form submission complete!"; 459 | } }); 460 | break; 461 | } 462 | } 463 | }); 464 | ``` 465 | 466 | Note how doing a replace navigation to `/destination` overrides the in-progress navigation to `/form-submit`, so that like in the server-driven approach, the session history ends up going straight from the original page to `/destination`, with no entry for `/form-submit`. (This example uses the `navigation.navigate()` API introduced [further down](#the-navigate-and-reload-methods), but you could also do `location.replace("/destination")` for much the same effect.) 467 | 468 | What's cool about this example is that, if the browser does not support the new navigation API, then the server-driven post/redirect/get flow will still go through, i.e. the user will see a full-page navigation that leaves them at `/destination`. So this is purely a progressive enhancement. 469 | 470 | See [this interactive demo](https://selective-heliotrope-dumpling.glitch.me/) to check out the technique in action, in browsers with or without the new navigation API implemented. 471 | 472 | #### Restrictions on firing, canceling, and responding 473 | 474 | 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. 475 | 476 | First, the following navigations **will not fire `navigate`** at all: 477 | 478 | - User-initiated [cross-document](#appendix-types-of-navigations) navigations via non-back/forward browser UI, such as the URL bar, bookmarks, or the reload button 479 | - [Cross-document](#appendix-types-of-navigations) navigations initiated from other cross origin windows, e.g. via `window.open(url, nameOfYourWindow)`, or clicking on `` 480 | - [`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 481 | 482 | 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. (We do fire a `navigate` event for browser-UI back/forward buttons; see more discussion below.) 483 | 484 | Similarly, cross-document navigations initiated from other windows are not something that can be intercepted today, and for security reasons, we don't want to introduce the ability for your origin to mess with the operation of another origin's scripts. (Even if the purpose of those scripts is to navigate your frame.) 485 | 486 | As for `document.open()`, it is a terrible legacy API with lots of strange side effects, which makes supporting it not worth the implementation cost. Modern sites which use the new navigation API should never be using `document.open()`. 487 | 488 | Second, **traversals have special restrictions on canceling the navigation** via `event.preventDefault()`. Traversals are: 489 | - User-initiated traversals via the browser's back/forward buttons (either same- or cross-document) 490 | - Programmatic traversals via `history.back()`/`history.forward()`/`history.go()` 491 | - Programmatic traversals via `navigation.back()`/`navigation.forward()`/`navigation.traverseTo()` 492 | 493 | Traversals may only be canceled (and `event.cancelable` will be equal to true) if: 494 | - The navigate event is firing in the top window 495 | - The traversal is same-document 496 | - The traversal was not user-initiated, or there is a consumable activation in the current window. 497 | 498 | Allowing cancelation only in the top window is to ensure that there is a single authoritative source for deciding whether or not to cancel the traversal. If all windows were allowed to cancel and a traversal navigated multiple windows, and some canceled but others proceeded, there would not be a good way to keep all windows in sync with the joint session history. Cross-document traversals are uncancelable because of performance concerns. If a cross-document traversal were cancelable, it would need to block before any network requests are sent in order to fire `navigate`. This is already necessary for `beforeunload`, but `beforeunload` is a single-purpose event and if no `beforeunload` handler is present, the blocking step can be skipped. `navigate`, on the other hand, is intended to be used for many purposes, and it is wasteful to impose a performance penalty on all cross-document traversals simply because `navigate` was being used for something unrelated to canceling cross-document navigations. Finally, user activation is required for user-initiated traversals in order to minimize the possibility of trapping: `event.preventDefault()` on a traversal consumes the user activation, ensuring that the user can always break out of an application that is canceling traversals by, e.g., pressing the browser's back button twice in a row. 499 | 500 | By _consumable activation_, we mean a variant of [user activation](https://html.spec.whatwg.org/multipage/interaction.html#tracking-user-activation) that we wish to add to the HTML spec. _Sticky activation_ is obviously not correct for preventing trapping the user, because then a single errant click or tap could disable back/forward navigations entirely. However, we are also concerned about using _transient activation_: it meets our requirement that the user activation can be used once before it is consumed, but the possibility of it expiring due to its _transient activation duration_ elapsing means that web applications may suddenly and unexpectedly get an uncancelable traversal if a back or forward button is pressed and the user happens not to have interacted with the page for a modest period of time. We therefore intend to add a third mode of user activation to the HTML spec, _consumable activation_, which can be consumed like _transient activation_, but does not expire based on the _transient activation duration_. 501 | 502 | Because canceling is limited to same-document traversals, no special timing or handler is required for firing `navigate`; the standard fragment navigation timing just works. If the performance concerns around canceling cross-document traversals were to be resolved at some point in the future, special consideration would need to be given to firing `navigate` at the correct time in the traversal process (presumably at the same time that `beforeunload` is fired). 503 | 504 | Finally, the following navigations **cannot be replaced with same-document navigations** by using `event.intercept()`, and as such will have `event.canIntercept` equal to false: 505 | 506 | - 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.) 507 | - 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. 508 | 509 | We'll note that these restrictions 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 `` `click` events, or modify their code that would set `location.href`, even if the destination URL is cross-origin. 510 | 511 | #### Measuring standardized single-page navigations 512 | 513 | Continuing with the theme of `intercept()` giving ecosystem benefits beyond just web developer convenience, telling the browser about the start time, duration, end time, and success/failure if a single-page app navigation has benefits for metrics gathering. 514 | 515 | In particular, analytics frameworks would be able to consume this information from the browser in a way that works across all applications using the navigation API. See the discussion on [performance timeline API integration](#performance-timeline-api-integration) for what we are proposing there. 516 | 517 | This standardized notion of single-page navigations also gives a hook for other useful metrics to build off of. For example, you could imagine variants of the `"first-paint"` and `"first-contentful-paint"` APIs which are collected after the `navigate` event is fired. Or, you could imagine vendor-specific or application-specific measurements like [Cumulative Layout Shift](https://web.dev/cls/) or React hydration time being reset after such navigations begin. 518 | 519 | This isn't a complete panacea: in particular, such metrics are gameable by bad actors. Such bad actors could try to drive down average measured "load time" by generating excessive `navigate` events that don't actually do anything. So in scenarios where the web application is less interested in measuring itself, and more interested in driving down specific metrics, those creating the metrics will need to take into account such misuse of the API. Some potential countermeasures against such gaming could include: 520 | 521 | - Only using the start time of the navigation in creating such metrics, and not using the promise-settling time. This avoids gaming via code such as `event.intercept(/* no handler */); await doActualNavigation();` which makes the navigation appear instant to the browser. 522 | 523 | - Filtering to only count navigations where `event.userInitiated` is true. 524 | 525 | - Filtering to only count navigations where the URL changes (i.e., `navigation.currentEntry.url !== event.destination.url`). 526 | 527 | - We hope that most analytics vendors will come to automatically track `navigate` events as page views, and measure their duration. Then, apps using such analytics vendors would have an incentive to keep their page view statistics meaningful, and thus be disincentivized to generate spurious navigations. 528 | 529 | #### Aborted navigations 530 | 531 | As shown in [the example above](#example-replacing-navigations-with-single-page-app-navigations), the `navigate` event comes with an `event.signal` property that is an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal). This signal will transition to the aborted state if any of the following occur before any promises returned by handlers passed to `intercept()` settle: 532 | 533 | - The user presses their browser's stop button (or similar UI, such as the Esc key). 534 | - Another navigation is started, either by the user or programmatically. This includes back/forward navigations, e.g. the user pressing their browser's back button. 535 | 536 | The signal will not transition to the aborted state if `intercept()` is not called. This means it cannot be used to observe the interruption of a [cross-document](#appendix-types-of-navigations) navigation, if that cross-document navigation was left alone and not converted into a same-document navigation by using `intercept()`. 537 | 538 | Whether and how the application responds to this abort is up to the web developer. In many cases, such as in [the example above](#example-replacing-navigations-with-single-page-app-navigations), this will automatically work: by passing the `event.signal` through to any `AbortSignal`-consuming APIs like `fetch()`, those APIs will get aborted, and the resulting `"AbortError"` `DOMException` propagated from the handler passed to `intercept()`. But it's possible to ignore it completely, as in the following example: 539 | 540 | ```js 541 | navigation.addEventListener("navigate", event => { 542 | event.intercept({ async handler(){ 543 | await new Promise(r => setTimeout(r, 10_000)); 544 | document.body.innerHTML = `Navigated to ${event.destination.url}`; 545 | } }); 546 | }); 547 | ``` 548 | 549 | In this case: 550 | 551 | - The user pressing the stop button will have no effect, and after ten seconds `document.body` will get updated anyway with the destination URL of the original navigation. 552 | - Navigation to another URL will not prevent the fact that in ten seconds `document.body.innerHTML` will be updated to show the original destination URL. 553 | 554 | ### Customizations and consequences of navigation interception 555 | 556 | #### Accessibility technology announcements 557 | 558 | With [cross-document](#appendix-types-of-navigations) navigations, accessibility technology will announce the start of the navigation, and its completion. But traditionally, same-document navigations (i.e. single-page app navigations) have not been communicated in the same way to accessibility technology. This is in part because it is not clear to the browser when a user interaction causes a single-page navigation, because of the app-specific JavaScript that intermediates between such interactions and the eventual call to `history.pushState()`/`history.replaceState()`. In particular, it's unclear exactly when the navigation begins and ends: trying to use the URL change as a signal doesn't work, since when applications call `history.pushState()` during the content loading process varies. 559 | 560 | Any navigation that is intercepted and converted into a single-page navigation using `navigateEvent.intercept()` will be communicated to accessibility technology in the same way as a cross-document one. Using `intercept()` serves as a opt-in to this new behavior, and the provided promise allows the browser to know how long the navigation takes, and whether or not it succeeds. 561 | 562 | #### Loading spinners and stop buttons 563 | 564 | It is a long-requested feature (see [whatwg/fetch#19](https://github.com/whatwg/fetch/issues/19) and [whatwg/html#330](https://github.com/whatwg/html/issues/330)) to give pages control over the browser's loading indicator, i.e. the one they show while cross-document navigations are ongoing. This proposal gives the browsers the tools to do this: they can display the loading indicator while any promises returned by handlers passed to `navigateEvent.intercept()` are settling. 565 | 566 | Additionally, in modern browsers, the reload button is replaced with a stop button while such loading is taking place. This can be done for navigation API-intercepted navigations as well, with the result communicated to the developer using `navigateEvent.signal`. 567 | 568 | You can see a [demo](https://gigantic-honored-octagon.glitch.me/) and [screencast](https://twitter.com/i/status/1471604621470846979) of this behavior in Chromium. 569 | 570 | _Note: specifications do not mandate browser UI, so this is not guaranteed behavior. But it's a nice feature that we hope browsers do end up implementing!_ 571 | 572 | #### Focus management 573 | 574 | Like [accessibility technology announcements](#accessibility-technology-announcements), focus management currently behaves differently between same-document navigations and cross-document navigations. As [this post discusses](https://www.gatsbyjs.com/blog/2019-07-11-user-testing-accessible-client-routing/): 575 | 576 | > ... a user’s keyboard focus point may be kept in the same place as where they clicked, which isn’t intuitive. In layouts where the page changes partially to include a deep-linked modal dialog or other view layer, a user’s focus point could be left in an entirely wrong spot on the page. 577 | 578 | The navigation API's navigation interception again gives us the tool to fix this. 579 | 580 | By default, any navigation that is intercepted and converted into a single-page navigation using `navigateEvent.intercept()` will cause focus to reset to the `` element, or to the first element with the `autofocus=""` attribute set (if there is one). This focus reset will happen after any promises returned by handlers passed to `intercept()` settle. However, this focus reset will not take place if the user or developer has manually changed focus while the promise was settling, and that element is still visible and focusable. 581 | 582 | This behavior can be customized using `intercept()`'s `focusReset` option: 583 | 584 | - `e.intercept({ handler, focusReset: "after-transition" })`: the default behavior, described above. 585 | - `e.intercept({ handler, focusReset: "manual" })`: does not reset the focus, and leaves it where it is. (Although, it might get [reset anyway](https://html.spec.whatwg.org/#focus-fixup-rule) if the element is removed from the DOM or similar.) The application will manually manage focus changes. 586 | 587 | In general, the default behavior is a best-effort attempt at cross-document navigation parity. But if developers invest some extra work, they can do even better: 588 | 589 | - Per the above-linked [research by Fable Tech Labs](https://www.gatsbyjs.com/blog/2019-07-11-user-testing-accessible-client-routing/), screen reader users generally prefer focus to be reset to a heading or wrapper element, instead of the `` element. So to get the optimal experience with navigation interception, developers should use `autofocus=""` appropriately on such elements. 590 | 591 | - For traversals (i.e. cases where `navigateEvent.navigationType === "traverse"`), getting parity with the [back/forward cache experience](https://web.dev/bfcache/) requires restoring focus to the same element that was previously focused when that history entry was active. Unfortunately, this isn't something the browser can do automatically for client-side rendered applications; the notion of "the same element" [is not generally stable](https://github.com/WICG/navigation-api/issues/190#:~:text=On%20the%20other%20hand%2C%20in%20the%20general%20case%20we%20won%27t%20be%20able%20to%20identify%20%22the%20same%20element%22!) in such cases. So for such cases, using `focusReset: "manual"`, storing some identifier for the currently-focused element in the navigation API state, and calling `element.focus()` appropriately upon transition could give a better experience, as in the following example: 592 | 593 | ```js 594 | navigation.addEventListener("navigate", e => { 595 | const focusedIdentifier = computeIdentifierFor(document.activeElement); 596 | navigation.updateCurrentEntry({ ...navigation.currentEntry.getState(), focusedIdentifier }); 597 | 598 | if (e.canIntercept) { 599 | const handler = figureOutHandler(e); 600 | const focusReset = e.navigationType === "traverse" ? "manual" : "after-transition"; 601 | e.intercept({ handler, focusReset }); 602 | } 603 | }); 604 | 605 | navigation.addEventListener("navigatesuccess", () => { 606 | if (navigation.transition.navigationType === "traverse") { 607 | const { focusedIdentifier } = navigation.currentEntry.getState(); 608 | const elementToFocus = findByIdentifier(focusedIdentifier); 609 | if (elementToFocus) { 610 | elementToFocus.focus(); 611 | } 612 | } 613 | }); 614 | ``` 615 | 616 | An additional API that would be helpful, both for cases like these and more generally, would be one for [setting and getting the sequential focus navigation start point](https://github.com/whatwg/html/issues/5326). Especially for the custom traversals case, this would give even higher-fidelity focus restoration. (But that proposal is separate from the new navigation API.) 617 | 618 | We can also extend the `focusReset` option with other behaviors in the future. Here are a couple which have been proposed, but we are not planning to include in the initial version unless we get strong developer feedback that they would be helpful: 619 | 620 | - `focusReset: "immediate"`: immediately resets the focus to the `` element, without waiting for the promise to settle. 621 | - `focusReset: "two-stage"`: immediately resets the focus to the `` element, and then has the same behavior as `"after-transition"`. 622 | 623 | #### Scrolling to fragments and scroll resetting 624 | 625 | _Note that the discussion in this section applies only to `"push"` and `"replace"` navigations. For the behavior for `"traverse"` and `"reload"` navigations, see the [next section](#scroll-position-restoration)._ 626 | 627 | When you change the URL with `history.pushState()`/`history.replaceState()`, the user's scroll position stays where it is. This is true even if you try to navigate to a fragment, e.g. by doing `history.pushState("/article#subheading")`. The latter has caused significant pain in client-side router libraries; see e.g. [remix-run/react-router#394](https://github.com/remix-run/react-router/issues/394), or the manual code that is needed to handle this case in [Vue](https://sourcegraph.com/github.com/vuejs/router/-/blob/src/scrollBehavior.ts?L81-140), [Angular](https://github.com/angular/angular/blob/main/packages/router/src/router_scroller.ts#L76-L77), [React Router Hash Link](https://github.com/rafgraph/react-router-hash-link/blob/main/src/HashLink.jsx), and others. 628 | 629 | With the navigation API, there is a different default behavior, controllable via another option to `navigateEvent.intercept()`: 630 | 631 | - `e.intercept({ handler, scroll: "after-transition" })`: the default behavior. After the promise returned by `handler` fulfills, the browser will attempt to scroll to the fragment given by `e.destination.url`, or if there is no fragment, it will reset the scroll position to the top of the page (like in a cross-document navigation). 632 | - `e.intercept({ handler, scroll: "manual" })`: the browser will not change the user's scroll position, although you can later perform the same logic manually using `e.scroll()`. 633 | 634 | The `navigateEvent.scroll()` method could be useful if you know you have loaded the element referenced by the hash, or if you know you want to reset the scroll position to the top of the document early, before the full transition has finished. For example: 635 | 636 | ```js 637 | const freshEntry = 638 | navigateEvent.navigationType === "push" || 639 | navigateEvent.navigationType === "replace"; 640 | 641 | navigateEvent.intercept({ 642 | scroll: freshEntry ? "manual" : "after-transition", 643 | async handler() { 644 | await fetchDataAndSetUpDOM(navigateEvent.url); 645 | 646 | if (freshEntry) { 647 | navigateEvent.scroll(); 648 | } 649 | 650 | // Note: navigateEvent.scroll() will update what :target points to. 651 | await fadeInTheScrolledToElement(document.querySelector(":target")); 652 | }, 653 | }); 654 | ``` 655 | 656 | If you want to only perform the scroll-to-a-fragment behavior, and not reset the scroll position to the top if there is no matching fragment, then you can use `"manual"` combined with only calling `navigateEvent.scroll()` when `(new URL(navigateEvent.destination.url)).hash` points to an element that exists. 657 | 658 | #### Scroll position restoration 659 | 660 | A common pain point for web developers is scroll restoration during `"traverse"` and `"reload"` navigations. The essential problem is that scroll restoration happens unpredictably, and often at the wrong times. For example: 661 | 662 | - The browser tries to restore the user's scroll position, but the application logic is still setting up the DOM and the relevant elements aren't ready yet. 663 | - The browser tries to restore the user's scroll position, but the page's contents have changed and scroll restoration doesn't work that well. (For example, going back to a listing of files in a shared folder, after a different user deleted a bunch of the files.) 664 | - The application needs to perform some measurements in order to do a proper transition, but the browser does scroll restoration during the transition, which messes up those measurements. ([Demo of this problem](https://nifty-blossom-meadow.glitch.me/legacy-history/transition.html): notice how when going back to the grid view, the transition sends the square to the wrong location.) 665 | 666 | The same `scroll` option to `navigateEvent.intercept()` that we described above for `"push"` and `"replace"` navigations, similarly controls scroll restoration for `"traverse"` and `"reload"` navigations. And similarly to that case, using `intercept()` opts you into a more sensible default behavior: 667 | 668 | - `e.intercept({ handler, scroll: "after-transition" })`: the default behavior. The browser delays its scroll restoration logic until `promise` fulfills; it will perform no scroll restoration if the promise rejects. If the user has scrolled during the transition then no scroll restoration will be performed ([like for multi-page navs](https://neat-equal-cent.glitch.me/)). 669 | - `e.intercept({ handler, scroll: "manual" })`: The browser will perform no automatic scroll restoration. However, the developer can use the `e.scroll()` API to get semi-automatic scroll restoration, or can use [`window.scrollTo()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollTo) or similar APIs to take full control. 670 | 671 | For `"traverse"` and `"reload"`, the `navigateEvent.scroll()` API performs the browser's scroll restoration logic at the specified time. This allows cases that require precise control over scroll restoration timing, such as a non-broken version of the [demo referenced above](https://nifty-blossom-meadow.glitch.me/legacy-history/transition.html), to be written like so: 672 | 673 | ```js 674 | if (navigateEvent.navigationType === "traverse" || navigateEvent.navigationType === "reload") { 675 | navigateEvent.intercept({ 676 | scroll: "manual", 677 | async handler() { 678 | await fetchDataAndSetUpDOM(); 679 | navigateEvent.scroll(); 680 | await measureLayoutAndDoTransition(); 681 | }, 682 | }); 683 | } 684 | ``` 685 | 686 | Some more details on how the navigation API handles scrolling with `"traverse"` and `"reload"` navigations: 687 | 688 | - `navigateEvent.scroll()` will silently do nothing if called after the user has started scrolling the document. 689 | 690 | - `navigateEvent.scroll()` doesn't actually perform a single update of the scroll position. Rather, it puts the page in scroll-position-restoring mode. The scroll position could update several times as more elements load and [scroll anchoring](https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-anchor/Guide_to_scroll_anchoring) kicks in. 691 | 692 | - By default, any navigations which are intercepted with `navigateEvent.intercept()` will _ignore_ the value of `history.scrollRestoration` from the classic history API. This allows developers to use `history.scrollRestoration` for controlling cross-document scroll restoration, while using the more-granular option to `intercept()` to control individual same-document navigations. 693 | 694 | #### Precommit handlers 695 | 696 | The default behavior of immediately "committing" (i.e., updating `location.href` and `navigation.currentEntry`) works well for most situations, but some developers may find they do not want to immediately update the URL, and may want to retain the option of aborting the navigation without needing to rollback a URL update or cancel-and-restart. This behavior can be customized passing a `precommitHandler` callback alongside or instead of the `handler` callback: 697 | 698 | - `e.intercept({ handler })`: the default behavior, immediately commit the navigation and update `location.href` and `navigation.currentEntry`. 699 | - `e.intercept({ precommitHandler })`: start the navigation (e.g., show a loading spinner if the UI has one), but do not immediately commit. 700 | 701 | The object passed to intercept can include both a `handler` and a `precommitHandler`. If both are included, they are called individually at the appropriate phase. 702 | 703 | When precommit handlers are used, the navigation will commit (and a `committed` promise will resolve if present) once all those handlers are fulfilled. 704 | 705 | If a `precommitHandler` passed to `intercept()` rejects, then the navigation will be treated as canceled (both `committed` and `finished` promises will reject, and no URL update will occur). 706 | 707 | Because precommit handlers can be used to cancel the navigation before the URL updates, they are only available when `e.cancelable` is true. See [above](#restrictions-on-firing-canceling-and-responding) for details on when `e.cancelable` is set to false, and thus precommit handlers are not available. Calling `intercept()` with a `precommitHandler` on a non-cancelable event would throw a `"SecurityError"` `DOMException`. 708 | 709 | #### Redirects during deferred commit 710 | 711 | The `precommitHandler` callback accepts an argument, which is a `controller` that can perform certain actions on the precommitted navigation, in particular redirecting. This updates the eventual destination, and potentially the state/info, of the `"push"` or `"replace"` navigation. 712 | An example usage is as follows: 713 | 714 | ```js 715 | navigation.addEventListener("navigate", e => { 716 | e.intercept({ 717 | async precommitHandler(controller) { 718 | if (await isLoginGuarded(e.destination)) { 719 | controller.redirect("/login", {state: "login-redirect", info: "some-info"}); 720 | } 721 | } 722 | 723 | async handler() { 724 | // apply committed navigation state to document 725 | } 726 | }); 727 | }); 728 | ``` 729 | 730 | This is simpler than the alternative of canceling the original navigation and starting a new one to the redirect location, because it avoids exposing the intermediate state. For example, only one `navigatesuccess` or `navigateerror` event fires, and if the navigation was triggered by a call to `navigation.navigate()`, the promise only fulfills once the redirect destination is reached. 731 | 732 | It's possible in the future we could contemplate allowing something similar for `{ commit: "immediate" }` navigations as well. There, we would not be able to hide the intermediate state perfectly, as code would still be able to observe the intermediate `location.href` values and such. But we could treat such post-commit redirects as special types of replace navigations, which "take over" any promises returned from `navigation.navigate()`, delay `navigatesuccess`/`navigateerror` events, etc. 733 | 734 | ### Transitional time after navigation interception 735 | 736 | As part of calling `event.intercept()` to [intercept a navigation](#navigation-monitoring-and-interception) and convert it into a single-page navigation, the handlers passed to `intercept()` can return promises that might not settle for a while. During this transitional time, before the promise settles and the `navigatesuccess` or `navigateerror` events fire, an additional API is available, `navigation.transition`. It has the following properties: 737 | 738 | - `navigationType`: either `"reload"`, `"push"`, `"replace"`, or `"traverse"` indicating what type of navigation this is 739 | - `from`: the `NavigationHistoryEntry` that was the current one before the transition 740 | - `finished`: a promise which fulfills with undefined when the `navigatesuccess` event fires on `navigation`, or rejects with the corresponding error when the `navigateerror` event fires on `navigation` 741 | 742 | #### Example: handling failed navigations 743 | 744 | To handle failed single-page navigations, i.e. navigations where a promise returned by a handler passed to `event.intercept()` eventually rejects, you can listen to the `navigateerror` event and perform application-specific interactions. This event will be an [`ErrorEvent`](https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent) so you can retrieve the promise's rejection reason. For example, to display an error, you could do something like: 745 | 746 | ```js 747 | navigation.addEventListener("navigateerror", e => { 748 | document.body.textContent = `Could not load ${location.href}: ${e.message}`; 749 | analyticsPackage.send("navigateerror", { stack: e.error.stack }); 750 | }); 751 | ``` 752 | 753 | This would give your users an experience most like a multi-page application, where server errors or broken links take them to a dedicated, server-generated error page. 754 | 755 | ### The `navigate()` and `reload()` methods 756 | 757 | In a single-page app using `window.history`, the typical flow is: 758 | 759 | 1. Application code triggers a router's navigation infrastructure, giving it a destination URL and possibly additional state or info. 760 | 1. The router updates the URL displayed to the user and visible with `location.href`, by using `history.pushState()` or `history.replaceState()`. 761 | 1. The router or its surrounding framework loads the data necessary to render the new URL, and does so. 762 | 763 | (Sometimes steps (2) and (3) are switched.) Note in particular the extra care an application needs to take to ensure that all navigations go through the router. This means that they can't easily use traditional APIs like `` or `location.href`. 764 | 765 | In a single-page app using the new navigation API, instead the router listens to the `navigate` event. This automatically takes care of step (2), and provides a centralized place for the router and framework to perform step (3). And now the application code can use traditional navigation mechanisms, like `` or `location.href`, without any extra code; the browser takes care of sending all of those to the `navigate` event! 766 | 767 | There's one gap remaining, which is the ability to send additional state or info along with a navigation. We solve this by introducing new APIs, `navigation.navigate()` and `navigation.reload()`, which can be thought of as an augmented and combined versions of `location.assign()`, `location.replace()`, and `location.reload()`. The non-replace usage of `navigation.navigate()` is as follows: 768 | 769 | ```js 770 | // Navigate to a new URL, resetting the state to undefined: 771 | // (equivalent to `location.assign(url)`) 772 | navigation.navigate(url); 773 | 774 | // Use a new URL and state. 775 | navigation.navigate(url, { state }); 776 | 777 | // You can also pass info for the navigate event handler to receive: 778 | navigation.navigate(url, { state, info }); 779 | ``` 780 | 781 | Note how unlike `history.pushState()`, `navigation.navigate()` will by default perform a full navigation, e.g. scrolling to a fragment or navigating across documents. Single-page apps will usually intercept these using the `navigate` event, and convert them into same-document navigations by using `event.intercept()`. 782 | 783 | 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.) 784 | 785 | `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: 786 | 787 | ```js 788 | // Performs a navigation to the given URL, but replace the current history entry 789 | // instead of pushing a new one. 790 | // (equivalent to `location.replace(url)`) 791 | navigation.navigate(url, { history: "replace" }); 792 | 793 | // Replace the URL and state at the same time. 794 | navigation.navigate(url, { history: "replace", state }); 795 | 796 | // You can still pass along info: 797 | navigation.navigate(url, { history: "replace", state, info }); 798 | ``` 799 | 800 | 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`. 801 | 802 | 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"`. 803 | 804 | 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: 805 | 806 | ```js 807 | // Just like location.reload(). 808 | navigation.reload(); 809 | 810 | // Leave the state as-is, but pass some info. 811 | navigation.reload({ info }); 812 | 813 | // Overwrite the state with a new value. 814 | navigation.reload({ state, info }); 815 | ``` 816 | 817 | Note that all of these methods return `{ committed, finished }` promise pairs, [as described above](#navigation-through-the-history-entry-list) for the traversal methods. That is, in the event that the navigations get converted into same-document navigations via `event.intercept()` in a `navigate` handler, `committed` will fulfill immediately, and `finished` will settle based on the promise returned by the handler (if there is one). This gives your navigation call site an indication of the navigation's success or failure. (If they are non-intercepted fragment navigations, or intercepted navigations with no handler, then `finished` will fulfill immediately. And if they are non-intercepted cross-document navigations, then the returned promises, along with the entire JavaScript global environment, will disappear as the current document gets unloaded.) 818 | 819 | #### Example: using `info` 820 | 821 | The `info` option to `navigation.navigate()` gets passed to the `navigate` event handler as the `event.info` property. The intended use of this value is to convey transient information about this particular navigation, such as how it happened. In this way, it's different from the persisted value retrievable using `event.destination.getState()`. 822 | 823 | One example of how this might be used is to trigger different single-page navigation renderings depending on how a certain route was reached. For example, consider a photo gallery app, where you can reach the same photo URL and state via various routes: 824 | 825 | - Clicking on it in a gallery view 826 | - Clicking "next" or "previous" when viewing another photo in the album 827 | - Etc. 828 | 829 | Each of these wants a different animation at navigate time. This information doesn't make sense to store in the persistent URL or history entry state, but it's still important to communicate from the rest of the application, into the router (i.e. `navigate` event handler). This could be done using code such as 830 | 831 | ```js 832 | document.addEventListener("keydown", e => { 833 | if (e.key === "ArrowLeft" && hasPreviousPhoto()) { 834 | navigation.navigate(getPreviousPhotoURL(), { info: { via: "go-left" } }); 835 | } 836 | if (e.key === "ArrowRight" && hasNextPhoto()) { 837 | navigation.navigate(getNextPhotoURL(), { info: { via: "go-right" } }); 838 | } 839 | }); 840 | 841 | photoGallery.addEventListener("click", e => { 842 | if (e.target.closest(".photo-thumbnail")) { 843 | navigation.navigate(getPhotoURL(e.target), { info: { via: "gallery", thumbnail: e.target } }); 844 | } 845 | }); 846 | 847 | navigation.addEventListener("navigate", e => { 848 | if (isPhotoNavigation(e)) { 849 | e.intercept({ async handler() { 850 | switch (e.info.?via) { 851 | case "go-left": { 852 | await animateLeft(); 853 | break; 854 | } 855 | case "go-right": { 856 | await animateRight(); 857 | break; 858 | } 859 | case "gallery": { 860 | await animateZoomFromThumbnail(e.info.thumbnail); 861 | break; 862 | } 863 | } 864 | 865 | // TODO: actually load the photo. 866 | } }); 867 | } 868 | }); 869 | ``` 870 | 871 | Note that in addition to `navigation.navigate()` and `navigation.reload()`, the previously-discussed `navigation.back()`, `navigation.forward()`, and `navigation.traverseTo()` methods can also take a `info` option. 872 | 873 | #### Example: next/previous buttons 874 | 875 | Consider trying to code next/previous buttons that perform single-page navigations, for example in a photo gallery application. This has some interesting properties: 876 | 877 | - If the user presses next five times quickly, you want to be sure to skip them ahead five photos, and not to waste work on the intermediate four. 878 | - If the user presses next five times and then previous twice, you want to load the third photo, not wasting work on any others. 879 | - If the user presses previous/next and the previous/next item in their history list is the previous/next photo, then you want to just navigate them through the history list, like their browser back and forward buttons. 880 | - You'll want to make sure any "permalink" or "share" UI is updated ASAP after such button presses, even if the photo is still loading. 881 | 882 | All of this basically "just works" with the `navigate` event and other parts of the new navigation API. A large part of this is because `navigate`-event created single-page navigations [synchronously update the current URL](#navigation-monitoring-and-interception), and [abort any ongoing navigations](#aborted-navigations). The code to handle it would look like the following: 883 | 884 | ```js 885 | const appState = { 886 | currentPhoto: 0, 887 | totalPhotos: 10 888 | }; 889 | const next = document.querySelector("button#next"); 890 | const previous = document.querySelector("button#previous"); 891 | const permalink = document.querySelector("span#permalink"); 892 | 893 | next.onclick = () => { 894 | const nextPhotoInHistory = photoNumberFromURL(navigation.entries()[navigation.currentEntry.index + 1]?.url); 895 | if (nextPhotoInHistory === appState.currentPhoto + 1) { 896 | navigation.forward(); 897 | } else { 898 | navigation.navigate(`/photos/${appState.currentPhoto + 1}`); 899 | } 900 | }; 901 | 902 | previous.onclick = () => { 903 | const prevPhotoInHistory = photoNumberFromURL(navigation.entries()[navigation.currentEntry.index - 1]?.url); 904 | if (nextPhotoInHistory === appState.currentPhoto - 1) { 905 | navigation.back(); 906 | } else { 907 | navigation.navigate(`/photos/${appState.currentPhoto - 1}`); 908 | } 909 | }; 910 | 911 | navigation.addEventListener("navigate", e => { 912 | const photoNumber = photoNumberFromURL(e.destination.url); 913 | 914 | if (photoNumber && e.canIntercept) { 915 | e.intercept({ async handler() { 916 | // Synchronously update app state and next/previous/permalink UI: 917 | appState.currentPhoto = photoNumber; 918 | previous.disabled = appState.currentPhoto === 0; 919 | next.disabled = appState.currentPhoto === appState.totalPhotos - 1; 920 | permalink.textContent = e.destination.url; 921 | 922 | // Asynchronously update the photo, passing along the signal so that 923 | // it all gets aborted if another navigation interrupts us: 924 | const blob = await (await fetch(`/raw-photos/${photoNumber}.jpg`, { signal: e.signal })).blob(); 925 | const url = URL.createObjectURL(blob); 926 | document.querySelector("#current-photo").src = url; 927 | } }); 928 | } 929 | }); 930 | 931 | function photoNumberFromURL(url) { 932 | if (!url) { 933 | return null; 934 | } 935 | 936 | const result = /\/photos/(\d+)/.exec((new URL(url)).pathname); 937 | if (result) { 938 | return Number(result[1]); 939 | } 940 | 941 | return null; 942 | } 943 | ``` 944 | 945 | Let's look at our scenarios again: 946 | 947 | - If the user presses next five times quickly, you want to be sure to skip them ahead five photos, and not to waste work on the intermediate four: this works as intended, as the first four fetches (for photos 1, 2, 3, 4) get aborted via their `e.signal`, while the last one (for photo 5) completes. 948 | - If the user presses next five times and then previous twice, you want to load the third photo, not wasting work on any others: this works as intended, as the first six fetches (for photos 1, 2, 3, 4, 5, 4 again) get aborted via their `e.signal`, while the last one (for photo 3 again) completes. 949 | - If the user presses previous/next and the previous/next item in their history is the previous/next photo, then you want to just navigate them through the history list, like their browser back and forward buttons: this works as intended, due to the if statements inside the `click` handlers for the next and previous buttons. 950 | - You'll want to make sure any "permalink" or "share" UI is updated ASAP after such button presses, even if the photo is still loading: this works as intended, since we can do this work synchronously before loading the photo. 951 | 952 | ### Setting the current entry's state without navigating 953 | 954 | We believe that in the majority of cases, single-page apps will be best served by updating their state via `navigation.navigate({ state: newState })`, which goes through the `navigate` event. That is, coupling state updates with navigations, which are handled by centralized router code. This is generally superior to the classic history API's model, where state (and URL) updates are done in a way disconnected from navigation, using `history.replaceState()`. 955 | 956 | However, there is one type of case where the navigation-centric model doesn't work well. This is when you need to update the current entry's state in response to an external event, often caused by user interaction. 957 | 958 | For example, consider a page with expandable/collapsable `
` elements. You want to store the expanded/collapsed state of these `
` elements in your navigation API state, so that when the user traverses back and forward through history, or restarts their browser, your app can read the restored navigation API state and expand the `
` elements appropriately, showing the user what they saw previously. 959 | 960 | Creating this experience with `navigation.navigate()` and the `navigate` event is awkward. You would need to listen for the `
` element's `toggle` event, and then do `navigation.reload({ state: newState })`. And then you would need to have your `navigate` handler do `e.intercept()`, _and not actually do anything_, because the `
` element is already open. This can be made to work, but is not pretty. 961 | 962 | For cases like this, where the current history entry's state needs to be updated to capture something that has already happened, we have `navigation.updateCurrentEntry({ state: newState })`. We would write our above example like so: 963 | 964 | ```js 965 | detailsEl.addEventListener("toggle", () => { 966 | navigation.updateCurrentEntry({ 967 | state: { ...navigation.currentEntry.getState(), detailsOpen: detailsEl.open } 968 | }); 969 | }); 970 | ``` 971 | 972 | Another example of this sort of situation is shown in the following section. 973 | 974 | ### Notifications on entry disposal 975 | 976 | Each `NavigationHistoryEntry` has a `dispose` event, which occurs when that history entry is permanently evicted and unreachable. The most common scenario where this occurs is when doing a push navigation that prunes the forward history, like so: 977 | 978 | ```js 979 | const startingKey = navigation.currentEntry.key; 980 | 981 | const entry1 = await navigation.navigate("/1").committed; 982 | entry1.addEventListener("dispose", () => console.log(1)); 983 | 984 | const entry2 = await navigation.navigate("/2").committed; 985 | entry2.addEventListener("dispose", () => console.log(2)); 986 | 987 | const entry3 = await navigation.navigate("/3").committed; 988 | entry3.addEventListener("dispose", () => console.log(3)); 989 | 990 | await navigation.traverseTo(startingKey).finished; 991 | await navigation.navigate("/1-b").finished; 992 | 993 | // Logs 1, 2, 3 as that branch of the tree gets pruned. 994 | ``` 995 | 996 | This event can be useful for cleaning up any information in secondary stores, such as `sessionStorage` or caches, when we're guaranteed to never reach those particular history entries again. 997 | 998 | ### Current entry change monitoring 999 | 1000 | 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. 1001 | 1002 | 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. 1003 | 1004 | This event is mainly used for code that want to watch for navigation commits, but are separated from the part of the codebase that actually perform the navigation (since the navigating code can just use `navigation.navigate().committed`). This is especially useful when migrating from `popstate` and `hashchange`, but it could also be used by e.g. analytics packages that don't care about the navigation finishing, just committing. It can also be used to set up relevant [per-entry events](#per-entry-events): 1005 | 1006 | ```js 1007 | navigation.addEventListener("currententrychange", () => { 1008 | navigation.currentEntry.addEventListener("dispose", genericDisposeHandler); 1009 | }); 1010 | ``` 1011 | 1012 | It is best _not_ to use this event as part of the main routing flow of the application, which updates the main content area in response to URL changes. For that, use the `navigate` event, which provides [interception and cancelation support](#navigation-monitoring-and-interception). 1013 | 1014 | The event comes with a property, `from`, which is the previous value of `navigation.currentEntry`. It also has a `navigationType` property, which is either `"reload"`, `"push"`, `"replace"`, `"traverse"`, or `null`. There are a few interesting cases to consider: 1015 | 1016 | - The entry can be the same as before, i.e. `navigation.currentEntry === event.from`. This happens when `navigation.updateCurrentEntry()` is called, or when `navigation.reload()` is converted into a same-document reload. Note that in this case the old state cannot be retrieved, i.e. `event.from.getState()` will return the current navigation API state. 1017 | 1018 | - `event.navigationType` will be `null` when `navigation.updateCurrentEntry()` is called, since that is not a navigation (despite updating `navigation.currentEntry`). 1019 | 1020 | - During traversals, i.e. when `event.navigationType` is `"traverse"`, you can get the delta by using `navigation.currentEntry.index - event.from.index`. (Note that this can return `NaN` in `"replace"` cases where `event.from.index` is `null`.) 1021 | 1022 | ### Complete event sequence 1023 | 1024 | Between the `dispose` events, the `window.navigation` events, and various promises, there's a lot of events floating around. Here's how they all come together: 1025 | 1026 | 1. `navigate` fires on `window.navigation`. 1027 | 1. If the event is canceled using `event.preventDefault()`, then: 1028 | 1. If the process was initiated by a call to a `navigation` API that returns a promise, then that promise gets rejected with an `"AbortError"` `DOMException`. 1029 | 1. Otherwise: 1030 | 1. `navigation.transition` is created. 1031 | 1. If `event.intercept()` was not called, or `event.intercept()` was called with no `commit` option, or `event.intercept()` was called with a `commit` option of `immediate`, run the commit steps (see below). 1032 | 1. Any loading spinner UI starts, if `event.intercept()` was called. 1033 | 1. When `event.commit()` is called, if `event.intercept()` was called with a `commit` option of `"after-transition"`, run the commit steps (see below). 1034 | 1. After all the promises returned by handlers passed to `event.intercept()` fulfill, or after one microtask if `event.intercept()` was not called: 1035 | 1. If the commit steps (see below) have not run yet, run them now. 1036 | 1. `navigatesuccess` is fired on `navigation`. 1037 | 1. Any loading spinner UI stops. 1038 | 1. If the process was initiated by a call to a `navigation` API that returns a promise, then that promise gets fulfilled. 1039 | 1. `navigation.transition.finished` fulfills with undefined. 1040 | 1. `navigation.transition` becomes null. 1041 | 1. Alternately, if any of these promises reject: 1042 | 1. `navigateerror` fires on `window.navigation` with the rejection reason as its `error` property. 1043 | 1. Any loading spinner UI stops. 1044 | 1. If the process was initiated by a call to a `navigation` API that returns a promise, then that promise gets rejected with the same rejection reason. 1045 | 1. `navigation.transition.finished` rejects with the same rejection reason. 1046 | 1. `navigation.transition` becomes null. 1047 | 1. Alternately, if the navigation gets [aborted](#aborted-navigations) before either of those two things occur: 1048 | 1. `navigateerror` fires on `window.navigation` with an `"AbortError"` `DOMException` as its `error` property. 1049 | 1. Any loading spinner UI stops. (But potentially restarts, or maybe doesn't stop at all, if the navigation was aborted due to a second navigation starting.) 1050 | 1. If the process was initiated by a call to a `navigation` API that returns a promise, then that promise gets rejected with the same `"AbortError"` `DOMException`. 1051 | 1. `navigation.transition.finished` rejects with the same `"AbortError"` `DOMException`. 1052 | 1. `navigation.transition` becomes null. 1053 | 1. One task after firing `currententrychange`, `hashchange` and/or `popstate` fire on `window`, if applicable. (Note: this can happen _before_ steps (ix)–(xi) if the promises take longer than a single task to settle.) 1054 | 1055 | The commit steps are: 1056 | 1057 | 1. `location.href` updates. 1058 | 1. `navigation.currentEntry` updates. 1059 | 1. `currententrychange` is fired on `navigation`. 1060 | 1. Any now-unreachable `NavigationHistoryEntry` instances fire `dispose`. 1061 | 1. The URL bar updates. 1062 | 1063 | ## Guide for migrating from the existing history API 1064 | 1065 | For web developers using the API, here's a guide to explain how you would replace usage of `window.history` with `window.navigation`. 1066 | 1067 | ### Performing navigations 1068 | 1069 | 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. 1070 | 1071 | 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`. 1072 | 1073 | 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. 1074 | 1075 | Also note that if the navigation doesn't have an effect, the `navigation` traversal methods will return rejected promises, unlike the `history` traversal methods which silently do nothing. You can detect this as follows: 1076 | 1077 | ```js 1078 | try { 1079 | await navigation.back().finished; 1080 | } catch (e) { 1081 | if (e.name === "InvalidStateError") { 1082 | console.log("We weren't able to go back, because there was nothing previous in the navigation API history entries list"); 1083 | } 1084 | } 1085 | ``` 1086 | 1087 | or you can avoid it using the `canGoBack` property: 1088 | 1089 | ```js 1090 | if (navigation.canGoBack) { 1091 | await navigation.back().finished; 1092 | } 1093 | ``` 1094 | 1095 | Note that unlike the `history` APIs, these `navigation` APIs will not go to another origin. For example, trying to call `navigation.back()` when the previous document in the joint session history is cross-origin will return a rejected promise, and trigger the `console.log()` call above. 1096 | 1097 | Instead of using `history.go(offset)`, use `navigation.traverseTo(key)` to navigate to a specific entry. As with `back()` and `forward()`, `navigation.traverseTo()` will ignore other frames, and will only control the navigation of your frame. If you specifically want to reproduce the pattern of navigating by an offset (not recommended), you can use code such as the following: 1098 | 1099 | ```js 1100 | const entry = navigation.entries()[navigation.currentEntry.index + offset]; 1101 | if (entry) { 1102 | await navigation.traverseTo(entry.key).finished; 1103 | } 1104 | ``` 1105 | 1106 | ### Warning: back/forward are not always opposites 1107 | 1108 | As a consequence of how the new navigation API is focused on manipulating the current frame, `navigation.back()` followed by `navigation.forward()` will not always take you back to the original situation, when all the different frames on a page are considered. This sometimes is the case with `history.back()` and `history.forward()` today, due to browser bugs. But for the navigation API, it's actually intentional and expected. 1109 | 1110 | This is because of the ambiguity where there can be multiple joint session history entries (representing the history state of the entire frame tree) for a given `NavigationHistoryEntry` (representing the history state of your particular frame). Consider the following example joint session history where an iframe is involved: 1111 | 1112 | ``` 1113 | A. https://example.com/outer#1 1114 | ┗ https://example.com/inner-1 1115 | B. https://example.com/outer#2 1116 | ┗ https://example.com/inner-1 1117 | C. https://example.com/outer#2 1118 | ┗ https://example.com/inner-2 1119 | D. https://example.com/outer#2 1120 | ┗ https://example.com/inner-3 1121 | E. https://example.com/outer#2 1122 | ┗ https://example.com/inner-4 1123 | ``` 1124 | 1125 | Let's say the user is looking at joint session history entry C. If code in the outer frame calls `navigation.back()`, this is a request to navigate backward to the nearest joint session history entry where the outer frame differs, i.e. to navigate to A. Then, if the outer frame in state A calls `navigation.forward()`, this is a request to navigate forward to the nearest joint session history entry where the outer frame differs, i.e. to navigate to B. So starting at C, a back/forward sequence took us to B. 1126 | 1127 | For similar reasons, if you started at state C, a forward/back sequence in the outer frame would fail (since there is no joint session history entry past C that differs for the outer frame). But a back/forward sequence would succeed, and take you to B per the above. 1128 | 1129 | Although this behavior can be a bit counterintuitive, we think it's worth the tradeoff of having `navigation.back()` and `navigation.forward()` predictably navigate your own frame, instead of sometimes only navigating some subframe (like `history.back()` and `history.forward()` can do). 1130 | 1131 | In the longer term, we think the best fix for this would be to introduce [a mode for iframes where they don't mess with the joint session history at all](https://github.com/whatwg/html/issues/6501). If a page used that on all its iframes, then it would never have to worry about such strange behavior. 1132 | 1133 | ### Using `navigate` handlers 1134 | 1135 | Many cases which use `history.pushState()` today can just be deleted when using `navigation`. This is because if you have a listener for the `navigate` event on `navigation`, that listener can use `event.intercept()` to transform navigations that would normally be new-document navigations into same-document navigations. So for example, instead of 1136 | 1137 | ```html 1138 | About us 1139 |