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

Extending <a> with more navigation capabilities #101

Open domenic opened 3 years ago

domenic commented 3 years ago

@dvoytenko mentioned today an interesting potential addition that could come along with the new navigation API: extending the <a> element (and <area>?? Or nah) to allow it more powerful navigation capabilities, similar to navigation.navigate().

This is somewhat of a sibling of #82, which talks about extending navigation.navigate() to match existing HTML <a> and <form> capabilities.

There are two relatively easy things we could do:

More interesting would be whether there'd be a way to allow an <a> to traverse to a specific history entry, e.g. by navigation API history entry key. This is trickier because for a simple version (like <a key="history entry key">), to actually figure out the keys you need to use JavaScript, which somewhat defeats the point. So we started discussing some ideas like letting pages declaratively set such keys... it gets complex.

Such additions to <a> would help encourage more "real" <a>s and less <a href="javascript:undefined" onclick="navigation.navigate(...)">. Note also that such "real" <a>s would have event.userInitiated set to true in the navigate handler, whereas as currently specced onclick=""-style <a>s would not.

In general Dima was of the opinion that by thinking about the declarative path as we design the API, we're likely to strengthen the API more generally, which I agree with.

bathos commented 3 years ago

It sounds like key would make “in-app back button that only traverses within your doc” (which comes up in standalone PWAs) simpler and safer regardless of whether it’s MegaDeclarative. Currently implementing this requires setting the new href on every URL change (for accessibility, normal context menu “open in new window” behavior, having hover show the correct bottom corner URL preview, etc), and that’s no different from having to update the key on each currentchange — but what is different is that the link would no longer require one-off unique click handling logic that prevents default and traverses backwards manually.

bahrus commented 3 years ago

Add a navigateinfo="" string attribute which allows you to provide a string value that is used as the info property of the corresponding navigate event. (Supporting non-string values is not a good idea here as it mismatches the HTML attribute model.)

I think something like this is really critical.

Is it out of the realm of possibilities to also (or instead?) include the dataset name/value pair object in info as well?

And I guess the question is in my mind -- is there no way to add the "triggering navigation element" somewhere in the event object?

domenic commented 2 years ago

I think adding the triggering element to the navigation event is possible, although it'd be good to have concrete examples (preferably with code) to support such an expansion in scope.

bathos commented 2 years ago

We currently rely on both of these patterns:

The latter arises (for example) for things like a [create new foo] link to a data entry state where field values may be initialized based on the link's context. When the link appears in a drill down preview of contact "Bob", the "recipient" field in the form would be initialized to Bob, and associating a state object with the element is one way to achieve that.

This sort of thing is currently a natural feeling pattern because generic link click handlers are unavoidable anyway. In some cases, you could instead expand the URL contract with e.g. new query parameters (though this might make it tougher to avoid duplicate API requests - it's not nec a frictionless change).

If one of the objectives of AppHistory is to reduce or eliminate the need for generic link click handlers, I agree with @bahrus that being able to tie the navigation to an originating element when applicable would make adapting existing apps to AppHistory a lot easier. OTOH, in an AppHistory-first world I would likely tend to favor the expand-the-URL-contract approach whenever possible.

bahrus commented 2 years ago

I can't yet think of an example where I need to access a property of the anchor tag.

It might be sufficient to provide access to the full attribute list.

(But see my second example below).

Example 1

    <a href=ro.html id=myAnchor>ro</a>
    <script>
        myAnchor.target = 'myIframe';
    </script>
    <iframe style="display:none" name=myIframe ></iframe>

Due to the script setting the target property, the link will open in the iframe below.

Suppose I don't want the iframe to display until a link is opened that targets the iframe? I think I would want to set that visibility in the navigate event handler.

But to my mild surprise, setting the target property on myAnchor actually reflects to the attribute, so I would be able to fulfill the requirement if I had access to the attribute list.

But I wonder if there are some properties of the anchor tag that doesn't automatically reflect? I'll keep pondering that question to see if I hit upon one that is relevant to navigation.

Example 2

<nav>
    <a href=a.html target=myAIframe>A</a>
    <a href=b.html target=myBIframe>B</a>
    <a href=c.html target=myCIframe>C</a>
</nav>
<iframe style="display:none" name=myAIframe></iframe>
<iframe style="display:none" name=myBIframe></iframe>
<iframe style="display:none" name=myCIframe></iframe>

Suppose we only want the last selected link's iframe to display? Sure, we could call out to a function that hardcodes the querySelector strings for the three iframes, passing in the target of the selected anchor tag. But a more general algorithm would be along the lines of:

  1. Find the containing nav element that contains the triggering anchor tag -- this would require having an actual reference to the triggering anchor tag.
  2. For the selected hyperlink, display the corresponding iframe (maybe after it finishes loading)
  3. Hide the other iframes that are targeted by the other anchor tags within the containing nav element.

I'll try to come up with more examples if this doesn't seem sufficient.

bahrus commented 2 years ago

Hmm, my example so far might not be right -- it seems that hyperlinks that target an iframe do not fire a navigate event.

bahrus commented 2 years ago

I guess the scenario that is applicable:

<nav>
<a href="/about" data-target=about-us>About us</a>
<a href="/contactus" data-target=contact-us>Contact us</a>
<a href="/home" data-target=home>Home</a>
</nav>

<div id=about style="display:none"></div>
<div id=contact-us  style="display:none"></div>
<div id=home  style="display:none"></div>

<script>
document.addEventListener("navigate", e => {
      //set the innerHTML of the element found by doing:
     const trigger = e.triggeringNavigationElement;
     document.querySelector(trigger.dataset.target).innerHTML = await loadContentFor(e.destination.url);
     document.querySelector(trigger.dataset.target).style.display = 'block';
     //now hide the other targeted elements
     const nav = trigger.closest('nav');
     nav.querySelectorAll('a[data-target]').forEach(const anchorTag => {
       if(anchorTag !== trigger){
          document.querySelector(anchorTag.dataset.target).style.display = 'none';
       }
     }
    });
  }
});
</script>
dvoytenko commented 2 years ago

