Skip to content

Added a $stateNotFound event which can prevent or redirect state transitions. #414

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 16, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 41 additions & 7 deletions src/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
}
}]);
}

return state;
}

Expand Down Expand Up @@ -220,6 +221,8 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $

var TransitionSuperseded = $q.reject(new Error('transition superseded'));
var TransitionPrevented = $q.reject(new Error('transition prevented'));
var TransitionAborted = $q.reject(new Error('transition aborted'));
var TransitionFailed = $q.reject(new Error('transition failed'));

root.locals = { resolve: null, globals: { $stateParams: {} } };
$state = {
Expand All @@ -236,20 +239,51 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
$state.transitionTo = function transitionTo(to, toParams, options) {
if (!isDefined(options)) options = (options === true || options === false) ? { location: options } : {};
toParams = toParams || {};
options = extend({ location: true, inherit: false, relative: null }, options);
options = extend({ location: true, inherit: false, relative: null, $retry: false }, options);

var from = $state.$current, fromParams = $state.params, fromPath = from.path;

var toState = findState(to, options.relative);

var evt;

if (!isDefined(toState)) {
if (options.relative) throw new Error("Could not resolve '" + to + "' from state '" + options.relative + "'");
throw new Error("No such state '" + to + "'");
// Broadcast not found event and abort the transition if prevented
var redirect = { to: to, toParams: toParams, options: options };
evt = $rootScope.$broadcast('$stateNotFound', redirect, from.self, fromParams);
if (evt.defaultPrevented) return TransitionAborted;

// Allow the handler to return a promise to defer state lookup retry
if (evt.retry) {
if (options.$retry) return TransitionFailed;
var retryTransition = $state.transition = $q.when(evt.retry);
retryTransition.then(function() {
if (retryTransition !== $state.transition) return TransitionSuperseded;
redirect.options.$retry = true;
return $state.transitionTo(redirect.to, redirect.toParams, redirect.options);
},
function() {
return TransitionAborted;
});
return retryTransition;
}

// Always retry once if the $stateNotFound was not prevented
// (handles either redirect changed or state lazy-definition)
to = redirect.to;
toParams = redirect.toParams;
options = redirect.options;
toState = findState(to, options.relative);
if (!isDefined(toState)) {
if (options.relative) throw new Error("Could not resolve '" + to + "' from state '" + options.relative + "'");
throw new Error("No such state '" + to + "'");
}
}
if (toState['abstract']) throw new Error("Cannot transition to abstract state '" + to + "'");
if (options.inherit) toParams = inheritParams($stateParams, toParams || {}, $state.$current, toState);
to = toState;

var toPath = to.path,
from = $state.$current, fromParams = $state.params, fromPath = from.path;
var toPath = to.path;

// Starting from the root of the path, keep all levels that haven't changed
var keep, state, locals = root.locals, toLocals = [];
Expand All @@ -272,7 +306,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
toParams = normalize(to.params, toParams || {});

// Broadcast start event and cancel the transition if requested
var evt = $rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams);
evt = $rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams);
if (evt.defaultPrevented) return TransitionPrevented;

// Resolve locals for the remaining states, but don't update any global state just
Expand All @@ -288,7 +322,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
resolved = resolveState(state, toParams, state===to, resolved, locals);
}

// Once everything is resolved, wer are ready to perform the actual transition
// Once everything is resolved, we are ready to perform the actual transition
// and return a promise for the new state. We also keep track of what the
// current promise is, so that we can detect overlapping transitions and
// keep only the outcome of the last transition.
Expand Down
104 changes: 104 additions & 0 deletions test/stateSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ describe('state', function () {
$rootScope.$on('$stateChangeStart', eventLogger);
$rootScope.$on('$stateChangeSuccess', eventLogger);
$rootScope.$on('$stateChangeError', eventLogger);
$rootScope.$on('$stateNotFound', eventLogger);
}));


