Skip to content

Commit

Permalink
[added] onAbortedTransition, onActiveStateChange, onTransitionError R…
Browse files Browse the repository at this point in the history
…outes props

Also, removed Routes.handleAsyncError and Routes.handleCancelledTransition
  • Loading branch information
mjackson committed Aug 14, 2014
1 parent 58073ca commit 6878120
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 101 deletions.
68 changes: 39 additions & 29 deletions modules/components/Routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,36 @@ var NAMED_LOCATIONS = {
disabled: RefreshLocation // TODO: Remove
};

/**
* The default handler for aborted transitions. Redirects replace
* the current URL and all others roll it back.
*/
function defaultAbortedTransitionHandler(transition) {
var reason = transition.abortReason;

if (reason instanceof Redirect) {
replaceWith(reason.to, reason.params, reason.query);
} else {
goBack();
}
}

/**
* The default handler for active state updates.
*/
function defaultActiveStateChangeHandler(state) {
ActiveStore.updateState(state);
}

/**
* The default handler for errors that were thrown asynchronously
* while transitioning. The default behavior is to re-throw the
* error so that it isn't silently swallowed.
*/
function defaultTransitionErrorHandler(error) {
throw error; // This error probably originated in a transition hook.
}

/**
* The <Routes> component configures the route hierarchy and renders the
* route matching the current location when rendered into a document.
Expand All @@ -39,33 +69,10 @@ var NAMED_LOCATIONS = {
var Routes = React.createClass({
displayName: 'Routes',

statics: {

/**
* Handles errors that were thrown asynchronously. By default, the
* error is re-thrown so we don't swallow them silently.
*/
handleAsyncError: function (error, route) {
throw error; // This error probably originated in a transition hook.
},

/**
* Handles aborted transitions. By default, redirects replace the
* current URL and all others roll it back.
*/
handleAbortedTransition: function (transition, routes) {
var reason = transition.abortReason;

if (reason instanceof Redirect) {
replaceWith(reason.to, reason.params, reason.query);
} else {
goBack();
}
}

},

propTypes: {
onAbortedTransition: React.PropTypes.func.isRequired,
onActiveStateChange: React.PropTypes.func.isRequired,
onTransitionError: React.PropTypes.func.isRequired,
preserveScrollPosition: React.PropTypes.bool,
location: function (props, propName, componentName) {
var location = props[propName];
Expand All @@ -77,6 +84,9 @@ var Routes = React.createClass({

getDefaultProps: function () {
return {
onAbortedTransition: defaultAbortedTransitionHandler,
onActiveStateChange: defaultActiveStateChangeHandler,
onTransitionError: defaultTransitionErrorHandler,
preserveScrollPosition: false,
location: HashLocation
};
Expand Down Expand Up @@ -176,9 +186,9 @@ var Routes = React.createClass({

var promise = syncWithTransition(routes, transition).then(function (newState) {
if (transition.isAborted) {
Routes.handleAbortedTransition(transition, routes);
routes.props.onAbortedTransition(transition);
} else if (newState) {
ActiveStore.updateState(newState);
routes.props.onActiveStateChange(newState);
}

return transition;
Expand All @@ -188,7 +198,7 @@ var Routes = React.createClass({
promise = promise.then(undefined, function (error) {
// Use setTimeout to break the promise chain.
setTimeout(function () {
Routes.handleAsyncError(error, routes);
routes.props.onTransitionError(error);
});
});
}
Expand Down
154 changes: 82 additions & 72 deletions specs/Route.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require('./helper');
var Route = require('../modules/components/Route');
var Routes = require('../modules/components/Routes');
var URLStore = require('../modules/stores/URLStore');

var App = React.createClass({
displayName: 'App',
Expand All @@ -9,113 +10,122 @@ var App = React.createClass({
}
});

describe('a Route that matches a URL', function () {
it('returns an array', function () {
var routes = renderComponent(
Routes(null,
Route({ handler: App },
Route({ path: '/a/b/c', handler: App })
)
)
);

var matches = routes.match('/a/b/c');
assert(matches);
expect(matches.length).toEqual(2);

var rootMatch = getRootMatch(matches);
expect(rootMatch.params).toEqual({});
describe('Route', function() {

removeComponent(routes);
afterEach(function() {
URLStore.teardown();
window.location.hash = '';
});

describe('that contains dynamic segments', function () {
it('returns an array with the correct params', function () {
describe('a Route that matches a URL', function () {
it('returns an array', function () {
var routes = renderComponent(
Routes(null,
Route({ handler: App },
Route({ path: '/posts/:id/edit', handler: App })
Route({ path: '/a/b/c', handler: App })
)
)
);

var matches = routes.match('/posts/abc/edit');
var matches = routes.match('/a/b/c');
assert(matches);
expect(matches.length).toEqual(2);

var rootMatch = getRootMatch(matches);
expect(rootMatch.params).toEqual({ id: 'abc' });
expect(rootMatch.params).toEqual({});

// this causes tests to fail, no clue why ...
//removeComponent(routes);
});

describe('that contains dynamic segments', function () {
it('returns an array with the correct params', function () {
var routes = renderComponent(
Routes(null,
Route({ handler: App },
Route({ path: '/posts/:id/edit', handler: App })
)
)
);

var matches = routes.match('/posts/abc/edit');
assert(matches);
expect(matches.length).toEqual(2);

var rootMatch = getRootMatch(matches);
expect(rootMatch.params).toEqual({ id: 'abc' });

removeComponent(routes);
//removeComponent(routes);
});
});
});
});

describe('a Route that does not match the URL', function () {
it('returns null', function () {
var routes = renderComponent(
Routes(null,
Route({ handler: App },
Route({ path: '/a/b/c', handler: App })
describe('a Route that does not match the URL', function () {
it('returns null', function () {
var routes = renderComponent(
Routes(null,
Route({ handler: App },
Route({ path: '/a/b/c', handler: App })
)
)
)
);
);

expect(routes.match('/not-found')).toBe(null);
expect(routes.match('/not-found')).toBe(null);

removeComponent(routes);
//removeComponent(routes);
});
});
});

describe('a nested Route that matches the URL', function () {
it('returns the appropriate params for each match', function () {
var routes = renderComponent(
Routes(null,
Route({ handler: App },
Route({ name: 'posts', path: '/posts/:id', handler: App },
Route({ name: 'comment', path: '/posts/:id/comments/:commentId', handler: App })
describe('a nested Route that matches the URL', function () {
it('returns the appropriate params for each match', function () {
var routes = renderComponent(
Routes(null,
Route({ handler: App },
Route({ name: 'posts', path: '/posts/:id', handler: App },
Route({ name: 'comment', path: '/posts/:id/comments/:commentId', handler: App })
)
)
)
)
);
);

var matches = routes.match('/posts/abc/comments/123');
assert(matches);
expect(matches.length).toEqual(3);
var matches = routes.match('/posts/abc/comments/123');
assert(matches);
expect(matches.length).toEqual(3);

var rootMatch = getRootMatch(matches);
expect(rootMatch.route.props.name).toEqual('comment');
expect(rootMatch.params).toEqual({ id: 'abc', commentId: '123' });
var rootMatch = getRootMatch(matches);
expect(rootMatch.route.props.name).toEqual('comment');
expect(rootMatch.params).toEqual({ id: 'abc', commentId: '123' });

var postsMatch = matches[1];
expect(postsMatch.route.props.name).toEqual('posts');
expect(postsMatch.params).toEqual({ id: 'abc' });
var postsMatch = matches[1];
expect(postsMatch.route.props.name).toEqual('posts');
expect(postsMatch.params).toEqual({ id: 'abc' });

removeComponent(routes);
//removeComponent(routes);
});
});
});

describe('multiple nested Router that match the URL', function () {
it('returns the first one in the subtree, depth-first', function () {
var routes = renderComponent(
Routes(null,
Route({ handler: App },
Route({ path: '/a', handler: App },
Route({ path: '/a/b', name: 'expected', handler: App })
),
Route({ path: '/a/b', handler: App })
describe('multiple nested Router that match the URL', function () {
it('returns the first one in the subtree, depth-first', function () {
var routes = renderComponent(
Routes(null,
Route({ handler: App },
Route({ path: '/a', handler: App },
Route({ path: '/a/b', name: 'expected', handler: App })
),
Route({ path: '/a/b', handler: App })
)
)
)
);
);

var matches = routes.match('/a/b');
assert(matches);
expect(matches.length).toEqual(3);
var matches = routes.match('/a/b');
assert(matches);
expect(matches.length).toEqual(3);

var rootMatch = getRootMatch(matches);
expect(rootMatch.route.props.name).toEqual('expected');
var rootMatch = getRootMatch(matches);
expect(rootMatch.route.props.name).toEqual('expected');

removeComponent(routes);
//removeComponent(routes);
});
});
});

Expand Down
90 changes: 90 additions & 0 deletions specs/Routes.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
require('./helper');
var URLStore = require('../modules/stores/URLStore');
var Route = require('../modules/components/Route');
var Routes = require('../modules/components/Routes');

describe('Routes', function() {

afterEach(function() {
URLStore.teardown();
window.location.hash = '';
});

describe('a change in active state', function () {
it('triggers onActiveStateChange', function (done) {
var App = React.createClass({
render: function () {
return React.DOM.div();
}
});

function handleActiveStateChange(state) {
assert(state);
removeComponent(routes);
done();
}

var routes = renderComponent(
Routes({ onActiveStateChange: handleActiveStateChange },
Route({ handler: App })
)
);
});
});

describe('a cancelled transition', function () {
it('triggers onCancelledTransition', function (done) {
var App = React.createClass({
statics: {
willTransitionTo: function (transition) {
transition.abort();
}
},
render: function () {
return React.DOM.div();
}
});

function handleCancelledTransition(transition) {
assert(transition);
removeComponent(routes);
done();
}

var routes = renderComponent(
Routes({ onCancelledTransition: handleCancelledTransition },
Route({ handler: App })
)
);
});
});

describe('an error in a transition hook', function () {
it('triggers onTransitionError', function (done) {
var App = React.createClass({
statics: {
willTransitionTo: function (transition) {
throw new Error('boom!');
}
},
render: function () {
return React.DOM.div();
}
});

function handleTransitionError(error) {
assert(error);
expect(error.message).toEqual('boom!');
removeComponent(routes);
done();
}

var routes = renderComponent(
Routes({ onTransitionError: handleTransitionError },
Route({ handler: App })
)
);
});
});

});

0 comments on commit 6878120

Please sign in to comment.