Last active
December 11, 2015 02:38
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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