Skip to content

Instantly share code, notes, and snippets.

@marksoper
Last active December 11, 2015 02:38
Show Gist options
  • Save marksoper/4531770 to your computer and use it in GitHub Desktop.
Save marksoper/4531770 to your computer and use it in GitHub Desktop.
Extension of Backbone.Router so that the router can function as a simple state machine while retaining normal Backbone Router characteristics
//
// StateRouter
//
// An example of an extended version of Backbone.Router
// that implements a simple state machine
// without altering the router's ability to function as a normal Backbone.Router
// states can be normal Backbone routes that function as intended
// states can also be non-route states that can't be routed into
// they're transitioned into using the transitionTo method
//
// IMPORTANT: This is untested, experimental code intended to express a pattern,
// not intended to be used verbatim without careful thought
//
var StateRouter = Backbone.Router.extend({
//
// when router gets initialized
// go through this.states and create route on the router for any state that defines a route property
//
initialize: function() {
this.states = this.states || {};
for (var stateName in this.states) {
if (this.states[stateName].hasOwnProperty("route")) {
this.createRouteHandlerForState(stateName, this.states[stateName]);
}
//
// IMPORTANT:
//
// a benefit of using the state machine approach is that it allows standardizing
// the handling of similar situations - like errors
//
// that standardized logic can be implemented here
//
// e.g. maybe you want to have a state.error boolean property that indicates the state is an error
// If the state.error flag is set, you may want to wrap the state.handler function with some standard error
// logic (like logging), or automatically generate a state.handler function if no specialized handling
// was needed for this error
//
}
this.stateHistory = [];
},
//
// transitionTo is how a state transition happens
//
transitionTo: function(destinationStateName, data) {
var destinationState = this.states[destinationStateName];
if (!destinationState) {
throw new Error("attempt to transition to a non-existant state: " + destinationStateName);
}
var stateHistoryEntry = {
stateName: destinationStateName,
transitionTime: Date.now()
};
if (data) {
stateHistoryEntry.data = data;
}
destinationState.handler.call(this, data);
// TODO: Should place limit here on the size of this.stateHistory -- e.g. only keep last 10 states or whatever
// So it doesn't grow indefinitely
this.stateHistory.push(stateHistoryEntry);
// TODO: This would be a good place to emit state transition event for anyone listening
},
//
// if state.route is defined
// generate a routeHandler that simply transitions to this state
// and register this route in the normal Backbone way
//
createRouteHandlerForState: function(stateName, state) {
//
var thisRouter = this;
//
// update thisRouter.routes if necessary
//
if (state.hasOwnProperty("route")) {
//
// generate a route handler that effects a state transition, passing arguments through as state transition data
//
thisRouter.routes = thisRouter.routes || {}; // this.routes can be previously defined literally as well, but if name conflict occurs, will be overwritten by routes spec'd in this.states
var routeHandlerName = "_routeHandlerForState_" + stateName; // create name for route handler with arbitrary prefix
thisRouter.routes[state.route] = routeHandlerName;
thisRouter[routeHandlerName] = function() {
// may not need to convert arguments into a proper array, but not fully confident in passing arguments directly
// looking forward to ES6 Rest Params !!!
var args = Array.prototype.slice.call(arguments);
thisRouter.transitionTo.apply(stateName, args);
};
thisRouter.route(state.route, routeHandlerName, thisRouter[routeHandlerName]); // use standard Backbone route registration logic
}
}
});
//
// An Example of StateRouter Usage
//
// For reference, here the standard Backbone Router example
// from Backbonejs.org
var AppRouter = Backbone.Router.extend({
routes: {
"help": "help", // #help
"search/:query": "search", // #search/kiwis
"search/:query/p:page": "search" // #search/kiwis/p7
},
help: function() {
// ...
},
search: function(query, page) {
// ...
}
});
// Here's how that same router would be defined using the StateRouter
var AppStateRouter = StateRouter.extend({
//
// define a states property instead of a routes property
// this.routes will be populated automatically, with
// routes added to this.routes for states that define a route property
//
states: {
"help": {
route: "help",
handler: "help"
},
"search": {
route: "search/:query",
handler: "search"
},
"paginatedSearch": {
route: "search/:query/p:page",
handler: "search"
},
"searchError": {
handler: "searchError"
}
},
help: function() {
// ...
},
search: function(query, page) {
// ...
},
searchError: function(err) {
// ... handle the error here
}
});
//
// Example of using the searchError state (e.g. in a view)
//
var router = new AppStateRouter();
// assume an error has occurred resulting in an error object err
router.transitionTo("searchError", err);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment