You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Part of #94. As discussed there, this uses Promise.all()-like semantics, so that the navigatesuccess/navigateerror event and navigate() promise resolution are delayed on the aggregate of all promises passed to respondWith().
This does mean that navigate() no longer fulfills with the same value as the promise passed to respondWith() fulfills with, since we allow multiple such promises and it's not clear which value to choose now. But that was always a bit of a strange way of smuggling information around.
This also slightly changes the interaction with event cancelation (i.e., preventDefault()). Previously, calling respondWith() would cancel the event (observable using, e.g., defaultPrevented). Thus, canceling the event after calling respondWith() was a no-op. Now, they are independent operations: calling respondWith() does not cancel the event, and if you cancel the event after calling respondWith(), this will immediately cancel the navigation.
This also contains a bugfix where previously calling appHistory.navigate(url, { state: newState }) and then removing the relevant iframe from the DOM during a navigate event handler would still attempt to set the new state, even though doing so canceled the navigation. Now all paths to "synchronously finalize with an aborted navigation error" (formerly "signal an aborted navigation") clear the pending app history state change.
Copy file name to clipboardExpand all lines: README.md
+3-1Lines changed: 3 additions & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -259,7 +259,7 @@ Unlike the existing history API's `history.go()` method, which navigates by offs
259
259
260
260
All of these methods return promises, 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:
261
261
262
-
- The `navigate` event responds to the navigation using `event.respondWith()`, in which case the promise fulfills or rejects according to the promise passed to `respondWith()`. (However, even if the promise rejects, `location.href` and `appHistory.current` will change.)
262
+
- The `navigate` event responds to the navigation using `event.respondWith()`, in which case the promise fulfills or rejects according to the promise(s) passed to `respondWith()`. (However, even if the promise rejects, `location.href` and `appHistory.current` will change.)
263
263
264
264
- The `navigate` event cancels the navigation without responding to it, in which case the promise rejects with an `"AbortError"``DOMException`, and `location.href` and `appHistory.current` stay on their original value.
265
265
@@ -327,6 +327,8 @@ The event object has a special method `event.respondWith(promise)`. This works o
327
327
328
328
Note that the browser does not wait for the promise to settle in order to update its URL/history-displaying UI (such as URL bar or back button), or to update `location.href` and `appHistory.current`.
329
329
330
+
If `respondWith()` is called multiple times (e.g., by multiple different listeners to the `navigate` event), then all of the given promises 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.
331
+
330
332
_TODO: is it OK for web developers that the URL bar updates immediately? See [#66](https://github.com/WICG/app-history/issues/66)._
331
333
332
334
#### Example: replacing navigations with single-page app navigations
Copy file name to clipboardExpand all lines: spec.bs
+25-25Lines changed: 25 additions & 25 deletions
Original file line number
Diff line number
Diff line change
@@ -540,7 +540,7 @@ An {{AppHistoryNavigateEvent}} has the following associated values which are onl
540
540
541
541
One of these is set appropriately when the event is [[#navigate-event-firing|fired]].
542
542
543
-
An {{AppHistoryNavigateEvent}} also has an associated {{Promise}}-or-null <dfn for="AppHistoryNavigateEvent">navigation action promise</dfn>, initially null.
543
+
An {{AppHistoryNavigateEvent}} also has an associated <dfn for="AppHistoryNavigateEvent">navigation action promises list</dfn>, which is a [=list=] of {{Promise}} objects, initially empty.
544
544
545
545
<div algorithm>
546
546
The <dfn method for="AppHistoryNavigateEvent">respondWith(|newNavigationAction|)</dfn> method steps are:
@@ -550,8 +550,7 @@ An {{AppHistoryNavigateEvent}} also has an associated {{Promise}}-or-null <dfn f
550
550
1. If [=this=]'s {{AppHistoryNavigateEvent/canRespond}} attribute was initialized to false, then throw a "{{SecurityError}}" {{DOMException}}.
551
551
1. If [=this=]'s [=Event/dispatch flag=] is unset, then throw an "{{InvalidStateError}}" {{DOMException}}.
552
552
1. If [=this=]'s [=Event/canceled flag=] is set, then throw an "{{InvalidStateError}}" {{DOMException}}.
553
-
1. Set [=this=]'s [=Event/canceled flag=].
554
-
1. Set [=this=]'s [=AppHistoryNavigateEvent/navigation action promise=] to |newNavigationAction|.
553
+
1. [=list/Append=] |newNavigationAction| to [=this=]'s [=AppHistoryNavigateEvent/navigation action promises list=].
@@ -647,45 +646,47 @@ The <dfn attribute for="AppHistoryDestination">sameDocument</dfn> getter steps a
647
646
1. If |destinationURL| is [=rewritable=] relative to |currentURL|, and either |isSameDocument| is true or |navigationType| is not "{{AppHistoryNavigationType/traverse}}", then initialize |event|'s {{AppHistoryNavigateEvent/canRespond}} to true. Otherwise, initialize it to false.
648
647
1. If either |userInvolvement| is not "<code>[=user navigation involvement/browser UI=]</code>" or |navigationType| is not "{{AppHistoryNavigationType/traverse}}", then initialize |event|'s {{Event/cancelable}} to true.
649
648
1. If |userInvolvement| is "<code>[=user navigation involvement/none=]</code>", then initialize |event|'s {{AppHistoryNavigateEvent/userInitiated}} to false. Otherwise, initialize it to true.
650
-
1. If |formDataEntryList| is not null, then initialize |event|'s {{AppHistoryNavigateEvent/formData}} to a [=new=]{{FormData}} created in |realm|, associated to |formDataEntryList|. Otherwise, initialize it to null.
649
+
1. If |formDataEntryList| is not null, then initialize |event|'s {{AppHistoryNavigateEvent/formData}} to a [=new=] {{FormData}} created in |appHistory|'s [=relevant Realm=], associated to |formDataEntryList|. Otherwise, initialize it to null.
651
650
1. [=Assert=]: |appHistory|'s [=AppHistory/ongoing navigate event=] is null.
652
651
1. Set |appHistory|'s [=AppHistory/ongoing navigate event=] to |event|.
653
652
1. Let |result| be the result of [=dispatching=] |event| at |appHistory|.
654
653
1. Set |appHistory|'s [=AppHistory/ongoing navigate event=] to null.
655
-
1. If |appHistory|'s [=relevant global object=]'s [=active Document=] is not [=Document/fully active=], then return false.
656
-
1. [=Signal an aborted navigation=] for |appHistory|.
654
+
1. If |appHistory|'s [=relevant global object=]'s [=active Document=] is not [=Document/fully active=], then:
655
+
1. [=Synchronously finalize with an aborted navigation error=] for |appHistory|.
657
656
1. Return false.
658
657
659
658
<p class="note">This can occur if an event listener disconnected the <{iframe}> corresponding to [=this=]'s [=relevant global object=].</p>
660
-
1. If |event|'s [=AppHistoryNavigateEvent/navigation action promise=] is non-null, then:
659
+
1. If |event|'s [=AppHistoryNavigateEvent/navigation action promises list=] is not empty, then:
661
660
1. If |navigationType| is "{{AppHistoryNavigationType/traverse}}":
662
661
1. <a spec="HTML">Traverse the history</a> of |event|'s [=relevant global object=]'s [=Window/browsing context=] to [=AppHistoryNavigateEvent/destination entry=].
663
662
1. Otherwise:
664
663
1. Let |isPush| be true if |navigationType| is "{{AppHistoryNavigationType/push}}"; otherwise, false.
665
664
1. Run the <a spec="HTML">URL and history update steps</a> given |event|'s [=relevant global object=]'s [=associated document=] and |event|'s {{AppHistoryNavigateEvent/destination}}'s [=AppHistoryDestination/URL=], with <i>[=URL and history update steps/serializedData=]</i> set to |event|'s [=AppHistoryNavigateEvent/classic history API serialized data=] and <i>[=URL and history update steps/isPush=]</i> set to |isPush|.
666
-
1. If |event|'s [=AppHistoryNavigateEvent/navigation action promise=] is non-null, or both |result| and |isSameDocument| are true, then:
667
-
1. If |event|'s [=AppHistoryNavigateEvent/navigation action promise=] is null, then set it to [=a promise resolved with=] undefined, created in |realm|.
668
-
1. Let |navigateMethodCallPromise| be |appHistory|'s [=AppHistory/navigate method call promise=].
669
-
1. [=promise/React=] to |event|'s [=AppHistoryNavigateEvent/navigation action promise=] with the following fulfillment steps given |fulfillmentValue|:
670
-
1. [=Fire an event=] named {{AppHistory/navigatesuccess}} at |appHistory|.
671
-
1. If |navigateMethodCallPromise| is non-null, then [=resolve=] |navigateMethodCallPromise| with |fulfillmentValue|.
672
-
and the following rejection steps given reason |rejectionReason|:
673
-
1. [=Fire an event=] named {{AppHistory/navigateerror}} at |appHistory| using {{ErrorEvent}}, with {{ErrorEvent/error}} initialized to |rejectionReason|, and {{ErrorEvent/message}}, {{ErrorEvent/filename}}, {{ErrorEvent/lineno}}, and {{ErrorEvent/colno}} initialized to appropriate values that can be extracted from |rejectionReason| in the same underspecified way the user agent typically does for the <a spec="HTML">report an exception</a> algorithm.
674
-
1. If |navigateMethodCallPromise| is non-null, then [=reject=] |navigateMethodCallPromise| with |rejectionReason|.
675
-
676
-
<p class="note">If |event|'s [=AppHistoryNavigateEvent/navigation action promise=] is non-null, then {{AppHistoryNavigateEvent/respondWith()}} was called and so we're performing a same-document navigation, for which we want to fire {{AppHistory/navigatesuccess}} or {{AppHistory/navigateerror}} events, and resolve or reject the promise returned by the corresponding {{AppHistory/navigate()|appHistory.navigate()}} call if one exists. Otherwise, if the navigation is same-document and was not canceled, we still perform these actions after a microtask, treating them as an instantly-successful navigation.
677
-
1. Otherwise:
678
-
1. Set |appHistory|'s [=AppHistory/navigate method call serialized state=] to null.
679
-
680
-
<p class="note">This ensures that any call to {{AppHistory/navigate()|appHistory.navigate()}} which triggered this algorithm does not overwrite the [=session history entry/app history state=] of the [=session history/current entry=] for cross-document navigations or canceled navigations.
681
-
1. If |event|'s [=AppHistoryNavigateEvent/navigation action promise=] is null and |result| is false, then [=signal an aborted navigation=] for |appHistory|.
665
+
1. If |result| is true:
666
+
1. If |event|'s [=AppHistoryNavigateEvent/navigation action promises list=] is not empty or |isSameDocument| is true, then:
667
+
1. [=Wait for all=] of |event|'s [=AppHistoryNavigateEvent/navigation action promises list=], with the following success steps:
668
+
1. [=Fire an event=] named {{AppHistory/navigatesuccess}} at |appHistory|.
669
+
1. If |navigateMethodCallPromise| is non-null, then [=resolve=] |navigateMethodCallPromise| with undefined.
670
+
and the following failure steps given reason |rejectionReason|:
671
+
1. [=Fire an event=] named {{AppHistory/navigateerror}} at |appHistory| using {{ErrorEvent}}, with {{ErrorEvent/error}} initialized to |rejectionReason|, and {{ErrorEvent/message}}, {{ErrorEvent/filename}}, {{ErrorEvent/lineno}}, and {{ErrorEvent/colno}} initialized to appropriate values that can be extracted from |rejectionReason| in the same underspecified way the user agent typically does for the <a spec="HTML">report an exception</a> algorithm.
672
+
1. If |navigateMethodCallPromise| is non-null, then [=reject=] |navigateMethodCallPromise| with |rejectionReason|.
673
+
674
+
<p class="note">If |event|'s [=AppHistoryNavigateEvent/navigation action promises list=] is non-empty, then {{AppHistoryNavigateEvent/respondWith()}} was called and so we're performing a same-document navigation, for which we want to fire {{AppHistory/navigatesuccess}} or {{AppHistory/navigateerror}} events, and resolve or reject the promise returned by the corresponding {{AppHistory/navigate()|appHistory.navigate()}} call if one exists. Otherwise, if the navigation is same-document and was not canceled, we still perform these actions after a microtask, treating them as an instantly-successful navigation.
675
+
1. Otherwise:
676
+
1. Set |appHistory|'s [=AppHistory/navigate method call serialized state=] to null.
677
+
678
+
<p class="note">This ensures that any call to {{AppHistory/navigate()|appHistory.navigate()}} which triggered this algorithm does not overwrite the [=session history entry/app history state=] of the [=session history/current entry=] for cross-document navigations.
679
+
1. Otherwise, [=synchronously finalize with an aborted navigation error=] for |appHistory|.
682
680
1. Return |result|.
683
681
</div>
684
682
685
683
<!-- TODO hook this up to the stop button etc. For now it just centralizes the ways a couple ways a navigate event handler can cause an abort. -->
686
684
<div algorithm>
687
-
To <dfn>signal an aborted navigation</dfn> for an {{AppHistory}} |appHistory|:
685
+
To <dfn>synchronously finalize with an aborted navigation error</dfn> for an {{AppHistory}} |appHistory|:
686
+
687
+
1. Set |appHistory|'s [=AppHistory/navigate method call serialized state=] to null.
688
688
689
+
<p class="note">This ensures that any call to {{AppHistory/navigate()|appHistory.navigate()}} which triggered this algorithm does not overwrite the [=session history entry/app history state=] of the [=session history/current entry=] for aborted navigations.
689
690
1. [=Queue a microtask=] on |appHistory|'s [=relevant agent=]'s [=agent/event loop=] to perform the following steps:
690
691
1. Let |error| be a [=new=] "{{AbortError}}" {{DOMException}}, created in |appHistory|'s [=relevant Realm=].
691
692
1. [=Fire an event=] named {{AppHistory/navigateerror}} at |appHistory| using {{ErrorEvent}}, with {{ErrorEvent/error}} initialized to |error|, {{ErrorEvent/message}} initialized to the value of |error|'s {{DOMException/message}} property, {{ErrorEvent/filename}} initialized to the empty string, and {{ErrorEvent/lineno}} and {{ErrorEvent/colno}} initialized to 0.
@@ -697,7 +698,6 @@ The <dfn attribute for="AppHistoryDestination">sameDocument</dfn> getter steps a
697
698
698
699
1. If |appHistory|'s [=AppHistory/ongoing navigate event=] is non-null, then:
699
700
1. Set |appHistory|'s [=AppHistory/ongoing navigate event=]'s [=Event/canceled flag=] to true.
700
-
1. Set |appHistory|'s [=AppHistory/ongoing navigate event=]'s [=AppHistoryNavigateEvent/navigation action promise=] to null.
701
701
1. Set |appHistory|'s [=AppHistory/ongoing navigate event=] to null.
0 commit comments