WICG / navigation-api

The new navigation API provides a new interface for navigations and session history, with a focus on single-page application navigations.
https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigation-api
484 stars 30 forks source link

Allow waiting for pending traversals before committing new push #252

Open tbondwilkinson opened 1 year ago

tbondwilkinson commented 1 year ago

Here is an example of something we do in our applications:

navigation.addEventListener('navigate', (e) => {
  e.intercept({handler: () => Promise.resolve()});
});
const key = navigation.currentEntry.key;
navigation.navigate('/foo');
navigation.traverseTo(key);
navigation.navigate('/bar');

A concrete example of this is:

  1. View A (say, a modal) adds a history entry, '/foo' when it opens.
  2. View A closes, due to user activation, and the result of that user activation is to synchronously close the modal.
  3. View B (say, another modal, but maybe not) opens, synchronously due to the same user activation, but in code after the effects of (2), and adds a history entry, '/bar' when it opens.

The resulting history, based on the above is [/, /foo, /bar] with the current URL being / rather than what the code might expect, as [/, /bar] with the current URL being /bar.

The reason for this is that navigate requests synchronously resolve, regardless of existing traversal requests.

For traversals, the navigation event and the transition property are called and populated asynchronously. Meaning that immediately after a call to navigation.traverseTo we have no ability to detect that a traversal is occurring and we might want to wait for it to commit (or cancel).

We work around this issue in the existing API only partially, but essentially if a user requests a traversal in JS, we wait for the next popstate to ensure that the traversal has resolved before we allow further requests to the browser to change history. So, we queue up push navigations until ready.

Proposed resolution:

My proposal is:

  1. Synchronously expose information about pending traversals so that the router can choose whether to wait for or cancel existing traversals.
  2. In conjunction with #66, enable deferring the commit for a navigation until the pending traversals have finished.

This would move the code that currently has to wait for existing traversals before navigate() can be called into the navigate handler, which would catch any and all calls to navigate.traverseTo and history.back.

domenic commented 1 year ago

So, the intention here was that you would write such code as

await navigation.navigate('/foo').finished;
await navigation.traverseTo(key).finished;
await navigation.navigate('/bar').finished;

I'm guessing that's not an option in your case, e.g. because the third line is located somewhere else in the codebase where the return value of navigation.traverseTo() is not easy to access.

And indeed, it sure seems like navigation.transition was intended to be helpful here. Then the code could look something like

await navigation.transition.finished;
navigation.navigate('/foo');

await navigation.transition.finished;
navigation.traverseTo(key);

await navigation.transition.finished;
navigation.navigate('/bar');

which is nice and easy to divvy up into multiple disconnected files. And indeed, if we work on #66, you could add the await navigation.transition.finished inside the navigate handler, so that your original code works as-is with no workarounds.

So I think this is a matter of ensuring we populate navigation.transition the moment a traversal is requested, instead of the moment it actually starts. That seems reasonable to me.