¶ IntroductionModern web applications have many moving parts, and traditional webapp routing is far too restrictive to deal with real-world apps. A modern webapp may have many independent bits of serializable state that must be correctly transmitted when a URL is sent to another user. For example, a music app may want to send the current song, position within the song, and location within a browsing window. A search app may want to transmit the current query, selected results, expansion of those results, and user preferences. It is not always possible to store complex state in localStorage or cookies, if you want to transmit that complex state to other users via a URL. It can very quickly become unwieldy to create massive 'multi-routes', where sections of the URL delegate to subrouters. Every time a new widget with state is added, a new section must be added to the route, and all links updated. Querystrings are a perfect solution to this problem, and with HTML5 pushState, they can easily be used on the client and the server. |
|
¶ Example
|
|
¶ Annotated Code |
|
CommonJS includes. This is a browserify module and will run both inside Node and in the browser. |
|
Backbone.History overrides. Type
Backbone.History
|
|
Override history constructor to init some properties and set the embedded query model listener. Type
Backbone.History
|
|
Extracts querystrings from routes. Type
RegExp
|
|
Model emcompassing current query state. You can read and set properties
on this Model and Type
Backbone.Model
|
|
Parse a fragment into a query object and call handlers matching. Params
fragment
String
Route fragment.
options
Object
Navigation options.
|
|
Save previous query. We intentionally do not use |
|
Diff new and old queries. |
|
Set embedded model to new query object, firing 'change' events. |
|
Call each function that subscribes to these items. This is intentional, rather than fire events on each changed item; this way, you don't have to debounce your handlers since they are only called once, even if multiple query items change. |
|
Compare previous base fragment to current base fragment. If it is the same, do not fire the url handler. Params
{String} fragment History fragment.
Returns
Boolean
True if a route was matched.
|
|
Add loadQuery hook. Params
fragment
String
History fragment.
options
Object
Navigation options.
|
|
Save base fragment for comparison in loadUrl. |
|
Fire querystring routes. |
|
Call navigate on prototype since we just overrode it. |
|
Navigate a base route only, while maintaining the current query. Strips any querystrings from the input fragment and appends the current querystring. Params
{String} fragment Route fragment.
{Object} options Navigation options.
|
|
When the query model changes, run all associated routes. Params
{Model} model Attached model.
{Object} options Change options.
|
|
If the old querystring exists, replace it with the new one, otherwise just append. |
|
No existing querystring, add it directly to the fragment. |
|
Add a query to be tested when the fragment changes. Params
{Array} bindings Query keys to listen to.
{Function} callback Callback to call when these keys change.
|
|
Reset the internal query model to a certain state. Performs set() and unset() internally to reset the model's attributes to the correct state, while firing the correct events. Similar to Backbone.Collection.reset(), but with model attributes rather than models. Params
queryObject
Object
New query object.
|
|
Suppresses intermediate 'change' events; 'change:key' will still fire.
This has the added benefit of making the internal |
|
Unset any keys inside the existing query. To disable,
set |
|
Set new keys. To disable, set |
|
Unset changing flag and fire change event. |
|
Given two objects, compute their differences and list them. When diffing deep objects, return one string for the object and one for each child. This allows functions to bind to deep properties or its parent. E.g. a change to a.b.c returns ['a', 'a.b', 'a.b.c'] This uses DeepDiff (flitbit/diff), which can detect changes deep within objects. We don't use objects in querystrings quite yet, but we do arrays. And that might change. Params
{Object} lhs Left hand object.
{Object} rhs Right hand (new) object.
Returns
Array
Array of string differences.
|
254
255 _getDiffs: function(lhs, rhs) {
256 var diffs = diff(lhs, rhs);
257 var diffKeys = _.reduce(diffs, function(result, diff) {
258 var paths = _.map(diff.path, function(path, i) {
259 return _.first(diff.path, i + 1).join('.');
260 });
261 return result.concat(paths);
262 }, []);
263 return _.uniq(diffKeys);
264 },
265
266
267 |
Given a fragment, return a query object. Params
{String} fragment Route fragment.
Returns
Object
Query object.
|
|
Given a fragment, return a query string. Params
{String} fragment Route fragment.
Returns
String
Query string.
|
|
Strip a querystring from a fragment. Params
{String} fragment Route fragment.
Returns
String
Fragment without query.
|
|
Backbone.Router overrides. Type
Backbone.Router
|
|
Bind query routes. Remember that handlers will only fire once per navigation. If for some reason you'd like a handler to fire for each individual change, bind to the 'change:{key}' events on Backbone.history.query, which is just a Backbone.Model (and fires all of the usual events). They are expected to be attached in the following configuration:
|
323
324 _bindRoutes: function() {
325 if (!this.queryRoutes) return;
326 this.queryRoutes = _.result(this, 'queryRoutes');
327 var qRoute, qRoutes = _.keys(this.queryRoutes);
328 while ((qRoute = qRoutes.pop()) != null) {
329 this.queryHandler(qRoute, this.queryRoutes[qRoute]);
330 }
331 RouterProto._bindRoutes.apply(this, arguments);
332 },
333
334
335 |
Navigate a base route only, while maintaining the current query.
Delegates to Params
{String} fragment Route fragment.
{Object} options Navigation options.
|
|
Bind a queryHandler. Very similar to Backbone.Router#route, except that args are provided by Backbone.history#queryHandler, rather than being extracted in the router from the fragment. Params
{String|array} bindings Query key bindings.
{String} [name] Listener name.
{Function} callback Listener callback.
|
352
353 queryHandler: function(bindings, name, callback) {
354 bindings = this._normalizeBindings(bindings);
355 if (_.isFunction(name)) {
356 callback = name;
357 name = '';
358 }
359 if (!callback) callback = this[name];
360 if (!callback) throw new Error("QueryHandler not found: " + this[name]);
361 var router = this;
362 Backbone.history.queryHandler(bindings, function(fragment, queryKeys, queryObj) {
363 router.execute(callback, [queryKeys, queryObj]);
364 router.trigger.apply(router, ['route:' + name].concat(queryKeys));
365 router.trigger('route', name, queryKeys, queryObj);
366 Backbone.history.trigger('route', router, name, queryKeys, queryObj);
367 });
368 return this;
369 },
370
371
372 |
Normalize bindings - convert to array and trim whitespace. Params
{String} bindings Bindings definition.
Returns
Array
Normalized bindings.
|
|
Override default Backbone.Router constructor. |
|
Replace Backbone.history. |