Expand Down Expand Up @@ -139,6 +140,109 @@ describe('state', function () {
expect(resolvedError(promise)).toBeTruthy();
}));

it('triggers $stateNotFound', inject(function ($state, $q, $rootScope) {
initStateTo(E, { i: 'iii' });
var called;
$rootScope.$on('$stateNotFound', function (ev, redirect, from, fromParams) {
expect(from).toBe(E);
expect(fromParams).toEqual({ i: 'iii' });
expect(redirect.to).toEqual('never_defined');
expect(redirect.toParams).toEqual({ x: '1', y: '2' });

expect($state.current).toBe(from); // $state not updated yet
expect($state.params).toEqual(fromParams);
called = true;
});
var message;
try {
$state.transitionTo('never_defined', { x: '1', y: '2' });
} catch(err) {
message = err.message;
}
$q.flush();
expect(message).toEqual('No such state \'never_defined\'');
expect(called).toBeTruthy();
expect($state.current).toBe(E);
}));

it('can be cancelled by preventDefault() in $stateNotFound', inject(function ($state, $q, $rootScope) {
initStateTo(A);
var called;
$rootScope.$on('$stateNotFound', function (ev) {
ev.preventDefault();
called = true;
});
var promise = $state.transitionTo('never_defined', {});
$q.flush();
expect(called).toBeTruthy();
expect($state.current).toBe(A);
expect(resolvedError(promise)).toBeTruthy();
}));

it('can be redirected in $stateNotFound', inject(function ($state, $q, $rootScope) {
initStateTo(A);
var called;
$rootScope.$on('$stateNotFound', function (ev, redirect) {
redirect.to = D;
redirect.toParams = { x: '1', y: '2' };
called = true;
});
var promise = $state.transitionTo('never_defined', { z: 3 });
$q.flush();
expect(called).toBeTruthy();
expect($state.current).toBe(D);
expect($state.params).toEqual({ x: '1', y: '2' });
}));

it('can lazy-define a state in $stateNotFound', inject(function ($state, $q, $rootScope) {
initStateTo(DD, { x: 1, y: 2, z: 3 });
var called;
$rootScope.$on('$stateNotFound', function (ev, redirect) {
stateProvider.state(redirect.to, { parent: DD, params: [ 'x', 'y', 'z', 'w' ]});
called = true;
});
var promise = $state.go('DDD', { w: 4 });
$q.flush();
expect(called).toBeTruthy();
expect($state.current.name).toEqual('DDD');
expect($state.params).toEqual({ x: '1', y: '2', z: '3', w: '4' });
}));

it('can defer a state transition in $stateNotFound', inject(function ($state, $q, $rootScope) {
initStateTo(A);
var called;
var deferred = $q.defer();
$rootScope.$on('$stateNotFound', function (ev, redirect) {
ev.retry = deferred.promise;
called = true;
});
var promise = $state.go('AA', { a: 1 });
stateProvider.state('AA', { parent: A, params: [ 'a' ]});
deferred.resolve();
$q.flush();
expect(called).toBeTruthy();
expect($state.current.name).toEqual('AA');
expect($state.params).toEqual({ a: '1' });
}));

it('can defer and supersede a state transition in $stateNotFound', inject(function ($state, $q, $rootScope) {
initStateTo(A);
var called;
var deferred = $q.defer();
$rootScope.$on('$stateNotFound', function (ev, redirect) {
ev.retry = deferred.promise;
called = true;
});
var promise = $state.go('AA', { a: 1 });
$state.go(B);
stateProvider.state('AA', { parent: A, params: [ 'a' ]});
deferred.resolve();
$q.flush();
expect(called).toBeTruthy();
expect($state.current).toEqual(B);
expect($state.params).toEqual({});
}));

it('triggers $stateChangeSuccess', inject(function ($state, $q, $rootScope) {
initStateTo(E, { i: 'iii' });
var called;
Expand Down