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

Support navigating to hash URLs for in-page navigation #709

Open
NullVoxPopuli opened this issue Jan 19, 2021 · 16 comments
Open

Support navigating to hash URLs for in-page navigation #709

NullVoxPopuli opened this issue Jan 19, 2021 · 16 comments

Comments

@NullVoxPopuli
Copy link
Contributor

if supported, maybe hash / anchor / id navigation would look like this in user-land:

export default class Router extends EmberRouter {
  location = config.locationType;
  rootURL = config.rootURL;

  constructor() {
    super(...arguments);
    this.on('routeDidChange', this.handleHashTarget);
  }

  willDestroy() {
    this.off('routeDidChange', this.handleHashTarget);
  }

  @action
  handleHashTarget() {
     schedule('afterRender', () => later(this, 'scrollToId', 16));
  }
  
  @action
  scrollToId() {
    let id = this.location.hash;
    
    if (id) {
      document.getElementById(id)?.scrollIntoView();
    }
  }
}

(and then letting the actual router (router_js) handle # navigations. atm, the navigation look like:

Attempting URL transition to /my-route#hashId
Attempting URL transition to /my-route

It just drops the hash

@jelhan
Copy link
Contributor

jelhan commented Jan 25, 2021

How would this play together with location type hash? I guess it could be only supported for history location?

@sandstrom
Copy link
Contributor

sandstrom commented Mar 6, 2021

I like this proposal!

Perhaps because I've proposed something similar earlier: 😄

What if the Ember Router introduced a new type of "sub-leaf" (could be called e.g. variation), that could be encoded as my/path#variation-{name} (for example)?

With routable variations we could, for example, show modals (i.e. routable modals) or toggle tabs on a page. Such route variations would share the modal hook (that hook wouldn't re-run when switching between variations) and the variation state would be accessible from templates and controllers (if Ember decides to keep them around).

Link to my similar idea: #662

We should think about what other use-cases there are. For example encoding open modals, scroll position, etc. As mentioned in my quoted issue above, there is some precedent in CSS :target, in using this for more than just scroll position.


@jelhan For location type hash we could split the hash on some character, e.g. /index.html#/my/route~hashId or some of the other valid chars as separators.

https://stackoverflow.com/questions/2849756/list-of-valid-characters-for-the-fragment-identifier-in-an-url/2849800

@NullVoxPopuli
Copy link
Contributor Author

Made a thing: https://github.com/CrowdStrike/ember-url-hash-polyfill

@NullVoxPopuli
Copy link
Contributor Author

location type hash would not / can't be supported, afaik

@jelhan
Copy link
Contributor

jelhan commented Nov 26, 2021

I think a first iteration on that feature should add support for URL fragments in general. This would enable different use cases. Scrolling to an anchor after transition would be one of them. Persisting state, which should not be sent to the server in the URL, would be another.

To support URLs fragments, I think the following changes are needed:

  1. RouterService.transitionTo() and RouterService.replaceWith() methods accept a hash as part of the existing options object. I think the following rules should apply:
    1. If hash is present, the URL fragment of the target URL should be set to the provided value.
    2. If hash is not present and
      1. transitionTo or replaceWith is invoked with route and/or models arguments, an existing URL fragment is removed.
      2. transitionTo or replaceWith is invoked with options has only, an existing URL fragment stays as it.
  2. RouterService.urlFor() accepts a hash as part of existing options object. If present it is added as URL fragment to the generated URL.
  3. RouterService.currentURL returns the current URL including the URL fragment. (I think this already works.)
  4. <LinkTo> accept a @hash argument.
    1. The URL fragment of the URL used as href attribute is,
      1. the value of @hash argument if set,
      2. not present if @hash argument is not set and either @route, @model and/or @models argument is set,
      3. the URL fragment of the current URL if not set and neither @route, nor @model nor @models arguments are set.
    2. @hash argument is treated the same as for RouterService.transitionTo() and RouterService.replaceWith() on the transition triggered on click.

Open questions:

  • Should a hash property be added to RouteInfo? The URL fragment is not associated with any specific route. But it would be helpful to have access to it in Transition.to and Transition.from. I guess it's the same question as if url property should be added to RouteInfo or not.
  • We we add support for hash option to Route.transitionTo(), Route.replaceWith(), Controller.transitionToRoute() and Controller.replaceRoute() even though they are deprecated? What to do with Replace.intermediateTransitionTo(), which is not (yet) deprecated?
  • Should we support transition type hash? Or should we limit support to transition type history at least for the first iteration?

@NullVoxPopuli
Copy link
Contributor Author

NullVoxPopuli commented Nov 26, 2021

Sounds good! I think we should swap @hash for @anchor tho, per the nomenclature here: https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_is_a_URL

This also avoids the naming overload we'd have with the (hash) helper

@jelhan
Copy link
Contributor

jelhan commented Nov 26, 2021

Sounds good! I think we should swap @hash for @anchor tho, per the nomenclature here: https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_is_a_URL

This also avoids the naming overload we'd have with the (hash) helper

Naming is very difficult here in my opinion. I used hash to follow the naming of URL interface. But it doesn't seem to be a clear case:

  1. In RFC 3986: Uniform Resource Identifier (URI): Generic Syntax it is called fragment.
  2. URL standard maintained by WHATWG uses both terms fragment and hash in 2.6 URLs. It calls the component of the URI fragment, while the attribute representing it in the Interfaces for URL manipulation is named hash.
  3. I also heard the term anchor you proposed often. But I wasn't able to find it in any spec with that meaning. In HTML spec I found the term anchor used in HTMLAnchorElement interface, which represents a <a> tag. But I didn't found it referencing a specific part of the URL.

I fully agree that it is confusing if {{hash}} helper would be used together with a @hash argument:

<LinkTo @query={{hash sortBy="title"}} @hash="person-13">

It doesn't get easier as existing argument for setting query parameters does not follow naming in URL interface and relevant specification. It is named @query for <LinkTo> and queryParams for the options object accepted by RouterService.transitionTo() etc. This is inline with RFC 3986, which reference them as query. But on URL interface has search and searchParams properties. URL standard maintained by WHATWG uses both terms (again): query for the URI component and search as attribute name on the interface for URL manipulation.

I would tend to follow the URL interface so that it maps well to native JavaScript API. But that is already not true for existing query and queryParams. 😭

@NullVoxPopuli
Copy link
Contributor Author

I used hash to follow the naming of URL interface.

oh no. even the URL docs are contradictory! 🙃

yeah, following URL interface is probs best

@jelhan
Copy link
Contributor

jelhan commented Nov 28, 2021

@jelhan For location type hash we could split the hash on some character, e.g. /index.html#/my/route~hashId or some of the other valid chars as separators.

There doesn't seem to be any character, which is allowed in a fragment but is not allowed in a path or query component of a URI. 😢

Accordingly to RFC 3986: Uniform Resource Identifier (URI): Generic Syntax a fragment may contain the following characters:

      fragment    = *( pchar / "/" / "?" )

That's exactly the same as for query component of an URI:

      query       = *( pchar / "/" / "?" )

And a subset of which is allowed in path component of an URI:

      path          = path-abempty    ; begins with "/" or is empty
                    / path-absolute   ; begins with "/" but not "//"
                    / path-noscheme   ; begins with a non-colon segment
                    / path-rootless   ; begins with a segment
                    / path-empty      ; zero characters

      path-abempty  = *( "/" segment )
      path-absolute = "/" [ segment-nz *( "/" segment ) ]
      path-noscheme = segment-nz-nc *( "/" segment )
      path-rootless = segment-nz *( "/" segment )
      path-empty    = 0<pchar>

      segment       = *pchar
      segment-nz    = 1*pchar
      segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" )
                    ; non-zero-length segment without any colon ":"

      pchar         = unreserved / pct-encoded / sub-delims / ":" / "@"

@sandstrom
Copy link
Contributor

I think it would be fine to split on an uncommon character, such as ~. I know there is a theoretical collision risk, but maybe we could just raise an exception instead of setting a hash where it would collide. Also, could make the separator character configurable (with a default), so anyone who happens to use ~ (or whatever we go for; $ and * could also work) could swap it out.

@wagenet
Copy link
Member

wagenet commented Jul 23, 2022

This would be a great feature. Is there an actual path to RFC here?

@NullVoxPopuli
Copy link
Contributor Author

NullVoxPopuli commented Jul 23, 2022

potentially. 🤔 the least hacky thing we'd need for this is to have some for he framework to let us know that rendering after a transition has "settled" (even keeping in mind that pages could spin up intervals and all that, which keep us from normal settled state)

The re-working of the routing layer may be a prereq for this tho, as the existing routing layer is opt-in to everything, which is kind of annoying to work with (QPs, no hashes at all, etc). We'd have to add in hash support to RouteInfo, etc, where keeping that information should just be default.

@wagenet
Copy link
Member

wagenet commented Jul 23, 2022

I wonder if the new Polaris router ideas would resolve this.

@sandstrom
Copy link
Contributor

I saw this comment in another thread:

I got feedback from a framework team member early this year that it is unlikely to land any RFC changing existing routing behavior currently. It sounded as framework team wants to have a clear vision for a Polaris router before touching existing stuff. Mainly to avoid unnecessary churn. I paused all activities on this topic due to that feedback.

— Jelhan (#787 (comment))

@wagenet
Copy link
Member

wagenet commented Aug 2, 2022

@sandstrom I was just on a core team meeting where the router stuff was being discussed. We want to be clear that active progress is being made here and very soon we should have some more public information and be able to help rope the community in a bit more!

@bryanhickerson
Copy link

@wagenet did things ever get to a place where you could share more publicly?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants