diff options
| -rw-r--r-- | Rakefile | 1 | ||||
| -rw-r--r-- | jsTestDriver-jquery.conf | 1 | ||||
| -rw-r--r-- | jsTestDriver-perf.conf | 1 | ||||
| -rw-r--r-- | jsTestDriver.conf | 1 | ||||
| -rw-r--r-- | src/angular-bootstrap.js | 1 | ||||
| -rw-r--r-- | src/service/route.js | 242 | ||||
| -rw-r--r-- | src/service/routeParams.js | 31 | ||||
| -rw-r--r-- | src/widgets.js | 5 | ||||
| -rw-r--r-- | test/service/routeParamsSpec.js | 41 | ||||
| -rw-r--r-- | test/service/routeSpec.js | 94 | 
10 files changed, 286 insertions, 132 deletions
@@ -27,6 +27,7 @@ ANGULAR = [    'src/service/log.js',    'src/service/resource.js',    'src/service/route.js', +  'src/service/routeParams.js',    'src/service/window.js',    'src/service/xhr.bulk.js',    'src/service/xhr.cache.js', diff --git a/jsTestDriver-jquery.conf b/jsTestDriver-jquery.conf index 018e836c..705ade10 100644 --- a/jsTestDriver-jquery.conf +++ b/jsTestDriver-jquery.conf @@ -30,6 +30,7 @@ load:    - src/service/log.js    - src/service/resource.js    - src/service/route.js +  - src/service/routeParams.js    - src/service/window.js    - src/service/xhr.bulk.js    - src/service/xhr.cache.js diff --git a/jsTestDriver-perf.conf b/jsTestDriver-perf.conf index a46cbf71..6ddc018e 100644 --- a/jsTestDriver-perf.conf +++ b/jsTestDriver-perf.conf @@ -28,6 +28,7 @@ load:    - src/service/log.js    - src/service/resource.js    - src/service/route.js +  - src/service/routeParams.js    - src/service/updateView.js    - src/service/window.js    - src/service/xhr.bulk.js diff --git a/jsTestDriver.conf b/jsTestDriver.conf index b55a387c..24349094 100644 --- a/jsTestDriver.conf +++ b/jsTestDriver.conf @@ -30,6 +30,7 @@ load:    - src/service/log.js    - src/service/resource.js    - src/service/route.js +  - src/service/routeParams.js    - src/service/window.js    - src/service/xhr.bulk.js    - src/service/xhr.cache.js diff --git a/src/angular-bootstrap.js b/src/angular-bootstrap.js index fdf07c3e..f88973e0 100644 --- a/src/angular-bootstrap.js +++ b/src/angular-bootstrap.js @@ -114,6 +114,7 @@               'service/log.js',               'service/resource.js',               'service/route.js', +             'service/routeParams.js',               'service/window.js',               'service/xhr.bulk.js',               'service/xhr.cache.js', diff --git a/src/service/route.js b/src/service/route.js index b7e4814b..523a244a 100644 --- a/src/service/route.js +++ b/src/service/route.js @@ -5,6 +5,7 @@   * @ngdoc service   * @name angular.service.$route   * @requires $location + * @requires $routeParams   *   * @property {Object} current Reference to the current route definition.   * @property {Array.<Object>} routes Array of all configured routes. @@ -14,7 +15,7 @@   * 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. + * widget and the {@link angular.service.$routeParams $routeParams} service.   *   * @example     This example shows how changing the URL hash causes the <tt>$route</tt> @@ -24,23 +25,23 @@      <doc:example>        <doc:source jsfiddle="false">          <script> -          function MainCntl($route, $location) { +          function MainCntl($route, $routeParams, $location) {              this.$route = $route;              this.$location = $location; +            this.$routeParams = $routeParams;              $route.when('/Book/:bookId', {template: 'examples/book.html', controller: BookCntl});              $route.when('/Book/:bookId/ch/:chapterId', {template: 'examples/chapter.html', controller: ChapterCntl}); -            $route.onChange(function() { -              $route.current.scope.params = $route.current.params; -            });            } -          function BookCntl() { +          function BookCntl($routeParams) {              this.name = "BookCntl"; +            this.params = $routeParams;            } -          function ChapterCntl() { +          function ChapterCntl($routeParams) {              this.name = "ChapterCntl"; +            this.params = $routeParams;            }          </script> @@ -54,6 +55,7 @@            <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>            <hr />            <ng:view></ng:view>          </div> @@ -62,37 +64,69 @@        </doc:scenario>      </doc:example>   */ -angularServiceInject('$route', function($location) { +angularServiceInject('$route', function($location, $routeParams) { +  /** +   * @workInProgress +   * @ngdoc event +   * @name angular.service.$route#$beforeRouteChange +   * @eventOf angular.service.$route +   * @eventType Broadcast on root scope +   * @description +   * Broadcasted before a route change. +   * +   * @param {Route} next Future route information. +   * @param {Route} current Current route information. +   * +   * The `Route` object extends the route definition with the following properties. +   * +   *    * `scope` - The instance of the route controller. +   *    * `params` - The current {@link angular.service.$routeParams params}. +   * +   */ + +  /** +   * @workInProgress +   * @ngdoc event +   * @name angular.service.$route#$afterRouteChange +   * @eventOf angular.service.$route +   * @eventType Broadcast on root scope +   * @description +   * Broadcasted after a route change. +   * +   * @param {Route} current Current route information. +   * @param {Route} previous Previous route information. +   * +   * The `Route` object extends the route definition with the following properties. +   * +   *    * `scope` - The instance of the route controller. +   *    * `params` - The current {@link angular.service.$routeParams params}. +   * +   */ + +  /** +   * @workInProgress +   * @ngdoc event +   * @name angular.service.$route#$routeUpdate +   * @eventOf angular.service.$route +   * @eventType Emit on the current route scope. +   * @description +   * +   * The `reloadOnSearch` property has been set to false, and we are reusing the same +   * instance of the Controller. +   */ +    var routes = {}, -      onChange = [],        matcher = switchRouteMatcher,        parentScope = this, +      rootScope = this,        dirty = 0, -      lastHashPath, -      lastRouteParams, +      allowReload = true,        $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           * @@ -114,7 +148,7 @@ angularServiceInject('$route', function($location) {           * @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 +         * @param {Object} route Mapping information to be assigned to `$route.current` on route           *    match.           *           *    Object properties: @@ -139,14 +173,15 @@ angularServiceInject('$route', function($location) {           *      to update `$location.hash`.           *           *    - `[reloadOnSearch=true]` - {boolean=} - reload route when $location.hashSearch -         *      changes. If this option is disabled, you should set up a $watch to be notified of -         *      param (hashSearch) changes as follows: +         *      changes. +         * +         *      If the option is set to false and url in the browser changes, then +         *      $routeUpdate event is emited on the current route scope. You can use this event to +         *      react to {@link angular.service.$routeParams} changes:           * -         *            function MyCtrl($route) { -         *              this.$watch(function() { -         *                return $route.current.params; -         *              }, function(scope, params) { -         *                //do stuff with params +         *            function MyCtrl($route, $routeParams) { +         *              this.$on('$routeUpdate', function() { +         *                // do stuff with $routeParams           *              });           *            }           * @@ -155,13 +190,13 @@ angularServiceInject('$route', function($location) {           * @description           * Adds a new route definition to the `$route` service.           */ -        when:function (path, params) { +        when:function (path, route) {            if (isUndefined(path)) return routes; //TODO(im): remove - not needed! -          var route = routes[path]; -          if (!route) route = routes[path] = {reloadOnSearch: true}; -          if (params) extend(route, params); //TODO(im): what the heck? merge two route definitions? +          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?            dirty++; -          return route; +          return routeDef;          },          /** @@ -192,10 +227,18 @@ angularServiceInject('$route', function($location) {           */          reload: function() {            dirty++; +          allowReload = false;          }        }; + +  this.$watch(function(){ return dirty + $location.hash; }, updateRoute); + +  return $route; + +  ///////////////////////////////////////////////////// +    function switchRouteMatcher(on, when, dstName) {      var regex = '^' + when.replace(/[\.\\\(\)\^\$]/g, "\$1") + '$',          params = [], @@ -219,79 +262,72 @@ angularServiceInject('$route', function($location) {      return match ? dst : null;    } -    function updateRoute(){ -    var selectedRoute, pathParams, segmentMatch, key, redir; +    var next = parseRoute(), +        last = $route.current; -    if ($route.current) { -      if (!$route.current.reloadOnSearch && (lastHashPath == $location.hashPath)) { -        $route.current.params = extend($location.hashSearch, lastRouteParams); -        return; -      } - -      if ($route.current.scope) { -        $route.current.scope.$destroy(); +    if (next && last && next.$route === last.$route +        && equals(next.pathParams, last.pathParams) && !next.reloadOnSearch && allowReload) { +      $route.current = next; +      copy(next.params, $routeParams); +      last.scope && last.scope.$emit('$routeUpdate'); +    } else { +      allowReload = true; +      rootScope.$broadcast('$beforeRouteChange', next, last); +      last && last.scope && last.scope.$destroy(); +      $route.current = next; +      if (next) { +        if (next.redirectTo) { +          $location.update(isString(next.redirectTo) +              ? {hashSearch: next.params, hashPath: interpolate(next.redirectTo, next.params)} +          : {hash: next.redirectTo(next.pathParams, +              $location.hash, $location.hashPath, $location.hashSearch)}); +        } else { +          copy(next.params, $routeParams); +          next.scope = parentScope.$new(next.controller); +        }        } +      rootScope.$broadcast('$afterRouteChange', next, last);      } +  } + -    lastHashPath = $location.hashPath; -    $route.current = null; +  /** +   * @returns the current active route, by matching it against the URL +   */ +  function parseRoute(){      // Match a route -    forEach(routes, function(rParams, rPath) { -      if (!pathParams) { -        if ((pathParams = matcher($location.hashPath, rPath))) { -          selectedRoute = rParams; -        } +    var params, match; +    forEach(routes, function(route, path) { +      if (!match && (params = matcher($location.hashPath, path))) { +        match = inherit(route, { +          params: extend({}, $location.hashSearch, params), +          pathParams: params}); +        match.$route = route;        }      }); -      // No route matched; fallback to "otherwise" route -    selectedRoute = selectedRoute || routes[null]; - -    if(selectedRoute) { -      if (selectedRoute.redirectTo) { -        if (isString(selectedRoute.redirectTo)) { -          // interpolate the redirectTo string -          redir = {hashPath: '', -                   hashSearch: extend({}, $location.hashSearch, pathParams)}; - -          forEach(selectedRoute.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: selectedRoute.redirectTo(pathParams, $location.hash, $location.hashPath, -                                                $location.hashSearch)}; -        } +    return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}}); +  } -        $location.update(redir); -        return; +  /** +   * @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];        } - -      $route.current = extend({}, selectedRoute); -      $route.current.params = extend({}, $location.hashSearch, pathParams); -      lastRouteParams = pathParams; -    } - -    //fire onChange callbacks -    forEach(onChange, parentScope.$eval, parentScope); - -    // Create the scope if we have matched a route -    if ($route.current) { -      $route.current.scope = parentScope.$new($route.current.controller); -    } +    }); +    return result.join('');    } -  this.$watch(function(){ return dirty + $location.hash; }, updateRoute); - -  return $route; -}, ['$location']); +}, ['$location', '$routeParams']); diff --git a/src/service/routeParams.js b/src/service/routeParams.js new file mode 100644 index 00000000..8a69903f --- /dev/null +++ b/src/service/routeParams.js @@ -0,0 +1,31 @@ +'use strict'; + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$routeParams + * @requires $route + * + * @description + * Current set of route parameters. The route parameters are a combination of the + * {@link angular.service.$location $location} `hashSearch`, and `path`. The `path` parameters + * are extracted when the {@link angular.service.$route $route} path is matched. + * + * In case of parameter name collision, `path` params take precedence over `hashSearch` params. + * + * The service guarantees that the identity of the `$routeParams` object will remain unchanged + * (but its properties will likely change) even when a route change occurs. + * + * @example + * <pre> + *  // Given: + *  // URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby + *  // Route: /Chapter/:chapterId/Section/:sectionId + *  // + *  // Then + *  $routeParams ==> {chapterId:1, sectionId:2, search:'moby'} + * </pre> + */ +angularService('$routeParams', function(){ +  return {}; +}); diff --git a/src/widgets.js b/src/widgets.js index a1c4c5d6..42e608dd 100644 --- a/src/widgets.js +++ b/src/widgets.js @@ -1422,10 +1422,9 @@ angularWidget('ng:view', function(element) {        var template;        var changeCounter = 0; -      $route.onChange(function(){ +      this.$on('$afterRouteChange', function(){          changeCounter++; -      })(); //initialize the state forcefully, it's possible that we missed the initial -            //$route#onChange already +      });        this.$watch(function(){return changeCounter;}, function() {          var template = $route.current && $route.current.template; diff --git a/test/service/routeParamsSpec.js b/test/service/routeParamsSpec.js new file mode 100644 index 00000000..58a37f2e --- /dev/null +++ b/test/service/routeParamsSpec.js @@ -0,0 +1,41 @@ +'use strict'; + +describe('$routeParams', function(){ +  it('should publish the params into a service', function(){ +    var scope = angular.scope(), +        $location = scope.$service('$location'), +        $route = scope.$service('$route'), +        $routeParams = scope.$service('$routeParams'); + +    $route.when('/foo'); +    $route.when('/bar/:barId'); + +    $location.hash = '/foo?a=b'; +    scope.$digest(); +    expect($routeParams).toEqual({a:'b'}); + +    $location.hash = '/bar/123?x=abc'; +    scope.$digest(); +    expect($routeParams).toEqual({barId:'123', x:'abc'}); +  }); + + +  it('should preserve object identity during route reloads', function(){ +    var scope = angular.scope(), +        $location = scope.$service('$location'), +        $route = scope.$service('$route'), +        $routeParams = scope.$service('$routeParams'), +        firstRouteParams = $routeParams; + +    $route.when('/foo'); +    $route.when('/bar/:barId'); + +    $location.hash = '/foo?a=b'; +    scope.$digest(); +    expect(scope.$service('$routeParams')).toBe(firstRouteParams); + +    $location.hash = '/bar/123?x=abc'; +    scope.$digest(); +    expect(scope.$service('$routeParams')).toBe(firstRouteParams); +  }); +}); diff --git a/test/service/routeSpec.js b/test/service/routeSpec.js index 72c7745c..b1dde915 100644 --- a/test/service/routeSpec.js +++ b/test/service/routeSpec.js @@ -15,37 +15,48 @@ describe('$route', function() {    it('should route and fire change event', function(){      var log = '', -        $location, $route; +        $location, $route, +        lastRoute, +        nextRoute;      function BookChapter() { -      log += '<init>'; +      log += '<init>;';      }      scope = compile('<div></div>')();      $location = scope.$service('$location');      $route = scope.$service('$route');      $route.when('/Book/:book/Chapter/:chapter', {controller: BookChapter, template:'Chapter.html'});      $route.when('/Blank'); -    $route.onChange(function(){ -      log += 'onChange();'; +    scope.$on('$beforeRouteChange', function(event, next, current){ +      log += 'before();'; +      expect(current).toBe($route.current); +      lastRoute = current; +      nextRoute = next; +    }); +    scope.$on('$afterRouteChange', function(event, current, last){ +      log += 'after();'; +      expect(current).toBe($route.current); +      expect(lastRoute).toBe(last); +      expect(nextRoute).toBe(current);      });      $location.update('http://server#/Book/Moby/Chapter/Intro?p=123');      scope.$digest(); -    expect(log).toEqual('onChange();<init>'); +    expect(log).toEqual('before();<init>;after();');      expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', p:'123'});      var lastId = $route.current.scope.$id;      log = '';      $location.update('http://server#/Blank?ignore');      scope.$digest(); -    expect(log).toEqual('onChange();'); +    expect(log).toEqual('before();after();');      expect($route.current.params).toEqual({ignore:true});      expect($route.current.scope.$id).not.toEqual(lastId);      log = '';      $location.update('http://server#/NONE');      scope.$digest(); -    expect(log).toEqual('onChange();'); +    expect(log).toEqual('before();after();');      expect($route.current).toEqual(null);      $route.when('/NONE', {template:'instant update'}); @@ -54,15 +65,6 @@ describe('$route', function() {    }); -  it('should return fn registered with onChange()', function() { -    var scope = angular.scope(), -        $route = scope.$service('$route'), -        fn = function() {}; - -    expect($route.onChange(fn)).toBe(fn); -  }); - -    it('should allow routes to be defined with just templates without controllers', function() {      var scope = angular.scope(),          $location = scope.$service('$location'), @@ -70,7 +72,7 @@ describe('$route', function() {          onChangeSpy = jasmine.createSpy('onChange');      $route.when('/foo', {template: 'foo.html'}); -    $route.onChange(onChangeSpy); +    scope.$on('$beforeRouteChange', onChangeSpy);      expect($route.current).toBeUndefined();      expect(onChangeSpy).not.toHaveBeenCalled(); @@ -93,7 +95,7 @@ describe('$route', function() {      $route.when('/foo', {template: 'foo.html'});      $route.otherwise({template: '404.html', controller: NotFoundCtrl}); -    $route.onChange(onChangeSpy); +    scope.$on('$beforeRouteChange', onChangeSpy);      expect($route.current).toBeUndefined();      expect(onChangeSpy).not.toHaveBeenCalled(); @@ -163,7 +165,7 @@ describe('$route', function() {        $route.when('/bar', {template: 'bar.html'});        $route.when('/baz', {redirectTo: '/bar'});        $route.otherwise({template: '404.html'}); -      $route.onChange(onChangeSpy); +      scope.$on('$beforeRouteChange', onChangeSpy);        expect($route.current).toBeUndefined();        expect(onChangeSpy).not.toHaveBeenCalled(); @@ -172,7 +174,8 @@ describe('$route', function() {        expect($location.hash).toBe('/foo');        expect($route.current.template).toBe('foo.html'); -      expect(onChangeSpy.callCount).toBe(1); +      expect(onChangeSpy.callCount).toBe(2); +        onChangeSpy.reset();        $location.updateHash(''); @@ -181,7 +184,7 @@ describe('$route', function() {        expect($location.hash).toBe('/foo');        expect($route.current.template).toBe('foo.html'); -      expect(onChangeSpy.callCount).toBe(1); +      expect(onChangeSpy.callCount).toBe(2);        onChangeSpy.reset();        $location.updateHash('/baz'); @@ -190,7 +193,7 @@ describe('$route', function() {        expect($location.hash).toBe('/bar');        expect($route.current.template).toBe('bar.html'); -      expect(onChangeSpy.callCount).toBe(1); +      expect(onChangeSpy.callCount).toBe(2);      }); @@ -267,10 +270,11 @@ describe('$route', function() {        var scope = angular.scope(),            $location = scope.$service('$location'),            $route = scope.$service('$route'), +          $rouetParams = scope.$service('$routeParams'),            reloaded = jasmine.createSpy('route reload');        $route.when('/foo', {controller: FooCtrl}); -      $route.onChange(reloaded); +      scope.$on('$beforeRouteChange', reloaded);        function FooCtrl() {          reloaded(); @@ -279,12 +283,14 @@ describe('$route', function() {        $location.updateHash('/foo');        scope.$digest();        expect(reloaded).toHaveBeenCalled(); +      expect($rouetParams).toEqual({});        reloaded.reset();        // trigger reload        $location.hashSearch.foo = 'bar';        scope.$digest();        expect(reloaded).toHaveBeenCalled(); +      expect($rouetParams).toEqual({foo:'bar'});      }); @@ -293,13 +299,15 @@ describe('$route', function() {        var scope = angular.scope(),            $location = scope.$service('$location'),            $route = scope.$service('$route'), -          reloaded = jasmine.createSpy('route reload'); +          reloaded = jasmine.createSpy('route reload'), +          routeUpdateEvent = jasmine.createSpy('route reload');        $route.when('/foo', {controller: FooCtrl, reloadOnSearch: false}); -      $route.onChange(reloaded); +      scope.$on('$beforeRouteChange', reloaded);        function FooCtrl() {          reloaded(); +        this.$on('$routeUpdate', routeUpdateEvent);        }        expect(reloaded).not.toHaveBeenCalled(); @@ -307,12 +315,14 @@ describe('$route', function() {        $location.updateHash('/foo');        scope.$digest();        expect(reloaded).toHaveBeenCalled(); +      expect(routeUpdateEvent).not.toHaveBeenCalled();        reloaded.reset();        // don't trigger reload        $location.hashSearch.foo = 'bar';        scope.$digest();        expect(reloaded).not.toHaveBeenCalled(); +      expect(routeUpdateEvent).toHaveBeenCalled();      }); @@ -324,7 +334,7 @@ describe('$route', function() {            onRouteChange = jasmine.createSpy('onRouteChange');        $route.when('/foo/:fooId', {controller: FooCtrl, reloadOnSearch: false}); -      $route.onChange(onRouteChange); +      scope.$on('$beforeRouteChange', onRouteChange);        function FooCtrl() {          reloaded(); @@ -394,5 +404,37 @@ describe('$route', function() {        scope.$digest();        expect(routeParams).toHaveBeenCalledWith({barId: '123', foo: 'bar'});      }); + + +    describe('reload', function(){ + +      it('should reload even if reloadOnSearch is false', function(){ +        var scope = angular.scope(), +            $location = scope.$service('$location'), +            $route = scope.$service('$route'), +            $routeParams = scope.$service('$routeParams'), +            count = 0; + +        $route.when('/bar/:barId', {controller: FooCtrl, reloadOnSearch: false}); + +        function FooCtrl() { count ++; } + +        $location.updateHash('/bar/123'); +        scope.$digest(); +        expect($routeParams).toEqual({barId:'123'}); +        expect(count).toEqual(1); + +        $location.hash = '/bar/123?a=b'; +        scope.$digest(); +        expect($routeParams).toEqual({barId:'123', a:'b'}); +        expect(count).toEqual(1); + +        $route.reload(); +        scope.$digest(); +        expect($routeParams).toEqual({barId:'123', a:'b'}); +        expect(count).toEqual(2); +      }); +    }); +    });  });  | 
