Skip to content

Commit 5a04411

Browse files
implement transition hooks
1 parent 27fafbf commit 5a04411

File tree

5 files changed

+220
-92
lines changed

5 files changed

+220
-92
lines changed

files.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ routerFiles = {
2121
// 'test/resolveSpec.js',
2222
// 'test/urlMatcherFactorySpec.js',
2323
// 'test/urlRouterSpec.js',
24-
'test/*Spec.js',
24+
'test/transitionSpec.js',
2525
'test/compat/matchers.js'
2626
],
2727
angular: function(version) {

src/common.js

+17
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,23 @@ function flattenPrototypeChain(obj) {
220220
return result;
221221
}
222222

223+
// Return a completely flattened version of an array.
224+
function flatten (array) {
225+
function _flatten(input, output) {
226+
forEach(input, function(value) {
227+
if (angular.isArray(value)) {
228+
_flatten(value, output);
229+
} else {
230+
output.push(value);
231+
}
232+
});
233+
return output;
234+
}
235+
236+
return _flatten(array, []);
237+
}
238+
239+
223240
var GlobBuilder = (function() {
224241

225242
function Glob(text) {

src/resolve.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ function $Resolve( $q, $injector) {
9797
});
9898
};
9999

100+
// Eager resolves are resolved before the transition starts.
101+
// Lazy resolves are resolved before their state is entered.
102+
// JIT resolves are resolved just-in-time, right before an injected function that depends on them is invoked.
100103
var resolvePolicies = { eager: 3, lazy: 2, jit: 1 };
101104
var defaultResolvePolicy = "eager"; // TODO: make this configurable
102105

@@ -203,7 +206,7 @@ function $Resolve( $q, $injector) {
203206
toPathElement = toPathElement || elements[elements.length - 1];
204207
var elementIdx = elements.indexOf(toPathElement);
205208
if (elementIdx == -1) throw new Error("this Path does not contain the toPathElement");
206-
return new ResolveContext(self.slice(0, elementIdx));
209+
return new ResolveContext(self.slice(0, elementIdx + 1));
207210
}
208211

209212
// Public API

src/transition.js

+193-87
Original file line numberDiff line numberDiff line change
@@ -7,51 +7,125 @@
77
$TransitionProvider.$inject = [];
88
function $TransitionProvider() {
99

10-
var $transition = {}, events, stateMatcher = angular.noop, abstractKey = 'abstract';
10+
var $transition = {}, stateMatcher = angular.noop, abstractKey = 'abstract';
11+
var transitionEvents = { on: [], entering: [], exiting: [], success: [], error: [] };
1112

12-
function matchState(current, states) {
13-
var toMatch = angular.isArray(states) ? states : [states];
13+
function matchState(state, globStrings) {
14+
var toMatch = angular.isArray(globStrings) ? globStrings : [globStrings];
1415

1516
for (var i = 0; i < toMatch.length; i++) {
1617
var glob = GlobBuilder.fromString(toMatch[i]);
1718

18-
if ((glob && glob.matches(current.name)) || (!glob && toMatch[i] === current.name)) {
19+
if ((glob && glob.matches(state.name)) || (!glob && toMatch[i] === state.name)) {
1920
return true;
2021
}
2122
}
2223
return false;
2324
}
2425

25-
// $transitionProvider.on({ from: "home", to: "somewhere.else" }, function($transition$, $http) {
26-
// // ...
27-
// });
28-
this.on = function(states, callback) {
29-
};
30-
31-
// $transitionProvider.onEnter({ from: "home", to: "somewhere.else" }, function($transition$, $http) {
32-
// // ...
33-
// });
34-
this.entering = function(states, callback) {
35-
};
36-
37-
// $transitionProvider.onExit({ from: "home", to: "somewhere.else" }, function($transition$, $http) {
38-
// // ...
39-
// });
40-
this.exiting = function(states, callback) {
41-
};
42-
43-
// $transitionProvider.onSuccess({ from: "home", to: "somewhere.else" }, function($transition$, $http) {
44-
// // ...
45-
// });
46-
this.onSuccess = function(states, callback) {
47-
};
48-
49-
// $transitionProvider.onError({ from: "home", to: "somewhere.else" }, function($transition$, $http) {
50-
// // ...
51-
// });
52-
this.onError = function(states, callback) {
53-
};
26+
// Return a registration function of the requested type.
27+
function registerEventHook(eventType) {
28+
return function(stateGlobs, callback) {
29+
transitionEvents[eventType].push(new EventHook(stateGlobs, callback));
30+
};
31+
}
32+
33+
/**
34+
* @ngdoc function
35+
* @name ui.router.state.$transitionProvider#on
36+
* @methodOf ui.router.state.$transitionProvider
37+
*
38+
* @description
39+
* Registers a function to be injected and invoked when a transition between the matched 'to' and 'from' states
40+
* starts.
41+
*
42+
* @param {object} transitionCriteria An object that specifies which transitions to invoke the callback for.
43+
*
44+
* - **`to`** - {string} - A glob string that matches the 'to' state's name.
45+
* - **`from`** - {string|RegExp} - A glob string that matches the 'from' state's name.
46+
*
47+
* @param {function} callback The function which will be injected and invoked, when a matching transition is started.
48+
*
49+
* @return {boolean|object|array} May optionally return:
50+
* - **`false`** to abort the current transition
51+
* - **A promise** to suspend the current transition until the promise resolves
52+
* - **Array of Resolvable objects** to add additional resolves to the current transition, which will be available
53+
* for injection to further steps in the transition.
54+
*/
55+
this.on = registerEventHook("on");
56+
57+
/**
58+
* @ngdoc function
59+
* @name ui.router.state.$transitionProvider#entering
60+
* @methodOf ui.router.state.$transitionProvider
61+
*
62+
* @description
63+
* Registers a function to be injected and invoked during a transition between the matched 'to' and 'from' states,
64+
* when the matched 'to' state is being entered. This function is in injected with the entering state's resolves.
65+
* @param {object} transitionCriteria See transitionCriteria in {@link ui.router.state.$transitionProvider#on $transitionProvider.on}.
66+
* @param {function} callback See callback in {@link ui.router.state.$transitionProvider#on $transitionProvider.on}.
67+
*
68+
* @return {boolean|object|array} May optionally return:
69+
* - **`false`** to abort the current transition
70+
* - **A promise** to suspend the current transition until the promise resolves
71+
* - **Array of Resolvable objects** to add additional resolves to the current transition, which will be available
72+
* for injection to further steps in the transition.
73+
*/
74+
this.entering = registerEventHook("entering");
75+
76+
/**
77+
* @ngdoc function
78+
* @name ui.router.state.$transitionProvider#exiting
79+
* @methodOf ui.router.state.$transitionProvider
80+
*
81+
* @description
82+
* Registers a function to be injected and invoked during a transition between the matched 'to' and 'from states,
83+
* when the matched 'from' state is being exited. This function is in injected with the exiting state's resolves.
84+
* @param {object} transitionCriteria See transitionCriteria in {@link ui.router.state.$transitionProvider#on $transitionProvider.on}.
85+
* @param {function} callback See callback in {@link ui.router.state.$transitionProvider#on $transitionProvider.on}.
86+
*
87+
* @return {boolean|object|array} May optionally return:
88+
* - **`false`** to abort the current transition
89+
* - **A promise** to suspend the current transition until the promise resolves
90+
* - **Array of Resolvable objects** to add additional resolves to the current transition, which will be available
91+
* for injection to further steps in the transition.
92+
*/
93+
this.exiting = registerEventHook("exiting");
94+
95+
/**
96+
* @ngdoc function
97+
* @name ui.router.state.$transitionProvider#onSuccess
98+
* @methodOf ui.router.state.$transitionProvider
99+
*
100+
* @description
101+
* Registers a function to be injected and invoked when a transition has successfully completed between the matched
102+
* 'to' and 'from' state is being exited.
103+
* This function is in injected with the 'to' state's resolves.
104+
* @param {object} transitionCriteria See transitionCriteria in {@link ui.router.state.$transitionProvider#on $transitionProvider.on}.
105+
* @param {function} callback See callback in {@link ui.router.state.$transitionProvider#on $transitionProvider.on}.
106+
*/
107+
this.onSuccess = registerEventHook("success");
108+
109+
/**
110+
* @ngdoc function
111+
* @name ui.router.state.$transitionProvider#onError
112+
* @methodOf ui.router.state.$transitionProvider
113+
*
114+
* @description
115+
* Registers a function to be injected and invoked when a transition has failed for any reason between the matched
116+
* 'to' and 'from' state is being exited. This function is in injected with the 'to' state's resolves. The transition
117+
* rejection reason is injected as `$transitionError$`.
118+
* @param {object} transitionCriteria See transitionCriteria in {@link ui.router.state.$transitionProvider#on $transitionProvider.on}.
119+
* @param {function} callback See callback in {@link ui.router.state.$transitionProvider#on $transitionProvider.on}.
120+
*/
121+
this.onError = registerEventHook("error");
54122

123+
function EventHook(stateGlobs, callback) {
124+
this.callback = callback;
125+
this.matches = function matches(to, from) {
126+
return matchState(to, stateGlobs.to) && matchState(from, stateGlobs.from);
127+
};
128+
}
55129

56130
/**
57131
* @ngdoc service
@@ -67,7 +141,6 @@ function $TransitionProvider() {
67141
this.$get = $get;
68142
$get.$inject = ['$q', '$injector', '$resolve', '$stateParams'];
69143
function $get( $q, $injector, $resolve, $stateParams) {
70-
71144
var from = { state: null, params: null },
72145
to = { state: null, params: null };
73146
var _fromPath = null; // contains resolved data
@@ -142,55 +215,6 @@ function $TransitionProvider() {
142215
hasCalculated = true;
143216
}
144217

145-
function transitionStep(fn, resolveContext) {
146-
return function() {
147-
if ($transition.transition !== transition) return transition.SUPERSEDED;
148-
return pathElement.invokeAsync(fn, { $stateParams: undefined, $transition$: transition }, resolveContext)
149-
.then(function(result) {
150-
return result ? result : $q.reject(transition.ABORTED);
151-
});
152-
};
153-
}
154-
155-
function buildTransitionSteps() {
156-
// create invokeFn fn.
157-
// - checks if current transition has been superseded
158-
// - invokes Fn async
159-
// - checks result. If falsey, rejects promise
160-
161-
// get exiting & reverse them
162-
// get entering
163-
164-
// walk exiting()
165-
// - InvokeAsync
166-
// resolve all eager Path resolvables
167-
// walk entering()
168-
// - resolve PathElement lazy resolvables
169-
// - then, invokeAsync onEnter
170-
171-
var exitingElements = transition.exiting().slice(0).reverse().elements;
172-
var enteringElements = transition.entering().elements;
173-
var promiseChain = $q.when(true);
174-
175-
forEach(exitingElements, function(elem) {
176-
if (elem.state.onExit) {
177-
var nextStep = transitionStep(elem.state.onExit, fromPath.resolveContext(elem));
178-
promiseChain.then(nextStep);
179-
}
180-
});
181-
182-
forEach(enteringElements, function(elem) {
183-
var resolveContext = toPath.resolveContext(elem);
184-
promiseChain.then(function() { return elem.resolve(resolveContext, { policy: "lazy" }); });
185-
if (elem.state.onEnter) {
186-
var nextStep = transitionStep(elem.state.onEnter, resolveContext);
187-
promiseChain.then(nextStep);
188-
}
189-
});
190-
191-
return promiseChain;
192-
}
193-
194218
extend(this, {
195219
/**
196220
* @ngdoc function
@@ -341,12 +365,93 @@ function $TransitionProvider() {
341365
ignored: function() {
342366
return (toState === fromState && !options.reload);
343367
},
368+
344369
run: function() {
345370
calculateTreeChanges();
346-
var pathContext = new ResolveContext(toPath);
347-
return toPath.resolve(pathContext, { policy: "eager" })
348-
.then( buildTransitionSteps );
371+
372+
function TransitionStep(pathElement, fn, locals, resolveContext, otherData) {
373+
this.state = pathElement.state;
374+
this.otherData = otherData;
375+
this.fn = fn;
376+
377+
this.invokeStep = function invokeStep() {
378+
if ($transition.transition !== transition) return transition.SUPERSEDED;
379+
380+
/** Returns a map containing any Resolvables found in result as an object or Array */
381+
function resolvablesFromResult(result) {
382+
var resolvables = [];
383+
if (result instanceof Resolvable) {
384+
resolvables.push(result);
385+
} else if (angular.isArray(result)) {
386+
resolvables.push(filter(result, function(obj) { return obj instanceof Resolvable; }));
387+
}
388+
return indexBy(resolvables, 'name');
389+
}
390+
391+
/** Adds any returned resolvables to the resolveContext for the current state */
392+
function handleHookResult(result) {
393+
var newResolves = resolvablesFromResult(result);
394+
extend(resolveContext.$$resolvablesByState[pathElement.state.name], newResolves);
395+
return result === false ? transition.ABORTED : result;
396+
}
397+
398+
return pathElement.invokeLater(fn, locals, resolveContext).then(handleHookResult);
399+
};
400+
}
401+
402+
/**
403+
* returns an array of transition steps (promises) that matched
404+
* 1) the eventType
405+
* 2) the to state
406+
* 3) the from state
407+
*/
408+
function makeSteps(eventType, to, from, pathElement, locals, resolveContext) {
409+
var extraData = { eventType: eventType, to: to, from: from, pathElement: pathElement, locals: locals, resolveContext: resolveContext }; // internal debugging stuff
410+
var hooks = transitionEvents[eventType];
411+
var matchingHooks = filter(hooks, function(hook) { return hook.matches(to, from); });
412+
return map(matchingHooks, function(hook) {
413+
return new TransitionStep(pathElement, hook.callback, locals, resolveContext, extraData);
414+
});
415+
}
416+
417+
var tLocals = { $transition$: transition };
418+
var rootPE = new PathElement(stateMatcher("", {}));
419+
var rootPath = new Path([rootPE]);
420+
var exitingElements = transition.exiting().slice(0).reverse().elements;
421+
var enteringElements = transition.entering().elements;
422+
var to = transition.to(), from = transition.from();
423+
424+
// Build a bunch of arrays of promises for each step of the transition
425+
var transitionOnHooks = makeSteps("on", to, from, rootPE, tLocals, rootPath.resolveContext());
426+
427+
var exitingStateHooks = map(exitingElements, function(elem) {
428+
var enterLocals = extend({}, tLocals, { $stateParams: $stateParams.$localize(elem.state, $stateParams) });
429+
return makeSteps("exiting", to, from, elem, enterLocals, fromPath.resolveContext(elem));
430+
});
431+
var enteringStateHooks = map(enteringElements, function(elem) {
432+
var exitLocals = extend({}, tLocals, { $stateParams: $stateParams.$localize(elem.state, $stateParams) });
433+
return makeSteps("entering", to, from, elem, exitLocals, toPath.resolveContext(elem));
434+
});
435+
436+
var successHooks = makeSteps("onSuccess", to, from, rootPE, tLocals, rootPath.resolveContext());
437+
var errorHooks = makeSteps("onError", to, from, rootPE, tLocals, rootPath.resolveContext());
438+
439+
var eagerResolves = function () { return toPath.resolve(toPath.resolveContext(), { policy: "eager" }); };
440+
441+
var allSteps = flatten(transitionOnHooks, eagerResolves, exitingStateHooks, enteringStateHooks, successHooks);
442+
443+
444+
// Set up a promise chain. Add the promises in appropriate order to the promise chain.
445+
var chain = $q.when(true);
446+
forEach(allSteps, function (step) {
447+
chain.then(step.invokeStep);
448+
});
449+
450+
// TODO: call errorHooks.
451+
452+
return chain;
349453
},
454+
350455
begin: function(compare, exec) {
351456
if (!compare()) return this.SUPERSEDED;
352457
if (!exec()) return this.ABORTED;
@@ -377,7 +482,8 @@ function $TransitionProvider() {
377482

378483
$transition.start = function start(state, params, options) {
379484
to = { state: state, params: params || {} };
380-
return new Transition(from.state, from.params, state, params || {}, options || {});
485+
this.transition = new Transition(from.state, from.params, state, params || {}, options || {});
486+
return this.transition;
381487
};
382488

383489
$transition.isActive = function isActive() {

0 commit comments

Comments
 (0)