App History API

Draft Community Group Report,

This version:
https://wicg.github.io/app-history/
Editor:
Domenic Denicola ( Google )
Participate:
GitHub WICG/app-history ( new issue , open issues )
Commits:
GitHub spec.bs commits
Not Ready For Implementation

This spec is not yet ready for implementation. It exists in this repository to record the ideas and promote discussion.

Before attempting to implement this spec, please contact the editors.


Abstract

The app history API provides a web application-focused way of managing same-origin same-frame history entries and navigations.

Status of this document

This specification was published by the Web Platform Incubator Community Group . It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups .

1. The AppHistory class

partial interface Window {
  readonly attribute AppHistory appHistory;
};

Each Window object has an associated app history , which is a new AppHistory instance created alongside the Window .

The appHistory getter steps are to return this 's app history .

[Exposed=Window]
interface AppHistory : EventTarget {
  attribute AppHistoryEntry? current;
  sequence<AppHistoryEntry> entries();
  readonly attribute boolean canGoBack;
  readonly attribute boolean canGoForward;
  Promise<undefined> navigate(USVString url, optional AppHistoryNavigateOptions options = {});
  Promise<undefined> navigate(optional AppHistoryNavigateOptions options = {});
  attribute EventHandler onnavigate;
  attribute EventHandler onnavigatesuccess;
  attribute EventHandler onnavigateerror;
};
dictionary AppHistoryNavigationOptions {
  any navigateInfo;
};
dictionary AppHistoryNavigateOptions : AppHistoryNavigationOptions {  any state;  boolean replace = false;
};

Each AppHistory object has an associated entry list , a list of AppHistoryEntry objects, initially empty.

Each AppHistory object has an associated current index , an integer, initially −1.

Each AppHistory object has an associated ongoing navigate event , an AppHistoryNavigateEvent or null, initially null.

Each AppHistory object has an associated navigate method call promise , which is either a Promise or null, initially null.

Each AppHistory object has an associated navigate method call serialized state , which is either a serialized state or null, initially null.

1.1. Introspecting the app history entry list

appHistory . current

The current AppHistoryEntry .

appHistory . entries()

Returns an array of AppHistoryEntry instances representing the current app history list, i.e. all session history entries for this Window that are same origin and contiguous to the current session history entry.

appHistory . canGoBack

Returns true if the current AppHistoryEntry is not the first one in the app history entries list.

appHistory . canGoForward

Returns true if the current AppHistoryEntry is not the last one in the app history entries list.

Each AppHistory object has an associated entry list , a list of AppHistoryEntry objects, initially empty. Each AppHistory object has an associated current index , an integer, initially −1. Each AppHistory object has an associated ongoing navigate event , an AppHistoryNavigateEvent or null, initially null.
The current getter steps are:
  1. If this 's relevant global object 's associated Document is not fully active , then return null.

  2. If this 's current index is −1, then return null.

    This occurs if accessing the API while on the initial about:blank Document , where the update the entries algorithm will not yet have been run to completion.

  3. Return this 's entry list [ this 's current index ].

The entries() method steps are:
  1. If this 's relevant global object 's associated Document is not fully active , then return the empty list.

  2. Return this 's entries list .

The canGoBack getter steps are:
  1. If this 's relevant global object 's associated Document is not fully active , then return false.

  2. If this 's current index is −1, then return false.

  3. If this 's current index is 0, then return false.

  4. Return true.

The canGoForward getter steps are:
  1. If this 's relevant global object 's associated Document is not fully active , then return false.

  2. If this 's current index is −1, then return false.

  3. If this 's current index is equal to this 's entry list 's size − 1, then return false.

  4. Return true.

The following are the event handlers (and their corresponding event handler event types ) that must be supported, as event handler IDL attributes , by objects implementing the AppHistory interface: Event handler Event handler event type onnavigate navigate onnavigatesuccess navigatesuccess onnavigateerror navigateerror
To update the entries for an AppHistory instance appHistory :
  1. Let browsingContext be appHistory ’s relevant global object 's associated Document 's browsing context .

  2. Assert : browsingContext is not null.

  3. Let sessionHistory be browsingContext ’s session history .

  4. If browsingContext is still on its initial about:blank Document , then:

    1. Assert: appHistory ’s entry list is empty .

    2. Return.

    This can occur if running the URL and history update steps , e.g. via document.open() , on the initial about:blank Document . The app history API chooses not to expose the initial about:blank Document , so we bail early.

  5. Let appHistorySHEs be a new empty list.

  6. Let currentSHE be sessionHistory ’s current entry .

  7. Let backwardIndex be the index of currentSHE within sessionHistory , minus 1.

  8. While backwardIndex > 0:

    1. Let she be sessionHistory [ backwardIndex ].

    2. If she ’s origin is same origin with currentSHE ’s origin , then prepend she to appHistorySHEs .

    3. Otherwise, break .

    4. Set backwardIndex to backwardIndex − 1.

  9. Append currentSHE to appHistorySHEs .

  10. Let forwardIndex be the index of currentSHE within sessionHistory , plus 1.

  11. While forwardIndex < sessionHistory ’s size :

    1. Let she be sessionHistory [ forwardIndex ].

    2. If she ’s origin is same origin with currentSHE ’s origin , then append she to appHistorySHEs .

    3. Otherwise, break .

    4. Set forwardIndex to forwardIndex + 1.

  12. Let newCurrentIndex be the index of currentSHE within appHistorySHEs .

  13. Let newEntryList be an empty list.

  14. For each oldAHE of appHistory ’s entry list :

    1. Set oldAHE ’s index to −1.

  15. Let index be 0.

  16. For each she of appHistorySHEs :

    1. If appHistory ’s entry list contains an AppHistoryEntry existingAHE whose session history entry is she , then append existingAHE to newEntryList .

    2. Otherwise:

      1. Let newAHE be a new AppHistoryEntry created in the relevant realm of appHistory .

      2. Set newAHE ’s session history entry to she .

      3. Append newAHE to newEntryList .

    3. Set newEntryList [ index ]'s index to index .

    4. Set index to index + 1.

  17. Set appHistory ’s entry list to newEntryList .

  18. Set appHistory ’s current index to newCurrentIndex .

For same-document navigations only the current index and the indices of the various AppHistoryEntry objects will ultimately be updated by this algorithm. Implementations can special-case same-document navigations and avoid reassembling the entry list .

await appHistory . navigate ( url )
await appHistory . navigate ( url , options )

Navigates the current page to the given url . options can contain the following values:

By default this will perform a full navigation (i.e., a cross-document navigation, unless the given URL differs only in a fragment from the current one). The navigate event’s respondWith() method can be used to convert it into a same-document navigation.

The returned promise will behave as follows:

  • For same-document navigations created by using the navigate event’s respondWith() method, it will fulfill or reject according to the promise passed to respondWith() .

  • For other same-document navigations (e.g., non-intercepted fragment navigations ), it will fulfill immediately.

  • For cross-document navigations, it will never settle.

await appHistory . navigate ( options )

Navigates to the same URL as the current page. options needs to contain at least one of navigateInfo or state , which behave as described above.

The default behavior of performing a full navigation to the current page can be overriden by using the navigate event’s respondWith() method. Doing so will mean this call only updates state or passes along the appropriate navigateInfo .

The returned promise behaves as described above.

The navigate( url , options ) method steps are:
  1. Parse url relative to this 's relevant settings object . If that returns failure, then return a promise rejected with a " SyntaxError " DOMException . Otherwise, let urlRecord be the resulting URL record .

  2. Return the result of performing an app history navigation given this , urlRecord , and options .

The navigate( options ) method steps are:
  1. If neither options [" navigateInfo "] nor options [" state "] exists , then return a promise rejected with a TypeError .

  2. Set options [" replace "] to true.

    This is not technically necessary, as the main navigate algorithm will override any non-" replace " history handling behavior for same-URL navigations.

  3. Let urlRecord be this 's relevant global object 's active document 's URL .

  4. Return the result of performing an app history navigation given this , urlRecord , and options .

To perform an app history navigation given an AppHistory object appHistory , a URL url , and an AppHistoryNavigateOptions options :
  1. Let browsingContext be appHistory ’s relevant global object 's browsing context .

  2. Let historyHandling be " replace " if options [" replace "] exists and is true; otherwise, " default ".

  3. Let navigateInfo be options [" navigateInfo "] if it exists; otherwise, undefined.

  4. Let serializedState be null.

  5. If options [" state "] exists , then set serializedState to StructuredSerializeForStorage ( options [" state "]). If this throws an exception, return a promise rejected with that exception.

  6. Let promise be a new promise created in appHistory ’s relevant Realm .

  7. Let previousPromise be appHistory ’s navigate method call promise .

  8. Let previousState be appHistory ’s navigate method call serialized state .

  9. Set appHistory ’s navigate method call promise to promise .

  10. Set appHistory ’s navigate method call serialized state to serializedState .

  11. Navigate browsingContext to url with historyHandling set to historyHandling , appHistoryInfo set to navigateInfo , appHistoryState set to serializedState , and the source browsing context set to browsingContext .

  12. If navigate method call serialized state is non-null, then set browsingContext ’s session history 's current entry 's app history state to appHistory ’s navigate method call serialized state .

  13. Set appHistory ’s navigate method call promise to previousPromise .

  14. Set appHistory ’s navigate method call serialized state to previousState .

  15. Return promise .

