Skip to content
This repository was archived by the owner on Sep 20, 2020. It is now read-only.

Commit 92c4f81

Browse files
Implemented $transition$ promise
1 parent 4afbab8 commit 92c4f81

File tree

4 files changed

+289
-5
lines changed

4 files changed

+289
-5
lines changed

files.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@ var files = {
1414
'src/stickyState.js',
1515
'src/futureState.js',
1616
'src/previousState.js',
17+
'src/transition.js',
1718
'src/noop.js'
1819
],
1920
test: [
20-
// 'test/temp.js',
21+
'test/temp.js',
2122
'test/deepStateRedirectSpec.js',
2223
'test/futureStateSpec.js',
2324
'test/stickyStateSpec.js',
2425
'test/previousStateSpec.js',
26+
'test/transitionSpec.js',
2527
'src/fsfactories/ngload.js',
2628
'src/fsfactories/iframe.js',
2729
'src/noop.js'

package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
"grunt-contrib-uglify": "~0.2.5",
1313
"grunt-contrib-jshint": "~0.7.0",
1414
"grunt-karma": "^0.8.3",
15-
"karma-chrome-launcher": "^0.1.4",
16-
"karma-phantomjs-launcher": "^0.1.4",
17-
"karma": "^0.12.16",
18-
"karma-jasmine": "^0.1.5",
15+
"karma-chrome-launcher": "latest",
16+
"karma-phantomjs-launcher": "latest",
17+
"karma": "latest",
18+
"karma-jasmine": "0.2.0",
1919
"lodash": "latest"
2020
},
2121
"scripts": {

src/transition.js

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
2+
angular.module("ct.ui.router.extras").config( [ "$provide", function ($provide) {
3+
// Decorate the $state service, so we can replace $state.transitionTo()
4+
$provide.decorator("$state", ['$delegate', '$rootScope', '$q', '$injector',
5+
function ($state, $rootScope, $q, $injector) {
6+
// Keep an internal reference to the real $state.transitionTo function
7+
var $state_transitionTo = $state.transitionTo;
8+
// $state.transitionTo can be re-entered. Keep track of re-entrant stack
9+
var transitionDepth = -1;
10+
var tDataStack = [];
11+
var restoreFnStack = [];
12+
13+
// This function decorates the $injector, adding { $transition$: tData } to invoke() and instantiate() locals.
14+
// It returns a function that restores $injector to its previous state.
15+
function decorateInjector(tData) {
16+
var oldinvoke = $injector.invoke;
17+
var oldinstantiate = $injector.instantiate;
18+
$injector.invoke = function (fn, self, locals) {
19+
return oldinvoke(fn, self, angular.extend({$transition$: tData}, locals));
20+
};
21+
$injector.instantiate = function (fn, locals) {
22+
return oldinstantiate(fn, angular.extend({$transition$: tData}, locals));
23+
};
24+
25+
return function restoreItems() {
26+
$injector.invoke = oldinvoke;
27+
$injector.instantiate = oldinstantiate;
28+
};
29+
}
30+
31+
function popStack() {
32+
restoreFnStack.pop()();
33+
tDataStack.pop();
34+
transitionDepth--;
35+
}
36+
37+
// This promise callback (for when the real transitionTo is successful) runs the restore function for the
38+
// current stack level, then broadcasts the $transitionSuccess event.
39+
function transitionSuccess(deferred, tSuccess) {
40+
return function successFn(data) {
41+
popStack();
42+
$rootScope.$broadcast("$transitionSuccess", tSuccess);
43+
return deferred.resolve(data);
44+
};
45+
}
46+
47+
// This promise callback (for when the real transitionTo fails) runs the restore function for the
48+
// current stack level, then broadcasts the $transitionError event.
49+
function transitionFailure(deferred, tFail) {
50+
return function failureFn(error) {
51+
popStack();
52+
$rootScope.$broadcast("$transitionError", tFail, error);
53+
return deferred.reject(error);
54+
};
55+
}
56+
57+
// Decorate $state.transitionTo.
58+
$state.transitionTo = function (to, toParams, options) {
59+
// Create a deferred/promise which can be used earlier than UI-Router's transition promise.
60+
var deferred = $q.defer();
61+
// Place the promise in a transition data, and place it on the stack to be used in $stateChangeStart
62+
var tData = tDataStack[++transitionDepth] = {
63+
promise: deferred.promise
64+
};
65+
// placeholder restoreFn in case transitionTo doesn't reach $stateChangeStart (state not found, etc)
66+
restoreFnStack[transitionDepth] = function() { };
67+
// Invoke the real $state.transitionTo
68+
var tPromise = $state_transitionTo.apply($state, arguments);
69+
70+
// insert our promise callbacks into the chain.
71+
return tPromise.then(transitionSuccess(deferred, tData), transitionFailure(deferred, tData));
72+
};
73+
74+
// This event is handled synchronously in transitionTo call stack
75+
$rootScope.$on("$stateChangeStart", function (evt, toState, toParams, fromState, fromParams) {
76+
var depth = transitionDepth;
77+
// To/From is now normalized by ui-router. Add this information to the transition data object.
78+
var tData = angular.extend(tDataStack[depth], {
79+
to: { state: toState, params: toParams },
80+
from: { state: fromState, params: fromParams }
81+
});
82+
83+
var restoreFn = decorateInjector(tData);
84+
restoreFnStack[depth] = restoreFn;
85+
$rootScope.$broadcast("$transitionStart", tData);
86+
}
87+
);
88+
89+
return $state;
90+
}]);
91+
}
92+
]
93+
);

test/transitionSpec.js

+189
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
"use strict";
2+
var $get, $state, $rootScope, $q, $compile,
3+
_stateProvider,
4+
el;
5+
6+
describe('$transition$', function () {
7+
beforeEach(module('ct.ui.router.extras', function ($stateProvider) {
8+
// Load and capture $stickyStateProvider and $stateProvider
9+
_stateProvider = $stateProvider;
10+
_stateProvider.state("A", { url: '/A' , template: '<ui-view/>' });
11+
_stateProvider.state("A.AA", { url: '/AA' , template: '<ui-view/>' });
12+
_stateProvider.state("A.AA.AAA", { url: '/AAA' , template: '<ui-view/>', onEnter: function($state) { $state.go("B"); }});
13+
_stateProvider.state("B", { url: '/B' , template: '<ui-view/>' });
14+
_stateProvider.state("C", { url: '/C' , template: '<ui-view/>',
15+
onEnter: function($transition$, $rootScope) { $rootScope.$broadcast("t_onEnterInjected", $transition$); },
16+
onExit: function($transition$, $rootScope) { $rootScope.$broadcast("t_onExitInjected", $transition$); }
17+
});
18+
_stateProvider.state("D", { url: '/D' , template: '<ui-view/>'
19+
, controller: function($transition$, $rootScope) { $rootScope.$broadcast("t_controllerInjected", $transition$); }
20+
});
21+
22+
}));
23+
24+
// Capture $injector.get, $state, and $q
25+
beforeEach(inject(function ($injector) {
26+
$get = $injector.get;
27+
$state = $get('$state');
28+
$rootScope = $get('$rootScope');
29+
$q = $get('$q');
30+
$compile = $get('$compile');
31+
el = $compile("<ui-view/>")($rootScope);
32+
}));
33+
34+
describe('service', function () {
35+
it("should allow transitions normally", function () {
36+
$state.go("A");
37+
$q.flush();
38+
expect($state.current.name).toBe("A");
39+
});
40+
});
41+
42+
describe('$transitionStart event', function () {
43+
var startEventCount = 0;
44+
var t;
45+
46+
beforeEach(function() {
47+
startEventCount = 0;
48+
t = undefined;
49+
$rootScope.$on("$transitionStart", function (evt, $transition$) {
50+
startEventCount++;
51+
t = $transition$;
52+
});
53+
});
54+
55+
it("should fire when a transition starts", function () {
56+
expect(startEventCount).toBe(0);
57+
$state.go("A"); // Triggers $transitionState
58+
expect(startEventCount).toBe(1);
59+
$q.flush();
60+
});
61+
62+
it("should pass $transition$ as the second arg", function () {
63+
$state.go("B"); // Triggers $transitionState
64+
expect(t).toBeDefined();
65+
expect(t.promise).toBeDefined();
66+
expect(t.from).toBeDefined();
67+
expect(t.to).toBeDefined();
68+
expect(t.from.state.name).toBe("");
69+
expect(t.to.state.name).toBe("B");
70+
$q.flush();
71+
});
72+
73+
it("$transition$ arg should async invoke .then", function () {
74+
var invokeCount = 0;
75+
$state.go("A"); // Triggers $transitionState
76+
t.promise.then(function() { invokeCount++; });
77+
expect(invokeCount).toBe(0);
78+
$q.flush();
79+
expect(invokeCount).toBe(1);
80+
});
81+
82+
it("should be correct when TransitionSuperseded", function () {
83+
// $compile("<ui-view/>")($rootScope);
84+
var invokeCount = 0;
85+
$state.go("A.AA.AAA");
86+
t.promise.then(function() {
87+
throw Error("Shouldn't get here.");
88+
}, function() {
89+
invokeCount++;
90+
});
91+
expect(invokeCount).toBe(0);
92+
$q.flush();
93+
expect($state.current.name).toBe("B");
94+
expect(invokeCount).toBe(1);
95+
});
96+
97+
describe('promise arg of $transitionStart', function () {
98+
var promiseCount = 0;
99+
it("1 should resolve when the state change occurs", function () {
100+
expect($state.current.name).toBe("");
101+
$state.go("B");
102+
103+
t.promise.then(function (data) {
104+
promiseCount++;
105+
expect($state.current.name).toBe("B");
106+
expect(data.name).toBe("B");
107+
});
108+
109+
$q.flush();
110+
expect(startEventCount).toBe(1);
111+
expect(promiseCount).toBe(1);
112+
});
113+
});
114+
115+
describe('promise injected in onEnter', function () {
116+
var promiseCount = 0;
117+
it("2 should resolve when the state change occurs", function (done) {
118+
var cancelListen = $rootScope.$on("t_onEnterInjected", function(evt, $transition$) {
119+
expect($transition$).toBeDefined();
120+
expect($transition$.to).toBeDefined();
121+
122+
$transition$.promise.then(function (data) {
123+
promiseCount++;
124+
expect($state.current.name).toBe("C");
125+
expect(data.name).toBe("C");
126+
expect(startEventCount).toBe(1);
127+
expect(promiseCount).toBe(1);
128+
done();
129+
cancelListen();
130+
});
131+
});
132+
$state.go("C");
133+
$q.flush();
134+
});
135+
});
136+
137+
describe('promise injected in onExit', function () {
138+
var promiseCount = 0;
139+
it("3 should resolve when the state change occurs", function (done) {
140+
var cancelListen = $rootScope.$on("t_onExitInjected", function(evt, $transition$) {
141+
expect($transition$).toBeDefined();
142+
expect($transition$.to).toBeDefined();
143+
expect($transition$.to.state.name).toBe("B");
144+
145+
$transition$.promise.then(function (data) {
146+
promiseCount++;
147+
expect($state.current.name).toBe("B");
148+
expect(data.name).toBe("B");
149+
expect(startEventCount).toBe(2);
150+
expect(promiseCount).toBe(1);
151+
done();
152+
cancelListen();
153+
});
154+
});
155+
156+
$state.go("C");
157+
$q.flush();
158+
$state.go("B");
159+
$q.flush();
160+
});
161+
});
162+
163+
describe('promise injected in controller', function () {
164+
var promiseCount = 0;
165+
it("4 should resolve when the state change occurs", function (done) {
166+
var cancelListen = $rootScope.$on("t_controllerInjected", function(evt, $transition$) {
167+
expect($transition$).toBeDefined();
168+
expect($transition$.to).toBeDefined();
169+
expect($transition$.to.state.name).toBe("D");
170+
171+
$transition$.promise.then(function (data) {
172+
promiseCount++;
173+
expect($state.current.name).toBe("D");
174+
expect(data.name).toBe("D");
175+
expect(startEventCount).toBe(2);
176+
expect(promiseCount).toBe(1);
177+
done();
178+
cancelListen();
179+
});
180+
});
181+
182+
$state.go("C");
183+
$q.flush();
184+
$state.go("D");
185+
$q.flush();
186+
});
187+
});
188+
});
189+
});

0 commit comments

Comments
 (0)