Skip to content

Commit cbd8c02

Browse files
committed
Merge pull request angular-ui#414 from Demeterr/add-stateNotFound
Added a $stateNotFound event which can prevent or redirect state transitions.
2 parents 10b5123 + ecb5023 commit cbd8c02

File tree

2 files changed

+145
-7
lines changed

2 files changed

+145
-7
lines changed

src/state.js

+41-7
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,8 @@ 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'));
225+
var TransitionFailed = $q.reject(new Error('transition failed'));
223226

224227
root.locals = { resolve: null, globals: { $stateParams: {} } };
225228
$state = {
@@ -236,20 +239,51 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
236239
$state.transitionTo = function transitionTo(to, toParams, options) {
237240
if (!isDefined(options)) options = (options === true || options === false) ? { location: options } : {};
238241
toParams = toParams || {};
239-
options = extend({ location: true, inherit: false, relative: null }, options);
242+
options = extend({ location: true, inherit: false, relative: null, $retry: false }, options);
243+
244+
var from = $state.$current, fromParams = $state.params, fromPath = from.path;
240245

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

248+
var evt;
249+
243250
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 + "'");
251+
// Broadcast not found event and abort the transition if prevented
252+
var redirect = { to: to, toParams: toParams, options: options };
253+
evt = $rootScope.$broadcast('$stateNotFound', redirect, from.self, fromParams);
254+
if (evt.defaultPrevented) return TransitionAborted;
255+
256+
// Allow the handler to return a promise to defer state lookup retry
257+
if (evt.retry) {
258+
if (options.$retry) return TransitionFailed;
259+
var retryTransition = $state.transition = $q.when(evt.retry);
260+
retryTransition.then(function() {
261+
if (retryTransition !== $state.transition) return TransitionSuperseded;
262+
redirect.options.$retry = true;
263+
return $state.transitionTo(redirect.to, redirect.toParams, redirect.options);
264+
},
265+
function() {
266+
return TransitionAborted;
267+
});
268+
return retryTransition;
269+
}
270+
271+
// Always retry once if the $stateNotFound was not prevented
272+
// (handles either redirect changed or state lazy-definition)
273+
to = redirect.to;
274+
toParams = redirect.toParams;
275+
options = redirect.options;
276+
toState = findState(to, options.relative);
277+
if (!isDefined(toState)) {
278+
if (options.relative) throw new Error("Could not resolve '" + to + "' from state '" + options.relative + "'");
279+
throw new Error("No such state '" + to + "'");
280+
}
246281
}
247282
if (toState['abstract']) throw new Error("Cannot transition to abstract state '" + to + "'");
248283
if (options.inherit) toParams = inheritParams($stateParams, toParams || {}, $state.$current, toState);
249284
to = toState;
250285

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

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

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

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

291-
// Once everything is resolved, wer are ready to perform the actual transition
325+
// Once everything is resolved, we are ready to perform the actual transition
292326
// and return a promise for the new state. We also keep track of what the
293327
// current promise is, so that we can detect overlapping transitions and
294328
// keep only the outcome of the last transition.

test/stateSpec.js

+104
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,109 @@ 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+
197+
it('can lazy-define a state in $stateNotFound', inject(function ($state, $q, $rootScope) {
198+
initStateTo(DD, { x: 1, y: 2, z: 3 });
199+
var called;
200+
$rootScope.$on('$stateNotFound', function (ev, redirect) {
201+
stateProvider.state(redirect.to, { parent: DD, params: [ 'x', 'y', 'z', 'w' ]});
202+
called = true;
203+
});
204+
var promise = $state.go('DDD', { w: 4 });
205+
$q.flush();
206+
expect(called).toBeTruthy();
207+
expect($state.current.name).toEqual('DDD');
208+
expect($state.params).toEqual({ x: '1', y: '2', z: '3', w: '4' });
209+
}));
210+
211+
it('can defer a state transition in $stateNotFound', inject(function ($state, $q, $rootScope) {
212+
initStateTo(A);
213+
var called;
214+
var deferred = $q.defer();
215+
$rootScope.$on('$stateNotFound', function (ev, redirect) {
216+
ev.retry = deferred.promise;
217+
called = true;
218+
});
219+
var promise = $state.go('AA', { a: 1 });
220+
stateProvider.state('AA', { parent: A, params: [ 'a' ]});
221+
deferred.resolve();
222+
$q.flush();
223+
expect(called).toBeTruthy();
224+
expect($state.current.name).toEqual('AA');
225+
expect($state.params).toEqual({ a: '1' });
226+
}));
227+
228+
it('can defer and supersede a state transition in $stateNotFound', inject(function ($state, $q, $rootScope) {
229+
initStateTo(A);
230+
var called;
231+
var deferred = $q.defer();
232+
$rootScope.$on('$stateNotFound', function (ev, redirect) {
233+
ev.retry = deferred.promise;
234+
called = true;
235+
});
236+
var promise = $state.go('AA', { a: 1 });
237+
$state.go(B);
238+
stateProvider.state('AA', { parent: A, params: [ 'a' ]});
239+
deferred.resolve();
240+
$q.flush();
241+
expect(called).toBeTruthy();
242+
expect($state.current).toEqual(B);
243+
expect($state.params).toEqual({});
244+
}));
245+
142246
it('triggers $stateChangeSuccess', inject(function ($state, $q, $rootScope) {
143247
initStateTo(E, { i: 'iii' });
144248
var called;

0 commit comments

Comments
 (0)