Skip to content

Commit 47d4464

Browse files
committed
Added a $stateNotFound event which can prevent or redirect state transitions.
1 parent 69818c9 commit 47d4464

File tree

2 files changed

+77
-5
lines changed

2 files changed

+77
-5
lines changed

src/state.js

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
169169
}
170170
}]);
171171
}
172+
172173
return state;
173174
}
174175

@@ -220,6 +221,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
220221

221222
var TransitionSuperseded = $q.reject(new Error('transition superseded'));
222223
var TransitionPrevented = $q.reject(new Error('transition prevented'));
224+
var TransitionAborted = $q.reject(new Error('transition aborted'));
223225

224226
root.locals = { resolve: null, globals: { $stateParams: {} } };
225227
$state = {
@@ -238,18 +240,33 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
238240
toParams = toParams || {};
239241
options = extend({ location: true, inherit: false, relative: null }, options);
240242

243+
var from = $state.$current, fromParams = $state.params, fromPath = from.path;
244+
241245
var toState = findState(to, options.relative);
242246

247+
var evt;
248+
243249
if (!isDefined(toState)) {
244-
if (options.relative) throw new Error("Could not resolve '" + to + "' from state '" + options.relative + "'");
245-
throw new Error("No such state '" + to + "'");
250+
// Broadcast not found event and abort the transition if prevented
251+
var redirect = { to: to, toParams: toParams, options: options };
252+
evt = $rootScope.$broadcast('$stateNotFound', redirect, from.self, fromParams);
253+
if (evt.defaultPrevented) return TransitionAborted;
254+
// Always retry once if the $stateNotFound was not prevented
255+
// (handles either redirect changed or state lazy-definition)
256+
to = redirect.to;
257+
toParams = redirect.toParams;
258+
options = redirect.options;
259+
toState = findState(to, options.relative);
260+
if (!isDefined(toState)) {
261+
if (options.relative) throw new Error("Could not resolve '" + to + "' from state '" + options.relative + "'");
262+
throw new Error("No such state '" + to + "'");
263+
}
246264
}
247265
if (toState['abstract']) throw new Error("Cannot transition to abstract state '" + to + "'");
248266
if (options.inherit) toParams = inheritParams($stateParams, toParams || {}, $state.$current, toState);
249267
to = toState;
250268

251-
var toPath = to.path,
252-
from = $state.$current, fromParams = $state.params, fromPath = from.path;
269+
var toPath = to.path;
253270

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

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

278295
// Resolve locals for the remaining states, but don't update any global state just

test/stateSpec.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ describe('state', function () {
7272
$rootScope.$on('$stateChangeStart', eventLogger);
7373
$rootScope.$on('$stateChangeSuccess', eventLogger);
7474
$rootScope.$on('$stateChangeError', eventLogger);
75+
$rootScope.$on('$stateNotFound', eventLogger);
7576
}));
7677

7778

@@ -139,6 +140,60 @@ describe('state', function () {
139140
expect(resolvedError(promise)).toBeTruthy();
140141
}));
141142

143+
it('triggers $stateNotFound', inject(function ($state, $q, $rootScope) {
144+
initStateTo(E, { i: 'iii' });
145+
var called;
146+
$rootScope.$on('$stateNotFound', function (ev, redirect, from, fromParams) {
147+
expect(from).toBe(E);
148+
expect(fromParams).toEqual({ i: 'iii' });
149+
expect(redirect.to).toEqual('never_defined');
150+
expect(redirect.toParams).toEqual({ x: '1', y: '2' });
151+
152+
expect($state.current).toBe(from); // $state not updated yet
153+
expect($state.params).toEqual(fromParams);
154+
called = true;
155+
});
156+
var message;
157+
try {
158+
$state.transitionTo('never_defined', { x: '1', y: '2' });
159+
} catch(err) {
160+
message = err.message;
161+
}
162+
$q.flush();
163+
expect(message).toEqual('No such state \'never_defined\'');
164+
expect(called).toBeTruthy();
165+
expect($state.current).toBe(E);
166+
}));
167+
168+
it('can be cancelled by preventDefault() in $stateNotFound', inject(function ($state, $q, $rootScope) {
169+
initStateTo(A);
170+
var called;
171+
$rootScope.$on('$stateNotFound', function (ev) {
172+
ev.preventDefault();
173+
called = true;
174+
});
175+
var promise = $state.transitionTo('never_defined', {});
176+
$q.flush();
177+
expect(called).toBeTruthy();
178+
expect($state.current).toBe(A);
179+
expect(resolvedError(promise)).toBeTruthy();
180+
}));
181+
182+
it('can be redirected in $stateNotFound', inject(function ($state, $q, $rootScope) {
183+
initStateTo(A);
184+
var called;
185+
$rootScope.$on('$stateNotFound', function (ev, redirect) {
186+
redirect.to = D;
187+
redirect.toParams = { x: '1', y: '2' };
188+
called = true;
189+
});
190+
var promise = $state.transitionTo('never_defined', { z: 3 });
191+
$q.flush();
192+
expect(called).toBeTruthy();
193+
expect($state.current).toBe(D);
194+
expect($state.params).toEqual({ x: '1', y: '2' });
195+
}));
196+
142197
it('triggers $stateChangeSuccess', inject(function ($state, $q, $rootScope) {
143198
initStateTo(E, { i: 'iii' });
144199
var called;

0 commit comments

Comments
 (0)