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

Commit

Permalink
feat($route): resolve local route promises
Browse files Browse the repository at this point in the history
Resolve all promises on route before we fire $afterRouteChange which then renders the ngView.
  • Loading branch information
mhevery committed Jun 1, 2012
1 parent 4361efb commit 885fb0d
Show file tree
Hide file tree
Showing 4 changed files with 367 additions and 79 deletions.
64 changes: 28 additions & 36 deletions src/ng/directive/ngView.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,7 @@ var ngViewDirective = ['$http', '$templateCache', '$route', '$anchorScroll', '$c
restrict: 'ECA',
terminal: true,
link: function(scope, element, attr) {
var changeCounter = 0,
lastScope,
var lastScope,
onloadExp = attr.onload || '';

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

function clearContent() {
element.html('');
destroyLastScope();
}

function update() {
var template = $route.current && $route.current.template,
thisChangeId = ++changeCounter;

function clearContent() {
// ignore callback if another route change occured since
if (thisChangeId === changeCounter) {
element.html('');
destroyLastScope();
}
}
var locals = $route.current && $route.current.locals,
template = locals && locals.$template;

if (template) {
$http.get(template, {cache: $templateCache}).success(function(response) {
// ignore callback if another route change occured since
if (thisChangeId === changeCounter) {
element.html(response);
destroyLastScope();

var link = $compile(element.contents()),
current = $route.current,
controller;

lastScope = current.scope = scope.$new();
if (current.controller) {
controller = $controller(current.controller, {$scope: lastScope});
element.contents().data('$ngControllerController', controller);
}

link(lastScope);
lastScope.$emit('$viewContentLoaded');
lastScope.$eval(onloadExp);

// $anchorScroll might listen on event...
$anchorScroll();
}
}).error(clearContent);
element.html(template);
destroyLastScope();

var link = $compile(element.contents()),
current = $route.current,
controller;

lastScope = current.scope = scope.$new();
if (current.controller) {
locals.$scope = lastScope;
controller = $controller(current.controller, locals);
element.contents().data('$ngControllerController', controller);
}

link(lastScope);
lastScope.$emit('$viewContentLoaded');
lastScope.$eval(onloadExp);

// $anchorScroll might listen on event...
$anchorScroll();
} else {
clearContent();
}
Expand Down
106 changes: 96 additions & 10 deletions src/ng/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function $RouteProvider(){
* @methodOf angular.module.ng.$routeProvider
*
* @param {string} path Route path (matched against `$location.path`). If `$location.path`
* contains redudant trailing slash or is missing one, the route will still match and the
* contains redundant trailing slash or is missing one, the route will still match and the
* `$location.path` will be updated to add or drop the trailing slash to exacly match the
* route definition.
* @param {Object} route Mapping information to be assigned to `$route.current` on route
Expand All @@ -32,6 +32,17 @@ function $RouteProvider(){
* - `template` – `{string=}` – path to an html template that should be used by
* {@link angular.module.ng.$compileProvider.directive.ngView ngView} or
* {@link angular.module.ng.$compileProvider.directive.ngInclude ngInclude} directives.
* - `resolve` - `{Object.<string, function>=}` - An optional map of dependencies which should
* be injected into the controller. If any of these dependencies are promises, they will be
* resolved and converted to a value before the controller is instantiated and the
* `$aftreRouteChange` event is fired. The map object is:
*
* - `key` – `{string}`: a name of a dependency to be injected into the controller.
* - `factory` - `{string|function}`: If `string` then it is an alias for a service.
* Otherwise if function, then it is {@link api/angular.module.AUTO.$injector#invoke injected}
* and the return value is treated as the dependency. If the result is a promise, it is resolved
* before its value is injected into the controller.
*
* - `redirectTo` – {(string|function())=} – value to update
* {@link angular.module.ng.$location $location} path with and trigger route redirection.
*
Expand Down Expand Up @@ -89,8 +100,8 @@ function $RouteProvider(){
};


this.$get = ['$rootScope', '$location', '$routeParams',
function( $rootScope, $location, $routeParams) {
this.$get = ['$rootScope', '$location', '$routeParams', '$q', '$injector', '$http', '$templateCache',
function( $rootScope, $location, $routeParams, $q, $injector, $http, $templateCache) {

/**
* @ngdoc object
Expand All @@ -99,6 +110,16 @@ function $RouteProvider(){
* @requires $routeParams
*
* @property {Object} current Reference to the current route definition.
* The route definition contains:
*
* - `controller`: The controller constructor as define in route definition.
* - `locals`: A map of locals which is used by {@link angular.module.ng.$controller $controller} service for
* controller instantiation. The `locals` contain
* the resolved values of the `resolve` map. Additionally the `locals` also contain:
*
* - `$scope` - The current route scope.
* - `$template` - The current route template HTML.
*
* @property {Array.<Object>} routes Array of all configured routes.
*
* @description
Expand Down Expand Up @@ -153,7 +174,15 @@ function $RouteProvider(){
angular.module('ngView', [], function($routeProvider, $locationProvider) {
$routeProvider.when('/Book/:bookId', {
template: 'book.html',
controller: BookCntl
controller: BookCntl,
resolve: {
// I will cause a 1 second delay
delay: function($q, $timeout) {
var delay = $q.defer();
$timeout(delay.resolve, 1000);
return delay.promise;
}
}
});
$routeProvider.when('/Book/:bookId/ch/:chapterId', {
template: 'chapter.html',
Expand Down Expand Up @@ -190,6 +219,7 @@ function $RouteProvider(){
expect(content).toMatch(/Chapter Id\: 1/);
element('a:contains("Scarlet")').click();
sleep(2); // promises are not part of scenario waiting
content = element('.doc-example-live [ng-view]').text();
expect(content).toMatch(/controller\: BookCntl/);
expect(content).toMatch(/Book Id\: Scarlet/);
Expand All @@ -204,7 +234,11 @@ function $RouteProvider(){
* @eventOf angular.module.ng.$route
* @eventType broadcast on root scope
* @description
* Broadcasted before a route change.
* Broadcasted before a route change. At this point the route services starts
* resolving all of the dependencies needed for the route change to occurs.
* Typically this involves fetching the view template as well as any dependencies
* defined in `resolve` route property. Once all of the dependencies are resolved
* `$afterRouteChange` is fired.
*
* @param {Route} next Future route information.
* @param {Route} current Current route information.
Expand All @@ -216,12 +250,27 @@ function $RouteProvider(){
* @eventOf angular.module.ng.$route
* @eventType broadcast on root scope
* @description
* Broadcasted after a route change.
* Broadcasted after a route dependencies are resolved.
* {@link angular.module.ng.$compileProvider.directive.ngView ngView} listens for the directive
* to instantiate the controller and render the view.
*
* @param {Route} current Current route information.
* @param {Route} previous Previous route information.
*/

/**
* @ngdoc event
* @name angular.module.ng.$route#$routeChangeError
* @eventOf angular.module.ng.$route
* @eventType broadcast on root scope
* @description
* Broadcasted if any of the resolve promises are rejected.
*
* @param {Route} current Current route information.
* @param {Route} previous Previous route information.
* @param {Route} rejection Rejection of the promise. Usually the error of the failed promise.
*/

/**
* @ngdoc event
* @name angular.module.ng.$route#$routeUpdate
Expand All @@ -245,7 +294,7 @@ function $RouteProvider(){
* @methodOf angular.module.ng.$route
*
* @description
* Causes `$route` service to reload theR current route even if
* Causes `$route` service to reload the current route even if
* {@link angular.module.ng.$location $location} hasn't changed.
*
* As a result of that, {@link angular.module.ng.$compileProvider.directive.ngView ngView}
Expand Down Expand Up @@ -309,11 +358,48 @@ function $RouteProvider(){
$location.url(next.redirectTo(next.pathParams, $location.path(), $location.search()))
.replace();
}
} else {
copy(next.params, $routeParams);
}
}
$rootScope.$broadcast('$afterRouteChange', next, last);

$q.when(next).
then(function() {
if (next) {
var keys = [],
values = [];

forEach(next.resolve || {}, function(value, key) {
keys.push(key);
values.push(isFunction(value) ? $injector.invoke(value) : $injector.get(value));
});
if (next.template) {
keys.push('$template');
values.push($http.
get(next.template, {cache: $templateCache}).
then(function(response) { return response.data; }));
}
return $q.all(values).then(function(values) {
var locals = {};
forEach(values, function(value, index) {
locals[keys[index]] = value;
});
return locals;
});
}
}).
// after route change
then(function(locals) {
if (next == $route.current) {
if (next) {
next.locals = locals;
copy(next.params, $routeParams);
}
$rootScope.$broadcast('$afterRouteChange', next, last);
}
}, function(error) {
if (next == $route.current) {
$rootScope.$broadcast('$routeChangeError', next, last, error);
}
});
}
}

Expand Down
22 changes: 2 additions & 20 deletions test/ng/directive/ngViewSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,24 +229,6 @@ describe('ngView', function() {
});


it('should clear the content when error during xhr request', function() {
module(function($routeProvider) {
$routeProvider.when('/foo', {controller: noop, template: 'myUrl1'});
});

inject(function($route, $location, $rootScope, $httpBackend) {
$location.path('/foo');
$httpBackend.expect('GET', 'myUrl1').respond(404, '');
element.text('content');

$rootScope.$digest();
$httpBackend.flush();

expect(element.text()).toBe('');
});
});


it('should be async even if served from cache', function() {
module(function($routeProvider) {
$routeProvider.when('/foo', {controller: noop, template: 'myUrl1'});
Expand Down Expand Up @@ -293,8 +275,8 @@ describe('ngView', function() {
$rootScope.$digest();

expect(element.text()).toBe('bound-value');
expect(log).toEqual(['$beforeRouteChange', '$afterRouteChange', 'init-ctrl',
'$viewContentLoaded']);
expect(log).toEqual([
'$beforeRouteChange', 'init-ctrl', '$viewContentLoaded', '$afterRouteChange' ]);
});
});

Expand Down
Loading

0 comments on commit 885fb0d

Please sign in to comment.