angular.js/src/ng/route.js

352 lines
12 KiB
JavaScript
Raw Normal View History

'use strict';
/**
* @ngdoc object
* @name angular.module.ng.$routeProvider
* @function
*
* @description
*
* Used for configuring routes. See {@link angular.module.ng.$route $route} for an example.
*/
function $RouteProvider(){
var routes = {};
/**
* @ngdoc method
* @name angular.module.ng.$routeProvider#when
* @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
* `$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
* match.
*
* Object properties:
*
* - `controller` `{function()=}` Controller fn that should be associated with newly
* created scope.
* - `template` `{string=}` path to an html template that should be used by
2012-03-09 08:00:05 +00:00
* {@link angular.module.ng.$compileProvider.directive.ng-view ng-view} or
* {@link angular.module.ng.$compileProvider.directive.ng-include ng-include} directives.
* - `redirectTo` {(string|function())=} value to update
* {@link angular.module.ng.$location $location} path with and trigger route redirection.
*
* If `redirectTo` is a function, it will be called with the following parameters:
*
* - `{Object.<string>}` - route parameters extracted from the current
* `$location.path()` by applying the current route template.
* - `{string}` - current `$location.path()`
* - `{Object}` - current `$location.search()`
*
* The custom `redirectTo` function is expected to return a string which will be used
* to update `$location.path()` and `$location.search()`.
*
* - `[reloadOnSearch=true]` - {boolean=} - reload route when only $location.search()
* changes.
*
* If the option is set to `false` and url in the browser changes, then
* `$routeUpdate` event is broadcasted on the root scope.
*
* @returns {Object} route object
*
* @description
* Adds a new route definition to the `$route` service.
*/
this.when = function(path, route) {
var routeDef = routes[path];
if (!routeDef) routeDef = routes[path] = {reloadOnSearch: true};
if (route) extend(routeDef, route); // TODO(im): what the heck? merge two route definitions?
// create redirection for trailing slashes
if (path) {
var redirectPath = (path[path.length-1] == '/')
? path.substr(0, path.length-1)
: path +'/';
routes[redirectPath] = {redirectTo: path};
}
return routeDef;
};
/**
* @ngdoc method
* @name angular.module.ng.$routeProvider#otherwise
* @methodOf angular.module.ng.$routeProvider
*
* @description
* Sets route definition that will be used on route change when no other route definition
* is matched.
*
* @param {Object} params Mapping information to be assigned to `$route.current`.
*/
this.otherwise = function(params) {
this.when(null, params);
};
this.$get = ['$rootScope', '$location', '$routeParams',
function( $rootScope, $location, $routeParams) {
/**
* @ngdoc object
* @name angular.module.ng.$route
* @requires $location
* @requires $routeParams
*
* @property {Object} current Reference to the current route definition.
* @property {Array.<Object>} routes Array of all configured routes.
*
* @description
* Is used for deep-linking URLs to controllers and views (HTML partials).
* It watches `$location.url()` and tries to map the path to an existing route definition.
*
* You can define routes through {@link angular.module.ng.$routeProvider $routeProvider}'s API.
*
2012-03-09 08:00:05 +00:00
* The `$route` service is typically used in conjunction with {@link angular.module.ng.$compileProvider.directive.ng-view ng-view}
* directive and the {@link angular.module.ng.$routeParams $routeParams} service.
*
* @example
This example shows how changing the URL hash causes the `$route` to match a route against the
2012-03-09 08:00:05 +00:00
URL, and the `ng-view` pulls in the partial.
2012-03-14 02:36:09 +00:00
Note that this example is using {@link angular.module.ng.$compileProvider.directive.script inlined templates}
to get it working on jsfiddle as well.
<doc:example module="route">
<doc:source>
<script type="text/ng-template" id="examples/book.html">
controller: {{name}}<br />
Book Id: {{params.bookId}}<br />
</script>
<script type="text/ng-template" id="examples/chapter.html">
controller: {{name}}<br />
Book Id: {{params.bookId}}<br />
Chapter Id: {{params.chapterId}}
</script>
<script>
angular.module('route', [], function($routeProvider, $locationProvider) {
$routeProvider.when('/Book/:bookId', {template: 'examples/book.html', controller: BookCntl});
$routeProvider.when('/Book/:bookId/ch/:chapterId', {template: 'examples/chapter.html', controller: ChapterCntl});
// configure html5 to get links working on jsfiddle
$locationProvider.html5Mode(true);
});
function MainCntl($scope, $route, $routeParams, $location) {
$scope.$route = $route;
$scope.$location = $location;
$scope.$routeParams = $routeParams;
}
function BookCntl($scope, $routeParams) {
$scope.name = "BookCntl";
$scope.params = $routeParams;
}
function ChapterCntl($scope, $routeParams) {
$scope.name = "ChapterCntl";
$scope.params = $routeParams;
}
</script>
2012-03-09 08:00:05 +00:00
<div ng-controller="MainCntl">
Choose:
<a href="/Book/Moby">Moby</a> |
<a href="/Book/Moby/ch/1">Moby: Ch1</a> |
<a href="/Book/Gatsby">Gatsby</a> |
<a href="/Book/Gatsby/ch/4?key=value">Gatsby: Ch4</a> |
<a href="/Book/Scarlet">Scarlet Letter</a><br/>
2012-03-09 08:00:05 +00:00
<div ng-view></div>
<hr />
<pre>$location.path() = {{$location.path()}}</pre>
<pre>$route.current.template = {{$route.current.template}}</pre>
<pre>$route.current.params = {{$route.current.params}}</pre>
<pre>$route.current.scope.name = {{$route.current.scope.name}}</pre>
<pre>$routeParams = {{$routeParams}}</pre>
</div>
</doc:source>
<doc:scenario>
it('should load and compile correct template', function() {
element('a:contains("Moby: Ch1")').click();
2012-03-09 08:00:05 +00:00
var content = element('.doc-example-live [ng-view]').text();
expect(content).toMatch(/controller\: ChapterCntl/);
expect(content).toMatch(/Book Id\: Moby/);
expect(content).toMatch(/Chapter Id\: 1/);
element('a:contains("Scarlet")').click();
2012-03-09 08:00:05 +00:00
content = element('.doc-example-live [ng-view]').text();
expect(content).toMatch(/controller\: BookCntl/);
expect(content).toMatch(/Book Id\: Scarlet/);
});
</doc:scenario>
</doc:example>
*/
/**
* @ngdoc event
* @name angular.module.ng.$route#$beforeRouteChange
* @eventOf angular.module.ng.$route
* @eventType broadcast on root scope
* @description
* Broadcasted before a route change.
*
* @param {Route} next Future route information.
* @param {Route} current Current route information.
*/
/**
* @ngdoc event
* @name angular.module.ng.$route#$afterRouteChange
* @eventOf angular.module.ng.$route
* @eventType broadcast on root scope
* @description
* Broadcasted after a route change.
*
* @param {Route} current Current route information.
* @param {Route} previous Previous route information.
*/
/**
* @ngdoc event
* @name angular.module.ng.$route#$routeUpdate
* @eventOf angular.module.ng.$route
* @eventType broadcast on root scope
* @description
*
* The `reloadOnSearch` property has been set to false, and we are reusing the same
* instance of the Controller.
*/
var matcher = switchRouteMatcher,
dirty = 0,
forceReload = false,
$route = {
routes: routes,
/**
* @ngdoc method
* @name angular.module.ng.$route#reload
* @methodOf angular.module.ng.$route
*
* @description
* Causes `$route` service to reload the current route even if
* {@link angular.module.ng.$location $location} hasn't changed.
*
2012-03-09 08:00:05 +00:00
* As a result of that, {@link angular.module.ng.$compileProvider.directive.ng-view ng-view}
* creates new scope, reinstantiates the controller.
*/
reload: function() {
dirty++;
forceReload = true;
}
};
$rootScope.$watch(function() { return dirty + $location.url(); }, updateRoute);
return $route;
/////////////////////////////////////////////////////
function switchRouteMatcher(on, when) {
// TODO(i): this code is convoluted and inefficient, we should construct the route matching
// regex only once and then reuse it
var regex = '^' + when.replace(/([\.\\\(\)\^\$])/g, "\\$1") + '$',
params = [],
dst = {};
forEach(when.split(/\W/), function(param) {
if (param) {
var paramRegExp = new RegExp(":" + param + "([\\W])");
if (regex.match(paramRegExp)) {
regex = regex.replace(paramRegExp, "([^\\/]*)$1");
params.push(param);
}
}
});
var match = on.match(new RegExp(regex));
if (match) {
forEach(params, function(name, index) {
dst[name] = match[index + 1];
});
}
return match ? dst : null;
}
function updateRoute() {
var next = parseRoute(),
last = $route.current;
if (next && last && next.$route === last.$route
&& equals(next.pathParams, last.pathParams) && !next.reloadOnSearch && !forceReload) {
last.params = next.params;
copy(last.params, $routeParams);
$rootScope.$broadcast('$routeUpdate', last);
} else if (next || last) {
forceReload = false;
$rootScope.$broadcast('$beforeRouteChange', next, last);
$route.current = next;
if (next) {
if (next.redirectTo) {
if (isString(next.redirectTo)) {
$location.path(interpolate(next.redirectTo, next.params)).search(next.params)
.replace();
} else {
$location.url(next.redirectTo(next.pathParams, $location.path(), $location.search()))
.replace();
}
} else {
copy(next.params, $routeParams);
}
}
$rootScope.$broadcast('$afterRouteChange', next, last);
}
}
/**
* @returns the current active route, by matching it against the URL
*/
function parseRoute() {
// Match a route
var params, match;
forEach(routes, function(route, path) {
if (!match && (params = matcher($location.path(), path))) {
match = inherit(route, {
params: extend({}, $location.search(), params),
pathParams: params});
match.$route = route;
}
});
// No route matched; fallback to "otherwise" route
return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}});
}
/**
* @returns interpolation of the redirect path with the parametrs
*/
function interpolate(string, params) {
var result = [];
forEach((string||'').split(':'), function(segment, i) {
if (i == 0) {
result.push(segment);
} else {
var segmentMatch = segment.match(/(\w+)(.*)/);
var key = segmentMatch[1];
result.push(params[key]);
result.push(segmentMatch[2] || '');
delete params[key];
}
});
return result.join('');
}
}];
}