diff options
Diffstat (limited to 'src/ng/route.js')
| -rw-r--r-- | src/ng/route.js | 351 | 
1 files changed, 351 insertions, 0 deletions
| diff --git a/src/ng/route.js b/src/ng/route.js new file mode 100644 index 00000000..2b9d187a --- /dev/null +++ b/src/ng/route.js @@ -0,0 +1,351 @@ +'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 +   *      {@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. +     * +     * 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 +       URL, and the `ng-view` pulls in the partial. + +       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> + +          <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/> + +            <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(); +            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(); +            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. +           * +           * 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(''); +    } +  }]; +} | 
