Backbone: router.navigate doesn't call router unless hash is changed

Created on 2 Oct 2011  ·  22Comments  ·  Source: jashkenas/backbone

I'm trying to use URLs without hashbangs which is causing some grief.

As I test I did this.

<a href="#" onclick="router.navigate('/albums/<%= album.id %>', true); return false;">Show</a>

The URL is updated but my router isn't called. At first I thought the route didn't match but if I refresh the page the route is successfully matched.

Then I tried this:

<a href="#" onclick="router.navigate('/albums/<%= album.id %>', true);">Show</a>

And everything works just fine, seems like the router is only called if a hashchange is triggered.

Most helpful comment

This is indeed an interesting behavior of router.navigate() that I've run into before when trying to force a route to reload. Here's what I ended up doing:

Backbone.history.loadUrl( Backbone.history.fragment )

That's not _explictly_ documented as part of the API, but it certainly gets the job done. Seems reasonable to me that router.navigate( 'current/path', true ) should trigger the route handler to be called again.

Anyone else care to chime in on this?

All 22 comments

Works if I remove the inital /
Guess it must be an exact match to your routes

This is indeed an interesting behavior of router.navigate() that I've run into before when trying to force a route to reload. Here's what I ended up doing:

Backbone.history.loadUrl( Backbone.history.fragment )

That's not _explictly_ documented as part of the API, but it certainly gets the job done. Seems reasonable to me that router.navigate( 'current/path', true ) should trigger the route handler to be called again.

Anyone else care to chime in on this?

I ran into this issue as well. Here's my specific use case:

I'm using media queries to provide a responsive interface for all screen sizes. My views have some elements in them that don't respond to media queries, so I wrote a function to listen to resize events and re-render the page if I need to. Rather than reference and render individual views I thought I would just call

router.navigate(Backbone.history.fragment, true)

I'd rather not have to reference Backbone.history at all. It would be best if the router had a refresh or reload method:

router.refresh(true);

I had the same issue. @Ansman can you show what are your routes ? My problem was that leading slash is automatically removed by history when getting the hash, as it is the default root.

Yes -- you should never have a leading slash ... either in your routes or in your navigate() calls. That said, there's a commit on master that forcefully strips all leading slashes, so this should no longer lead to buggy behavior.

Finally, yes, your router should only ever fire if the route has actually been changed.

I would like to chime in that an explict call to router.navigate("route", true) should trigger the route even if the route has not been changed in the hash

@danroberts It doesn't seem possible at the moment to force trigger the route if the hash hasn't changed. I tried router.navigate("route/", {trigger:true}); and it doesn't work. A quick hack is to simply call router.route_name().

@olalonde I've been using @wookiehangover's method above with good results. I just wrote a custom function that checks if the route i'm trying to go to is the same as the current hash, and then calls either router.navigate or history.loadURL.

It seems reasonable that this could be part of the options array, something like refresh:true if you want it to trigger the route regardless. Also, maybe an update of the documentation seems in order, since {trigger: true} is logically going to trigger the route...

Here's the problem I have with this closing this bug:

A key point of using backbone is to handle parsing of routes, and then calling a function based on the route. Lacking the ability to determine cases where a "reload" is ok through backbone directly, we have to do something to this effect:

        if (url == window.location.pathname + window.location.search) {
            Backbone.history.loadUrl(url);
            return false;
        }
        else if (app.router.isHandled(url)) {
            Backbone.history.navigate(url, {
                trigger: true
            });
            return false;
        }

Does this work? yea, but it does feel like this is a totally valid use case that would be nice to have in the platform.

FYI I saw #1214 before this thread and posted the same comment.

Backbone Router does not allow this type of action because it listen to a url change event to trigger any routed actions.

You need to intercept the control who trigger the action and reset the Router's action.

I created a jsFiddle example here:
http://goo.gl/wPulo

and a blog post here:
http://goo.gl/mJM2i

Ran into this issue today and would just like to add my 2 cents:

Seems like un-intuitive behavior that when explicitly setting trigger: true it won't trigger the event unless the hash has changed.

My vote is in favor of trigger: true triggering the route no matter the condition.

One real world example is when you want to refresh the current page you are on, that would be impossible to do with the current behavior of trigger: true.

+1 for KenPerkins method or something similar to be integrated into BB routers.

+1 for process navigate on the same page if { trigger: true } is passed

I have spent 3 hours trying to find why the roter.navigate() method did not work on my application. I think, BB docs should include a notification about this issue.

My ugly but quick solution was to call a router.navigate() method before the actual one.

router.navigate();
router.navigate("app", true);

For now, it worked. But if there was a refresh:true option, I would definitely use it...

Just leaving my thoughts on this as it's an issue that I just today encountered. It seems highly unintuitive that the route isn't called when you explicitly pass the {trigger: true} options hash to router.navigate. I would love to see the addition of a refresh:true option such as @onuradsay has suggested.

+1 for the practicality of @patrickod and @onuradsay's idea

+1. We should still be able to receive the event and then determine in our app if a refresh in called for. We can figure this out with many of the hacks mentioned above but it's not very clean. I think this behavior is common enough to put it in the Router.

I'm afraid it's not going to happen. The fact that folks are asking for it is actually more impetus to _remove_ trigger:true than it is to unconditionally fire the event. Let me explain:

Events in Backbone are all about being notified when state is changed. Just like how in models, doing:

model.set({title: "Boom"})
model.set({title: "Boom"})

... will not trigger an event the second time, as no state has been changed ... trying to navigate to the location that you're already at is _not_ a change in state, and will not trigger an event.

If you want to have a callback fire whenever a button is clicked -- just add the callback. Or if you want to use Backbone's events to do it -- just call object.trigger("myEvent")

Seems like instead of trigger:true it should assume true and have silent:false option.

If we don't add this functionality though, we will always have to do something like this in our button handlers -

if (Backbone.history.fragment === 'foo') {
    Backbone.history.loadUrl(Backbone.history.fragment);
 } else {
     router.navigate('foo', {'trigger': true});
}

Thanks @dankantor for the great trick!

If you do have to do this use case (to force reload of same page/hash), wouldn't you want to Backbone.history.getFragment() instead of history.fragment so you don't reach in to history's properties? Seems cleaner.

I created a Backbone.history.refresh method to force the reload. Then, in some projects, I override the default navigate behavior to reload regardless if hash has changed:

      _.extend(Backbone.History.prototype, {
            refresh: function() {
                this.loadUrl(this.fragment);
            }
        });

        var routeStripper = /^[#\/]/;
        var origNavigate = Backbone.History.prototype.navigate;
        Backbone.History.prototype.navigate = function (fragment, options) {
            var frag = (fragment || '').replace(routeStripper, '');
            if (this.fragment == frag)
                this.refresh();
            else
                origNavigate.call(this, fragment, options);
        };
Was this page helpful?
0 / 5 - 0 ratings

Related issues

rafde picture rafde  ·  9Comments

etler picture etler  ·  13Comments

spawnedc picture spawnedc  ·  9Comments

inf3rno picture inf3rno  ·  17Comments

rubiii picture rubiii  ·  12Comments