Unlike location.assign() and friends, which are exposed across origin-domain boundaries, appHistory.navigate() can only be accessed by code with direct synchronous access to the appHistory property. Thus, we avoid the complications around tracking source browsing contexts , and we don’t need to deal with the allowed to navigate check and its accompanying exceptionsEnabled flag. We just treat all navigations as being initiated by the AppHistory object itself.

1.3. Event handlers

The following are the event handlers (and their corresponding event handler event types ) that must be supported, as event handler IDL attributes , by objects implementing the AppHistory interface:

Event handler Event handler event type
onnavigate navigate
onnavigatesuccess navigatesuccess
onnavigateerror navigateerror
[Exposed=Window]
interface AppHistoryNavigateEvent : Event {
  constructor(DOMString type, optional AppHistoryNavigateEventInit eventInit = {});
  readonly attribute AppHistoryNavigationType navigationType;
  ;
  ;
  ;
//  readonly attribute AppHistoryEntry destination;

  readonly attribute AppHistoryDestination destination;
  readonly attribute boolean canRespond;
  readonly attribute boolean userInitiated;
  readonly attribute boolean hashChange;
//  readonly attribute AbortSignal signal;
  readonly attribute FormData? formData;
  ;

  readonly attribute any info;
  );

  undefined respondWith(Promise<undefined> newNavigationAction);
};
dictionary AppHistoryNavigateEventInit : EventInit {
  AppHistoryNavigationType navigationType = "push";
  ;
  ;
  ;
//  required AppHistoryEntry destination;

  required AppHistoryDestination destination;
  boolean canRespond = false;
  boolean userInitiated = false;
  boolean hashChange = false;
//  required AbortSignal signal;
  FormData? formData = null;
  ;

  any info = null;
};
enum AppHistoryNavigationType {
  "push",
  "replace",
  "traverse"
};
event . navigationType

One of " push ", " replace ", or " traverse ", indicating what type of navigation this is.

event . destination

An AppHistoryDestination representing the destination of the navigation.

event . canRespond

True if respondWith() can be called to convert this navigation into a single-page navigation; false otherwise.

Generally speaking, this will be true whenever the destination URL is rewritable relative to the page’s current URL, except for cross-document back/forward navigations, where it will always be false.

event . userInitiated

True if this navigation was due to a user clicking on an a element, submitting a form element, or using the browser UI to navigate; false otherwise.

event . hashChange

True if this navigation is a fragment navigation ; false otherwise.

event . formData

The FormData representing the submitted form entries for this navigation, if this navigation is a POST form submission ; null otherwise.

event . info

An arbitrary JavaScript value passed via other app history APIs that initiated this navigation, or null if the navigation was initiated by the user or via a non-app history API.

event . respondWith ( newNavigationAction )

Synchronously converts this navigation into a same-document navigation to the destination URL.

The given newNavigationAction promise is used to signal the duration, and success or failure, of the navigation. After it settles, the browser signals to the user (e.g. via a loading spinner UI, or assistive technology) that the navigation is finished. Additionally, it fires navigatesuccess or navigateerror events as appropriate, which other parts of the web application can respond to.

This method will throw a " SecurityError " DOMException if canRespond is false, or if isTrusted is false. It will throw an " InvalidStateError " DOMException if not called synchronously, during event dispatch.

The navigationType , destination , canRespond , userInitiated , hashChange , formData , and info getter steps are to return the value that the corresponding attribute was initialized to.

An AppHistoryNavigateEvent has the following associated values that which are only conditionally used:

These are One of these is set appropriately when the event is fired .

An AppHistoryNavigateEvent also has an associated Promise -or-null navigation action promise , initially null.

The respondWith( newNavigationAction ) method steps are:
  1. If this 's relevant global object 's browsing context is null, then throw an " InvalidStateError " DOMException .

  2. If this 's isTrusted attribute was initialized to false, then throw a " SecurityError " DOMException .

  3. If this 's canRespond attribute was initialized to false, then throw a " SecurityError " DOMException .

  4. If this 's dispatch flag is unset, then throw an " InvalidStateError " DOMException .

  5. If this 's canceled flag is set, then throw an " InvalidStateError " DOMException .

  6. Set this 's canceled flag .

  7. Set this 's navigation action promise to newNavigationAction .

