Like jQuery BBQ, but for Backbone. HashMate extends Backbone.History to store and respond to information contained in the URL's hash fragment. Useful for state management, referral handling, history, SEO, and more.
The purpose of backbone-hashMate is to save information about the position (or state) of either a given resource, or an entire document. This is most useful in single page apps using something similar to pushState
to manage history, where over the course of loading many distinct paths, a "backlog" of resource specific and global states might build up that may be useful to save between disparate instances.
For example, suppose that you operate some sort of single page news app at mynewsapp.com
. A user has landed on your page, and clicks over to an article, located at mynewsapp.com/article/1234
. They then click on a picture in the article, thereby maximizing it. Then they click on a "share" button below the maximized picture, in order to send that to a friend. How could we communicate this state - that the picture is maximized, and that it was shared by user XYZ? Of course, we could make a unique path fragment for this, something like mynewsapp.com/article/1234/picture/1?referrer=XYZ
, but this carries the risk of hurting SEO by creating duplicate content, not to mention muddling resource state into the path fragment.
A better way is to put all of this extra state and location information in the hash. Our example above could be rewritten like mynewsapp.com/article/1234#article/picture=1234&&referrer=XYZ
. Now, the information in the hash fragment is opaque to crawlers like Google. Using this format creates a logical separation between unique resources (path fragments), and the states and locations within those resources (hashes). Backbone.hashMate streamlines this process for Backbone apps.
Installing hashMate is easy. You can pull it from Bower...
bower install backbone-hashMate
...or grab it from NPM and manually include it as a script tag...
npm install backbone-hashMate --save
or just download this repo manually and include the file as a dependency. Make sure you load Backbone before loading Backbone.hashMate, otherwise you'll cause all sorts of trouble for yourself!
<script src="./lib/backbone.js"></script>
<script src="./lib/backbone-hashMate.js"></script>
You can check out a working demo at https://azaslavsky.github.io/backbone-hashMate/demo. While this works fine, there is a drawback: since gitbhub pages don't support aliases by default, the only entry point to the single page demo app is at /backbone-hashMate/demo
. URLs like /backbone-hashMate/demo/article/Some_Title
and /backbone-hashMate/demo/options
return a 404, even though there is routing support for them.
A better way to fire up the demo is to install the repository locally, then run the following in the CLI:
node demo/server.js
Then, point your browser to localhost:4040/backbone-hashMate/demo
, or any of the other valid paths mentioned above.
While the full API for hashMate is quite robust, allowing for very detailed use of the library, really there are only three steps to creating a hashMate enabled app. First, after loading both backbone and the backbone-hashMate library (in that order), Backbone's history is initialized using Backbone.history.start()
exactly as described in the Backbone docs, with the additional hashMate: true
property in the options to activate hashMate.
Backbone.history.start({
pushState: true,
hashMate: true
});
When you add routers, make sure that they are able to process the hash parameters that would be relevant to that route. For example:
//A sample router for testing
var MyRouter = Backbone.Router.extend({
//Private method to get the parameters for a particular group
_getHashParams: function(group){
var stripped;
var mapped = {};
var hash = Backbone.history.pluckHash(Backbone.history.parseHashString(), group);
//Remove route prefixes
for (var k in hash) {
stripped = k.split('/');
if (stripped.length === 2) {
mapped[stripped[1]] = hash[k];
}
}
return mapped;
},
routes: {
'sample/:id': 'sample',
'demo': 'demo'
},
sample: function(id){
var params = this._getHashParams('sample');
//Do something with the id and params
var sampleView = new SampleView({
params: params
});
Backbone.$('body').append(sampleView.el);
},
demo: function(){
var params = this._getHashParams('demo');
//Do something with the params
},
});
Then, whenever a state change occurs in your view, simply use Backbone.history.navigate
like always, setting the new "addHash" property to trigger the navigation
//This will work
Backbone.history.navigate('sample/1234', {
trigger: false,
addHash: {
'foo': 'bar'
}
});
//So will this
Backbone.history.navigate('sample/1234#foo=bar', {
trigger: false
});
##API ###Backbone.History An extended version of the default Backbone.History API
- class: .History
- instance
- .start(options)
- .navigate([fragment], [opts])
- .deleteHash([opts], [target]) ⇒
Object
- .pluckHash([params], [group]) ⇒
string
|Object
- .setHash(params, [target], [opts]) ⇒
Object
- .matchHashString(stringA, [stringB]) ⇒
boolean
- .parseHashString([string]) ⇒
Object
- .getHashString([string]) ⇒
Object
- .setHashString(params, [opts]) ⇒
string
- instance
####history.start(options) Extension of the default startup functionality; wraps the default method, available at: http://backbonejs.org/#History-start
Param | Type | Description |
---|---|---|
options | Object |
The default options object, but if both pushState and hashMate are true, it will enable router reaction to hash changes as well as popstate events |
####history.navigate([fragment], [opts]) Extension of the default navigation functionality; wraps the default method, available at: http://backbonejs.org/#Router-navigate
Param | Type | Description |
---|---|---|
[fragment] | string |
The new fragment |
[opts] | Object |
An extended version of the default options object, with the following properties available |
[opts.deleteHash=false] | boolean | Object |
True means we reset the entire hash, false means that nothing is cleared |
[opts.deleteHash.globals=false] | boolean | Array.<string> |
Setting true will clear all global variables, or an array can be specified for more granular deletion |
[opts.deleteHash.groups=false] | boolean | Array.<string> |
Setting true will clear all prefixed variables, or an array can be specified for more granular deletion |
[opts.addHash] | string | Object |
Either an encoded string or a key->value dictionary of hash parameters to be changed along with the fragment; this will be applied after the "clear" variables are processed |
[opts.forceTrigger=false] | boolean |
True forces a triggered URL to load, even if the URL matches the current one; only used it "opts.trigger" is also true |
[opts.replace=false] | boolean |
Works exactly like the default "navigate" implementation, see http://backbonejs.org/#Router-navigate |
[opts.trigger=false] | boolean |
Works exactly like the default "navigate" implementation, see http://backbonejs.org/#Router-navigate |
####history.deleteHash([opts], [target]) ⇒ Object
Clear all or part of the hash
Param | Type | Description |
---|---|---|
[opts] | Object |
No options means we clear the entire string |
[opts.params=false] | string | Array.<string> |
A string, or an array of them, of specifying a parameter to clear |
[opts.groups=false] | string | Array.<string> | boolean |
True means clear all grouped parameters; can also be array of specific groups to clear |
[opts.globals=false] | boolean |
True means clear all global parameters |
[opts.apply=true] | boolean |
True means the actual window.location.hash will be cleared immediately; if opts.target is set, this will be forced into a false state |
[target] | string |
The hash string that is being updated - this will default to window.location.hash if omitted |
Returns: Object
- The new hash string
####history.pluckHash([params], [group]) ⇒ string
| Object
Retrieve one or more hash parameters
Param | Type | Description |
---|---|---|
[params] | Array.<string> |
A string or a list of parameters to extract; not providing it means all parameters (either global or of the requested group) will be returned |
[group] | Array.<string> |
A string that serves as the group prefix for all provided parameters - if parameters have their own prefix, it will be overridden! |
Returns: string
| Object
- An object containing the requested hash parameters, or a single value if we only submit a single param
####history.setHash(params, [target], [opts]) ⇒ Object
Set one or more hash parameters
Param | Type | Description |
---|---|---|
params | string | Object |
Either an encoded URI string or a key value object representing hash parameters and their respective values |
[target] | string |
The hash string that is being updated - this will default to window.location.hash if omitted |
[opts] | Object |
Some options |
[opts.apply=true] | boolean |
If true, we'll just set the window.location.hash variable directly; otherwise, that responsibility falls to whatever function called this method |
[opts.replace=false] | boolean |
If true, replace URL instead of updating it, preventing a new history state from being recorded |
[opts.retrunLiteral=false] | boolean |
If true, instead of returning the processed string, this function will return the object literal that it was compiled from |
Returns: Object
- The new hash string
####history.matchHashString(stringA, [stringB]) ⇒ boolean
Compare a hashString against the currently set one
Param | Type | Description |
---|---|---|
stringA | string |
A hash string to try and match |
[stringB] | string |
A hash string to try and match (defaults to the current window.location.hash) |
Returns: boolean
- True if they match, false if they don't
####history.parseHashString([string]) ⇒ Object
Take a hash string, and split it into its constituent parameters
Param | Type | Description |
---|---|---|
[string] | string |
A hash string to parse, which will default wo window.location.hash if not provided |
Returns: Object
- A key value object representing hash parameters and their respective decoded values
####history.getHashString([string]) ⇒ Object
Grab the current hash string
Param | Type | Description |
---|---|---|
[string] | Object |
A hash string to return, which will default to window.location.hash if not provided |
Returns: Object
- The hash string, with the leading hash symbol symbol removed
####history.setHashString(params, [opts]) ⇒ string
Apply a new hash string
Param | Type | Description |
---|---|---|
params | Object |
A set of key value pairs to combine into a single encoded string, which we can then set as the hash |
[opts] | Object |
Some options |
[opts.apply=true] | boolean |
If true, we'll just set the window.location.hash variable directly; otherwise, that responsibility falls to whatever function called this method |
[opts.replace=false] | boolean |
If true, replace URL instead of updating it, preventing a new history state from being recorded |
Returns: string
- The resulting hash string
Backbone.hashMate is, as of now, only compatible with pushState
friendly browsers. If your application targets browsers that do not have this capability (basically any IE before IE 10), you might want to avoid hashMate until we add support for older hashChange
style browsers.
Some might also take issue with the fact that we are "modifying" an object we do not own, namely Backbone.History
. This is true, but unavoidable. Backbone automatically initializes a Backbone.History
instance as soon as it loads called Backbone.history
, which makes total sense, because a page could not possibly need more than one instance of history tracking. This means that the page's native and already initialized history tracking is tied to the Backbone.History
prototype. To remedy this, we'd need to make a new class that inherits from Backbone.History
, stop the already running Backbone.history
instance, and start a new instance of Backbone.OurClassThatInheritedFromHistory
. This would be a nightmare for end users. We'll just have to be vigilant about new versions of Backbone, and make sure that this library is still compatible.
You can give test suite for hashMate a quick run through in the browser of your choice here. You can also view results from local Chrome tests, or the entire browser compatibility suite.
Feel free to pull and contribute! If you do, please make a separate branch on your Pull Request, rather than pushing your changes to the Master. It would also be greatly appreciated if you ran the appropriate tests before submitting the request. Before submitting the request, you should do two or three sets of tests.
For unit testing the Chrome browser, which is the base target for functionality, type the following in the CLI:
gulp unit-chrome
To record the code coverage after your changes, use:
gulp coverage
And, if you have them all installed and are feeling so kind, you can also do the entire browser compatibility suite (Chrome, Canary, Firefox ESR, Firefox Developer Edition, IE 11, IE 10):
gulp unit-browsers
If you make changes that you feel need to be documented in the readme, please update the relevant files in the /docs
directory, then run:
gulp docs
The MIT License (MIT)
Copyright (c) 2014 Alex Zaslavksy
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.