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
487 stars 26 forks source link

Group- or stack-based navigation history entry patterns #226

Open k-egor-smirnov opened 2 years ago

k-egor-smirnov commented 2 years ago

I've tried Navigation API on Chrome Canary and have some thoughts about a navigation tree restoration. I would like to have an ability to traverse to entry that have been overridden. For example, I have navigation structure like this:

graph TB
    A["Init"]-->second_a["first page"];
    A-->first_a["first page"];
    subgraph second["Tab 2"];
    second_a-->second_b["second page"];
    end;
    subgraph first["Tab 1"];
    first_a-->first_b["second page"];
    end;

And a navigation flow: Init page -> Tab 1 first page -> Tab 1 second page -> Init page (traverse back) -> Tab 2 first page.

When I've tried to save the navigation entry key of the Tab 1 second page and restore it when I'm on the Tab 2 first page, it will throw error that key doesn't exist cause the navigation tree was replaced with the new. Is it possible to have several navigation trees? It would be great to have this ability cause our web app has tab structure and having several versions of navigation tree will bring us closer to native-like behavior! image

frehner commented 2 years ago

I think #9 and #20 have discussions of this use case? I could be wrong though.

domenic commented 2 years ago

Thanks for trying the API! And for making a clear diagram to illustrate your point.

It's unfortunately not really possible to change the browser's model of history into something tree-based instead of list-based. Fundamentally we always need to be able to answer the question "what does the back button do" and "what does the forward button do" and "what happens if the user holds down the back button and picks an entry two entries further back in the list" and so on. Which forces us into the list-based model with a single current-entry pointer.

However, it should be possible to generate a more tree- or stack-like experience on the web, with sufficient web developer work. It might need new capabilities like those mentioned in #9. I admit we haven't thought this through fully. Is that something you'd be able to help us think through? Like, if you had the ability to delete or add or rearrange history entries within the linear list, how would you use that to implement the tree-like experience that you want? It might give some weird experiences for the forward button or for holding down back and going back multiple entries; is that acceptable to you? Questions like that would be great to have someone dig into.

Another thing to consider here is that adding new entries has the potential for abuse, so it's not as easy to implement. But I think we can figure out ways to make it work, if it's necessary for the types of experiences web developers want to create.

k-egor-smirnov commented 2 years ago

@frehner thank you for mentioning #9 issue. This is almost what I want, but I want to propose some changes to the original request.

As @domenic said, we have to answer 3 questions:

My idea is about creating something like a "Navigation Group". It should start with navigation.beginNavigationGroup() and return a unique id. All the new navigation entries should be added to this group simultaneously with the global navigation history and the group should save a pointer to the last navigation entry. A developer may end this group with navigation.endNavigationGroup(), so all next entries would add without assigning to it. When it's needed to "replace tab in UI", it could be simply performed with navigation.traverseToGroup(<group id>). navigation.traverseToGroup would replace the current navigation group with the one that was requested and with the history entry that was saved in this group.

In other words, it's something like history.replace and can be partially fallback-ed with history.back many times to the start of navigation group, fill all of it entries to history with history.pushState and history.back again to the saved pointer. Of course, it's a very bad example, but it possibly working :)

Backing to @domenic questions:

Sure, we can't rearrange the order of history entries or delete old one, but should we even have these features? I can't imagine use cases for now.

Remaining questions I still have no idea:

domenic commented 2 years ago

@k-egor-smirnov I'd like to caution you away from jumping to quickly to a proposed solution and API (your "navigation groups" idea). Let's first try to get the problem statement clear, and also be clear on what is possible with current or planned extensions.

The way I suggest doing that is to outline use cases (you already did this, in your OP!) and then fully think through the consequences of them. So let's say we take your example from the OP. After the flow Init page -> Tab 1 first page -> Tab 1 second page -> Init page (traverse back) -> Tab 2 first page, what should the linear history list look like, to give the experience you want for the back button/back-2 button/etc.?

Let's use shorter abbreviations for your scenario: we'll call your "init page" A, "tab 1" X, and "tab 2" Y. Let's use [] to denote the current entry. So the user journey looks like:

k-egor-smirnov commented 2 years ago

@domenic sure, it's not about real API proposal, just to explain the flow. I think the journey should looks like this, but let's also assign Z for non-grouped entries: [A] A, [X1] A, X1, [X2] A, X1, X2, [X3] A, X1, [X2], X3 --- user jump to Y --- A, [Y1] A, Y1, [Y2] --- Y group ends --- A, Y1, Y2, [Z1] A, Y1, Y2, Z1, [Z2] --- user jump back to X; Z entries are dropped; group X history was restored --- A, X1, [X2], X3

domenic commented 2 years ago

Perfect, that is super-helpful! I understand the problem space a lot more now. It also makes it clear why you jumped to a stash-then-restore approach.

The kind of primitives in #9 would technically allow what you have above:

However I can see the disadvantages pretty clearly:

To some extent app developer (or router developer) work is going to be unavoidable here, to deal with exactly what the stash and restore steps mean for your application. (Do you keep stuff off-screen, but hidden? Do you re-render everything from scratch? Etc.) I think the most the browser will do is set up the history entry list to contain the right entries, in particular with the right URL and navigation API states. But that seems like a reasonable thing to ask.

Another aspect worth considering, especially from a browser-implementer point of view, is how this interacts with abuse protections (i.e., not allowing you to stuff tons of entries in to prevent the back button from escaping your site). As https://github.com/whatwg/html/issues/7832 mentions, browsers generally stamp some entries as legitimate, and others as spammy, so that the back button will skip the spammy entries.

I think your framing of stash-then-restore gives a good answer here. Basically, you are "borrowing" certified-legitimate entries for later use. This is better than the manual version I outlined above, involving deletions and then navigations; there, deleting the entry loses the signal that it was legitimate.

A consequence of this is that we'd need to make sure you couldn't restore a group twice, thus doubling your count of legitimate entries. But that seems like a reasonable constraint.

At this point I'm tempted to jump back into API design myself, but I'll hold off for a day or two to let discussions on the problem space continue...

Yay295 commented 2 years ago

Using an iframe for each tab would probably accomplish this. The top level history list would look like this at the end:
A, [X], Y
the X iframe entries would be:
X1, [X2], X3
and the Y iframe entries would be: Y1, [Y2]

tbondwilkinson commented 1 year ago

This use case came up again in discussions with @sebmarkbage, so I do think it's a further signal that it would be useful for the ability to stash-then-restore history entries. I think most cases involve these flows developing naturally so there's not concern about user activation.

I think the best way to approach this is a new history operation mode, that acts like a replace on a specific history entry but disconnects the current and forward entries so that they can be later restored.