[Exposed=Window]interface AppHistoryDestination {  readonly attribute USVString url;  readonly attribute boolean sameDocument;  any getState();
};
event . destination . url

The URL being navigated to.

event . destination . sameDocument

Indicates whether or not this navigation is to the same Document as the current document value, or not. This will be true, for example, in cases of fragment navigations or history.pushState() navigations.

Note that this property indicates the original nature of the navigation. If a cross-document navigation is converted into a same-document navigation using event.respondWith() , that will not change the value of this property.

event . destination . getState()

For " traverse " navigations, returns the deserialization of the state stored in the destination session history entry.

For " push " and " replace " navigations, returns the deserialization of the state passed to appHistory.navigate() , if the navigation was initiated in that way, or null if it wasn’t.

An AppHistoryDestination has an associated URL , which is a URL .

An AppHistoryDestination has an associated state , which is a serialized state -or-null.

An AppHistoryDestination has an associated is same document , which is a boolean.

The url getter steps are to return this 's URL , serialized .

The sameDocument getter steps are to return this 's is same document .

The getState() method steps are:
  1. If this 's state is null, then return null.

  2. Return StructuredDeserialize ( this 's state ).

To fire a traversal navigate event at an AppHistory appHistory given a session history entry destinationEntry , a boolean isSameDocument , an optional user navigation involvement userInvolvement (default " none "), and an optional JavaScript value info (default undefined):
  1. Let destinationURL be destinationEntry ’s URL .

  2. Let destinationState be destinationEntry ’s app history state .

  3. Let event be the result of creating an event given AppHistoryNavigateEvent , in appHistory ’s relevant Realm .

  4. Set event ’s destination entry to destinationEntry .

  5. Return the result of performing the inner navigate event firing algorithm given appHistory , event , " traverse ", isSameDocument , destinationURL , destinationState , userInvolvement , info , and null.

To fire a push or replace navigate event at an AppHistory appHistory given an AppHistoryNavigationType navigationType , a URL destinationURL , a boolean isSameDocument , an optional user navigation involvement userInvolvement (default " none "), and an optional value navigateInfo info (default undefined), an optional serialized state -or-null state (default null), an optional list of FormData entries or null formDataEntryList (default null), and an optional URL destinationURL , an optional serialized state -or-null classicHistoryAPISerializedData (default null), and null):
  1. Let event be the result of creating an optional session history entry event given AppHistoryNavigateEvent , in destinationEntry appHistory ’s relevant Realm .

  2. Set event ’s classic history API serialized data to classicHistoryAPISerializedData .

  3. Return the result of performing the inner navigate event firing algorithm given appHistory , event , navigationType , isSameDocument , destinationURL , state , userInvolvement , info , and formDataEntryList .

The inner navigate event firing algorithm is the following steps, given an : AppHistory appHistory , an AppHistoryNavigateEvent event , an AppHistoryNavigationType navigationType , a boolean isSameDocument , a URL destinationURL , a serialized state -or-null destinationState , a user navigation involvement userInvolvement , a JavaScript value info , and a list of FormData entries or null formDataEntryList :
  1. If appHistory ’s relevant global object 's browsing context is still on its initial about:blank Document , then return true.

  2. Let realm be appHistory ’s relevant Realm . Let event be the result of creating an event given AppHistoryNavigateEvent , in realm . Initialize event ’s type to " navigate ".

  3. Initialize event ’s navigationType to navigationType .

  4. Initialize event ’s info to navigateInfo info .

  5. If Let navigationType destination is either " push be a new " or " replace AppHistoryDestination ": Assert : destinationURL was given, and created in destinationEntry appHistory was not. ’s relevant Realm .

  6. Set event destination ’s destination URL to destinationURL .

  7. Set event destination ’s classic history API serialized data state to classicHistoryAPISerializedData destinationState .

    Otherwise: Assert : destinationEntry was given
  8. Assert : destinationURL was not given, and Set classicHistoryAPISerializedData destination and ’s is same document to formDataEntryList are null. isSameDocument .

  9. Set Initialize event ’s destination entry to destinationEntry destination .

  10. Let currentURL be appHistory ’s relevant global object 's associated document 's URL .

  11. If all of the following are true:

    then initialize event ’s hashChange to true. Otherwise, initialize it to false.

  12. If destinationURL is rewritable relative to currentURL , and either isSameDocument is true or navigationType is not " traverse ", then initialize event ’s canRespond to true. Otherwise, initialize it to false.

  13. If either userInvolvement is not " browser UI " or navigationType is not " traverse ", then initialize event ’s cancelable to true.

  14. If userInvolvement is " none ", then initialize event ’s userInitiated to false. Otherwise, initialize it to true.

  15. If formDataEntryList is not null, then initialize event ’s formData to a new FormData created in realm , associated to formDataEntryList . Otherwise, initialize it to null.

  16. Assert : appHistory ’s ongoing navigate event is null.

  17. Set appHistory ’s ongoing navigate event to event .

  18. Let result be the result of dispatching event at appHistory .

  19. Set appHistory ’s ongoing navigate event to null.

  20. If this 's appHistory ’s relevant global object 's browsing context is null, then return false.

    1. Signal an aborted navigation for appHistory .

    2. Return false.

    This can occurr occur if an event listener disconnected the iframe corresponding to this 's relevant global object .

  21. If event ’s navigation action promise is non-null, then:

    1. If event ’s navigationType attribute was initialized to " push " or " replace ":

      1. Let isPush be true if event ’s navigationType attribute was initialized to " push "; otherwise, false.

      2. Run the URL and history update steps given event ’s relevant global object 's associated document and event ’s destination 's URL , with serializedData set to event ’s classic history API serialized data and isPush set to isPush .

    2. Otherwise:

      1. Traverse the history of event ’s relevant global object 's browsing context to destination entry .

  22. If event ’s navigation action promise is non-null, or both result and isSameDocument are true, then:

    1. If event ’s navigation action promise is null, then set it to a promise resolved with undefined, created in realm .

    2. Let navigateMethodCallPromise be appHistory ’s navigate method call promise .

    3. React to event ’s navigation action promise with the following fulfillment steps: steps given fulfillmentValue :

      1. Fire an event named navigatesuccess at appHistory .

      2. If navigateMethodCallPromise is non-null, then resolve navigateMethodCallPromise with fulfillmentValue .

      and the following rejection steps given reason rejectionReason :
      1. Fire an event named navigateerror at appHistory using ErrorEvent , with error initialized to rejectionReason , and message , filename , lineno , and 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.

      2. If navigateMethodCallPromise is non-null, then reject navigateMethodCallPromise with rejectionReason .

    If event ’s navigation action promise is non-null, then respondWith() was called and so we’re performing a same-document navigation, for which we want to fire navigatesuccess or navigateerror events as appropriate. events, and resolve or reject the promise returned by the corresponding appHistory.navigate() call if one exists. Otherwise, if the navigation is same-document and was not canceled, we still fire perform these actions after a microtask, treating them as an instantly-successful navigation.

  23. Otherwise:

    1. Set appHistory ’s navigate method call serialized state to null.

    This ensures that any call to navigatesuccess appHistory.navigate() after a microtask. which triggered this algorithm does not overwrite the app history state of the current entry for cross-document navigations or canceled navigations.

  24. If event ’s navigation action promise is null and result is false, then signal an aborted navigation for appHistory .

  25. Return result .

To signal an aborted navigation for an AppHistory appHistory :
  1. Queue a microtask on appHistory ’s relevant agent 's event loop to perform the following steps:

    1. Let error be a new " AbortError " DOMException , created in appHistory ’s relevant Realm .

    2. Fire an event named navigateerror at appHistory using ErrorEvent , with error initialized to error , message initialized to the value of error ’s message property, filename initialized to the empty string, and lineno and colno initialized to 0.

    3. If appHistory ’s navigate method call promise is non-null, then reject appHistory ’s navigate method call promise with error .

To cancel any ongoing navigate event for an AppHistory appHistory :
  1. If appHistory ’s ongoing navigate event is non-null, then:

    1. Set appHistory ’s ongoing navigate event 's canceled flag to true.

    2. Set appHistory ’s ongoing navigate event 's navigation action promise to null.

A URL is rewritable relative to another URL if they differ in only the path , query , or fragment components.

https://example.com/foo?bar#baz is rewritable relative to https://example.com/qux .

However, the concept is not the same as the two URLs' origins being the same : https://user:password@example.com/qux is not rewritable relative to https://example.com/qux .

Similarly, about:blank or blob: URLs are not rewritable relative to https: URLs, despite there being cases where a https :- URL Document is same origin with an about:blank or blob: -derived Document .

3. App history entries

]
[Exposed=Window]
interface AppHistoryEntry : EventTarget {
  readonly attribute DOMString key;
  readonly attribute DOMString id;
  ;

  readonly attribute USVString url;
  readonly attribute long long index;
  ;

  readonly attribute boolean sameDocument;
  ();

  any getState();
  // TODO event handlers
};
entry . key

