Skip to content

Commit c357e20

Browse files
committed
Allow multiple calls to event.respondWith()
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.
1 parent 5a76650 commit c357e20

File tree

2 files changed

+28
-26
lines changed

2 files changed

+28
-26
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ Unlike the existing history API's `history.go()` method, which navigates by offs
259259
260260
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:
261261
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.)
263263
264264
- 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.
265265
@@ -327,6 +327,8 @@ The event object has a special method `event.respondWith(promise)`. This works o
327327
328328
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`.
329329
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+
330332
_TODO: is it OK for web developers that the URL bar updates immediately? See [#66](https://github.com/WICG/app-history/issues/66)._
331333
332334
#### Example: replacing navigations with single-page app navigations

spec.bs

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,7 @@ An {{AppHistoryNavigateEvent}} has the following associated values which are onl
540540

541541
One of these is set appropriately when the event is [[#navigate-event-firing|fired]].
542542

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.
544544

545545
<div algorithm>
546546
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
550550
1. If [=this=]'s {{AppHistoryNavigateEvent/canRespond}} attribute was initialized to false, then throw a "{{SecurityError}}" {{DOMException}}.
551551
1. If [=this=]'s [=Event/dispatch flag=] is unset, then throw an "{{InvalidStateError}}" {{DOMException}}.
552552
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=].
555554
</div>
556555

557556
<h3 id="navigate-event-destination">The {{AppHistoryDestination}} class</h3>
@@ -647,45 +646,47 @@ The <dfn attribute for="AppHistoryDestination">sameDocument</dfn> getter steps a
647646
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.
648647
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.
649648
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.
651650
1. [=Assert=]: |appHistory|'s [=AppHistory/ongoing navigate event=] is null.
652651
1. Set |appHistory|'s [=AppHistory/ongoing navigate event=] to |event|.
653652
1. Let |result| be the result of [=dispatching=] |event| at |appHistory|.
654653
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|.
657656
1. Return false.
658657

659658
<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:
661660
1. If |navigationType| is "{{AppHistoryNavigationType/traverse}}":
662661
1. <a spec="HTML">Traverse the history</a> of |event|'s [=relevant global object=]'s [=Window/browsing context=] to [=AppHistoryNavigateEvent/destination entry=].
663662
1. Otherwise:
664663
1. Let |isPush| be true if |navigationType| is "{{AppHistoryNavigationType/push}}"; otherwise, false.
665664
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|.
682680
1. Return |result|.
683681
</div>
684682

685683
<!-- 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. -->
686684
<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.
688688

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.
689690
1. [=Queue a microtask=] on |appHistory|'s [=relevant agent=]'s [=agent/event loop=] to perform the following steps:
690691
1. Let |error| be a [=new=] "{{AbortError}}" {{DOMException}}, created in |appHistory|'s [=relevant Realm=].
691692
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
697698

698699
1. If |appHistory|'s [=AppHistory/ongoing navigate event=] is non-null, then:
699700
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.
701701
1. Set |appHistory|'s [=AppHistory/ongoing navigate event=] to null.
702702
</div>
703703

0 commit comments

Comments
 (0)