diff --git a/examples/route-constraints/app.js b/examples/route-constraints/app.js new file mode 100644 index 0000000000..758a71df47 --- /dev/null +++ b/examples/route-constraints/app.js @@ -0,0 +1,78 @@ +/** @jsx React.DOM */ +var React = require('react'); +var Router = require('../../modules/main'); +var Route = Router.Route; +var Link = Router.Link; + +var NotFound = React.createClass({ + render : function() { return

{'404 - Not Found'}

; } +}); + +var App = React.createClass({ + mixins : [Router.Constrainable], + + statics : { + redirectTo : '404', + paramConstraints : { + userId : /^\d+$/ + } + }, + + render: function() { + return ( +
+ + {this.props.activeRoute} +
+ ); + } +}); + +var User = React.createClass({ + mixins : [Router.Constrainable], + + statics : { + redirectTo : '404', + paramConstraints : { + userId : /^\d+$/ + } + }, + + render: function() { + return ( +
+

User id: {this.props.params.userId}

+ + {this.props.activeRoute} +
+ ); + } +}); + +var Task = React.createClass({ + render: function() { + return ( +
+

User id: {this.props.params.userId}

+

Task id: {this.props.params.taskId}

+
+ ); + } +}); + +var routes = ( + + + + + + +); + +React.renderComponent(routes, document.body); diff --git a/examples/route-constraints/index.html b/examples/route-constraints/index.html new file mode 100644 index 0000000000..1c018b491d --- /dev/null +++ b/examples/route-constraints/index.html @@ -0,0 +1,5 @@ + +Route Constraints Example + + + diff --git a/modules/main.js b/modules/main.js index d8b4c3587b..8935c787c2 100644 --- a/modules/main.js +++ b/modules/main.js @@ -1,5 +1,6 @@ exports.Link = require('./components/Link'); exports.Route = require('./components/Route'); +exports.Constrainable = require('./mixins/constrainable'); exports.goBack = require('./helpers/goBack'); exports.replaceWith = require('./helpers/replaceWith'); diff --git a/modules/mixins/constrainable.js b/modules/mixins/constrainable.js new file mode 100644 index 0000000000..5f69a9462e --- /dev/null +++ b/modules/mixins/constrainable.js @@ -0,0 +1,67 @@ +function isObject(val) { + return typeof val === 'object'; +} + +function isRegExp(val) { + return val instanceof RegExp; +} + +var Constrainable = { + statics: { + willTransitionTo : function(transition, params) { + if (!this.validatePath(transition.path) || !this.validateParams(params)) { + transition.redirect(this.redirectTo); + } + }, + + /** + * Uses this.pathConstraint (defined in the component's statics) to validate + * the current matched path. If this.pathConstraint is not defined, or it is + * not a RegExp, then this method will return true (permissive by default). + * + * @param {string} path The path to validate against this.pathConstraint + * @return {bool} Whether the path matches the given constraint + */ + validatePath : function(path) { + if (! isRegExp(this.pathConstraint)) { + return true; + } + + return this.pathConstraint.test(path); + }, + + /** + * Uses this.paramConstraints (defined in the component's statics) to + * validate the current path's parameters. If this.paramConstraints is not + * defined or is not an object, then this method will return true. If a + * constraint is not provided for a particular parameter, it will assume + * that anything should match. + * + * @param {string} params The matched params to validate + * @return {bool} Whether the params matche the given constraints + */ + validateParams : function(params) { + if (! isObject(this.paramConstraints)) { + return true; + } + + for (var param in params) { + if (! params.hasOwnProperty(param)) { + continue; + } + + if (! isRegExp(this.paramConstraints[param])) { + continue; + } + + if (! this.paramConstraints[param].test(params[param])) { + return false; + } + } + + return true; + } + } +}; + +module.exports = Constrainable; diff --git a/specs/constrainable.spec.js b/specs/constrainable.spec.js new file mode 100644 index 0000000000..596253e4f9 --- /dev/null +++ b/specs/constrainable.spec.js @@ -0,0 +1,123 @@ +require('./helper'); +var Constrainable = require('../modules/mixins/constrainable'); + +describe('Constrainable', function() { + describe('validatePath', function() { + it('returns true when pathConstraint is not set', function () { + var constrained = Object.create(Constrainable); + + expect(constrained.statics.validatePath('/abc')).toBe(true); + }); + + it('returns true when pathConstraint is set and matches', function () { + var constrained = Object.create(Constrainable); + constrained.statics.pathConstraint = /^\/[A-Za-z]+$/; + expect(constrained.statics.validatePath('/abc')).toBe(true); + }); + + it('returns false when pathConstraint is set and does not match', function () { + var constrained = Object.create(Constrainable); + constrained.statics.pathConstraint = /^\/[A-Za-z]+$/; + expect(constrained.statics.validatePath('/123')).toBe(false); + }); + }); + + describe('validateParams', function() { + it('returns true when paramConstraints is not set', function () { + var constrained = Object.create(Constrainable); + + expect(constrained.statics.validateParams({ + alpha : 'abc', + numeric : '123' + })).toBe(true); + }); + + it('returns true when paramConstraints is set and params match', function () { + var constrained = Object.create(Constrainable); + + constrained.statics.paramConstraints = { + alpha : /^[A-Za-z]+$/, + numeric : /^\d+$/ + }; + + expect(constrained.statics.validateParams({ + alpha : 'abc', + numeric : '123' + })).toBe(true); + }); + + it('returns true when paramConstraints is set and params do not match', function () { + var constrained = Object.create(Constrainable); + + constrained.statics.paramConstraints = { + alpha : /^[A-Za-z]+$/, + numeric : /^\d+$/ + }; + + expect(constrained.statics.validateParams({ + alpha : '123', + numeric : 'abc' + })).toBe(false); + }); + + it('returns correct value when not all params have constraints', function () { + var constrained = Object.create(Constrainable); + + constrained.statics.paramConstraints = { + alpha : /^[A-Za-z]+$/ + }; + + expect(constrained.statics.validateParams({ + alpha : 'abc', + noConstraint : 'abc123' + })).toBe(true); + }); + }); +}); + +// describe('Path.testConstraints', function () { +// it('returns false when one or more constraints fail', function () { +// var params = { +// id : 123, +// name : 'Abc' +// }; + +// var constraints = { +// id : /^\d+$/, +// name : /^[a-z]+$/ +// }; + +// expect(Path.testConstraints(params, constraints)).toBe(false); +// }); + +// it('returns true when constraints pass', function () { +// var params = { +// id : 123, +// name : 'Abc' +// }; + +// var constraints = { +// id : /^\d+$/, +// name : /^[A-Za-z]+$/ +// }; + +// expect(Path.testConstraints(params, constraints)).toBe(true); +// }); +// }); + +// describe('when a pattern has dynamic segments with constraints', function() { +// var pattern = '/comments/:id/edit', +// constraints = { +// id : /\d+/ +// }; + +// describe('and the constraints match', function() { +// expect(Path.extractParams(pattern, '/comments/123/edit', constraints)) +// .toEqual({ id : 123 }); +// }); + +// describe('and the constraints do not match', function() { +// expect(Path.extractParams(pattern, '/comments/abc/edit', constraints)) +// .toBe(null); +// }); +// }); diff --git a/specs/main.js b/specs/main.js index f84f98c5f9..51033eedb6 100644 --- a/specs/main.js +++ b/specs/main.js @@ -5,4 +5,4 @@ require('./Path.spec.js'); require('./Route.spec.js'); require('./RouteStore.spec.js'); require('./URLStore.spec.js'); - +require('./constrainable.spec.js');