A user agent -generated random UUID string representing this app history entry’s place in the app history list. This value will be reused by other AppHistoryEntry instances that replace this one due to replace-style navigations. This value will survive session restores.

This is useful for navigating back to this location in the app history entry list, using appHistory.goTo(key) .

entry . id

A user agent -generated random UUID string representing this specific app history entry. This value will not be reused by other AppHistoryEntry instances. This value will survive session restores.

This is useful for associating data with this app history entry using other storage APIs.

entry . url

The URL of this app history entry.

entry . index

The index of this app history entry within appHistory.entries() , or −1 if the entry is not in the app history list.

entry . sameDocument

Indicates whether or not this app history entry is for the same Document as the current document value, or not. This will be true, for example, in cases of when the entry represents a fragment navigations navigation or single-page app navigations.

entry . getState()

Returns the deserialization of the state stored in this entry, which was added to the entry using appHistory.navigate() . This state survives session restores.

Note that in general, unless the state value is a primitive, entry.getState() !== entry.getState() , since a fresh copy is returned each time.

This state is unrelated to the classic history API’s history.state .

Each AppHistoryEntry has an associated session history entry , which is a session history entry .

Each AppHistoryEntry has an associated index , which is an integer.

The key getter steps are:
  1. If this 's relevant global object 's associated Document is not fully active , then return the empty string.

  2. Return this 's session history entry 's app history key .

The id getter steps are:
  1. If this 's relevant global object 's associated Document is not fully active , then return the empty string.

  2. Return this 's session history entry 's app history id .

The url getter steps are:
  1. If this 's relevant global object 's associated Document is not fully active , then return the empty string.

  2. Return this 's session history entry 's URL , serialized .

The index getter steps are:
  1. If this 's relevant global object 's associated Document is not fully active , then return −1.

  2. Return this 's session history entry 's index .

The sameDocument getter steps are:
  1. If this 's relevant global object 's associated Document is not fully active , then return false.

  2. Return true if this 's session history entry 's document equals this 's relevant global object 's associated Document , and false otherwise.

The getState() method steps are:
  1. If this 's relevant global object 's associated Document is not fully active , then return null.

  2. If this 's session history entry 's app history state is null, then return null.

  3. Return StructuredDeserialize ( this 's session history entry 's app history state ).

Unlike history.state , this will deserialize upon each access.

This can in theory throw an exception, if attempting to deserialize a large ArrayBuffer when not enough memory is available.

The following section details monkeypatches to [HTML] that cause the navigate event to be fired appropriately, and for canceling the event to cancel the navigation. The first few sections detail slight tweaks to existing algorithms to pass through useful information into the navigation and history traversal algorithms. Then, § 4.3 Navigation algorithm updates contains the actual firing of the event.

4.1. Form submission patches

To properly thread the form entry list from its creation through to AppHistoryNavigateEvent 's formData property, we need the following modifications:

Modify the navigate algorithm to take a list of entries or null entryList (default null), replacing its navigationType parameter. Then insert a step somewhere early in the algorithm to convert this back into the navigationType variable used by the in parallel section that is ultimately passed to [CSP] :
  1. Let navigationType be " form-submission " if entryList is non-null; otherwise, " other ".

Modify the plan to navigate algorithm to take an additional optional argument entryList (default null). Then, modify the step which calls navigate to pass it along:
  1. Navigate target browsing context to destination , with historyHandling set to historyHandling and navigationType set to " form-submission " entryList set to entryList .

Modify the submit as entity body algorithm to pass entry list along to plan to navigate as a second argument.

4.2. Browser UI/user-initiated patches

To more rigorously specify when a navigation is initiated from browser UI or by the user interacting with a , area , and form elements, both for the purposes of the AppHistoryNavigateEvent 's userInitiated property and for prohibiting interception of certain types of browser-UI-initiated navigations, we need the following modifications:

Introduce (right before the definition of the navigate algorithm) the concept of a user navigation involvement , which is one of the following:

" browser UI "

The navigation was initiated by the user via browser UI mechanisms

" activation "

The navigation was initiated by the user via the activation behavior of an element

" none "

The navigation was not initiated by the user

Define the user navigation involvement for an Event event as " activation " if event ’s isTrusted attribute is initialized to true, and " none " otherwise.

Modify the navigate algorithm to take an optional named argument userInvolvement (default " none "). Then, update the paragraph talking about browser-UI initiated navigation as follows:

A user agent may provide various ways for the user to explicitly cause a browsing context to navigate , in addition to those defined in this specification. Such cases must set the userInvolvement argument to " browser UI ".

