emberjs / rfcs

RFCs for changes to Ember
https://rfcs.emberjs.com/
792 stars 408 forks source link

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

Open NullVoxPopuli opened 3 years ago

NullVoxPopuli commented 3 years ago

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 commented 3 years ago

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

sandstrom commented 3 years ago

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: https://github.com/emberjs/rfcs/issues/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 commented 3 years ago

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

NullVoxPopuli commented 3 years ago

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

jelhan commented 2 years ago

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:

NullVoxPopuli commented 2 years ago

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 commented 2 years ago

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. :sob:

NullVoxPopuli commented 2 years ago

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 commented 2 years ago

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

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 commented 2 years ago

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 commented 2 years ago

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

NullVoxPopuli commented 2 years ago

potentially. :thinking: 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 commented 2 years ago

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

sandstrom commented 2 years ago

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 (https://github.com/emberjs/rfcs/issues/787#issuecomment-1195685756)

wagenet commented 2 years ago

@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 commented 1 year ago

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