@bahrus would it work for you to just derive target names from the URLs? E.g.

<nav>
  ...
  <a href="/contactus">Contact us</a>
  ...  ​
</nav>

...
<div id=contactus  style="display:none"></div>
...

<script>
document.addEventListener("navigate", e => {
  // Get the last segment in the URL.
  const targetId = e.destination.url.split('/').pop();
  const target = document.getElementById(targetId);
  target.innerHTML = await loadContentFor(e.destination.url);
  target.style.display = 'block';
  ...
});
</script>
bahrus commented 2 years ago

Yes, @dvoytenko, I'm sure without this feature, workarounds could be found, such as requiring that the url match the id of the target element. (A fleeting question in my mind is if end users can cause navigate events to trigger by playing around with the url, if that might inadvertently cause DOM element manipulation the developer wasn't expecting, based on this assumption).

Another issue with this workaround -- id's become global constants , which can break code. The url's might want to be simple strings that are used as genuine constants in the application. So now we can find workarounds to workarounds -- use a class or something instead of an id.

This also doesn't address how to hide the targets from previously selected anchor tags, however. Again, workarounds can be found (e.g. keep a variable in memory to remember the last selected url, I guess), but it raises questions in my mind why we need to find such workarounds, when passing in the triggering element would avoid needing to find workarounds?

I guess if there is some concern about including a reference to the triggering element, then it would be possible to weigh the costs / benefits. Without that, not sure what to say.

If a scenario with no possible workaround occurs to me, I will be sure to bring it up. But the web is robust enough that it is quite rare to find scenarios where some sort of work around can't be found :-), no matter what the situation.

bahrus commented 2 years ago

I'm sure there's a workaround for this, but this one might be a bit challenging:

Requirements:

  1. When a user clicks on a link, add a class to the link for as long as it takes for the requested URL to be retrieved / downloaded.
  2. Once it is downloaded, remove the class.
  3. The class provides a colorful loading effect to indicate that the requested url is being retrieved / downloaded.
  4. The color of the link should, during navigation, be a function of the text content of the link.
  5. The text content of the link is editable, thus the user can edit the text while the url is downloading, and the color of the link should change accordingly. Take the absolute values of the sine, cosine of the length of the text., and a random number between 0 and 1. Multiply by 256. That forms the RGB values.
  6. Add a class to all the other, non selected links in the same nav element containing the link so those links start flashing with neon light effects, until the navigation is complete, at which point the class is removed from all those adjacent elements.
tbondwilkinson commented 1 year ago

This came up again in conversations with @sebmarkbage where there's a desire to link the triggering element so that the navigate event handler can do something with it. I think something as simple as populating the <a> element object on the navigate event could help here, without going into all the other features in this thread.

domenic commented 1 year ago

That's #225. @natechapin interested in working on that next? Should be relative easy.

natechapin commented 1 year ago

See #225 for a link to a prototype, and discussion of some outstanding questions if we want to go the route of exposing the initiating element.

absurdprofit commented 2 months ago

More interesting would be whether there'd be a way to allow an <a> to traverse to a specific history entry, e.g. by navigation API history entry key. This is trickier because for a simple version (like <a key="history entry key">), to actually figure out the keys you need to use JavaScript, which somewhat defeats the point. So we started discussing some ideas like letting pages declaratively set such keys... it gets complex.

An alternative could be to add a traverse="" boolean attribute similar to replace="" which uses the rel attribute with prev or next as a hint. The default behaviour here could be rel="prev" if rel is unspecified.

However the key attribute would still be very useful for things like the nested router paradigm where the previous entry might not be the "back" entry of a parent router.