Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extending <a> with more navigation capabilities #101

Open
domenic opened this issue Apr 22, 2021 · 14 comments
Open

Extending <a> with more navigation capabilities #101

domenic opened this issue Apr 22, 2021 · 14 comments
Labels
addition A proposed addition which could be added later without impacting the rest of the API

Comments

@domenic
Copy link
Collaborator

domenic commented Apr 22, 2021

@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:

  • Add a replace="" boolean attribute which makes the navigation into a replacement navigation.
  • 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.)

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.

@domenic domenic added the addition A proposed addition which could be added later without impacting the rest of the API label Apr 22, 2021
@bathos
Copy link

bathos commented Apr 22, 2021

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
Copy link

bahrus commented Nov 13, 2021

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
Copy link
Collaborator Author

domenic commented Nov 16, 2021

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
Copy link

bathos commented Nov 17, 2021

We currently rely on both of these patterns:

  • <a href="/foo" replace>, where the global handler sees the attribute as "use replaceState, not pushState"
  • <a href="/foo" .state=...>, where state is a JS property used to provide the pushState/replaceState argument

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
Copy link

bahrus commented Nov 17, 2021

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
Copy link

bahrus commented Nov 17, 2021

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

@bahrus
Copy link

bahrus commented Nov 17, 2021

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
Copy link

@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
Copy link

bahrus commented Nov 17, 2021

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
Copy link

bahrus commented Nov 17, 2021

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
Copy link
Contributor

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
Copy link
Collaborator Author

domenic commented Mar 29, 2023

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

@natechapin
Copy link
Collaborator

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
Copy link
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addition A proposed addition which could be added later without impacting the rest of the API
Projects
None yet
Development

No branches or pull requests

7 participants