Skip to content

RFC: support URL fragments in ui-sref #701

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

Closed
beyang opened this issue Dec 19, 2013 · 22 comments
Closed

RFC: support URL fragments in ui-sref #701

beyang opened this issue Dec 19, 2013 · 22 comments

Comments

@beyang
Copy link
Contributor

beyang commented Dec 19, 2013

Proposal is to support # URL fragments/hashes in ui-sref. So you could do:
<a ui-sref="mystate(myparams)#myhash>...</a>

It appears that $state.go(href) already supports having a hash in href, so all this would require is making a change to the ui-sref directive (make it parse the hash and then pass this to $state.go).

@timkindberg
Copy link
Contributor

I'm fine with this.

@mismith
Copy link

mismith commented Mar 5, 2014

+1. Any update on this?

@timkindberg
Copy link
Contributor

@mismith I don't think anyone claimed it to work on. You want to?

@mismith
Copy link

mismith commented Mar 5, 2014

@timkindberg: I'll take a look at the source, but I don't know if I'm quite qualified :S

@mismith
Copy link

mismith commented Mar 5, 2014

Just thinking about this now, I don't think this would be particularly useful unless you could specify an expression as your hash, which makes the syntax @beyang proposes problematic because ui-sref="mystate(myparams)#'myhash'" would break semantic flow.

Would something like this make more sense: ui-sref="mystate({myparam1: myvalue1, myparam2: myvalue2, '#': myhashexpression})"?

Either way, a quick regex change here would be the place to start, right?

@timkindberg
Copy link
Contributor

I'd be fine with that I think

@christopherthielen
Copy link
Contributor

how does hash targetting work when html5Mode(false) where the url hash is used to represent the state? Is the idea that a named anchor is brought into view, but the URL's hash still represents the state?

@mismith
Copy link

mismith commented Mar 7, 2014

Good point @christopherthielen, not sure what's best there. Also on further consideration I realized you could use ui-sref="mystate(myparams)#{{ myhashexpression }}"-type syntax in addition to specifying a '#' property in the myparams object.

I'm not going to tackle this any further because it's far beyond my ability, but if I were, this is how I approach it:

Update stateDirectives.js#parseStateRef to:

function parseStateRef(ref) {
  var parsed = ref.replace(/\n/g, " ").match(/^([^(]+?)\s*(\((.*)\))?(#(.+?))?$/);
  if (!parsed || parsed.length < 4) throw new Error("Invalid state ref '" + ref + "'");
  return { state: parsed[1], paramExpr: parsed[3] || null, hashStr: parsed[5] };
}

In stateDirectives.js#$StateRefDirective's private update function, add before var newHref = …:

if (ref.hashStr) {
 if (angular.isObject(params)) {
    params['#'] = ref.hashStr;
  } else {
    params = { '#': ref.hashStr };
  }
}

And finally, at the end of state.js#$state.href, add:

if(params['#']) url += '#' + params['#'];

Completely untested (and likely even broken), but it's a barebones concept of what we'd need to get this running. Thoughts?

@albertboada
Copy link

Implementation is far beyond me as well, but +1 for the feature!!
With this, $uiViewScrollProvider.useAnchorScroll() will start making a lot more sense.

Both

  • ui-sref="mystate(myparams)#{{ myhashexpression }}"
  • ui-sref="mystate({myparam1: myvalue1, myparam2: myvalue2, '#': myhashexpression})"

look perfect!

@blowsie
Copy link

blowsie commented Apr 28, 2014

+1

@demetriusnunes
Copy link

For people looking for a workaround for this problem, I managed to find a temporary, but good solution. Define 2 routes, one with the # only with the url paramater in order for ui-sref and $state.href to be able to generate the urls correctly. Define another one without the # in the url option and with the actual options for controller, template, etc. This is the one that will get matched. Like this:

    .state('channel.content', {
        url: '/course/:courseSlug/content#:lessonPos,:contentPos',
    })
    .state('channel.content2', {
        url: '/course/:courseSlug/content',
        templateUrl: '/app/views/content.html',
        controller: 'contentCtrl'
    })

Note that in contentCtrl I have to parse the hash parameters from $location.hash(), not from $stateParams.

I am using this with HTML5 mode enabled. Don't know if it works with hashbang mode.

@mismith
Copy link

mismith commented May 15, 2014

I went ahead and gave it a shot, and got it working as I needed—no need for duplicate routes, and query params work as normal. Here are the 5 changes I made, in diff form: http://www.diffchecker.com/i9b9aeeq

Both formats work:

  • ui-sref="mystate(myparams)#{{ myhashexpression }}"
  • ui-sref="mystate({myparam1: myvalue1, myparam2: myvalue2, '#': myhashexpression})"

@sadlerw
Copy link

sadlerw commented Jul 3, 2014

👍 for the feature. @mismith can you submit as a PR?

@AkenRoberts
Copy link

For anyone else struggling with this particular issue, here's how I went around it:

angular.module('app').config(['$provide', function ($provide) {

  /**
   * Extend the UI Router $stateProvider, adding the ability to specify an
   * anchor hash as a parameter in the ui-sref directive.
   */
  $provide.decorator('$state', ['$delegate', function ($stateProvider) {

    // Save the orignal function for generating a state's URL.
    var $stateHref = $stateProvider.href;

    // Create our extended function.
    $stateProvider.href = function href(stateOrName, params, options) {
      var hash = '';
      params = params || {};
      var hashParam = params['#'];

      // Check if the anchor parameter was specified.
      if (typeof hashParam !== 'undefined') {
        // Ensure hash parameter is a string and not empty.
        if ((typeof hashParam === 'string' || hashParam instanceof String) && hashParam.length) {
          hash = '#' + hashParam;
        }

        delete params['#'];
      }

      // Return the original parsed URL with the hash appended.
      return $stateHref(stateOrName, params, options) + hash;
    };

    return $stateProvider;
  }]);

}]);

$state.href is where the state identifier / params / options are parsed into a path (as best as I can tell, anyway). The code is pretty easy to read, but the workflow is pretty much:

  1. Original function is saved, so it can be used again (not sure if there's a more OOP way to do this -- thoughts?)
  2. Checks if there is a # parameter supplied. If so, save its value, then remove it from the params object.
  3. Generate the path using the original function.
  4. Return the generated path with the anchor appended.

This is used by simply specifying a # parameter as part of your ui-sref directive:

<a ui-sref="my.state({ id: Model.id, '#': IDToScrollTo })" class="btn">

Untested with HTML5 mode enabled, as we aren't using that in our app right now.

@mismith
Copy link

mismith commented Apr 1, 2015

@cryode HTML5 user here. Looks great and technically works—in that the href tag gets filled with the proper #-having URL—but the problem is if a user clicks this link ui-router ignores the changes made and moves to the non-#-having URL anyway, essentially rendering the changes useless. I think there needs to be some deeper integration into ui-router, primarily in where it generate the string it'll replace the location bar URL with via HTML5

@AkenRoberts
Copy link

@mismith Thank you for the feedback with HTML5! I actually realized not too long after making this post that the above really only affects the URL generated with ui-sref, and not much else (as you've unfortunately discovered).

The problem lies with how UI router generates and handles URL parameters (both URI segments and search params as a whole). The process is done in a few places, and the code to do such is duplicated, rather than performed in a central location. I haven't taken the time to confirm if that is done with good reason, or is simply an oversight. Either way, the solution was far beyond implementation in the application level, and more than likely will need to be done on UI Router's end.

I will try to reply with the specifics of where the fragment breaks down on both sides (generation and consumption), and see if I can't work with someone here (a core contributor or otherwise) to get us to a working solution.

@mismith
Copy link

mismith commented Apr 1, 2015

That mirrors what I found when I looked into it myself. I'd be happy to help but ui-router is a pretty large project and I don't wanna screw anything up.

I know @nateabele offered to help coach a bit in #1455... :)

@AkenRoberts
Copy link

That's good news! Maybe I'll spend one of my evenings here putting together at least a recommended solution. Maybe not a full blown PR (I'll have to see what UI Router prefers re: PR format, code), but at least work with a couple of you to move this forward!

@mismith
Copy link

mismith commented Apr 1, 2015

I'm digging into things a bit over at #1187 too

@kikar
Copy link

kikar commented May 26, 2015

Any news?

@pinocchio-geppetto
Copy link

Its supported. You need to download ui-router v0.2.14.

Refer to this #1867. The syntax is {'#': 'frag'}

@samguergen
Copy link

samguergen commented Feb 12, 2018

These workarounds are hacks that really shouldn't take that many lines of code. Any chance to add the ui-sref="nav#anchor" feature? It's really one of the perks of traditional a href nav and a major thumbs down if ui-router can't feature it...

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

No branches or pull requests