This infrastructure partially solves whatwg/html#5381 , and it’d be ideal to update the ` Sec-Fetch-Site ` spec at the same time.

Modify the navigate to a fragment algorithm to take a new userInvolvement argument. Then, update the call to it from navigate to set userInvolvement to this userInvolvement value.

Modify the traverse the history by a delta argument to take an optional named argument userInvolvement (default " none "). Then, update the paragraph talking about user-initiated navigation as follows:

When the user navigates through a browsing context , e.g. using a browser’s back and forward buttons, the user agent must traverse the history by a delta with a delta equivalent to the action specified by the user and , the browsing context being operated on , and userInvolvement set to " browser UI " .

Modify the follow the hyperlink algorithm to take a new userInvolvement argument. Then, update the call to it from navigate to set userInvolvement to this userInvolvement value.

Modify the activation behavior of area elements by introducing the event argument and replacing the follow the hyperlink step with the following:
  1. Otherwise, follow the hyperlink created by element with the user navigation involvement for event .

Modify the activation behavior of a elements by replacing its follow the hyperlink step with the following:
  1. Otherwise, follow the hyperlink created by element with the user navigation involvement for event .

Expand the section on " Providing users with a means to follow hyperlinks created using the link element " by adding the following sentence:

Such invocations of follow the hyperlink algorithm must set the userInvolvement argument to " browser UI ".

Modify the plan to navigate algorithm to take a userInvolvement argument. Then, update the call to it from navigate to set userInvolvement to this userInvolvement value.

Modify the submit algorithm to take an optional userInvolvement argument (default " none "). Have the submit algorithm pass along its value to all invocations of plan to navigate .

Modify the definition of the activation behavior for input elements to take an event argument. Then, pass along this argument to the invocation of the input activation behavior .

Modify the Submit Button state’s input activation behavior by having it take an event argument and pass along the user navigation involvement for event as the final argument when it calls submit .

Modify the Image Button state’s input activation behavior by having it take an event argument and pass along the user navigation involvement for event as the final argument when it calls submit .

Modify the button element’s activation behavior by having it take an event argument and, in the Submit Button case, to pass along the user navigation involvement for event as the final argument when it calls submit .

Modify the no- submit button case for implicit form submission to pass along " activation " as the final argument when it calls submit .

The case of implicit submission when a submit button is present is automatically taken care of because it fires a (trusted) click event at the submit button.

With the above infrastructure in place, we can actually fire and handle the navigate event in the following locations:

Modify the shared history push/replace state steps by inserting the following steps right before the step that runs the URL and history update steps .
  1. Let appHistory be history ’s relevant global object 's app history .

  2. Cancel any ongoing navigate event for appHistory .

  3. Let navigationType be " push " if isPush is true, and " replace " otherwise.

  4. Let continue be the result of firing a push or replace navigate event at appHistory with navigationType set to navigationType , isSameDocument set to true, destinationURL set to newURL , and classicHistoryAPISerializedData set to serializedData .

  5. If continue is false, return.

Modify the navigate to a fragment algorithm by prepending the following steps. Recall that per § 4.2 Browser UI/user-initiated patches we have introduced a userInvolvement argument.
  1. Let appHistory be the current entry ’s 's document ’s relevant global object 's app history .

  2. Let navigationType be " push " if historyHandling is " default "; otherwise, " replace ".

  3. Let continue be the result of firing a push or replace navigate event at appHistory given with navigationType set to navigationType , isSameDocument set to true, userInvolvement set to userInvolvement , and destinationURL set to url .

  4. If continue is false, return.

Modify the traverse the history by a delta algorithm by inserting the following steps inside the queued task, before the call to traverse the history . Recall that per § 4.2 Browser UI/user-initiated patches we have introduced a userInvolvement argument.
  1. Let appHistory be specified browsing context ’s active window 's app history .

  2. Cancel any ongoing navigate event for appHistory .

  3. Let isSameDocument be true if specified browsing context ’s active document equals specified entry ’s document ; otherwise, false.

  4. If either isSameDocument is true or userInvolvement is not " browser UI ", then:

    1. Let continue be the result of firing a traversal navigate event at appHistory with navigationType destinationEntry set to " traverse ", specified entry , isSameDocument set to isSameDocument , and userInvolvement set to userInvolvement , and destinationEntry set to specified entry .

    2. If continue is false, abort these steps.

5. Patches to session history

This section details monkeypatches to [HTML] to track appropriate data for associating an AppHistory with a session history entry .

5.1. New session history entry items

Each session history entry gains the following new items :

5.2. Carrying over the app history key

