13

Let's say I'm using Backbone pushstate and I navigate to a page with query params:

domain.com/user/111?hello=123

When I execute this:

Backbone.history.navigate('/settings', true);

My settings page loads perfectly, but the ?hello=123 stays in the URL...

domain.com/settings?hello=123

And that query param stays in the URL everywhere I navigate the site...

TIMEX
  • 259,804
  • 351
  • 777
  • 1,080
  • Edit: I am using the https://github.com/jhudson8/backbone-query-parameters plugin. – TIMEX Mar 02 '13 at 23:42
  • Could you make the question a little more clear? You want to empty the `location.search` whenever navigating to another page? – Fabrício Matté Mar 03 '13 at 22:56
  • There seems to a issue posted about 2 days ago on [github](https://github.com/jhudson8/backbone-query-parameters/issues/29) too and the author does not seem to have worked on that feature. – Starx Mar 03 '13 at 23:05
  • Have you enabled the pushState? `Backbone.history.start( { pushState : true } );` – Starx Mar 03 '13 at 23:37

3 Answers3

19

Backbone routing and query parameters are an unhappy marriage. Problems are well documented in this GitHub issue.

The core problem is that Backbone.Router is designed to work with URL hash fragments as well as the pushState API. When using hash URLs the query string precedes the hash, and is never matched in the route. With pushState the query string is part of the URL fragment, and requires a different route expression.

Let's say you'd have a route search, and that route would optionally take parameters q, sort and type. As a query string that would look something like:

search?q=kittens&sort=asc&type=images

The problem is, that for users of older browsers, Backbone will revert to hashchange based routing, and the route will become:

?q=kittens&sort=asc&type=images#search

The plugin you use tries to work around this problem, but doesn't resolve the core issue.

If possible, you should consider not using query strings, and pass any state information using optional fragments in your route expressions. The previous example routes would then become:

//pushState
search/q/kittens/sort/asc/type/images

//hash fragment
#search/q/kittens/sort/asc/type/images

Using (optional) route parts and :captures (docs), you could represent this URL with the following expression:

var Router = Backbone.Router.extend({
  routes: {
    "search(/q/:query)(/sort/:sort)(/type/:type)": "search"
  },

  search: function(query, sort, type) {
    console.log(query, sort, type); //-> "kittens", "asc", "images" 
  }
});

As long as the route fragments are in the specified order, this will match urls with none, any and all parameters, for example:

search                       //->  undefined,  undefined, undefined
search/q/kittens/type/images //->  "kittens",  undefined, "images"
search/sort/asc/type/images  //->  undefined,  "asc",     "images"

This way you don't have to worry about third-party query string libraries or browser compatibility. And if you ask me, the latter type of URL looks cleaner as well.

jevakallio
  • 35,324
  • 3
  • 105
  • 112
  • 1
    Think putting too many parameters in the slash way goes against the principles of RESTful URL. Plus @TIMEX's issue is caused by the plugin. I know backbone is pushing the slash fashion but I do think get style parameter fits some specific functionalities like search best. – yujingz Apr 04 '13 at 07:12
  • Nice solution. This is just client-side so the idea of RESTful URLs doesn't apply here anyway. Any URL that works is ok really. – jasop Jul 08 '13 at 14:59
  • @jasop I disagree. RESTful URLs aren't just a matter of implementation details. They are user facing as well, and should ideally adhere to RESTful standards for the sake of consistency across the web. Of course, this is a rather benign breaking of RESTful conventions here. I'm not saying there aren't valid justifications for breaking convention, just that I think the fact that it's a client-side URL shouldn't be part of the justification. – acjay Jan 28 '14 at 15:17
  • @acjohnson55 Yes, for consistency it would be good to have the client URL also look RESTful. The point I was trying to make was that many people might be getting confused between the browser and services URLs. It is of utmost importance to make your services RESTful.. for many reasons I won't get into here. However the URL in the browser is separate to all of this. It would be nice to look like a RESTful URL, but browser URLs are nothing to do with REST whatsoever. It is important for people to understand the difference between the two. – jasop Jan 28 '14 at 23:45
2

You don't have to use the backbone-query-parameters plugin to deal with this anymore, just make sure that you have the latest version of Backbone and then overwrite the loadUrl method inside History.prototype.

// Regex to match search query strings
var searchStripper = /\?.*$/g;

// Attempt to load the current URL fragment. If a route succeeds with a
// match, returns `true`. If no defined routes matches the fragment,
// returns `false`.
loadUrl: function(fragmentOverride) {
  var fragment = this.fragment = this.getFragment(fragmentOverride);
  fragment = fragment.replace(searchStripper, ''); // remove the search query parameter
  var matched = _.any(this.handlers, function(handler) {
    if (handler.route.test(fragment)) {
      handler.callback(fragment);
      return true;
    }
  });
  return matched;
},
mateusmaso
  • 7,843
  • 6
  • 41
  • 54
  • Doesn't this also strip the URL params from the initial URL `user/111`, where @TIMEX presumably wants to keep the query string intact? It also doesn't solve the issue when using hash based URLs, where the query string is not part of the fragment at all. – jevakallio Mar 04 '13 at 14:04
0

Not sure if it's too late. I came to the same problem and I can say it with confidence: it's a bug caused by backbone-query-parameters.

I actually inspired by your post. In the early stage of my current project, I introduced backbone-query-parameters, it worked well. But later on, backbone made some changes so backbone-query-parameters cannot grap the parameters any more. Not until recently, I found the author upgraded backbone-query-parameters. I pulled it in again, and it worked.

Then, you know. I cam to the same problem and feel really frustrated. I never doubted about backbone-query-parameters, not until I saw your post.

I removed the plugin and equipped with my own code, now my history works like a charm.

There are numerous ways to fetch the get parameters. Mine is just one of them, but it does the job for me. Just for your reference I post it here.

  getParams: ->
    rtn = {}

    if window.location.search.length > 0
      queryStringRegex =  /^\?(.*)/
      match = queryStringRegex.exec window.location.search
      param_string = match[1]

      if param_string.length > 0
        params_array = param_string.split("&")

        for value in params_array
          temp = value.split("=")
          rtn[temp[0]] = decodeURI(temp[1])

    rtn

Just in case, my routes will be like this

"path(?*queryString)" : "pathAction"
yujingz
  • 2,271
  • 1
  • 25
  • 29