├── .gitignore ├── HISTORY_STATE_STORAGE.md ├── LICENSE ├── README.md ├── app_history.md ├── app_history_spq.md ├── history_and_modals.md ├── history_and_modals_spq.md └── navigator.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /HISTORY_STATE_STORAGE.md: -------------------------------------------------------------------------------- 1 | # Improved `history.state` 2 | 3 | One of the places applications can store arbitrary data is in the `history.state` Object which can be provided when calling `history.pushState` and `history.replaceState`. The Object provided can be anything that can be serialized. The Object can be retrieved by calling `history.state`, and is preserved even after page reloads or navigations. 4 | 5 | Use cases for `history.state`: 6 | 1. Many applications use the `history.state` Object to store UI data that should not be part of the public URL of the page (i.e., not sharable) but that should be avialable to a page when a user agent navigates back to a history entry or reloads the page. Applications use this like a session storage that is linked to the navigation model. This data is useful for restoring UI. 7 | 2. Some router libraries use `history.state` to add unique ids to history entries so that the application can better track its navigational model. In extreme cases, router libraries have attempted to serialize the entire history stack into `history.state` to provide better a navigational model to the application. This can be useful to have a better sense of what navigational path the user agent took to reach the current UI, which could inform UI display (e.g. showing a back button only if there is a previous entry) or navigational control (e.g. navigating backwards to a specific UI state) 8 | 9 | The existing `history.state` has some drawbacks: 10 | 1. Clicks on fragment-changing links and calls to `location.replace` and `location.assign` overwrite the history.state Object with `null`. This means that the history state Object is either overwritten if the URL is replacing the existing URL, or it is lost to future history entries. This makes state restoration difficult or impossible when this occurs. 11 | 2. JavaScript calls to `history.replaceState`, `history.pushState` need to copy and extend the existing state Object. This means that the application needs to agree on a top-level structure for the history state. Most applications use an Object literal with keys to represent namespaces. 12 | 13 | # Proposed API 14 | 15 | A simple approach to improve this situation would be to create a secondary state API that provides a session-storage like API for updating the current history.state Object. 16 | 17 | This API would be fairly straightforward: 18 | 19 | ``` 20 | // Akin to a call to history.replaceState({foo: serializedUiState}, ''); 21 | history.stateStorage.setItem('foo', serializedUiState); 22 | // Returns serializedUiState 23 | history.stateStorage.getItem('foo'); 24 | 25 | // Setting other items does not overwrite existing items 26 | history.stateStorage.setItem('bar', otherSerializedUiState); 27 | // Returns serializedUiState 28 | history.stateStorage.getItem('foo'); 29 | 30 | // When navigating back, the state storage for foo is no longer available. 31 | history.back(); 32 | // Returns undefined 33 | history.stateStorage.getItem('foo'); 34 | 35 | // When navigating forward, the state storage for foo is available again. 36 | history.forward(); 37 | // Returns serializedUiState 38 | history.stateStorage.getItem('foo'); 39 | 40 | // When pushing a new URL, the browser copies the current value of 'foo' automatically. 41 | history.pushState({}, '', newUrl); 42 | // Returns serializedUiState 43 | history.stateStorage.getItem('foo'); 44 | 45 | // Entries can be updated as well. 46 | history.stateStorage.setItem('foo', newSerializedUiState); 47 | // Returns newSerializedUiState 48 | history.stateStorage.getItem('foo'); 49 | // But the values in previous entries is not updated. 50 | history.back(); 51 | // Returns serializedUiState 52 | history.stateStorage.getItem('foo'); 53 | 54 | // The only thing that's a little tricky is that updating a previous entry 55 | // won't change future entries, so applications need to remember that. 56 | history.stateStorage.setItem('foo', differentSerializedUiState); 57 | history.forward(); 58 | // Returns newSerializedUiState, not differentSerializedUiState 59 | history.stateStorage.getItem('foo'); 60 | 61 | // Entries can be removed. 62 | history.stateStorage.removeItem('foo'); 63 | // Returns undefined 64 | history.stateStorage.getItem('foo'); 65 | 66 | // And to fully conform to the Storage interface, the whole storage can be cleared. 67 | // But this is probably rarely desirable. 68 | history.stateStorage.clear(); 69 | ``` 70 | 71 | The JS API for this `StateStorage` is identical to the existing [`Storage`](https://developer.mozilla.org/en-US/docs/Web/API/Storage) spec, the extra implicit API is in how the storage gets copied whenever the browser creates a new history entry, in response to calls to `pushState` or link clicks. 72 | 73 | ## Minor implementation note 74 | In order to be performant, browsers should likely use [copy-on-write](https://en.wikipedia.org/wiki/Copy-on-write) semantics for storing the actual items in the storage. Otherwise, it could be very expensive to create identical copies of large serialized items. 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Application History State 2 | 3 | ## Authors: 4 | 5 | - Alex Russell 6 | - Tom Wilkinson 7 | 8 | ## Participate 9 | - [Issue tracker](https://github.com/slightlyoff/history_api/issues) 10 | - [Discussion forum](https://github.com/slightlyoff/history_api/issues) 11 | 12 | ## Table of Contents 13 | 14 | 15 | 16 | 17 | 18 | - [Introduction](#introduction) 19 | - [Goals](#goals) 20 | - [Non-goals](#non-goals) 21 | - [Navigation Events](#navigation-events) 22 | - [UI State Fragments](#ui-state-fragments) 23 | - [Same-Origin History Stack Introspection](#same-origin-history-stack-introspection) 24 | - [Considered alternatives](#considered-alternatives) 25 | - [A Global History Manager](#a-global-history-manager) 26 | - [Service Worker Navigation Events](#service-worker-navigation-events) 27 | - [Built-in Client-Side Routing](#built-in-client-side-routing) 28 | - [Open Design Questions](#open-design-questions) 29 | - [References & acknowledgments](#references--acknowledgments) 30 | 31 | 32 | 33 | ## Introduction 34 | 35 | Application Developers face a range of challenges when performing client-side updates to dynamic web apps including (but not limited to): 36 | 37 | - Lack of clarity on when to rely on base URL, query parameters, or hash parameters to represent "persistent" state 38 | - Difficulty in serializing & sharing "UI state" separate from "application state" 39 | - Complexity in capturing browser-initiated page unloading for [client-side routers](https://blog.risingstack.com/writing-a-javascript-framework-client-side-routing/) 40 | - Tricky coordination problems between multiple page components which may each want to persist transient UI state but remain largely unaware of each other 41 | - Difficulty in understanding one's location in, and predicting effects of changes to, the [HTML5 History API stack](https://developer.mozilla.org/en-US/docs/Web/API/History_API) due to potentially co-mingled origins 42 | 43 | Taken together, these challenges create a "totalizing" effect in applications when client-side state management is introduced. Because a single router system must be responsible for so many aspects, and coordinate so many low-level details, it's challenging to create compatible solutions, or constrain code footprint whilst retaining valuable properties such as lightweight progressive enhancement. 44 | 45 | Existing building blocks create subtle, but large, problems. 46 | 47 | The History [`pushState`](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState) and [`replaceState`](https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState) APIs provide a mechanism for passing a [cloneable](https://html.spec.whatwg.org/multipage/structured-data.html#structuredserializeinternal) JavaScript state object, which are returned to a page on change in state (usually via user action), however it is left as an exercise to the developer to map potentially different levels of application semantics into this API. It usually "feels wrong" to encode the state of an accordion component's open/close state in _either_ the [path](https://url.spec.whatwg.org/#dom-url-pathname) or [query parameters](https://url.spec.whatwg.org/#dom-url-searchparams), both of which are passed to servers and may have semantic meaning beyond UI state. 48 | 49 | URL [hashes](https://url.spec.whatwg.org/#dom-url-hash) are a reasonable next place to look for a solution as they are shareable and not meaningful to server applications by default, but [HTML already assigns meaning](https://html.spec.whatwg.org/multipage/browsing-the-web.html#scroll-to-fragid) to [hashes for anchor elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#Examples), creating potential conflicts. Fragments have also become a [potential XSS vector](https://medium.com/iocscan/dom-based-cross-site-scripting-dom-xss-3396453364fd), in part, because no safe parsing is provided by default. The [`hashchange`](https://developer.mozilla.org/en-US/docs/Web/API/Window/hashchange_event) event can allow components to be notified of state changes, but doesn't provide any semantic for location in stack history or meaningfully integrate into the [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API). 50 | 51 | This leaves application authors torn. Shareable UI state either requires out-of-band mechanisms for persistence, or overloading of URLs, unwanted server-side implications, or potential loss of state when folding information into History API state objects. And that is to say nothing of navigating the APIs themselves and their [various warts](https://github.com/whatwg/html/issues/2174). 52 | 53 | Lastly, it should be noted that these problems largely arise only when developers have _already_ exerted enough to control to prevent "normal" link navigations via ``. Naturally-constructed applications will want to progressively enhance anchors, but the current system prevents this, forcing developers to be vigilant to add very specific local event handlers -- or forgo anchor elements, potentially harming accessibility and adding complexity. 54 | 55 | ## Goals 56 | 57 | We hope to enable application developers to: 58 | 59 | - Easily express transient, shareable UI state separately from semantic navigation state 60 | - Remove complexity and brittleness from responding to requests for state change via links _without_ full page reloads 61 | - Reduce security concerns regarding UI state serialized to URLs 62 | - Reduce conflicts with existing libraries for incremental adoption 63 | 64 | ## Non-goals 65 | 66 | These proposals do not seek to: 67 | 68 | - Add a full client-side router to the platform (although it should make these systems easier) 69 | - Solve all known issues with history introspection and `history.state` durability 70 | 71 | 72 | ## Navigation Events 73 | 74 | Currently, client-side libraries need to override the browser's default navigation affordances by [globally hooking nominally unrelated events, e.g. `onclick`](https://github.com/vaadin/vaadin-router/blob/master/src/triggers/click.js#L107). This is [brittle](https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/modules/Link.js#L43), as programmatic navigation (`window.location = ...`) can be missed, and may require explicit [decoration of elements to be handled within a system](https://angular.io/guide/router#add-the-router-outlet), complicating progressive enhancement. 75 | 76 | To reduce this pain, we propose the cancelable `onbeforenavigate` event. 77 | 78 | ```js 79 | // Fired *before* window.onbeforeunload 80 | 81 | // TODO: should this be on `window.location`` instead? 82 | // It's not currently an EventTarget 83 | history.onbeforenavigate = (e) => { 84 | // Cancel navigation; only available for same-origin navigations 85 | e.preventDefault(); 86 | console.log(e.url); // the destination URL; TODO: precedent? 87 | }; 88 | ``` 89 | 90 | The `onbeforenavigate` event is cancellable for all navigations that would affect _the current document context_. It's an open question as to whether or not it should be cancellable for links with the form `...`. 91 | 92 | `onbeforenavigate` also composes with form submission; for example, consider this form: 93 | 94 | ```html 95 |
96 | 97 | 98 | 101 | 102 |
103 | ``` 104 | 105 | Submission can be detected (in addition to the non-standard [bubbling behavior](https://source.chromium.org/chromium/chromium/src/+/master:third_party/blink/web_tests/fast/events/onsubmit-bubbling.html)) like this: 106 | 107 | ```js 108 | history.onbeforenavigate = (e) => { 109 | // Cancel navigation; only available for same-origin navigations 110 | e.preventDefault(); 111 | 112 | // Check to see if the source of the navigation is form submission: 113 | if (e.target.tagName == "FORM") { 114 | let form = e.target; 115 | 116 | // As we're handling it here, make sure other event handlers don't get a 117 | // crack at the event: 118 | e.stopImmediatePropagation(); 119 | 120 | // Submit the form via Ajax instead: 121 | fetch(form.action, { 122 | method: form.method, 123 | body: new FormData(form) 124 | }).then( 125 | // Update UI with response 126 | ); 127 | // Set loading UI here 128 | } 129 | }; 130 | ``` 131 | 132 | ## UI State Fragments 133 | 134 | UI State Fragments build on the [Fragment Directive](https://wicg.github.io/scroll-to-text-fragment/#the-fragment-directive) syntax `:~:`, 135 | with the first recently-launched fragment directive being [Text Fragments](https://web.dev/text-fragments/) `:~:text`. 136 | For those not familiar, Text Fragments enable browsers to highlight portions of a page using a syntax like: 137 | 138 | ``` 139 | https://example.com/#:~:text=prefix-,startText,endText,-suffix 140 | ``` 141 | 142 | This syntax was [designed to be extensible](https://github.com/WICG/scroll-to-text-fragment#multiple-text-directives) via the `&` joiner, e.g.: 143 | 144 | ``` 145 | https://example.com/#:~:text=sometext&anotherdirective=value&etc... 146 | ``` 147 | 148 | We build on this to encode a single (page-level) serialised object in a (name open for bikeshedding) UI State directive `:~:uistate`, e.g.: 149 | 150 | ``` 151 | https://example.com/#:~:uistate= 152 | ``` 153 | 154 | The value itself is the result of [URI component encoding](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) a [JSON serialization](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) of the subset of Structure Cloneable properties that can be naturally represented in JSON. In code, that makes these lines roughly equivalent: 155 | 156 | ```js 157 | let state = { "foo": true, "bar": [ 1, "ten" ] }; 158 | window.location.hash = `#:~:uistate=${ 159 | encodeURIComponent(JSON.stringify(state)) 160 | }`; 161 | 162 | ``` 163 | 164 | Is long-hand for: 165 | 166 | ```js 167 | history.uistate = { "foo": true, "bar": [ 1, "ten" ] }; 168 | ``` 169 | 170 | Both result in a URL like: 171 | 172 | ``` 173 | https://example.com/#:~:uistate=%7B"foo"%3Atrue%2C"bar"%3A%5B1%2C"ten"%5D%7D 174 | ``` 175 | 176 | Updates to `uistate` _do not persist to the history stack_. 177 | 178 | Pages can use `uistate` to re-construct visual context by consulting the (browser parsed) `history.uistate` property at any point; e.g.: 179 | 180 | ```js 181 | myAppJs.setInitialUIState(history.uistate); 182 | ``` 183 | 184 | ## Same-Origin History Stack Introspection 185 | 186 | The history stack has a long list of documented issues, chief among them mixing between first and third party contexts, which renders the [`go()`](https://developer.mozilla.org/en-US/docs/Web/API/History/go) and [`back()`](https://developer.mozilla.org/en-US/docs/Web/API/History/back) methods nearly pointless. 187 | 188 | We propose extensions to the history stack to provide visibility into same-origin stack state and extensions to existing methods to iterate through them. The following snippet captures the early proposal: 189 | 190 | ```js 191 | // Returns an iterator of HistoryStateEntries (a new type) 192 | let initialState = history.current; // wraps `.state`; name TBD 193 | let currentState = history.pushSatate({ foo: 1 }, "", ""); // return value 194 | let sameOriginStack = history.states(); // TODO: async? array? 195 | console.log(history.length); // perhaps 5 196 | console.log(sameOriginStack.length); // can be less 197 | let item = null; 198 | for (item of sameOriginStack) { 199 | console.log(currentState === item); // `true` for first iteration 200 | console.log(item.url); 201 | console.log(item.state); // the state in history.state 202 | } 203 | history.go(item); // new parameter type overload 204 | ``` 205 | 206 | > Lots of "spelling" issues to be worked out here 207 | 208 | ## Considered alternatives 209 | 210 | ### A Global History Manager 211 | 212 | We have previously considered (in private docs) a more fullsome rewrite of the history APIs to make it clearer that a single "installable" history manager should own potential navigation decisions. This might turn into the correct alternative as we explore, however this document seeks to find smaller potential changes in the short run. 213 | 214 | ### Service Worker Navigation Events 215 | 216 | It's possible control the responses for a document's content inside a Service Worker, and there are [proposals](https://github.com/WICG/sw-launch) for handling some subset of navigation disposition (new window? new tab? re-use?) from within that context. These aren't particularly satisfyign from both a performance and programmability perspecitve. Service Workers may be shut down and would need to be restarted to handle decisions about if/how to navigate. They also lack data to most DOM data, including form information, making it complex to offload enough state to them to make decisions. It also isn't clear that it's good layering to invoke them here. 217 | 218 | As a final reason not to go this route, Service Workers install asynchronously and may not be available in time to catch some navigations. The proposed deisgn does not have this problem. 219 | 220 | ### Built-in Client-Side Routing 221 | 222 | A new type of link could explicitly invoke client side routing, or we could imagine a more maximal route-pre-definition system inside the document to tell the system to "skip" full navigations to specific routes. This somewhat-more-declaraitve approach could give the UA a larger role in helping users decide what they want an app to do at the limit, however we don't have concrete need for this at the moment and such a system could be layered on later without much concern. 223 | 224 | ## Open Design Questions 225 | 226 | - naming for `history.uistate` and `onbeforenavigate` is very much TBD 227 | - positional param changes to `pushState` and `replaceState`? 228 | - we don't _think_ the new history stack introspection API adds more fingerprinting attacks, but need to validate 229 | 230 | ## References & acknowledgments 231 | 232 | Many thanks for valuable feedback and advice from: 233 | 234 | - Dima Voytenko 235 | - David Bokan 236 | - Jake Archibald 237 | 238 | Prior work in this area includes: 239 | 240 | - Many, many client-side routing packages (TODO: enumerate) 241 | - the [HTML5 History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) and associated [`hashchange` event](https://developer.mozilla.org/en-US/docs/Web/API/Window/hashchange_event) 242 | -------------------------------------------------------------------------------- /app_history.md: -------------------------------------------------------------------------------- 1 | Moved to [WICG/app-history](https://github.com/WICG/app-history/blob/main/README.md)! 2 | -------------------------------------------------------------------------------- /app_history_spq.md: -------------------------------------------------------------------------------- 1 | Moved to [WICG/app-history](https://github.com/WICG/app-history/blob/main/security-privacy-questionnaire.md)! 2 | -------------------------------------------------------------------------------- /history_and_modals.md: -------------------------------------------------------------------------------- 1 | Moved to [domenic/close-watcher](https://github.com/domenic/close-watcher/blob/main/README.md)! 2 | -------------------------------------------------------------------------------- /history_and_modals_spq.md: -------------------------------------------------------------------------------- 1 | Moved to [domenic/close-watcher](https://github.com/domenic/close-watcher/blob/main/security-privacy-questionnaire.md)! 2 | -------------------------------------------------------------------------------- /navigator.md: -------------------------------------------------------------------------------- 1 | # Goals 2 | 3 | This proposal aims to: 4 | 5 | - Collect information about history into one place. 6 | - Provide a unique identifier for history frames. 7 | - Allow inspection of the history stack. 8 | - Provide global listeners of history changes. 9 | - Provide local listenenrs of changes to a specific history frame. 10 | - Encourage developers to only update the part of the URL or state Object that they need. 11 | - Provide a new API for navigating to a specific past history frame, rather than relying on `go()`. 12 | - Provide Promise return values for all history mutating APIs. 13 | 14 | This proposal creates a new API surface, `window.history.navigator`. 15 | 16 | # Proposal and justifying use cases 17 | 18 | ## Creation of a `Frame` Object with a unique `key` identifier 19 | 20 | This proposes creating a new `Frame` Object to collect information about a single history frame in the history stack. 21 | 22 | ``` 23 | class Frame { 24 | // Unique identifier for this Frame. 25 | readonly key: string; 26 | // URL of this Frame. 27 | readonly url: URL; 28 | // Params passed into the original Frame. 29 | readonly params: {string: Any}; 30 | } 31 | ``` 32 | 33 | This feature supports the following use cases: 34 | 35 | - Collects all information about the history into a single place. 36 | - Provides a URL Object, rather than a string, for the URL. 37 | - Provides reading the specific `params` Object for the `Frame`, so that users can read the data they previously wrote to history. 38 | - Provides a unique key. This provides a primary key for applications to store information that is related to this history frame, such as additional state that should be used for restoration. Applications have used the URL in the past as a primary key, but it is not guaranteed to be unique. 39 | 40 | ## History frame events 41 | 42 | This proposes adding an additional method to `Frame`: 43 | 44 | ``` 45 | // focus - the frame navigated in. 46 | // blur - the frame navigated out 47 | // beforeRemove - the frame is about to be navigated out. Enables rejecting the removal of the frame under certain circumstances. 48 | // evicted - the frame is no longer navigable, due to being in a branch of history that was evicted or became unreachable. 49 | addListener('focus'|'blur'|'beforeRemove'|'evicted') {} 50 | ``` 51 | 52 | This features supports the following use cases: 53 | 54 | - Evicting information stored in secondary stores (sessionStorage, e.g.) when a history frame is permanently evicted (no longer in stack). 55 | - Enables updating UI in response to specific frames entering/removing the view. 56 | - Enables preventing a specific frame from being navigated away from. 57 | 58 | ## Inspection of the history stack 59 | 60 | This proposes adding two new methods to `window.history.navigator`: 61 | 62 | ``` 63 | // Returns the current Frame. 64 | getFrame(): Frame {} 65 | 66 | // Returns all Frames. 67 | getStack(): {frames: []Frame {}, index: number} 68 | ``` 69 | 70 | This feature supports the following use cases: 71 | 72 | - Creating (or hiding) a UI element that could allow users to choose which state they want to navigate back to. 73 | - No longer need to store information from previous history frames globally, because it would be accessible at any time. Today applications duplicate the history state Object entirely in memory to prevent it from being removed. Even if the history state Object went out of scope, an application could look in the back stack to find it. 74 | - No longer need to track history externally via popstate or other events, since applications could see for themself what is in their forward and back stack. 75 | - Could expose a view of the history stack that does NOT include iframe history frames, which could become invisible to the hosting application. 76 | 77 | ## Global history change listener 78 | 79 | This proposes adding a new method to `window.history.navigator`: 80 | 81 | ``` 82 | // frameChange - a new frame was pushed or replaced. 83 | // navigation - the history stack changed, but no new frame was added or removed. Equivalent to popstate, but with a better name. 84 | addListener('frameChange'|'navigation') {} 85 | ``` 86 | 87 | This feature supports the following use cases: 88 | 89 | - Enable listening for new frames being pushed or replaced (today popstate only fires on navigations) 90 | - Provides a navigation event that can be used to provide more information (user-initiated, back-forward) as well as provide user controls for preventing undesired navigations. 91 | 92 | ## Creating new history frames 93 | 94 | This proposes adding a new method to `window.history.navigator`. 95 | 96 | ``` 97 | // Takes as its only argument a callback that updates the input url params. 98 | // The return value is an Object with optional properties. 99 | // url: the url parameter possibly modified or undefined, meaning to preserve the existing url. 100 | // params: the params parameter possibly modified or undefined, meaning to preserve the existing params. 101 | // replace: true/false/undefined, if true replaces the current Frame instead of pushing a new one. 102 | createFrame(({url: URL, params: {string: Any}}): 103 | {url: URL|undefined, params: {string: Any}|undefined, replace: boolean|undefined} => {}): Promise {} 104 | ``` 105 | 106 | This feature supports the following use cases: 107 | 108 | - Supports pushState/replaceState-like behavior today but with a better API. 109 | - Returns a Promise that callers can listen to when the Frame has been changed. 110 | - Having a callback encourages users to mutate the url and params and return them. 111 | - Users no longer have to care about arguments they don't use. If they don't want to update the URL, they don't list it in their callback or return. If a value is not returned, the existing value is used (url or params). 112 | 113 | ## Navigate to specific `Frame` 114 | 115 | This proposes creating a method on `window.history.navigator`: 116 | 117 | ``` 118 | // Navigate to the Frame, if it is in the stack. 119 | navigateTo(key: string): Promise {} 120 | ``` 121 | 122 | This feature supports the following use cases: 123 | 124 | - Navigating by key rather than direction enables users to not care about other entries in the history stack. 125 | - Returning a Promise enables users to wait for their URL change to be reflected. 126 | 127 | ## Promises for all history navigations 128 | 129 | This proposes creating new methods on `window.history.navigator`: 130 | 131 | ``` 132 | // Navigates back one Frame. 133 | back(): Promise {} 134 | 135 | // Navigates forward one Frame. 136 | forward(): Promise {} 137 | ``` 138 | 139 | This features supports the following use cases: 140 | 141 | - back/forward navigations are asynchronous (URL changes not synchronously updated) but do not return Promises. This enables the application to wait for the history navigation to finish. 142 | - Allows the application to get the current Frame in the resolved value after its finished. 143 | 144 | # API summary 145 | 146 | ``` 147 | class Frame { 148 | // Unique identifier for this Frame. 149 | readonly key: string; 150 | // URL of this Frame. 151 | readonly url: URL; 152 | // Params passed into the original Frame. 153 | readonly params: {string: Any}|undefined; 154 | 155 | // focus - the frame navigated in. 156 | // blur - the frame navigated out 157 | // beforeRemove - the frame is about to be navigated out. Enables rejecting the removal of the frame under certain circumstances. 158 | // unreachable - the frame is no longer navigable, due to being in a branch of history that was evicted or became unreachable. 159 | addListener('focus'|'blur'|'beforeRemove'|'unreachable') {} 160 | } 161 | 162 | class Navigator { 163 | // Takes as its only argument a callback that updates the input url params. 164 | // The return value is an Object with optional properties. 165 | // url: the url parameter possibly modified or undefined, meaning to preserve the existing url. 166 | // params: the params parameter possibly modified or undefined, meaning to preserve the existing params. 167 | // replace: true/false/undefined, if true replaces the current Frame instead of pushing a new one. 168 | // The function rejects if the return value is not the exact same for both url and params. 169 | update(({url: URL, params: {string: Any}}): 170 | {url: URL|undefined, params: {string: Any}|undefined, replace: boolean|undefined} => {}): Promise {} 171 | 172 | // Navigate to the Frame. 173 | navigateTo(key: string): Promise {} 174 | 175 | // Returns the current Frame. 176 | getFrame(): Frame {} 177 | 178 | // Returns all Frames. 179 | getStack(): {frames: []Frame {}, index: number} 180 | 181 | // Navigates back one Frame. 182 | back(): Promise {} 183 | 184 | // Navigates forward one Frame. 185 | forward(): Promise {} 186 | 187 | // frameChange - a new frame was pushed or replaced. 188 | // navigation - the history stack changed, but no new frame was added or removed. equivalent to popstate, but with a better name. 189 | addListener('frameChange'|'navigation') {} 190 | } 191 | ``` 192 | --------------------------------------------------------------------------------