diff --git a/README.md b/README.md index f2eaae9..ec68e09 100644 --- a/README.md +++ b/README.md @@ -259,7 +259,7 @@ Unlike the existing history API's `history.go()` method, which navigates by offs 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: -- 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.) +- 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.) - 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. @@ -327,6 +327,8 @@ The event object has a special method `event.respondWith(promise)`. This works o 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`. +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. + _TODO: is it OK for web developers that the URL bar updates immediately? See [#66](https://github.com/WICG/app-history/issues/66)._ #### Example: replacing navigations with single-page app navigations diff --git a/spec.bs b/spec.bs index 8fd64f7..6e8e371 100644 --- a/spec.bs +++ b/spec.bs @@ -540,7 +540,7 @@ An {{AppHistoryNavigateEvent}} has the following associated values which are onl One of these is set appropriately when the event is [[#navigate-event-firing|fired]]. -An {{AppHistoryNavigateEvent}} also has an associated {{Promise}}-or-null navigation action promise, initially null. +An {{AppHistoryNavigateEvent}} also has an associated navigation action promises list, which is a [=list=] of {{Promise}} objects, initially empty.
The respondWith(|newNavigationAction|) method steps are: @@ -550,8 +550,7 @@ An {{AppHistoryNavigateEvent}} also has an associated {{Promise}}-or-null @@ -647,45 +646,47 @@ The sameDocument getter steps a 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. 1. If either |userInvolvement| is not "[=user navigation involvement/browser UI=]" or |navigationType| is not "{{AppHistoryNavigationType/traverse}}", then initialize |event|'s {{Event/cancelable}} to true. 1. If |userInvolvement| is "[=user navigation involvement/none=]", then initialize |event|'s {{AppHistoryNavigateEvent/userInitiated}} to false. Otherwise, initialize it to true. - 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. + 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. 1. [=Assert=]: |appHistory|'s [=AppHistory/ongoing navigate event=] is null. 1. Set |appHistory|'s [=AppHistory/ongoing navigate event=] to |event|. 1. Let |result| be the result of [=dispatching=] |event| at |appHistory|. 1. Set |appHistory|'s [=AppHistory/ongoing navigate event=] to null. - 1. If |appHistory|'s [=relevant global object=]'s [=active Document=] is not [=Document/fully active=], then return false. - 1. [=Signal an aborted navigation=] for |appHistory|. + 1. If |appHistory|'s [=relevant global object=]'s [=active Document=] is not [=Document/fully active=], then: + 1. [=Synchronously finalize with an aborted navigation error=] for |appHistory|. 1. Return false.

This can occur if an event listener disconnected the <{iframe}> corresponding to [=this=]'s [=relevant global object=].

- 1. If |event|'s [=AppHistoryNavigateEvent/navigation action promise=] is non-null, then: + 1. If |event|'s [=AppHistoryNavigateEvent/navigation action promises list=] is not empty, then: 1. If |navigationType| is "{{AppHistoryNavigationType/traverse}}": 1. Traverse the history of |event|'s [=relevant global object=]'s [=Window/browsing context=] to [=AppHistoryNavigateEvent/destination entry=]. 1. Otherwise: 1. Let |isPush| be true if |navigationType| is "{{AppHistoryNavigationType/push}}"; otherwise, false. 1. Run the URL and history update steps given |event|'s [=relevant global object=]'s [=associated document=] and |event|'s {{AppHistoryNavigateEvent/destination}}'s [=AppHistoryDestination/URL=], with [=URL and history update steps/serializedData=] set to |event|'s [=AppHistoryNavigateEvent/classic history API serialized data=] and [=URL and history update steps/isPush=] set to |isPush|. - 1. If |event|'s [=AppHistoryNavigateEvent/navigation action promise=] is non-null, or both |result| and |isSameDocument| are true, then: - 1. If |event|'s [=AppHistoryNavigateEvent/navigation action promise=] is null, then set it to [=a promise resolved with=] undefined, created in |realm|. - 1. Let |navigateMethodCallPromise| be |appHistory|'s [=AppHistory/navigate method call promise=]. - 1. [=promise/React=] to |event|'s [=AppHistoryNavigateEvent/navigation action promise=] with the following fulfillment steps given |fulfillmentValue|: - 1. [=Fire an event=] named {{AppHistory/navigatesuccess}} at |appHistory|. - 1. If |navigateMethodCallPromise| is non-null, then [=resolve=] |navigateMethodCallPromise| with |fulfillmentValue|. - and the following rejection steps given reason |rejectionReason|: - 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 report an exception algorithm. - 1. If |navigateMethodCallPromise| is non-null, then [=reject=] |navigateMethodCallPromise| with |rejectionReason|. - -

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. - 1. Otherwise: - 1. Set |appHistory|'s [=AppHistory/navigate method call serialized state=] to null. - -

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. - 1. If |event|'s [=AppHistoryNavigateEvent/navigation action promise=] is null and |result| is false, then [=signal an aborted navigation=] for |appHistory|. + 1. If |result| is true: + 1. If |event|'s [=AppHistoryNavigateEvent/navigation action promises list=] is not empty or |isSameDocument| is true, then: + 1. [=Wait for all=] of |event|'s [=AppHistoryNavigateEvent/navigation action promises list=], with the following success steps: + 1. [=Fire an event=] named {{AppHistory/navigatesuccess}} at |appHistory|. + 1. If |navigateMethodCallPromise| is non-null, then [=resolve=] |navigateMethodCallPromise| with undefined. + and the following failure steps given reason |rejectionReason|: + 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 report an exception algorithm. + 1. If |navigateMethodCallPromise| is non-null, then [=reject=] |navigateMethodCallPromise| with |rejectionReason|. + +

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. + 1. Otherwise: + 1. Set |appHistory|'s [=AppHistory/navigate method call serialized state=] to null. + +

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. + 1. Otherwise, [=synchronously finalize with an aborted navigation error=] for |appHistory|. 1. Return |result|.

- To signal an aborted navigation for an {{AppHistory}} |appHistory|: + To synchronously finalize with an aborted navigation error for an {{AppHistory}} |appHistory|: + + 1. Set |appHistory|'s [=AppHistory/navigate method call serialized state=] to null. +

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. 1. [=Queue a microtask=] on |appHistory|'s [=relevant agent=]'s [=agent/event loop=] to perform the following steps: 1. Let |error| be a [=new=] "{{AbortError}}" {{DOMException}}, created in |appHistory|'s [=relevant Realm=]. 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 sameDocument getter steps a 1. If |appHistory|'s [=AppHistory/ongoing navigate event=] is non-null, then: 1. Set |appHistory|'s [=AppHistory/ongoing navigate event=]'s [=Event/canceled flag=] to true. - 1. Set |appHistory|'s [=AppHistory/ongoing navigate event=]'s [=AppHistoryNavigateEvent/navigation action promise=] to null. 1. Set |appHistory|'s [=AppHistory/ongoing navigate event=] to null.