diff options
Diffstat (limited to 'src/service/route.js')
| -rw-r--r-- | src/service/route.js | 266 |
1 files changed, 266 insertions, 0 deletions
diff --git a/src/service/route.js b/src/service/route.js new file mode 100644 index 00000000..2de484f6 --- /dev/null +++ b/src/service/route.js @@ -0,0 +1,266 @@ +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$route + * @requires $location + * + * @property {Object} current Reference to the current route definition. + * @property {Array.<Object>} routes Array of all configured routes. + * + * @description + * Watches `$location.hashPath` and tries to map the hash to an existing route + * definition. It is used for deep-linking URLs to controllers and views (HTML partials). + * + * The `$route` service is typically used in conjunction with {@link angular.widget.ng:view ng:view} + * widget. + * + * @example + This example shows how changing the URL hash causes the <tt>$route</tt> + to match a route against the URL, and the <tt>[[ng:include]]</tt> pulls in the partial. + Try changing the URL in the input box to see changes. + + <doc:example> + <doc:source> + <script> + angular.service('myApp', function($route) { + $route.when('/Book/:bookId', {template:'rsrc/book.html', controller:BookCntl}); + $route.when('/Book/:bookId/ch/:chapterId', {template:'rsrc/chapter.html', controller:ChapterCntl}); + $route.onChange(function() { + $route.current.scope.params = $route.current.params; + }); + }, {$inject: ['$route']}); + + function BookCntl() { + this.name = "BookCntl"; + } + + function ChapterCntl() { + this.name = "ChapterCntl"; + } + </script> + + Chose: + <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><br/> + <input type="text" name="$location.hashPath" size="80" /> + <pre>$location={{$location}}</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> + <hr/> + <ng:include src="$route.current.template" scope="$route.current.scope"/> + </doc:source> + <doc:scenario> + </doc:scenario> + </doc:example> + */ +angularServiceInject('$route', function(location, $updateView) { + var routes = {}, + onChange = [], + matcher = switchRouteMatcher, + parentScope = this, + dirty = 0, + $route = { + routes: routes, + + /** + * @workInProgress + * @ngdoc method + * @name angular.service.$route#onChange + * @methodOf angular.service.$route + * + * @param {function()} fn Function that will be called when `$route.current` changes. + * @returns {function()} The registered function. + * + * @description + * Register a handler function that will be called when route changes + */ + onChange: function(fn) { + onChange.push(fn); + return fn; + }, + + /** + * @workInProgress + * @ngdoc method + * @name angular.service.$route#parent + * @methodOf angular.service.$route + * + * @param {Scope} [scope=rootScope] Scope to be used as parent for newly created + * `$route.current.scope` scopes. + * + * @description + * Sets a scope to be used as the parent scope for scopes created on route change. If not + * set, defaults to the root scope. + */ + parent: function(scope) { + if (scope) parentScope = scope; + }, + + /** + * @workInProgress + * @ngdoc method + * @name angular.service.$route#when + * @methodOf angular.service.$route + * + * @param {string} path Route path (matched against `$location.hash`) + * @param {Object} params 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 + * {@link angular.widget.ng:view ng:view} or + * {@link angular.widget.ng:include ng:include} widgets. + * - `redirectTo` – {(string|function())=} – value to update + * {@link angular.service.$location $location} hash 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.hashPath` by applying the current route template. + * - `{string}` - current `$location.hash` + * - `{string}` - current `$location.hashPath` + * - `{string}` - current `$location.hashSearch` + * + * The custom `redirectTo` function is expected to return a string which will be used + * to update `$location.hash`. + * + * @returns {Object} route object + * + * @description + * Adds a new route definition to the `$route` service. + */ + when:function (path, params) { + if (isUndefined(path)) return routes; //TODO(im): remove - not needed! + var route = routes[path]; + if (!route) route = routes[path] = {}; + if (params) extend(route, params); + dirty++; + return route; + }, + + /** + * @workInProgress + * @ngdoc method + * @name angular.service.$route#otherwise + * @methodOf angular.service.$route + * + * @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`. + */ + otherwise: function(params) { + $route.when(null, params); + }, + + /** + * @workInProgress + * @ngdoc method + * @name angular.service.$route#reload + * @methodOf angular.service.$route + * + * @description + * Causes `$route` service to reload (and recreate the `$route.current` scope) upon the next + * eval even if {@link angular.service.$location $location} hasn't changed. + */ + reload: function() { + dirty++; + } + }; + + + function switchRouteMatcher(on, when, dstName) { + 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]; + }); + if (dstName) this.$set(dstName, dst); + } + return match ? dst : _null; + } + + + function updateRoute(){ + var childScope, routeParams, pathParams, segmentMatch, key, redir; + + $route.current = _null; + forEach(routes, function(rParams, rPath) { + if (!pathParams) { + if (pathParams = matcher(location.hashPath, rPath)) { + routeParams = rParams; + } + } + }); + + // "otherwise" fallback + routeParams = routeParams || routes[_null]; + + if(routeParams) { + if (routeParams.redirectTo) { + if (isString(routeParams.redirectTo)) { + // interpolate the redirectTo string + redir = {hashPath: '', + hashSearch: extend({}, location.hashSearch, pathParams)}; + + forEach(routeParams.redirectTo.split(':'), function(segment, i) { + if (i==0) { + redir.hashPath += segment; + } else { + segmentMatch = segment.match(/(\w+)(.*)/); + key = segmentMatch[1]; + redir.hashPath += pathParams[key] || location.hashSearch[key]; + redir.hashPath += segmentMatch[2] || ''; + delete redir.hashSearch[key]; + } + }); + } else { + // call custom redirectTo function + redir = {hash: routeParams.redirectTo(pathParams, location.hash, location.hashPath, + location.hashSearch)}; + } + + location.update(redir); + $updateView(); //TODO this is to work around the $location<=>$browser issues + return; + } + + childScope = createScope(parentScope); + $route.current = extend({}, routeParams, { + scope: childScope, + params: extend({}, location.hashSearch, pathParams) + }); + } + + //fire onChange callbacks + forEach(onChange, parentScope.$tryEval); + + if (childScope) { + childScope.$become($route.current.controller); + } + } + + + this.$watch(function(){return dirty + location.hash;}, updateRoute); + + return $route; +}, ['$location', '$updateView']); |