Update the update the session history with the new page algorithm’s " replace " case by adding the following step after the construction of newEntry :
  1. If newEntry ’s origin is the same as sessionHistory ’s current entry 's origin , then set newEntry ’s app history key to sessionHistory ’s current entry 's app history key .

5.3. Carrying over the app history state

Update the navigate to a fragment algorithm by updating the step which creates and appends a new session history entry to carry over the app history state from the current entry as well.

5.4. Tracking the origin member

Update the update the session history with the new page algorithm’s " replace " and " default " cases to set newEntry ’s origin to newDocument ’s origin as part of its creation.

Update the navigate to a fragment algorithm to set the new session history entry 's origin to the current entry 's document 's origin .

Update the URL and history update steps algorithm to set the new session history entry 's origin to document ’s origin .

Potentially update the traverse the history algorithm to consult the new origin field, instead of checking the document 's origin , since the document can disappear?? Needs further investigation.

5.4. 5.5. Updating the AppHistory object

Update the traverse the history algorithm by adding the following step before the final step which fires various events:
  1. Update the entries of newDocument ’s relevant global object 's app history .

Update the URL and history update steps by appending the following final step:
  1. Update the entries of document ’s relevant global object 's app history .

We do not update the entries when initially creating a new browsing context , as we intentionally don’t want to include the initial about:blank Document in any app history entry list.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[DOM]
Anne van Kesteren. DOM Standard . Living Standard. URL: https://dom.spec.whatwg.org/
[HTML]
Anne van Kesteren; et al. HTML Standard . Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard . Living Standard. URL: https://infra.spec.whatwg.org/
[URL]
Anne van Kesteren. URL Standard . Living Standard. URL: https://url.spec.whatwg.org/
[WebIDL]
Boris Zbarsky. Web IDL . URL: https://heycam.github.io/webidl/
[XHR]
Anne van Kesteren. XMLHttpRequest Standard . Living Standard. URL: https://xhr.spec.whatwg.org/

Informative References

[CSP]
Mike West. Content Security Policy Level 3 . URL: https://w3c.github.io/webappsec-csp/
[WEBAPPSEC-FETCH-METADATA-1]
Fetch Metadata Request Headers URL: https://w3c.github.io/webappsec-fetch-metadata/

IDL Index

partial interface Window {
  readonly attribute AppHistory appHistory;
};
[Exposed=Window]
interface AppHistory : EventTarget {
  attribute AppHistoryEntry? current;
  sequence<AppHistoryEntry> entries();
  readonly attribute boolean canGoBack;
  readonly attribute boolean canGoForward;
  Promise<undefined> navigate(USVString url, optional AppHistoryNavigateOptions options = {});
  Promise<undefined> navigate(optional AppHistoryNavigateOptions options = {});
  attribute EventHandler onnavigate;
  attribute EventHandler onnavigatesuccess;
  attribute EventHandler onnavigateerror;
};
dictionary AppHistoryNavigationOptions {
  any navigateInfo;
};
dictionary AppHistoryNavigateOptions : AppHistoryNavigationOptions {  any state;  boolean replace = false;
};

[Exposed=Window]
interface AppHistoryNavigateEvent : Event {
  constructor(DOMString type, optional AppHistoryNavigateEventInit eventInit = {});
  readonly attribute AppHistoryNavigationType navigationType;
  readonly attribute AppHistoryDestination destination;
  readonly attribute boolean canRespond;
  readonly attribute boolean userInitiated;
  readonly attribute boolean hashChange;
//  readonly attribute AppHistoryEntry destination;

//  readonly attribute AbortSignal signal;
  readonly attribute FormData? formData;
  readonly attribute any info;
  undefined respondWith(Promise<undefined> newNavigationAction);
};
dictionary AppHistoryNavigateEventInit : EventInit {
  AppHistoryNavigationType navigationType = "push";
  required AppHistoryDestination destination;
  boolean canRespond = false;
  boolean userInitiated = false;
  boolean hashChange = false;
//  required AppHistoryEntry destination;

//  required AbortSignal signal;
  FormData? formData = null;
  any info = null;
};
enum AppHistoryNavigationType {
  "push",
  "replace",
  "traverse"
};
[Exposed=Window]
interface AppHistoryDestination {
  readonly attribute USVString url;
  readonly attribute boolean sameDocument;
  any getState();
};
[Exposed=Window]

interface AppHistoryEntry : EventTarget {
  readonly attribute DOMString key;
  readonly attribute DOMString id;
  readonly attribute USVString url;
  readonly attribute long long index;
  readonly attribute boolean sameDocument;
  any getState();
  // TODO event handlers
};