Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 885fb0d

Browse files
committed
feat($route): resolve local route promises
Resolve all promises on route before we fire $afterRouteChange which then renders the ngView.
1 parent 4361efb commit 885fb0d

File tree

4 files changed

+367
-79
lines changed

4 files changed

+367
-79
lines changed

src/ng/directive/ngView.js

+28-36
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,7 @@ var ngViewDirective = ['$http', '$templateCache', '$route', '$anchorScroll', '$c
112112
restrict: 'ECA',
113113
terminal: true,
114114
link: function(scope, element, attr) {
115-
var changeCounter = 0,
116-
lastScope,
115+
var lastScope,
117116
onloadExp = attr.onload || '';
118117

119118
scope.$on('$afterRouteChange', update);
@@ -127,43 +126,36 @@ var ngViewDirective = ['$http', '$templateCache', '$route', '$anchorScroll', '$c
127126
}
128127
}
129128

129+
function clearContent() {
130+
element.html('');
131+
destroyLastScope();
132+
}
133+
130134
function update() {
131-
var template = $route.current && $route.current.template,
132-
thisChangeId = ++changeCounter;
133-
134-
function clearContent() {
135-
// ignore callback if another route change occured since
136-
if (thisChangeId === changeCounter) {
137-
element.html('');
138-
destroyLastScope();
139-
}
140-
}
135+
var locals = $route.current && $route.current.locals,
136+
template = locals && locals.$template;
141137

142138
if (template) {
143-
$http.get(template, {cache: $templateCache}).success(function(response) {
144-
// ignore callback if another route change occured since
145-
if (thisChangeId === changeCounter) {
146-
element.html(response);
147-
destroyLastScope();
148-
149-
var link = $compile(element.contents()),
150-
current = $route.current,
151-
controller;
152-
153-
lastScope = current.scope = scope.$new();
154-
if (current.controller) {
155-
controller = $controller(current.controller, {$scope: lastScope});
156-
element.contents().data('$ngControllerController', controller);
157-
}
158-
159-
link(lastScope);
160-
lastScope.$emit('$viewContentLoaded');
161-
lastScope.$eval(onloadExp);
162-
163-
// $anchorScroll might listen on event...
164-
$anchorScroll();
165-
}
166-
}).error(clearContent);
139+
element.html(template);
140+
destroyLastScope();
141+
142+
var link = $compile(element.contents()),
143+
current = $route.current,
144+
controller;
145+
146+
lastScope = current.scope = scope.$new();
147+
if (current.controller) {
148+
locals.$scope = lastScope;
149+
controller = $controller(current.controller, locals);
150+
element.contents().data('$ngControllerController', controller);
151+
}
152+
153+
link(lastScope);
154+
lastScope.$emit('$viewContentLoaded');
155+
lastScope.$eval(onloadExp);
156+
157+
// $anchorScroll might listen on event...
158+
$anchorScroll();
167159
} else {
168160
clearContent();
169161
}

src/ng/route.js

+96-10
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ function $RouteProvider(){
1919
* @methodOf angular.module.ng.$routeProvider
2020
*
2121
* @param {string} path Route path (matched against `$location.path`). If `$location.path`
22-
* contains redudant trailing slash or is missing one, the route will still match and the
22+
* contains redundant trailing slash or is missing one, the route will still match and the
2323
* `$location.path` will be updated to add or drop the trailing slash to exacly match the
2424
* route definition.
2525
* @param {Object} route Mapping information to be assigned to `$route.current` on route
@@ -32,6 +32,17 @@ function $RouteProvider(){
3232
* - `template` – `{string=}` – path to an html template that should be used by
3333
* {@link angular.module.ng.$compileProvider.directive.ngView ngView} or
3434
* {@link angular.module.ng.$compileProvider.directive.ngInclude ngInclude} directives.
35+
* - `resolve` - `{Object.<string, function>=}` - An optional map of dependencies which should
36+
* be injected into the controller. If any of these dependencies are promises, they will be
37+
* resolved and converted to a value before the controller is instantiated and the
38+
* `$aftreRouteChange` event is fired. The map object is:
39+
*
40+
* - `key` – `{string}`: a name of a dependency to be injected into the controller.
41+
* - `factory` - `{string|function}`: If `string` then it is an alias for a service.
42+
* Otherwise if function, then it is {@link api/angular.module.AUTO.$injector#invoke injected}
43+
* and the return value is treated as the dependency. If the result is a promise, it is resolved
44+
* before its value is injected into the controller.
45+
*
3546
* - `redirectTo` – {(string|function())=} – value to update
3647
* {@link angular.module.ng.$location $location} path with and trigger route redirection.
3748
*
@@ -89,8 +100,8 @@ function $RouteProvider(){
89100
};
90101

91102

92-
this.$get = ['$rootScope', '$location', '$routeParams',
93-
function( $rootScope, $location, $routeParams) {
103+
this.$get = ['$rootScope', '$location', '$routeParams', '$q', '$injector', '$http', '$templateCache',
104+
function( $rootScope, $location, $routeParams, $q, $injector, $http, $templateCache) {
94105

95106
/**
96107
* @ngdoc object
@@ -99,6 +110,16 @@ function $RouteProvider(){
99110
* @requires $routeParams
100111
*
101112
* @property {Object} current Reference to the current route definition.
113+
* The route definition contains:
114+
*
115+
* - `controller`: The controller constructor as define in route definition.
116+
* - `locals`: A map of locals which is used by {@link angular.module.ng.$controller $controller} service for
117+
* controller instantiation. The `locals` contain
118+
* the resolved values of the `resolve` map. Additionally the `locals` also contain:
119+
*
120+
* - `$scope` - The current route scope.
121+
* - `$template` - The current route template HTML.
122+
*
102123
* @property {Array.<Object>} routes Array of all configured routes.
103124
*
104125
* @description
@@ -153,7 +174,15 @@ function $RouteProvider(){
153174
angular.module('ngView', [], function($routeProvider, $locationProvider) {
154175
$routeProvider.when('/Book/:bookId', {
155176
template: 'book.html',
156-
controller: BookCntl
177+
controller: BookCntl,
178+
resolve: {
179+
// I will cause a 1 second delay
180+
delay: function($q, $timeout) {
181+
var delay = $q.defer();
182+
$timeout(delay.resolve, 1000);
183+
return delay.promise;
184+
}
185+
}
157186
});
158187
$routeProvider.when('/Book/:bookId/ch/:chapterId', {
159188
template: 'chapter.html',
@@ -190,6 +219,7 @@ function $RouteProvider(){
190219
expect(content).toMatch(/Chapter Id\: 1/);
191220
192221
element('a:contains("Scarlet")').click();
222+
sleep(2); // promises are not part of scenario waiting
193223
content = element('.doc-example-live [ng-view]').text();
194224
expect(content).toMatch(/controller\: BookCntl/);
195225
expect(content).toMatch(/Book Id\: Scarlet/);
@@ -204,7 +234,11 @@ function $RouteProvider(){
204234
* @eventOf angular.module.ng.$route
205235
* @eventType broadcast on root scope
206236
* @description
207-
* Broadcasted before a route change.
237+
* Broadcasted before a route change. At this point the route services starts
238+
* resolving all of the dependencies needed for the route change to occurs.
239+
* Typically this involves fetching the view template as well as any dependencies
240+
* defined in `resolve` route property. Once all of the dependencies are resolved
241+
* `$afterRouteChange` is fired.
208242
*
209243
* @param {Route} next Future route information.
210244
* @param {Route} current Current route information.
@@ -216,12 +250,27 @@ function $RouteProvider(){
216250
* @eventOf angular.module.ng.$route
217251
* @eventType broadcast on root scope
218252
* @description
219-
* Broadcasted after a route change.
253+
* Broadcasted after a route dependencies are resolved.
254+
* {@link angular.module.ng.$compileProvider.directive.ngView ngView} listens for the directive
255+
* to instantiate the controller and render the view.
220256
*
221257
* @param {Route} current Current route information.
222258
* @param {Route} previous Previous route information.
223259
*/
224260

261+
/**
262+
* @ngdoc event
263+
* @name angular.module.ng.$route#$routeChangeError
264+
* @eventOf angular.module.ng.$route
265+
* @eventType broadcast on root scope
266+
* @description
267+
* Broadcasted if any of the resolve promises are rejected.
268+
*
269+
* @param {Route} current Current route information.
270+
* @param {Route} previous Previous route information.
271+
* @param {Route} rejection Rejection of the promise. Usually the error of the failed promise.
272+
*/
273+
225274
/**
226275
* @ngdoc event
227276
* @name angular.module.ng.$route#$routeUpdate
@@ -245,7 +294,7 @@ function $RouteProvider(){
245294
* @methodOf angular.module.ng.$route
246295
*
247296
* @description
248-
* Causes `$route` service to reload theR current route even if
297+
* Causes `$route` service to reload the current route even if
249298
* {@link angular.module.ng.$location $location} hasn't changed.
250299
*
251300
* As a result of that, {@link angular.module.ng.$compileProvider.directive.ngView ngView}
@@ -309,11 +358,48 @@ function $RouteProvider(){
309358
$location.url(next.redirectTo(next.pathParams, $location.path(), $location.search()))
310359
.replace();
311360
}
312-
} else {
313-
copy(next.params, $routeParams);
314361
}
315362
}
316-
$rootScope.$broadcast('$afterRouteChange', next, last);
363+
364+
$q.when(next).
365+
then(function() {
366+
if (next) {
367+
var keys = [],
368+
values = [];
369+
370+
forEach(next.resolve || {}, function(value, key) {
371+
keys.push(key);
372+
values.push(isFunction(value) ? $injector.invoke(value) : $injector.get(value));
373+
});
374+
if (next.template) {
375+
keys.push('$template');
376+
values.push($http.
377+
get(next.template, {cache: $templateCache}).
378+
then(function(response) { return response.data; }));
379+
}
380+
return $q.all(values).then(function(values) {
381+
var locals = {};
382+
forEach(values, function(value, index) {
383+
locals[keys[index]] = value;
384+
});
385+
return locals;
386+
});
387+
}
388+
}).
389+
// after route change
390+
then(function(locals) {
391+
if (next == $route.current) {
392+
if (next) {
393+
next.locals = locals;
394+
copy(next.params, $routeParams);
395+
}
396+
$rootScope.$broadcast('$afterRouteChange', next, last);
397+
}
398+
}, function(error) {
399+
if (next == $route.current) {
400+
$rootScope.$broadcast('$routeChangeError', next, last, error);
401+
}
402+
});
317403
}
318404
}
319405

test/ng/directive/ngViewSpec.js

+2-20
Original file line numberDiff line numberDiff line change
@@ -229,24 +229,6 @@ describe('ngView', function() {
229229
});
230230

231231

232-
it('should clear the content when error during xhr request', function() {
233-
module(function($routeProvider) {
234-
$routeProvider.when('/foo', {controller: noop, template: 'myUrl1'});
235-
});
236-
237-
inject(function($route, $location, $rootScope, $httpBackend) {
238-
$location.path('/foo');
239-
$httpBackend.expect('GET', 'myUrl1').respond(404, '');
240-
element.text('content');
241-
242-
$rootScope.$digest();
243-
$httpBackend.flush();
244-
245-
expect(element.text()).toBe('');
246-
});
247-
});
248-
249-
250232
it('should be async even if served from cache', function() {
251233
module(function($routeProvider) {
252234
$routeProvider.when('/foo', {controller: noop, template: 'myUrl1'});
@@ -293,8 +275,8 @@ describe('ngView', function() {
293275
$rootScope.$digest();
294276

295277
expect(element.text()).toBe('bound-value');
296-
expect(log).toEqual(['$beforeRouteChange', '$afterRouteChange', 'init-ctrl',
297-
'$viewContentLoaded']);
278+
expect(log).toEqual([
279+
'$beforeRouteChange', 'init-ctrl', '$viewContentLoaded', '$afterRouteChange' ]);
298280
});
299281
});
300282

0 commit comments

Comments
 (0)