From 5599b55b04788c2e327d7551a4a699d75516dd21 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Wed, 5 Jun 2013 15:30:31 -0700 Subject: refactor($route): pull $route and friends into angular-route.js $route, $routeParams and ngView have been pulled from core angular.js to angular-route.js/ngRoute module. This is was done to in order keep the core focused on most commonly used functionality and allow community routers to be freely used instead of $route service. There is no need to panic, angular-route will keep on being supported by the angular team. Note: I'm intentionally not fixing tutorial links. Tutorial will need bigger changes and those should be done when we update tutorial to 1.2. BREAKING CHANGE: applications that use $route will now need to load angular-route.js file and define dependency on ngRoute module. Before: ``` ... ... var myApp = angular.module('myApp', ['someOtherModule']); ... ``` After: ``` ... ... var myApp = angular.module('myApp', ['ngRoute', 'someOtherModule']); ... ``` Closes #2804 --- Gruntfile.js | 12 +- angularFiles.js | 38 +- docs/components/bootstrap/bootstrap-prettify.js | 1 + docs/content/cookbook/deeplinking.ngdoc | 10 +- .../dev_guide.mvc.understanding_controller.ngdoc | 2 +- .../guide/dev_guide.mvc.understanding_view.ngdoc | 2 +- .../dev_guide.services.managing_dependencies.ngdoc | 2 +- docs/content/guide/dev_guide.templates.ngdoc | 4 +- docs/content/guide/directive.ngdoc | 6 +- docs/src/templates/index.html | 1 + docs/src/templates/js/docs.js | 2 +- src/AngularPublic.js | 3 - src/ng/animator.js | 32 +- src/ng/compile.js | 2 +- src/ng/directive/ngController.js | 2 +- src/ng/directive/ngView.js | 226 ----- src/ng/route.js | 509 ----------- src/ng/routeParams.js | 30 - src/ngResource/resource.js | 8 +- src/ngRoute/directive/ngView.js | 226 +++++ src/ngRoute/route.js | 519 +++++++++++ src/ngRoute/routeParams.js | 33 + src/ngRoute/routeUtils.js | 17 + test/matchers.js | 14 + test/ng/directive/ngViewSpec.js | 667 -------------- test/ng/locationSpec.js | 13 - test/ng/routeParamsSpec.js | 44 - test/ng/routeSpec.js | 974 -------------------- test/ngRoute/directive/ngViewSpec.js | 669 ++++++++++++++ test/ngRoute/routeParamsSpec.js | 48 + test/ngRoute/routeSpec.js | 976 +++++++++++++++++++++ 31 files changed, 2563 insertions(+), 2529 deletions(-) delete mode 100644 src/ng/directive/ngView.js delete mode 100644 src/ng/route.js delete mode 100644 src/ng/routeParams.js create mode 100644 src/ngRoute/directive/ngView.js create mode 100644 src/ngRoute/route.js create mode 100644 src/ngRoute/routeParams.js create mode 100644 src/ngRoute/routeUtils.js delete mode 100644 test/ng/directive/ngViewSpec.js delete mode 100644 test/ng/routeParamsSpec.js delete mode 100644 test/ng/routeSpec.js create mode 100644 test/ngRoute/directive/ngViewSpec.js create mode 100644 test/ngRoute/routeParamsSpec.js create mode 100644 test/ngRoute/routeSpec.js diff --git a/Gruntfile.js b/Gruntfile.js index 5339b1bb..fd37e1f1 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -104,13 +104,22 @@ module.exports = function(grunt) { src: util.wrap([ 'src/ngSanitize/sanitize.js', 'src/ngSanitize/directive/ngBindHtml.js', - 'src/ngSanitize/filter/linky.js', + 'src/ngSanitize/filter/linky.js' ], 'module') }, resource: { dest: 'build/angular-resource.js', src: util.wrap(['src/ngResource/resource.js'], 'module') }, + route: { + dest: 'build/angular-route.js', + src: util.wrap([ + 'src/ngRoute/routeUtils.js', + 'src/ngRoute/route.js', + 'src/ngRoute/routeParams.js', + 'src/ngRoute/directive/ngView.js' + ], 'module') + }, cookies: { dest: 'build/angular-cookies.js', src: util.wrap(['src/ngCookies/cookies.js'], 'module') @@ -136,6 +145,7 @@ module.exports = function(grunt) { loader: 'build/angular-loader.js', mobile: 'build/angular-mobile.js', resource: 'build/angular-resource.js', + route: 'build/angular-route.js', sanitize: 'build/angular-sanitize.js', bootstrap: 'build/docs/components/angular-bootstrap.js', bootstrapPrettify: 'build/docs/components/angular-bootstrap-prettify.js', diff --git a/angularFiles.js b/angularFiles.js index 39ccec90..db672383 100755 --- a/angularFiles.js +++ b/angularFiles.js @@ -23,8 +23,6 @@ angularFiles = { 'src/ng/log.js', 'src/ng/parse.js', 'src/ng/q.js', - 'src/ng/route.js', - 'src/ng/routeParams.js', 'src/ng/rootScope.js', 'src/ng/sniffer.js', 'src/ng/window.js', @@ -60,7 +58,6 @@ angularFiles = { 'src/ng/directive/ngStyle.js', 'src/ng/directive/ngSwitch.js', 'src/ng/directive/ngTransclude.js', - 'src/ng/directive/ngView.js', 'src/ng/directive/script.js', 'src/ng/directive/select.js', 'src/ng/directive/style.js' @@ -69,6 +66,10 @@ angularFiles = { 'angularSrcModules': [ 'src/ngCookies/cookies.js', 'src/ngResource/resource.js', + 'src/ngRoute/routeUtils.js', + 'src/ngRoute/route.js', + 'src/ngRoute/routeParams.js', + 'src/ngRoute/directive/ngView.js', 'src/ngSanitize/sanitize.js', 'src/ngSanitize/directive/ngBindHtml.js', 'src/ngSanitize/filter/linky.js', @@ -106,19 +107,14 @@ angularFiles = { 'test/ngScenario/jstd-scenario-adapter/*.js', 'test/*.js', 'test/auto/*.js', - 'test/ng/*.js', - 'test/ng/directive/*.js', - 'test/ng/filter/*.js', + 'test/ng/**/*.js', 'test/ngCookies/*.js', 'test/ngResource/*.js', - 'test/ngSanitize/*.js', - 'test/ngSanitize/directive/*.js', - 'test/ngSanitize/filter/*.js', + 'test/ngRoute/**/*.js', + 'test/ngSanitize/**/*.js', 'test/ngMock/*.js', - 'test/ngMobile/*.js', - 'test/ngMobile/directive/*.js', - 'docs/component-spec/bootstrap/*.js', - 'docs/component-spec/*.js' + 'test/ngMobile/**/*.js', + 'docs/component-spec/**/*.js' ], 'jstd': [ @@ -153,25 +149,15 @@ angularFiles = { 'lib/jasmine/jasmine.js', 'lib/jasmine-jstd-adapter/JasmineAdapter.js', 'build/angular.js', - 'src/ngMock/angular-mocks.js', - 'src/ngCookies/cookies.js', - 'src/ngResource/resource.js', - 'src/ngMobile/mobile.js', - 'src/ngMobile/swipe.js', - 'src/ngMobile/directive/ngClick.js', - 'src/ngMobile/directive/ngSwipe.js', - 'src/ngSanitize/sanitize.js', - 'src/ngSanitize/directive/ngBindHtml.js', - 'src/ngSanitize/filter/linky.js', + '@angularSrcModules', 'src/ngScenario/browserTrigger.js', 'test/matchers.js', 'test/testabilityPatch.js', 'test/ngMock/*.js', 'test/ngCookies/*.js', + 'test/ngRoute/**/*.js', 'test/ngResource/*.js', - 'test/ngSanitize/*.js', - 'test/ngSanitize/directive/*.js', - 'test/ngSanitize/filter/*.js', + 'test/ngSanitize/**/*.js', 'test/ngMobile/**/*.js' ], diff --git a/docs/components/bootstrap/bootstrap-prettify.js b/docs/components/bootstrap/bootstrap-prettify.js index a9e61d4f..cbe7b53f 100644 --- a/docs/components/bootstrap/bootstrap-prettify.js +++ b/docs/components/bootstrap/bootstrap-prettify.js @@ -6,6 +6,7 @@ var service = { value: {} }; var DEPENDENCIES = { 'angular.js': 'http://code.angularjs.org/' + angular.version.full + '/angular.min.js', 'angular-resource.js': 'http://code.angularjs.org/' + angular.version.full + '/angular-resource.min.js', + 'angular-route.js': 'http://code.angularjs.org/' + angular.version.full + '/angular-route.min.js', 'angular-sanitize.js': 'http://code.angularjs.org/' + angular.version.full + '/angular-sanitize.min.js', 'angular-cookies.js': 'http://code.angularjs.org/' + angular.version.full + '/angular-cookies.min.js' }; diff --git a/docs/content/cookbook/deeplinking.ngdoc b/docs/content/cookbook/deeplinking.ngdoc index 2e22360e..a6fd4852 100644 --- a/docs/content/cookbook/deeplinking.ngdoc +++ b/docs/content/cookbook/deeplinking.ngdoc @@ -30,9 +30,9 @@ In this example we have a simple app which consist of two screens: * Welcome: url `welcome` Show the user contact information. * Settings: url `settings` Show an edit screen for user contact information. - + - angular.module('deepLinking', ['ngSanitize']) + angular.module('deepLinking', ['ngRoute', 'ngSanitize']) .config(function($routeProvider) { $routeProvider. when("/welcome", {templateUrl:'welcome.html', controller:WelcomeCntl}). @@ -141,11 +141,11 @@ In this example we have a simple app which consist of two screens: # Things to notice * Routes are defined in the `AppCntl` class. The initialization of the controller causes the - initialization of the {@link api/ng.$route $route} service with the proper URL + initialization of the {@link api/ngRoute.$route $route} service with the proper URL routes. -* The {@link api/ng.$route $route} service then watches the URL and instantiates the +* The {@link api/ngRoute.$route $route} service then watches the URL and instantiates the appropriate controller when the URL changes. -* The {@link api/ng.directive:ngView ngView} widget loads the +* The {@link api/ngRoute.directive:ngView ngView} widget loads the view when the URL changes. It also sets the view scope to the newly instantiated controller. * Changing the URL is sufficient to change the controller and view. It makes no difference whether the URL is changed programatically or by the user. diff --git a/docs/content/guide/dev_guide.mvc.understanding_controller.ngdoc b/docs/content/guide/dev_guide.mvc.understanding_controller.ngdoc index e3a570d4..2c82f949 100644 --- a/docs/content/guide/dev_guide.mvc.understanding_controller.ngdoc +++ b/docs/content/guide/dev_guide.mvc.understanding_controller.ngdoc @@ -84,7 +84,7 @@ instances). # Associating Controllers with Angular Scope Objects You can associate controllers with scope objects implicitly via the {@link api/ng.directive:ngController ngController -directive} or {@link api/ng.$route $route service}. +directive} or {@link api/ngRoute.$route $route service}. ## Controller Constructor and Methods Example diff --git a/docs/content/guide/dev_guide.mvc.understanding_view.ngdoc b/docs/content/guide/dev_guide.mvc.understanding_view.ngdoc index 567437bd..2827cf67 100644 --- a/docs/content/guide/dev_guide.mvc.understanding_view.ngdoc +++ b/docs/content/guide/dev_guide.mvc.understanding_view.ngdoc @@ -10,7 +10,7 @@ the DOM based on information in the template, controller and model. In the Angular implementation of MVC, the view has knowledge of both the model and the controller. The view knows about the model where two-way data-binding occurs. The view has knowledge of the controller through Angular directives, such as {@link api/ng.directive:ngController -ngController} and {@link api/ng.directive:ngView ngView}, and through bindings of this form: +ngController} and {@link api/ngRoute.directive:ngView ngView}, and through bindings of this form: `{{someControllerFunction()}}`. In these ways, the view can call functions in an associated controller function. diff --git a/docs/content/guide/dev_guide.services.managing_dependencies.ngdoc b/docs/content/guide/dev_guide.services.managing_dependencies.ngdoc index 6a769ca0..b069f4bb 100644 --- a/docs/content/guide/dev_guide.services.managing_dependencies.ngdoc +++ b/docs/content/guide/dev_guide.services.managing_dependencies.ngdoc @@ -91,7 +91,7 @@ Things to notice in this example: * The `batchLog` service depends on the built-in {@link api/ng.$timeout $timeout} and {@link api/ng.$log $log} services, and allows messages to be logged into the `console.log` in batches. -* The `routeTemplateMonitor` service depends on the built-in {@link api/ng.$route +* The `routeTemplateMonitor` service depends on the built-in {@link api/ngRoute.$route $route} service as well as our custom `batchLog` service. * Both of our services use the factory function signature and array notation for inject annotations to declare their dependencies. It is important that the order of the string identifiers in the array diff --git a/docs/content/guide/dev_guide.templates.ngdoc b/docs/content/guide/dev_guide.templates.ngdoc index 2b4e3ff1..b01b4448 100644 --- a/docs/content/guide/dev_guide.templates.ngdoc +++ b/docs/content/guide/dev_guide.templates.ngdoc @@ -41,8 +41,8 @@ with {@link expression expressions}: In a simple single-page app, the template consists of HTML, CSS, and angular directives contained in just one HTML file (usually `index.html`). In a more complex app, you can display multiple views within one main page using "partials", which are segments of template located in separate HTML -files. You "include" the partials in the main page using the {@link api/ng.$route -$route} service in conjunction with the {@link api/ng.directive:ngView ngView} directive. An +files. You "include" the partials in the main page using the {@link api/ngRoute.$route +$route} service in conjunction with the {@link api/ngRoute.directive:ngView ngView} directive. An example of this technique is shown in the {@link tutorial/ angular tutorial}, in steps seven and eight. diff --git a/docs/content/guide/directive.ngdoc b/docs/content/guide/directive.ngdoc index 72146125..11adc1f1 100644 --- a/docs/content/guide/directive.ngdoc +++ b/docs/content/guide/directive.ngdoc @@ -250,7 +250,7 @@ In this example we will build a directive that displays the current time. # Writing directives (long version) -There are different ways to declare a directive. The difference resides in the return +There are different ways to declare a directive. The difference resides in the return value of the factory function. You can either return a Directive Definition Object (see below) that defines the directive properties, or just the postLink function of such an object (all other properties will have the default values). @@ -284,7 +284,7 @@ Here's an example directive declared with a Directive Definition Object: In most cases you will not need such fine control and so the above can be simplified. You can still return a Directive Definition Object, but only setting the 'compile' function property of the Object, -and rely on the default values for other properties. +and rely on the default values for other properties. Therefore the above can be simplified as: @@ -462,7 +462,7 @@ The compile function deals with transforming the template DOM. Since most direct template transformation, it is not used often. Examples that require compile functions are directives that transform template DOM, such as {@link api/ng.directive:ngRepeat ngRepeat}, or load the contents -asynchronously, such as {@link api/ng.directive:ngView ngView}. The +asynchronously, such as {@link api/ngRoute.directive:ngView ngView}. The compile function takes the following arguments. * `tElement` - template element - The element where the directive has been declared. It is diff --git a/docs/src/templates/index.html b/docs/src/templates/index.html index da75cff6..83b02596 100644 --- a/docs/src/templates/index.html +++ b/docs/src/templates/index.html @@ -38,6 +38,7 @@ if (jQuery) addTag('script', {src: debug ? 'js/jquery.js' : 'js/jquery.min.js'}); addTag('script', {src: path('angular.js')}, sync); addTag('script', {src: path('angular-resource.js') }, sync); + addTag('script', {src: path('angular-route.js') }, sync); addTag('script', {src: path('angular-cookies.js') }, sync); addTag('script', {src: path('angular-sanitize.js') }, sync); addTag('script', {src: path('angular-mobile.js') }, sync); diff --git a/docs/src/templates/js/docs.js b/docs/src/templates/js/docs.js index 7cd3c9c6..2350e698 100644 --- a/docs/src/templates/js/docs.js +++ b/docs/src/templates/js/docs.js @@ -679,7 +679,7 @@ docsApp.controller.DocsController = function($scope, $location, $window, $cookie }; -angular.module('docsApp', ['ngResource', 'ngCookies', 'ngSanitize', 'bootstrap', 'bootstrapPrettify']). +angular.module('docsApp', ['ngResource', 'ngRoute', 'ngCookies', 'ngSanitize', 'bootstrap', 'bootstrapPrettify']). config(function($locationProvider) { $locationProvider.html5Mode(true).hashPrefix('!'); }). diff --git a/src/AngularPublic.js b/src/AngularPublic.js index 1fd18ce2..8330c067 100755 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -95,7 +95,6 @@ function publishExternalAPI(angular){ ngSwitchWhen: ngSwitchWhenDirective, ngSwitchDefault: ngSwitchDefaultDirective, ngOptions: ngOptionsDirective, - ngView: ngViewDirective, ngTransclude: ngTranscludeDirective, ngModel: ngModelDirective, ngList: ngListDirective, @@ -122,8 +121,6 @@ function publishExternalAPI(angular){ $location: $LocationProvider, $log: $LogProvider, $parse: $ParseProvider, - $route: $RouteProvider, - $routeParams: $RouteParamsProvider, $rootScope: $RootScopeProvider, $q: $QProvider, $sniffer: $SnifferProvider, diff --git a/src/ng/animator.js b/src/ng/animator.js index a9ec1616..d8495f2d 100644 --- a/src/ng/animator.js +++ b/src/ng/animator.js @@ -18,7 +18,7 @@ * | Directive | Supported Animations | * |========================================================== |====================================================| * | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave and move | - * | {@link ng.directive:ngView#animations ngView} | enter and leave | + * | {@link ngRoute.directive:ngView#animations ngView} | enter and leave | * | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave | * | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave | * | {@link ng.directive:ngIf#animations ngIf} | enter and leave | @@ -183,7 +183,7 @@ var $AnimatorProvider = function() { */ var AnimatorService = function(scope, attrs) { var animator = {}; - + /** * @ngdoc function * @name ng.animator#enter @@ -198,7 +198,7 @@ var $AnimatorProvider = function() { * @param {jQuery/jqLite element} after the sibling element (which is the previous element) of the element that will be the focus of the enter animation */ animator.enter = animateActionFactory('enter', insert, noop); - + /** * @ngdoc function * @name ng.animator#leave @@ -212,7 +212,7 @@ var $AnimatorProvider = function() { * @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the leave animation */ animator.leave = animateActionFactory('leave', noop, remove); - + /** * @ngdoc function * @name ng.animator#move @@ -228,7 +228,7 @@ var $AnimatorProvider = function() { * @param {jQuery/jqLite element} after the sibling element (which is the previous element) of the element that will be the focus of the move animation */ animator.move = animateActionFactory('move', move, noop); - + /** * @ngdoc function * @name ng.animator#show @@ -241,7 +241,7 @@ var $AnimatorProvider = function() { * @param {jQuery/jqLite element} element the element that will be rendered visible or hidden */ animator.show = animateActionFactory('show', show, noop); - + /** * @ngdoc function * @name ng.animator#hide @@ -262,14 +262,14 @@ var $AnimatorProvider = function() { * @description * Triggers a custom animation event to be executed on the given element * - * @param {string} event the name of the custom event + * @param {string} event the name of the custom event * @param {jQuery/jqLite element} element the element that will be animated */ animator.animate = function(event, element) { animateActionFactory(event, noop, noop)(element); } return animator; - + function animateActionFactory(type, beforeFn, afterFn) { return function(element, parent, after) { var ngAnimateValue = scope.$eval(attrs.ngAnimate); @@ -329,10 +329,10 @@ var $AnimatorProvider = function() { polyfillStart(element, done, memento); } else if (isFunction($window.getComputedStyle)) { //one day all browsers will have these properties - var w3cAnimationProp = 'animation'; + var w3cAnimationProp = 'animation'; var w3cTransitionProp = 'transition'; - //but some still use vendor-prefixed styles + //but some still use vendor-prefixed styles var vendorAnimationProp = $sniffer.vendorPrefix + 'Animation'; var vendorTransitionProp = $sniffer.vendorPrefix + 'Transition'; @@ -340,7 +340,7 @@ var $AnimatorProvider = function() { delayKey = 'Delay', animationIterationCountKey = 'IterationCount', duration = 0; - + //we want all the styles defined before and after var ELEMENT_NODE = 1; forEach(element, function(element) { @@ -387,15 +387,15 @@ var $AnimatorProvider = function() { } }; } - + function show(element) { element.css('display', ''); } - + function hide(element) { element.css('display', 'none'); } - + function insert(element, parent, after) { var afterNode = after && after[after.length - 1]; var parentNode = parent && parent[0] || afterNode && afterNode.parentNode; @@ -408,11 +408,11 @@ var $AnimatorProvider = function() { } }); } - + function remove(element) { element.remove(); } - + function move(element, parent, after) { // Do not remove element before insert. Removing will cause data associated with the // element to be dropped. Insert will implicitly do the remove. diff --git a/src/ng/compile.js b/src/ng/compile.js index 2dddf82d..d231fb40 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -172,7 +172,7 @@ function $CompileProvider($provide) { */ this.directive = function registerDirective(name, directiveFactory) { if (isString(name)) { - assertArg(directiveFactory, 'directive'); + assertArg(directiveFactory, 'directiveFactory'); if (!hasDirectives.hasOwnProperty(name)) { hasDirectives[name] = []; $provide.factory(name + Suffix, ['$injector', '$exceptionHandler', diff --git a/src/ng/directive/ngController.js b/src/ng/directive/ngController.js index 289ee034..47b233f9 100644 --- a/src/ng/directive/ngController.js +++ b/src/ng/directive/ngController.js @@ -15,7 +15,7 @@ * * Controller — The `ngController` directive specifies a Controller class; the class has * methods that typically express the business logic behind the application. * - * Note that an alternative way to define controllers is via the {@link ng.$route $route} service. + * Note that an alternative way to define controllers is via the {@link ngRoute.$route $route} service. * * @element ANY * @scope diff --git a/src/ng/directive/ngView.js b/src/ng/directive/ngView.js deleted file mode 100644 index 9b5694dd..00000000 --- a/src/ng/directive/ngView.js +++ /dev/null @@ -1,226 +0,0 @@ -'use strict'; - -/** - * @ngdoc directive - * @name ng.directive:ngView - * @restrict ECA - * - * @description - * # Overview - * `ngView` is a directive that complements the {@link ng.$route $route} service by - * including the rendered template of the current route into the main layout (`index.html`) file. - * Every time the current route changes, the included view changes with it according to the - * configuration of the `$route` service. - * - * Additionally, you can also provide animations via the ngAnimate attribute to animate the **enter** - * and **leave** effects. - * - * @animations - * enter - happens just after the ngView contents are changed (when the new view DOM element is inserted into the DOM) - * leave - happens just after the current ngView contents change and just before the former contents are removed from the DOM - * - * @scope - * @example - - -
- Choose: - Moby | - Moby: Ch1 | - Gatsby | - Gatsby: Ch4 | - Scarlet Letter
- -
-
- -
$location.path() = {{main.$location.path()}}
-
$route.current.templateUrl = {{main.$route.current.templateUrl}}
-
$route.current.params = {{main.$route.current.params}}
-
$route.current.scope.name = {{main.$route.current.scope.name}}
-
$routeParams = {{main.$routeParams}}
-
-
- - -
- controller: {{book.name}}
- Book Id: {{book.params.bookId}}
-
-
- - -
- controller: {{chapter.name}}
- Book Id: {{chapter.params.bookId}}
- Chapter Id: {{chapter.params.chapterId}} -
-
- - - .example-leave, .example-enter { - -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; - -moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; - -ms-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; - -o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; - transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; - } - - .example-animate-container { - position:relative; - height:100px; - } - - .example-animate-container > * { - display:block; - width:100%; - border-left:1px solid black; - - position:absolute; - top:0; - left:0; - right:0; - bottom:0; - padding:10px; - } - - .example-enter { - left:100%; - } - .example-enter.example-enter-active { - left:0; - } - - .example-leave { } - .example-leave.example-leave-active { - left:-100%; - } - - - - angular.module('ngView', [], function($routeProvider, $locationProvider) { - $routeProvider.when('/Book/:bookId', { - templateUrl: 'book.html', - controller: BookCntl, - controllerAs: 'book' - }); - $routeProvider.when('/Book/:bookId/ch/:chapterId', { - templateUrl: 'chapter.html', - controller: ChapterCntl, - controllerAs: 'chapter' - }); - - // configure html5 to get links working on jsfiddle - $locationProvider.html5Mode(true); - }); - - function MainCntl($route, $routeParams, $location) { - this.$route = $route; - this.$location = $location; - this.$routeParams = $routeParams; - } - - function BookCntl($routeParams) { - this.name = "BookCntl"; - this.params = $routeParams; - } - - function ChapterCntl($routeParams) { - this.name = "ChapterCntl"; - this.params = $routeParams; - } - - - - 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/); - }); - -
- */ - - -/** - * @ngdoc event - * @name ng.directive:ngView#$viewContentLoaded - * @eventOf ng.directive:ngView - * @eventType emit on the current ngView scope - * @description - * Emitted every time the ngView content is reloaded. - */ -var ngViewDirective = ['$http', '$templateCache', '$route', '$anchorScroll', '$compile', - '$controller', '$animator', - function($http, $templateCache, $route, $anchorScroll, $compile, - $controller, $animator) { - return { - restrict: 'ECA', - terminal: true, - link: function(scope, element, attr) { - var lastScope, - onloadExp = attr.onload || '', - animate = $animator(scope, attr); - - scope.$on('$routeChangeSuccess', update); - update(); - - - function destroyLastScope() { - if (lastScope) { - lastScope.$destroy(); - lastScope = null; - } - } - - function clearContent() { - animate.leave(element.contents(), element); - destroyLastScope(); - } - - function update() { - var locals = $route.current && $route.current.locals, - template = locals && locals.$template; - - if (template) { - clearContent(); - var enterElements = jqLite('
').html(template).contents(); - animate.enter(enterElements, element); - - var link = $compile(enterElements), - current = $route.current, - controller; - - lastScope = current.scope = scope.$new(); - if (current.controller) { - locals.$scope = lastScope; - controller = $controller(current.controller, locals); - if (current.controllerAs) { - lastScope[current.controllerAs] = controller; - } - element.children().data('$ngControllerController', controller); - } - - link(lastScope); - lastScope.$emit('$viewContentLoaded'); - lastScope.$eval(onloadExp); - - // $anchorScroll might listen on event... - $anchorScroll(); - } else { - clearContent(); - } - } - } - }; -}]; diff --git a/src/ng/route.js b/src/ng/route.js deleted file mode 100644 index 12e560c7..00000000 --- a/src/ng/route.js +++ /dev/null @@ -1,509 +0,0 @@ -'use strict'; - - -/** - * @ngdoc object - * @name ng.$routeProvider - * @function - * - * @description - * - * Used for configuring routes. See {@link ng.$route $route} for an example. - */ -function $RouteProvider(){ - var routes = {}; - - /** - * @ngdoc method - * @name ng.$routeProvider#when - * @methodOf ng.$routeProvider - * - * @param {string} path Route path (matched against `$location.path`). If `$location.path` - * 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 exactly match the - * route definition. - * - * * `path` can contain named groups starting with a colon (`:name`). All characters up - * to the next slash are matched and stored in `$routeParams` under the given `name` - * when the route matches. - * * `path` can contain named groups starting with a star (`*name`). All characters are - * eagerly stored in `$routeParams` under the given `name` when the route matches. - * - * For example, routes like `/color/:color/largecode/*largecode/edit` will match - * `/color/brown/largecode/code/with/slashs/edit` and extract: - * - * * `color: brown` - * * `largecode: code/with/slashs`. - * - * - * @param {Object} route Mapping information to be assigned to `$route.current` on route - * match. - * - * Object properties: - * - * - `controller` – `{(string|function()=}` – Controller fn that should be associated with newly - * created scope or the name of a {@link angular.Module#controller registered controller} - * if passed as a string. - * - `controllerAs` – `{string=}` – A controller alias name. If present the controller will be - * published to scope under the `controllerAs` name. - * - `template` – `{string=|function()=}` – html template as a string or function that returns - * an html template as a string which should be used by {@link ng.directive:ngView ngView} or - * {@link ng.directive:ngInclude ngInclude} directives. - * This property takes precedence over `templateUrl`. - * - * If `template` is a function, it will be called with the following parameters: - * - * - `{Array.}` - route parameters extracted from the current - * `$location.path()` by applying the current route - * - * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html - * template that should be used by {@link ng.directive:ngView ngView}. - * - * If `templateUrl` is a function, it will be called with the following parameters: - * - * - `{Array.}` - route parameters extracted from the current - * `$location.path()` by applying the current route - * - * - `resolve` - `{Object.=}` - 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 - * `$routeChangeSuccess` 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/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 ng.$location $location} path with and trigger route redirection. - * - * If `redirectTo` is a function, it will be called with the following parameters: - * - * - `{Object.}` - route parameters extracted from the current - * `$location.path()` by applying the current route templateUrl. - * - `{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. - * - * - `[caseInsensitiveMatch=false]` - {boolean=} - match routes without being case sensitive - * - * If the option is set to `true`, then the particular route can be matched without being - * case sensitive - * - * @returns {Object} self - * - * @description - * Adds a new route definition to the `$route` service. - */ - this.when = function(path, route) { - routes[path] = extend({reloadOnSearch: true, caseInsensitiveMatch: false}, route); - - // 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 this; - }; - - /** - * @ngdoc method - * @name ng.$routeProvider#otherwise - * @methodOf 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`. - * @returns {Object} self - */ - this.otherwise = function(params) { - this.when(null, params); - return this; - }; - - - this.$get = ['$rootScope', '$location', '$routeParams', '$q', '$injector', '$http', '$templateCache', - function( $rootScope, $location, $routeParams, $q, $injector, $http, $templateCache) { - - /** - * @ngdoc object - * @name ng.$route - * @requires $location - * @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 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.} 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 ng.$routeProvider $routeProvider}'s API. - * - * The `$route` service is typically used in conjunction with {@link ng.directive:ngView ngView} - * directive and the {@link 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 `ngView` pulls in the partial. - - Note that this example is using {@link ng.directive:script inlined templates} - to get it working on jsfiddle as well. - - - -
- Choose: - Moby | - Moby: Ch1 | - Gatsby | - Gatsby: Ch4 | - Scarlet Letter
- -
-
- -
$location.path() = {{$location.path()}}
-
$route.current.templateUrl = {{$route.current.templateUrl}}
-
$route.current.params = {{$route.current.params}}
-
$route.current.scope.name = {{$route.current.scope.name}}
-
$routeParams = {{$routeParams}}
-
-
- - - controller: {{name}}
- Book Id: {{params.bookId}}
-
- - - controller: {{name}}
- Book Id: {{params.bookId}}
- Chapter Id: {{params.chapterId}} -
- - - angular.module('ngView', [], function($routeProvider, $locationProvider) { - $routeProvider.when('/Book/:bookId', { - templateUrl: 'book.html', - 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', { - templateUrl: '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; - } - - - - 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(); - 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/); - }); - -
- */ - - /** - * @ngdoc event - * @name ng.$route#$routeChangeStart - * @eventOf ng.$route - * @eventType broadcast on root scope - * @description - * 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 - * `$routeChangeSuccess` is fired. - * - * @param {Route} next Future route information. - * @param {Route} current Current route information. - */ - - /** - * @ngdoc event - * @name ng.$route#$routeChangeSuccess - * @eventOf ng.$route - * @eventType broadcast on root scope - * @description - * Broadcasted after a route dependencies are resolved. - * {@link ng.directive:ngView ngView} listens for the directive - * to instantiate the controller and render the view. - * - * @param {Object} angularEvent Synthetic event object. - * @param {Route} current Current route information. - * @param {Route|Undefined} previous Previous route information, or undefined if current is first route entered. - */ - - /** - * @ngdoc event - * @name ng.$route#$routeChangeError - * @eventOf 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 ng.$route#$routeUpdate - * @eventOf 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 forceReload = false, - $route = { - routes: routes, - - /** - * @ngdoc method - * @name ng.$route#reload - * @methodOf ng.$route - * - * @description - * Causes `$route` service to reload the current route even if - * {@link ng.$location $location} hasn't changed. - * - * As a result of that, {@link ng.directive:ngView ngView} - * creates new scope, reinstantiates the controller. - */ - reload: function() { - forceReload = true; - $rootScope.$evalAsync(updateRoute); - } - }; - - $rootScope.$on('$locationChangeSuccess', updateRoute); - - return $route; - - ///////////////////////////////////////////////////// - - /** - * @param on {string} current url - * @param when {string} route when template to match the url against - * @param whenProperties {Object} properties to define when's matching behavior - * @return {?Object} - */ - function switchRouteMatcher(on, when, whenProperties) { - // TODO(i): this code is convoluted and inefficient, we should construct the route matching - // regex only once and then reuse it - - // Escape regexp special characters. - when = '^' + when.replace(/[-\/\\^$:*+?.()|[\]{}]/g, "\\$&") + '$'; - - var regex = '', - params = [], - dst = {}; - - var re = /\\([:*])(\w+)/g, - paramMatch, - lastMatchedIndex = 0; - - while ((paramMatch = re.exec(when)) !== null) { - // Find each :param in `when` and replace it with a capturing group. - // Append all other sections of when unchanged. - regex += when.slice(lastMatchedIndex, paramMatch.index); - switch(paramMatch[1]) { - case ':': - regex += '([^\\/]*)'; - break; - case '*': - regex += '(.*)'; - break; - } - params.push(paramMatch[2]); - lastMatchedIndex = re.lastIndex; - } - // Append trailing path part. - regex += when.substr(lastMatchedIndex); - - var match = on.match(new RegExp(regex, whenProperties.caseInsensitiveMatch ? 'i' : '')); - 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('$routeChangeStart', 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(); - } - } - } - - $q.when(next). - then(function() { - if (next) { - var locals = extend({}, next.resolve), - template; - - forEach(locals, function(value, key) { - locals[key] = isString(value) ? $injector.get(value) : $injector.invoke(value); - }); - - if (isDefined(template = next.template)) { - if (isFunction(template)) { - template = template(next.params); - } - } else if (isDefined(template = next.templateUrl)) { - if (isFunction(template)) { - template = template(next.params); - } - if (isDefined(template)) { - next.loadedTemplateUrl = template; - template = $http.get(template, {cache: $templateCache}). - then(function(response) { return response.data; }); - } - } - if (isDefined(template)) { - locals['$template'] = template; - } - return $q.all(locals); - } - }). - // after route change - then(function(locals) { - if (next == $route.current) { - if (next) { - next.locals = locals; - copy(next.params, $routeParams); - } - $rootScope.$broadcast('$routeChangeSuccess', next, last); - } - }, function(error) { - if (next == $route.current) { - $rootScope.$broadcast('$routeChangeError', next, last, error); - } - }); - } - } - - - /** - * @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 = switchRouteMatcher($location.path(), path, route))) { - 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 parameters - */ - 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(''); - } - }]; -} diff --git a/src/ng/routeParams.js b/src/ng/routeParams.js deleted file mode 100644 index 0202f8e5..00000000 --- a/src/ng/routeParams.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -/** - * @ngdoc object - * @name ng.$routeParams - * @requires $route - * - * @description - * Current set of route parameters. The route parameters are a combination of the - * {@link ng.$location $location} `search()`, and `path()`. The `path` parameters - * are extracted when the {@link ng.$route $route} path is matched. - * - * In case of parameter name collision, `path` params take precedence over `search` 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 - *
- *  // 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'}
- * 
- */ -function $RouteParamsProvider() { - this.$get = valueFn({}); -} diff --git a/src/ngResource/resource.js b/src/ngResource/resource.js index abb2bc56..827886a3 100644 --- a/src/ngResource/resource.js +++ b/src/ngResource/resource.js @@ -34,9 +34,9 @@ * `http://example.com:8080/api`), you'll need to escape the colon character before the port * number, like this: `$resource('http://example.com\\:8080/api')`. * - * If you are using a url with a suffix, just add the suffix, like this: + * If you are using a url with a suffix, just add the suffix, like this: * `$resource('http://example.com/resource.json')` or `$resource('http://example.com/:id.json') - * or even `$resource('http://example.com/resource/:resource_id.:format')` + * or even `$resource('http://example.com/resource/:resource_id.:format')` * If the parameter before the suffix is empty, :resource_id in this case, then the `/.` will be * collapsed down to a single `.`. If you need this sequence to appear and not collapse then you * can escape it with `/\.`. @@ -146,7 +146,7 @@ * * On success, the promise is resolved with the same resource instance or collection object, * updated with data from server. This makes it easy to use in - * {@link ng.$routeProvider resolve section of $routeProvider.when()} to defer view rendering + * {@link ngRoute.$routeProvider resolve section of $routeProvider.when()} to defer view rendering * until the resource(s) are loaded. * * On failure, the promise is resolved with the {@link ng.$http http response} object, @@ -376,7 +376,7 @@ angular.module('ngResource', ['ng']). url = url.replace(/\/\.(?=\w+($|\?))/, '.'); // replace escaped `/\.` with `/.` config.url = url.replace(/\/\\\./, '/.'); - + // set params - delegate param encoding to $http forEach(params, function(value, key){ diff --git a/src/ngRoute/directive/ngView.js b/src/ngRoute/directive/ngView.js new file mode 100644 index 00000000..935ba05d --- /dev/null +++ b/src/ngRoute/directive/ngView.js @@ -0,0 +1,226 @@ +'use strict'; + +ngRouteModule.directive('ngView', ngViewFactory); + +/** + * @ngdoc directive + * @name ngRoute.directive:ngView + * @restrict ECA + * + * @description + * # Overview + * `ngView` is a directive that complements the {@link ngRoute.$route $route} service by + * including the rendered template of the current route into the main layout (`index.html`) file. + * Every time the current route changes, the included view changes with it according to the + * configuration of the `$route` service. + * + * Additionally, you can also provide animations via the ngAnimate attribute to animate the **enter** + * and **leave** effects. + * + * @animations + * enter - happens just after the ngView contents are changed (when the new view DOM element is inserted into the DOM) + * leave - happens just after the current ngView contents change and just before the former contents are removed from the DOM + * + * @scope + * @example + + +
+ Choose: + Moby | + Moby: Ch1 | + Gatsby | + Gatsby: Ch4 | + Scarlet Letter
+ +
+
+ +
$location.path() = {{main.$location.path()}}
+
$route.current.templateUrl = {{main.$route.current.templateUrl}}
+
$route.current.params = {{main.$route.current.params}}
+
$route.current.scope.name = {{main.$route.current.scope.name}}
+
$routeParams = {{main.$routeParams}}
+
+
+ + +
+ controller: {{book.name}}
+ Book Id: {{book.params.bookId}}
+
+
+ + +
+ controller: {{chapter.name}}
+ Book Id: {{chapter.params.bookId}}
+ Chapter Id: {{chapter.params.chapterId}} +
+
+ + + .example-leave, .example-enter { + -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; + -moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; + -ms-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; + -o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; + transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; + } + + .example-animate-container { + position:relative; + height:100px; + } + + .example-animate-container > * { + display:block; + width:100%; + border-left:1px solid black; + + position:absolute; + top:0; + left:0; + right:0; + bottom:0; + padding:10px; + } + + .example-enter { + left:100%; + } + .example-enter.example-enter-active { + left:0; + } + + .example-leave { } + .example-leave.example-leave-active { + left:-100%; + } + + + + angular.module('ngViewExample', ['ngRoute'], function($routeProvider, $locationProvider) { + $routeProvider.when('/Book/:bookId', { + templateUrl: 'book.html', + controller: BookCntl, + controllerAs: 'book' + }); + $routeProvider.when('/Book/:bookId/ch/:chapterId', { + templateUrl: 'chapter.html', + controller: ChapterCntl, + controllerAs: 'chapter' + }); + + // configure html5 to get links working on jsfiddle + $locationProvider.html5Mode(true); + }); + + function MainCntl($route, $routeParams, $location) { + this.$route = $route; + this.$location = $location; + this.$routeParams = $routeParams; + } + + function BookCntl($routeParams) { + this.name = "BookCntl"; + this.params = $routeParams; + } + + function ChapterCntl($routeParams) { + this.name = "ChapterCntl"; + this.params = $routeParams; + } + + + + 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/); + }); + +
+ */ + + +/** + * @ngdoc event + * @name ngRoute.directive:ngView#$viewContentLoaded + * @eventOf ngRoute.directive:ngView + * @eventType emit on the current ngView scope + * @description + * Emitted every time the ngView content is reloaded. + */ +ngViewFactory.$inject = ['$route', '$anchorScroll', '$compile', '$controller', '$animator']; +function ngViewFactory( $route, $anchorScroll, $compile, $controller, $animator) { + return { + restrict: 'ECA', + terminal: true, + link: function(scope, element, attr) { + var lastScope, + onloadExp = attr.onload || '', + animate = $animator(scope, attr); + + scope.$on('$routeChangeSuccess', update); + update(); + + + function destroyLastScope() { + if (lastScope) { + lastScope.$destroy(); + lastScope = null; + } + } + + function clearContent() { + animate.leave(element.contents(), element); + destroyLastScope(); + } + + function update() { + var locals = $route.current && $route.current.locals, + template = locals && locals.$template; + + if (template) { + clearContent(); + var enterElements = jqLite('
').html(template).contents(); + animate.enter(enterElements, element); + + var link = $compile(enterElements), + current = $route.current, + controller; + + lastScope = current.scope = scope.$new(); + if (current.controller) { + locals.$scope = lastScope; + controller = $controller(current.controller, locals); + if (current.controllerAs) { + lastScope[current.controllerAs] = controller; + } + element.children().data('$ngControllerController', controller); + } + + link(lastScope); + lastScope.$emit('$viewContentLoaded'); + lastScope.$eval(onloadExp); + + // $anchorScroll might listen on event... + $anchorScroll(); + } else { + clearContent(); + } + } + } + }; +} diff --git a/src/ngRoute/route.js b/src/ngRoute/route.js new file mode 100644 index 00000000..32b7c626 --- /dev/null +++ b/src/ngRoute/route.js @@ -0,0 +1,519 @@ +'use strict'; + +/** + * @ngdoc overview + * @name ngRoute + * @description + * + * Module that provides routing and deeplinking services and directives for angular apps. + */ + +var ngRouteModule = angular.module('ngRoute', ['ng']). + provider('$route', $RouteProvider); + +/** + * @ngdoc object + * @name ngRoute.$routeProvider + * @function + * + * @description + * + * Used for configuring routes. See {@link ngRoute.$route $route} for an example. + */ +function $RouteProvider(){ + var routes = {}; + + /** + * @ngdoc method + * @name ngRoute.$routeProvider#when + * @methodOf ngRoute.$routeProvider + * + * @param {string} path Route path (matched against `$location.path`). If `$location.path` + * 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 exactly match the + * route definition. + * + * * `path` can contain named groups starting with a colon (`:name`). All characters up + * to the next slash are matched and stored in `$routeParams` under the given `name` + * when the route matches. + * * `path` can contain named groups starting with a star (`*name`). All characters are + * eagerly stored in `$routeParams` under the given `name` when the route matches. + * + * For example, routes like `/color/:color/largecode/*largecode/edit` will match + * `/color/brown/largecode/code/with/slashs/edit` and extract: + * + * * `color: brown` + * * `largecode: code/with/slashs`. + * + * + * @param {Object} route Mapping information to be assigned to `$route.current` on route + * match. + * + * Object properties: + * + * - `controller` – `{(string|function()=}` – Controller fn that should be associated with newly + * created scope or the name of a {@link angular.Module#controller registered controller} + * if passed as a string. + * - `controllerAs` – `{string=}` – A controller alias name. If present the controller will be + * published to scope under the `controllerAs` name. + * - `template` – `{string=|function()=}` – html template as a string or function that returns + * an html template as a string which should be used by {@link ngRoute.directive:ngView ngView} or + * {@link ng.directive:ngInclude ngInclude} directives. + * This property takes precedence over `templateUrl`. + * + * If `template` is a function, it will be called with the following parameters: + * + * - `{Array.}` - route parameters extracted from the current + * `$location.path()` by applying the current route + * + * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html + * template that should be used by {@link ngRoute.directive:ngView ngView}. + * + * If `templateUrl` is a function, it will be called with the following parameters: + * + * - `{Array.}` - route parameters extracted from the current + * `$location.path()` by applying the current route + * + * - `resolve` - `{Object.=}` - 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 + * `$routeChangeSuccess` 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/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 ng.$location $location} path with and trigger route redirection. + * + * If `redirectTo` is a function, it will be called with the following parameters: + * + * - `{Object.}` - route parameters extracted from the current + * `$location.path()` by applying the current route templateUrl. + * - `{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. + * + * - `[caseInsensitiveMatch=false]` - {boolean=} - match routes without being case sensitive + * + * If the option is set to `true`, then the particular route can be matched without being + * case sensitive + * + * @returns {Object} self + * + * @description + * Adds a new route definition to the `$route` service. + */ + this.when = function(path, route) { + routes[path] = extend({reloadOnSearch: true, caseInsensitiveMatch: false}, route); + + // 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 this; + }; + + /** + * @ngdoc method + * @name ngRoute.$routeProvider#otherwise + * @methodOf ngRoute.$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`. + * @returns {Object} self + */ + this.otherwise = function(params) { + this.when(null, params); + return this; + }; + + + this.$get = ['$rootScope', '$location', '$routeParams', '$q', '$injector', '$http', '$templateCache', + function( $rootScope, $location, $routeParams, $q, $injector, $http, $templateCache) { + + /** + * @ngdoc object + * @name ngRoute.$route + * @requires $location + * @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 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.} 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 ngRoute.$routeProvider $routeProvider}'s API. + * + * The `$route` service is typically used in conjunction with {@link ngRoute.directive:ngView ngView} + * directive and the {@link ngRoute.$routeParams $routeParams} service. + * + * @example + This example shows how changing the URL hash causes the `$route` to match a route against the + URL, and the `ngView` pulls in the partial. + + Note that this example is using {@link ng.directive:script inlined templates} + to get it working on jsfiddle as well. + + + +
+ Choose: + Moby | + Moby: Ch1 | + Gatsby | + Gatsby: Ch4 | + Scarlet Letter
+ +
+
+ +
$location.path() = {{$location.path()}}
+
$route.current.templateUrl = {{$route.current.templateUrl}}
+
$route.current.params = {{$route.current.params}}
+
$route.current.scope.name = {{$route.current.scope.name}}
+
$routeParams = {{$routeParams}}
+
+
+ + + controller: {{name}}
+ Book Id: {{params.bookId}}
+
+ + + controller: {{name}}
+ Book Id: {{params.bookId}}
+ Chapter Id: {{params.chapterId}} +
+ + + angular.module('ngView', ['ngRoute'], function($routeProvider, $locationProvider) { + $routeProvider.when('/Book/:bookId', { + templateUrl: 'book.html', + 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', { + templateUrl: '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; + } + + + + 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(); + 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/); + }); + +
+ */ + + /** + * @ngdoc event + * @name ngRoute.$route#$routeChangeStart + * @eventOf ngRoute.$route + * @eventType broadcast on root scope + * @description + * 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 + * `$routeChangeSuccess` is fired. + * + * @param {Route} next Future route information. + * @param {Route} current Current route information. + */ + + /** + * @ngdoc event + * @name ngRoute.$route#$routeChangeSuccess + * @eventOf ngRoute.$route + * @eventType broadcast on root scope + * @description + * Broadcasted after a route dependencies are resolved. + * {@link ngRoute.directive:ngView ngView} listens for the directive + * to instantiate the controller and render the view. + * + * @param {Object} angularEvent Synthetic event object. + * @param {Route} current Current route information. + * @param {Route|Undefined} previous Previous route information, or undefined if current is first route entered. + */ + + /** + * @ngdoc event + * @name ngRoute.$route#$routeChangeError + * @eventOf ngRoute.$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 ngRoute.$route#$routeUpdate + * @eventOf ngRoute.$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 forceReload = false, + $route = { + routes: routes, + + /** + * @ngdoc method + * @name ngRoute.$route#reload + * @methodOf ngRoute.$route + * + * @description + * Causes `$route` service to reload the current route even if + * {@link ng.$location $location} hasn't changed. + * + * As a result of that, {@link ngRoute.directive:ngView ngView} + * creates new scope, reinstantiates the controller. + */ + reload: function() { + forceReload = true; + $rootScope.$evalAsync(updateRoute); + } + }; + + $rootScope.$on('$locationChangeSuccess', updateRoute); + + return $route; + + ///////////////////////////////////////////////////// + + /** + * @param on {string} current url + * @param when {string} route when template to match the url against + * @param whenProperties {Object} properties to define when's matching behavior + * @return {?Object} + */ + function switchRouteMatcher(on, when, whenProperties) { + // TODO(i): this code is convoluted and inefficient, we should construct the route matching + // regex only once and then reuse it + + // Escape regexp special characters. + when = '^' + when.replace(/[-\/\\^$:*+?.()|[\]{}]/g, "\\$&") + '$'; + + var regex = '', + params = [], + dst = {}; + + var re = /\\([:*])(\w+)/g, + paramMatch, + lastMatchedIndex = 0; + + while ((paramMatch = re.exec(when)) !== null) { + // Find each :param in `when` and replace it with a capturing group. + // Append all other sections of when unchanged. + regex += when.slice(lastMatchedIndex, paramMatch.index); + switch(paramMatch[1]) { + case ':': + regex += '([^\\/]*)'; + break; + case '*': + regex += '(.*)'; + break; + } + params.push(paramMatch[2]); + lastMatchedIndex = re.lastIndex; + } + // Append trailing path part. + regex += when.substr(lastMatchedIndex); + + var match = on.match(new RegExp(regex, whenProperties.caseInsensitiveMatch ? 'i' : '')); + 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('$routeChangeStart', 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(); + } + } + } + + $q.when(next). + then(function() { + if (next) { + var locals = extend({}, next.resolve), + template; + + forEach(locals, function(value, key) { + locals[key] = isString(value) ? $injector.get(value) : $injector.invoke(value); + }); + + if (isDefined(template = next.template)) { + if (isFunction(template)) { + template = template(next.params); + } + } else if (isDefined(template = next.templateUrl)) { + if (isFunction(template)) { + template = template(next.params); + } + if (isDefined(template)) { + next.loadedTemplateUrl = template; + template = $http.get(template, {cache: $templateCache}). + then(function(response) { return response.data; }); + } + } + if (isDefined(template)) { + locals['$template'] = template; + } + return $q.all(locals); + } + }). + // after route change + then(function(locals) { + if (next == $route.current) { + if (next) { + next.locals = locals; + copy(next.params, $routeParams); + } + $rootScope.$broadcast('$routeChangeSuccess', next, last); + } + }, function(error) { + if (next == $route.current) { + $rootScope.$broadcast('$routeChangeError', next, last, error); + } + }); + } + } + + + /** + * @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 = switchRouteMatcher($location.path(), path, route))) { + 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 parameters + */ + 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(''); + } + }]; +} diff --git a/src/ngRoute/routeParams.js b/src/ngRoute/routeParams.js new file mode 100644 index 00000000..0c86e89d --- /dev/null +++ b/src/ngRoute/routeParams.js @@ -0,0 +1,33 @@ +'use strict'; + +ngRouteModule.provider('$routeParams', $RouteParamsProvider); + + +/** + * @ngdoc object + * @name ngRoute.$routeParams + * @requires $route + * + * @description + * Current set of route parameters. The route parameters are a combination of the + * {@link ng.$location $location} `search()`, and `path()`. The `path` parameters + * are extracted when the {@link ngRoute.$route $route} path is matched. + * + * In case of parameter name collision, `path` params take precedence over `search` 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 + *
+ *  // 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'}
+ * 
+ */ +function $RouteParamsProvider() { + this.$get = function() { return {}; }; +} diff --git a/src/ngRoute/routeUtils.js b/src/ngRoute/routeUtils.js new file mode 100644 index 00000000..0cff7213 --- /dev/null +++ b/src/ngRoute/routeUtils.js @@ -0,0 +1,17 @@ +'use strict'; + +var copy = angular.copy, + equals = angular.equals, + extend = angular.extend, + forEach = angular.forEach, + isDefined = angular.isDefined, + isFunction = angular.isFunction, + isString = angular.isString, + jqLite = angular.element, + noop = angular.noop, + toJson = angular.toJson; + + +function inherit(parent, extra) { + return extend(new (extend(function() {}, {prototype:parent}))(), extra); +} diff --git a/test/matchers.js b/test/matchers.js index 13d284e6..44d6b61e 100644 --- a/test/matchers.js +++ b/test/matchers.js @@ -190,3 +190,17 @@ jasmine.Matchers.prototype.toThrow = function(expected) { return result; }; + + +/** + * Create jasmine.Spy on given method, but ignore calls without arguments + * This is helpful when need to spy only setter methods and ignore getters + */ +function spyOnlyCallsWithArgs(obj, method) { + var spy = spyOn(obj, method); + obj[method] = function() { + if (arguments.length) return spy.apply(this, arguments); + return spy.originalValue.apply(this); + }; + return spy; +} diff --git a/test/ng/directive/ngViewSpec.js b/test/ng/directive/ngViewSpec.js deleted file mode 100644 index 01ca0d44..00000000 --- a/test/ng/directive/ngViewSpec.js +++ /dev/null @@ -1,667 +0,0 @@ -'use strict'; - -describe('ngView', function() { - var element; - - beforeEach(module(function($provide) { - $provide.value('$window', angular.mock.createMockWindow()); - return function($rootScope, $compile, $animator) { - element = $compile('')($rootScope); - $animator.enabled(true); - }; - })); - - - afterEach(function(){ - dealoc(element); - }); - - - it('should do nothing when no routes are defined', - inject(function($rootScope, $compile, $location) { - $location.path('/unknown'); - $rootScope.$digest(); - expect(element.text()).toEqual(''); - })); - - - it('should instantiate controller after compiling the content', function() { - var log = [], controllerScope, - Ctrl = function($scope) { - controllerScope = $scope; - log.push('ctrl-init'); - }; - - module(function($compileProvider, $routeProvider) { - $compileProvider.directive('compileLog', function() { - return { - compile: function() { - log.push('compile'); - } - }; - }); - - $routeProvider.when('/some', {templateUrl: '/tpl.html', controller: Ctrl}); - }); - - inject(function($route, $rootScope, $templateCache, $location) { - $templateCache.put('/tpl.html', [200, '
partial
', {}]); - $location.path('/some'); - $rootScope.$digest(); - - expect(controllerScope.$parent).toBe($rootScope); - expect(controllerScope).toBe($route.current.scope); - expect(log).toEqual(['compile', 'ctrl-init']); - }); - }); - - - it('should instantiate controller with an alias', function() { - var log = [], controllerScope, - Ctrl = function($scope) { - this.name = 'alias'; - controllerScope = $scope; - }; - - module(function($compileProvider, $routeProvider) { - $routeProvider.when('/some', {templateUrl: '/tpl.html', controller: Ctrl, controllerAs: 'ctrl'}); - }); - - inject(function($route, $rootScope, $templateCache, $location) { - $templateCache.put('/tpl.html', [200, '
', {}]); - $location.path('/some'); - $rootScope.$digest(); - - expect(controllerScope.ctrl.name).toBe('alias'); - }); - }); - - - it('should support string controller declaration', function() { - var MyCtrl = jasmine.createSpy('MyCtrl'); - - module(function($controllerProvider, $routeProvider) { - $controllerProvider.register('MyCtrl', ['$scope', MyCtrl]); - $routeProvider.when('/foo', {controller: 'MyCtrl', templateUrl: '/tpl.html'}); - }); - - inject(function($route, $location, $rootScope, $templateCache) { - $templateCache.put('/tpl.html', [200, '
', {}]); - $location.path('/foo'); - $rootScope.$digest(); - - expect($route.current.controller).toBe('MyCtrl'); - expect(MyCtrl).toHaveBeenCalledWith(element.contents().scope()); - }); - }); - - - it('should load content via xhr when route changes', function() { - module(function($routeProvider) { - $routeProvider.when('/foo', {templateUrl: 'myUrl1'}); - $routeProvider.when('/bar', {templateUrl: 'myUrl2'}); - }); - - inject(function($rootScope, $compile, $httpBackend, $location, $route) { - expect(element.text()).toEqual(''); - - $location.path('/foo'); - $httpBackend.expect('GET', 'myUrl1').respond('
{{1+3}}
'); - $rootScope.$digest(); - $httpBackend.flush(); - expect(element.text()).toEqual('4'); - - $location.path('/bar'); - $httpBackend.expect('GET', 'myUrl2').respond('angular is da best'); - $rootScope.$digest(); - $httpBackend.flush(); - expect(element.text()).toEqual('angular is da best'); - }); - }); - - - it('should use inline content route changes', function() { - module(function($routeProvider) { - $routeProvider.when('/foo', {template: '
{{1+3}}
'}); - $routeProvider.when('/bar', {template: 'angular is da best'}); - $routeProvider.when('/blank', {template: ''}); - }); - - inject(function($rootScope, $compile, $location, $route) { - expect(element.text()).toEqual(''); - - $location.path('/foo'); - $rootScope.$digest(); - expect(element.text()).toEqual('4'); - - $location.path('/bar'); - $rootScope.$digest(); - expect(element.text()).toEqual('angular is da best'); - - $location.path('/blank'); - $rootScope.$digest(); - expect(element.text()).toEqual(''); - }); - }); - - - it('should remove all content when location changes to an unknown route', function() { - module(function($routeProvider) { - $routeProvider.when('/foo', {templateUrl: 'myUrl1'}); - }); - - inject(function($rootScope, $compile, $location, $httpBackend, $route) { - $location.path('/foo'); - $httpBackend.expect('GET', 'myUrl1').respond('
{{1+3}}
'); - $rootScope.$digest(); - $httpBackend.flush(); - expect(element.text()).toEqual('4'); - - $location.path('/unknown'); - $rootScope.$digest(); - expect(element.text()).toEqual(''); - }); - }); - - - it('should chain scopes and propagate evals to the child scope', function() { - module(function($routeProvider) { - $routeProvider.when('/foo', {templateUrl: 'myUrl1'}); - }); - - inject(function($rootScope, $compile, $location, $httpBackend, $route) { - $rootScope.parentVar = 'parent'; - - $location.path('/foo'); - $httpBackend.expect('GET', 'myUrl1').respond('
{{parentVar}}
'); - $rootScope.$digest(); - $httpBackend.flush(); - expect(element.text()).toEqual('parent'); - - $rootScope.parentVar = 'new parent'; - $rootScope.$digest(); - expect(element.text()).toEqual('new parent'); - }); - }); - - - it('should be possible to nest ngView in ngInclude', function() { - - module(function($routeProvider) { - $routeProvider.when('/foo', {templateUrl: 'viewPartial.html'}); - }); - - inject(function($httpBackend, $location, $route, $compile, $rootScope) { - $httpBackend.whenGET('includePartial.html').respond('view: '); - $httpBackend.whenGET('viewPartial.html').respond('content'); - $location.path('/foo'); - - var elm = $compile( - '
' + - 'include: ' + - '
')($rootScope); - $rootScope.$digest(); - $httpBackend.flush(); - - expect(elm.text()).toEqual('include: view: content'); - expect($route.current.templateUrl).toEqual('viewPartial.html'); - dealoc(elm) - }); - }); - - - it('should initialize view template after the view controller was initialized even when ' + - 'templates were cached', function() { - //this is a test for a regression that was introduced by making the ng-view cache sync - function ParentCtrl($scope) { - $scope.log.push('parent'); - } - - module(function($routeProvider) { - $routeProvider.when('/foo', {controller: ParentCtrl, templateUrl: 'viewPartial.html'}); - }); - - - inject(function($rootScope, $compile, $location, $httpBackend, $route) { - $rootScope.log = []; - - $rootScope.ChildCtrl = function($scope) { - $scope.log.push('child'); - }; - - $location.path('/foo'); - $httpBackend.expect('GET', 'viewPartial.html'). - respond('
' + - '
' + - '
'); - $rootScope.$apply(); - $httpBackend.flush(); - - expect($rootScope.log).toEqual(['parent', 'init', 'child']); - - $location.path('/'); - $rootScope.$apply(); - expect($rootScope.log).toEqual(['parent', 'init', 'child']); - - $rootScope.log = []; - $location.path('/foo'); - $rootScope.$apply(); - - expect($rootScope.log).toEqual(['parent', 'init', 'child']); - }); - }); - - - it('should discard pending xhr callbacks if a new route is requested before the current ' + - 'finished loading', function() { - // this is a test for a bad race condition that affected feedback - - module(function($routeProvider) { - $routeProvider.when('/foo', {templateUrl: 'myUrl1'}); - $routeProvider.when('/bar', {templateUrl: 'myUrl2'}); - }); - - inject(function($route, $rootScope, $location, $httpBackend) { - expect(element.text()).toEqual(''); - - $location.path('/foo'); - $httpBackend.expect('GET', 'myUrl1').respond('
{{1+3}}
'); - $rootScope.$digest(); - $location.path('/bar'); - $httpBackend.expect('GET', 'myUrl2').respond('
{{1+1}}
'); - $rootScope.$digest(); - $httpBackend.flush(); // now that we have two requests pending, flush! - - expect(element.text()).toEqual('2'); - }); - }); - - - it('should be async even if served from cache', function() { - module(function($routeProvider) { - $routeProvider.when('/foo', {controller: noop, templateUrl: 'myUrl1'}); - }); - - inject(function($route, $rootScope, $location, $templateCache) { - $templateCache.put('myUrl1', [200, 'my partial', {}]); - $location.path('/foo'); - - var called = 0; - // we want to assert only during first watch - $rootScope.$watch(function() { - if (!called++) expect(element.text()).toBe(''); - }); - - $rootScope.$digest(); - expect(element.text()).toBe('my partial'); - }); - }); - - it('should fire $contentLoaded event when content compiled and linked', function() { - var log = []; - var logger = function(name) { - return function() { - log.push(name); - }; - }; - var Ctrl = function($scope) { - $scope.value = 'bound-value'; - log.push('init-ctrl'); - }; - - module(function($routeProvider) { - $routeProvider.when('/foo', {templateUrl: 'tpl.html', controller: Ctrl}); - }); - - inject(function($templateCache, $rootScope, $location) { - $rootScope.$on('$routeChangeStart', logger('$routeChangeStart')); - $rootScope.$on('$routeChangeSuccess', logger('$routeChangeSuccess')); - $rootScope.$on('$viewContentLoaded', logger('$viewContentLoaded')); - - $templateCache.put('tpl.html', [200, '{{value}}', {}]); - $location.path('/foo'); - $rootScope.$digest(); - - expect(element.text()).toBe('bound-value'); - expect(log).toEqual([ - '$routeChangeStart', 'init-ctrl', '$viewContentLoaded', '$routeChangeSuccess' ]); - }); - }); - - it('should destroy previous scope', function() { - module(function($routeProvider) { - $routeProvider.when('/foo', {templateUrl: 'tpl.html'}); - }); - - inject(function($templateCache, $rootScope, $location) { - $templateCache.put('tpl.html', [200, 'partial', {}]); - - expect($rootScope.$$childHead).toBeNull(); - expect($rootScope.$$childTail).toBeNull(); - - $location.path('/foo'); - $rootScope.$digest(); - - expect(element.text()).toBe('partial'); - expect($rootScope.$$childHead).not.toBeNull(); - expect($rootScope.$$childTail).not.toBeNull(); - - $location.path('/non/existing/route'); - $rootScope.$digest(); - - expect(element.text()).toBe(''); - expect($rootScope.$$childHead).toBeNull(); - expect($rootScope.$$childTail).toBeNull(); - }); - }); - - - it('should destroy previous scope if multiple route changes occur before server responds', - function() { - var log = []; - var createCtrl = function(name) { - return function($scope) { - log.push('init-' + name); - $scope.$on('$destroy', function() {log.push('destroy-' + name);}); - }; - }; - - module(function($routeProvider) { - $routeProvider.when('/one', {templateUrl: 'one.html', controller: createCtrl('ctrl1')}); - $routeProvider.when('/two', {templateUrl: 'two.html', controller: createCtrl('ctrl2')}); - }); - - inject(function($httpBackend, $rootScope, $location) { - $httpBackend.whenGET('one.html').respond('content 1'); - $httpBackend.whenGET('two.html').respond('content 2'); - - $location.path('/one'); - $rootScope.$digest(); - $location.path('/two'); - $rootScope.$digest(); - - $httpBackend.flush(); - expect(element.text()).toBe('content 2'); - expect(log).toEqual(['init-ctrl2']); - - $location.path('/non-existing'); - $rootScope.$digest(); - - expect(element.text()).toBe(''); - expect(log).toEqual(['init-ctrl2', 'destroy-ctrl2']); - - expect($rootScope.$$childHead).toBeNull(); - expect($rootScope.$$childTail).toBeNull(); - }); - }); - - - it('should $destroy scope after update and reload', function() { - // this is a regression of bug, where $route doesn't copy scope when only updating - - var log = []; - - function logger(msg) { - return function() { - log.push(msg); - }; - } - - function createController(name) { - return function($scope) { - log.push('init-' + name); - $scope.$on('$destroy', logger('destroy-' + name)); - $scope.$on('$routeUpdate', logger('route-update')); - }; - } - - module(function($routeProvider) { - $routeProvider.when('/bar', {templateUrl: 'tpl.html', controller: createController('bar')}); - $routeProvider.when('/foo', { - templateUrl: 'tpl.html', controller: createController('foo'), reloadOnSearch: false}); - }); - - inject(function($templateCache, $location, $rootScope) { - $templateCache.put('tpl.html', [200, 'partial', {}]); - - $location.url('/foo'); - $rootScope.$digest(); - expect(log).toEqual(['init-foo']); - - $location.search({q: 'some'}); - $rootScope.$digest(); - expect(log).toEqual(['init-foo', 'route-update']); - - $location.url('/bar'); - $rootScope.$digest(); - expect(log).toEqual(['init-foo', 'route-update', 'destroy-foo', 'init-bar']); - }); - }); - - - it('should evaluate onload expression after linking the content', function() { - module(function($routeProvider) { - $routeProvider.when('/foo', {templateUrl: 'tpl.html'}); - }); - - inject(function($templateCache, $location, $rootScope) { - $templateCache.put('tpl.html', [200, '{{1+1}}', {}]); - $rootScope.load = jasmine.createSpy('onload'); - - $location.url('/foo'); - $rootScope.$digest(); - expect($rootScope.load).toHaveBeenCalledOnce(); - }); - }); - - - it('should set $scope and $controllerController on the view', function() { - function MyCtrl($scope) { - $scope.state = 'WORKS'; - $scope.ctrl = this; - } - - module(function($routeProvider) { - $routeProvider.when('/foo', {templateUrl: 'tpl.html', controller: MyCtrl}); - }); - - inject(function($templateCache, $location, $rootScope, $route) { - $templateCache.put('tpl.html', [200, '
{{state}}
', {}]); - - $location.url('/foo'); - $rootScope.$digest(); - expect(element.text()).toEqual('WORKS'); - - var div = element.find('div'); - expect(nodeName_(div.parent())).toEqual('NG:VIEW'); - - expect(div.scope()).toBe($route.current.scope); - expect(div.scope().hasOwnProperty('state')).toBe(true); - expect(div.scope().state).toEqual('WORKS'); - - expect(div.controller()).toBe($route.current.scope.ctrl); - }); - }); - - it('should not set $scope or $controllerController on top level text elements in the view', function() { - function MyCtrl($scope) {} - - module(function($routeProvider) { - $routeProvider.when('/foo', {templateUrl: 'tpl.html', controller: MyCtrl}); - }); - - inject(function($templateCache, $location, $rootScope, $route) { - $templateCache.put('tpl.html', '
'); - $location.url('/foo'); - $rootScope.$digest(); - - forEach(element.contents(), function(node) { - if ( node.nodeType == 3 /* text node */) { - expect(jqLite(node).scope()).not.toBe($route.current.scope); - expect(jqLite(node).controller()).not.toBeDefined(); - } else { - expect(jqLite(node).scope()).toBe($route.current.scope); - expect(jqLite(node).controller()).toBeDefined(); - } - }); - }); - }); - - describe('ngAnimate ', function() { - var window, vendorPrefix; - var body, element; - - function html(html) { - body.html(html); - element = body.children().eq(0); - return element; - } - - function applyCSS(element, cssProp, cssValue) { - element.css(cssProp, cssValue); - element.css(vendorPrefix + cssProp, cssValue); - } - - beforeEach(function() { - // we need to run animation on attached elements; - body = jqLite(document.body); - }); - - afterEach(function(){ - dealoc(body); - dealoc(element); - }); - - - beforeEach(module(function($provide, $routeProvider) { - $provide.value('$window', window = angular.mock.createMockWindow()); - $routeProvider.when('/foo', {controller: noop, templateUrl: '/foo.html'}); - return function($sniffer, $templateCache, $animator) { - vendorPrefix = '-' + $sniffer.vendorPrefix + '-'; - $templateCache.put('/foo.html', [200, '
data
', {}]); - $animator.enabled(true); - } - })); - - it('should fire off the enter animation + add and remove the css classes', - inject(function($compile, $rootScope, $sniffer, $location, $templateCache) { - element = $compile(html('
'))($rootScope); - - $location.path('/foo'); - $rootScope.$digest(); - - //if we add the custom css stuff here then it will get picked up before the animation takes place - var child = jqLite(element.children()[0]); - applyCSS(child, 'transition', '1s linear all'); - - if ($sniffer.transitions) { - expect(child.attr('class')).toContain('custom-enter'); - window.setTimeout.expect(1).process(); - - expect(child.attr('class')).toContain('custom-enter-active'); - window.setTimeout.expect(1000).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } - - expect(child.attr('class')).not.toContain('custom-enter'); - expect(child.attr('class')).not.toContain('custom-enter-active'); - })); - - it('should fire off the leave animation + add and remove the css classes', - inject(function($compile, $rootScope, $sniffer, $location, $templateCache) { - $templateCache.put('/foo.html', [200, '
foo
', {}]); - element = $compile(html('
'))($rootScope); - - $location.path('/foo'); - $rootScope.$digest(); - - //if we add the custom css stuff here then it will get picked up before the animation takes place - var child = jqLite(element.children()[0]); - applyCSS(child, 'transition', '1s linear all'); - - $location.path('/'); - $rootScope.$digest(); - - if ($sniffer.transitions) { - expect(child.attr('class')).toContain('custom-leave'); - window.setTimeout.expect(1).process(); - - expect(child.attr('class')).toContain('custom-leave-active'); - window.setTimeout.expect(1000).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } - - expect(child.attr('class')).not.toContain('custom-leave'); - expect(child.attr('class')).not.toContain('custom-leave-active'); - })); - - it('should catch and use the correct duration for animations', - inject(function($compile, $rootScope, $sniffer, $location, $templateCache) { - $templateCache.put('/foo.html', [200, '
foo
', {}]); - element = $compile(html( - '
' + - '
' - ))($rootScope); - - $location.path('/foo'); - $rootScope.$digest(); - - //if we add the custom css stuff here then it will get picked up before the animation takes place - var child = jqLite(element.children()[0]); - applyCSS(child, 'transition', '0.5s linear all'); - - if($sniffer.transitions) { - window.setTimeout.expect(1).process(); - window.setTimeout.expect($sniffer.transitions ? 500 : 0).process(); - } else { - expect(window.setTimeout.queue).toEqual([]); - } - })); - - - it('should not double compile when route changes', function() { - module(function($routeProvider, $animationProvider, $provide) { - $routeProvider.when('/foo', {template: '
{{i}}
'}); - $routeProvider.when('/bar', {template: '
{{i}}
'}); - $animationProvider.register('my-animation-leave', function() { - return { - start: function(element, done) { - done(); - } - }; - }); - }); - - inject(function($rootScope, $compile, $location, $route, $window, $rootElement, $sniffer) { - element = $compile(html(''))($rootScope); - - $location.path('/foo'); - $rootScope.$digest(); - if ($sniffer.transitions) { - $window.setTimeout.expect(1).process(); - $window.setTimeout.expect(0).process(); - } - expect(element.text()).toEqual('12'); - - $location.path('/bar'); - $rootScope.$digest(); - expect(n(element.text())).toEqual('1234'); - if ($sniffer.transitions) { - $window.setTimeout.expect(1).process(); - $window.setTimeout.expect(1).process(); - } else { - $window.setTimeout.expect(1).process(); - } - expect(element.text()).toEqual('34'); - - function n(text) { - return text.replace(/\r\n/m, '').replace(/\r\n/m, ''); - } - }); - }); - }); -}); diff --git a/test/ng/locationSpec.js b/test/ng/locationSpec.js index 4aaa4d51..ee920ed9 100644 --- a/test/ng/locationSpec.js +++ b/test/ng/locationSpec.js @@ -1,18 +1,5 @@ 'use strict'; -/** - * Create jasmine.Spy on given method, but ignore calls without arguments - * This is helpful when need to spy only setter methods and ignore getters - */ -function spyOnlyCallsWithArgs(obj, method) { - var spy = spyOn(obj, method); - obj[method] = function() { - if (arguments.length) return spy.apply(this, arguments); - return spy.originalValue.apply(this); - }; - return spy; -} - describe('$location', function() { var url; diff --git a/test/ng/routeParamsSpec.js b/test/ng/routeParamsSpec.js deleted file mode 100644 index 52fe8d2a..00000000 --- a/test/ng/routeParamsSpec.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; - -describe('$routeParams', function() { - it('should publish the params into a service', function() { - module(function($routeProvider) { - $routeProvider.when('/foo', {}); - $routeProvider.when('/bar/:barId', {}); - }); - - inject(function($rootScope, $route, $location, $routeParams) { - $location.path('/foo').search('a=b'); - $rootScope.$digest(); - expect($routeParams).toEqual({a:'b'}); - - $location.path('/bar/123').search('x=abc'); - $rootScope.$digest(); - expect($routeParams).toEqual({barId:'123', x:'abc'}); - }); - }); - - it('should correctly extract the params when a param name is part of the route', function() { - module(function($routeProvider) { - $routeProvider.when('/bar/:foo/:bar', {}); - }); - - inject(function($rootScope, $route, $location, $routeParams) { - $location.path('/bar/foovalue/barvalue'); - $rootScope.$digest(); - expect($routeParams).toEqual({bar:'barvalue', foo:'foovalue'}); - }); - }); - - it('should support route params not preceded by slashes', function() { - module(function($routeProvider) { - $routeProvider.when('/bar:barId/foo:fooId/', {}); - }); - - inject(function($rootScope, $route, $location, $routeParams) { - $location.path('/barbarvalue/foofoovalue/'); - $rootScope.$digest(); - expect($routeParams).toEqual({barId: 'barvalue', fooId: 'foovalue'}); - }); - }); -}); diff --git a/test/ng/routeSpec.js b/test/ng/routeSpec.js deleted file mode 100644 index c2651286..00000000 --- a/test/ng/routeSpec.js +++ /dev/null @@ -1,974 +0,0 @@ -'use strict'; - -describe('$route', function() { - var $httpBackend; - - beforeEach(module(function() { - return function(_$httpBackend_) { - $httpBackend = _$httpBackend_; - $httpBackend.when('GET', 'Chapter.html').respond('chapter'); - $httpBackend.when('GET', 'test.html').respond('test'); - $httpBackend.when('GET', 'foo.html').respond('foo'); - $httpBackend.when('GET', 'baz.html').respond('baz'); - $httpBackend.when('GET', 'bar.html').respond('bar'); - $httpBackend.when('GET', '404.html').respond('not found'); - }; - })); - - it('should route and fire change event', function() { - var log = '', - lastRoute, - nextRoute; - - module(function($routeProvider) { - $routeProvider.when('/Book/:book/Chapter/:chapter', - {controller: noop, templateUrl: 'Chapter.html'}); - $routeProvider.when('/Blank', {}); - }); - inject(function($route, $location, $rootScope) { - $rootScope.$on('$routeChangeStart', function(event, next, current) { - log += 'before();'; - expect(current).toBe($route.current); - lastRoute = current; - nextRoute = next; - }); - $rootScope.$on('$routeChangeSuccess', function(event, current, last) { - log += 'after();'; - expect(current).toBe($route.current); - expect(lastRoute).toBe(last); - expect(nextRoute).toBe(current); - }); - - $location.path('/Book/Moby/Chapter/Intro').search('p=123'); - $rootScope.$digest(); - $httpBackend.flush(); - expect(log).toEqual('before();after();'); - expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', p:'123'}); - - log = ''; - $location.path('/Blank').search('ignore'); - $rootScope.$digest(); - expect(log).toEqual('before();after();'); - expect($route.current.params).toEqual({ignore:true}); - - log = ''; - $location.path('/NONE'); - $rootScope.$digest(); - expect(log).toEqual('before();after();'); - expect($route.current).toEqual(null); - }); - }); - - it('should route and fire change event when catch-all params are used', function() { - var log = '', - lastRoute, - nextRoute; - - module(function($routeProvider) { - $routeProvider.when('/Book1/:book/Chapter/:chapter/*highlight/edit', - {controller: noop, templateUrl: 'Chapter.html'}); - $routeProvider.when('/Book2/:book/*highlight/Chapter/:chapter', - {controller: noop, templateUrl: 'Chapter.html'}); - $routeProvider.when('/Blank', {}); - }); - inject(function($route, $location, $rootScope) { - $rootScope.$on('$routeChangeStart', function(event, next, current) { - log += 'before();'; - expect(current).toBe($route.current); - lastRoute = current; - nextRoute = next; - }); - $rootScope.$on('$routeChangeSuccess', function(event, current, last) { - log += 'after();'; - expect(current).toBe($route.current); - expect(lastRoute).toBe(last); - expect(nextRoute).toBe(current); - }); - - $location.path('/Book1/Moby/Chapter/Intro/one/edit').search('p=123'); - $rootScope.$digest(); - $httpBackend.flush(); - expect(log).toEqual('before();after();'); - expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', highlight:'one', p:'123'}); - - log = ''; - $location.path('/Blank').search('ignore'); - $rootScope.$digest(); - expect(log).toEqual('before();after();'); - expect($route.current.params).toEqual({ignore:true}); - - log = ''; - $location.path('/Book1/Moby/Chapter/Intro/one/two/edit').search('p=123'); - $rootScope.$digest(); - expect(log).toEqual('before();after();'); - expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', highlight:'one/two', p:'123'}); - - log = ''; - $location.path('/Book2/Moby/one/two/Chapter/Intro').search('p=123'); - $rootScope.$digest(); - expect(log).toEqual('before();after();'); - expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', highlight:'one/two', p:'123'}); - - log = ''; - $location.path('/NONE'); - $rootScope.$digest(); - expect(log).toEqual('before();after();'); - expect($route.current).toEqual(null); - }); - }); - - - it('should route and fire change event correctly whenever the case insensitive flag is utilized', function() { - var log = '', - lastRoute, - nextRoute; - - module(function($routeProvider) { - $routeProvider.when('/Book1/:book/Chapter/:chapter/*highlight/edit', - {controller: noop, templateUrl: 'Chapter.html', caseInsensitiveMatch: true}); - $routeProvider.when('/Book2/:book/*highlight/Chapter/:chapter', - {controller: noop, templateUrl: 'Chapter.html'}); - $routeProvider.when('/Blank', {}); - }); - inject(function($route, $location, $rootScope) { - $rootScope.$on('$routeChangeStart', function(event, next, current) { - log += 'before();'; - expect(current).toBe($route.current); - lastRoute = current; - nextRoute = next; - }); - $rootScope.$on('$routeChangeSuccess', function(event, current, last) { - log += 'after();'; - expect(current).toBe($route.current); - expect(lastRoute).toBe(last); - expect(nextRoute).toBe(current); - }); - - $location.path('/Book1/Moby/Chapter/Intro/one/edit').search('p=123'); - $rootScope.$digest(); - $httpBackend.flush(); - expect(log).toEqual('before();after();'); - expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', highlight:'one', p:'123'}); - - log = ''; - $location.path('/BOOK1/Moby/CHAPTER/Intro/one/EDIT').search('p=123'); - $rootScope.$digest(); - expect(log).toEqual('before();after();'); - expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', highlight:'one', p:'123'}); - - log = ''; - $location.path('/Blank').search('ignore'); - $rootScope.$digest(); - expect(log).toEqual('before();after();'); - expect($route.current.params).toEqual({ignore:true}); - - log = ''; - $location.path('/BLANK'); - $rootScope.$digest(); - expect(log).toEqual('before();after();'); - expect($route.current).toEqual(null); - - log = ''; - $location.path('/Book2/Moby/one/two/Chapter/Intro').search('p=123'); - $rootScope.$digest(); - expect(log).toEqual('before();after();'); - expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', highlight:'one/two', p:'123'}); - - log = ''; - $location.path('/BOOK2/Moby/one/two/CHAPTER/Intro').search('p=123'); - $rootScope.$digest(); - expect(log).toEqual('before();after();'); - expect($route.current).toEqual(null); - }); - }); - - - it('should not change route when location is canceled', function() { - module(function($routeProvider) { - $routeProvider.when('/somePath', {template: 'some path'}); - }); - inject(function($route, $location, $rootScope, $log) { - $rootScope.$on('$locationChangeStart', function(event) { - $log.info('$locationChangeStart'); - event.preventDefault(); - }); - - $rootScope.$on('$beforeRouteChange', function(event) { - throw new Error('Should not get here'); - }); - - $location.path('/somePath'); - $rootScope.$digest(); - - expect($log.info.logs.shift()).toEqual(['$locationChangeStart']); - }); - }); - - - describe('should match a route that contains special chars in the path', function() { - beforeEach(module(function($routeProvider) { - $routeProvider.when('/$test.23/foo*(bar)/:baz', {templateUrl: 'test.html'}); - })); - - it('matches the full path', inject(function($route, $location, $rootScope) { - $location.path('/test'); - $rootScope.$digest(); - expect($route.current).toBeUndefined(); - })); - - it('matches literal .', inject(function($route, $location, $rootScope) { - $location.path('/$testX23/foo*(bar)/222'); - $rootScope.$digest(); - expect($route.current).toBeUndefined(); - })); - - it('matches literal *', inject(function($route, $location, $rootScope) { - $location.path('/$test.23/foooo(bar)/222'); - $rootScope.$digest(); - expect($route.current).toBeUndefined(); - })); - - it('treats backslashes normally', inject(function($route, $location, $rootScope) { - $location.path('/$test.23/foo*\\(bar)/222'); - $rootScope.$digest(); - expect($route.current).toBeUndefined(); - })); - - it('matches a URL with special chars', inject(function($route, $location, $rootScope) { - $location.path('/$test.23/foo*(bar)/222'); - $rootScope.$digest(); - expect($route.current).toBeDefined(); - })); - }); - - - it('should change route even when only search param changes', function() { - module(function($routeProvider) { - $routeProvider.when('/test', {templateUrl: 'test.html'}); - }); - - inject(function($route, $location, $rootScope) { - var callback = jasmine.createSpy('onRouteChange'); - - $rootScope.$on('$routeChangeStart', callback); - $location.path('/test'); - $rootScope.$digest(); - callback.reset(); - - $location.search({any: true}); - $rootScope.$digest(); - - expect(callback).toHaveBeenCalled(); - }); - }); - - - it('should allow routes to be defined with just templates without controllers', function() { - module(function($routeProvider) { - $routeProvider.when('/foo', {templateUrl: 'foo.html'}); - }); - - inject(function($route, $location, $rootScope) { - var onChangeSpy = jasmine.createSpy('onChange'); - - $rootScope.$on('$routeChangeStart', onChangeSpy); - expect($route.current).toBeUndefined(); - expect(onChangeSpy).not.toHaveBeenCalled(); - - $location.path('/foo'); - $rootScope.$digest(); - - expect($route.current.templateUrl).toEqual('foo.html'); - expect($route.current.controller).toBeUndefined(); - expect(onChangeSpy).toHaveBeenCalled(); - }); - }); - - - it('should chain whens and otherwise', function() { - module(function($routeProvider){ - $routeProvider.when('/foo', {templateUrl: 'foo.html'}). - otherwise({templateUrl: 'bar.html'}). - when('/baz', {templateUrl: 'baz.html'}); - }); - - inject(function($route, $location, $rootScope) { - $rootScope.$digest(); - expect($route.current.templateUrl).toBe('bar.html'); - - $location.url('/baz'); - $rootScope.$digest(); - expect($route.current.templateUrl).toBe('baz.html'); - }); - }); - - - describe('otherwise', function() { - - it('should handle unknown routes with "otherwise" route definition', function() { - function NotFoundCtrl() {} - - module(function($routeProvider){ - $routeProvider.when('/foo', {templateUrl: 'foo.html'}); - $routeProvider.otherwise({templateUrl: '404.html', controller: NotFoundCtrl}); - }); - - inject(function($route, $location, $rootScope) { - var onChangeSpy = jasmine.createSpy('onChange'); - - $rootScope.$on('$routeChangeStart', onChangeSpy); - expect($route.current).toBeUndefined(); - expect(onChangeSpy).not.toHaveBeenCalled(); - - $location.path('/unknownRoute'); - $rootScope.$digest(); - - expect($route.current.templateUrl).toBe('404.html'); - expect($route.current.controller).toBe(NotFoundCtrl); - expect(onChangeSpy).toHaveBeenCalled(); - - onChangeSpy.reset(); - $location.path('/foo'); - $rootScope.$digest(); - - expect($route.current.templateUrl).toEqual('foo.html'); - expect($route.current.controller).toBeUndefined(); - expect(onChangeSpy).toHaveBeenCalled(); - }); - }); - - - it('should update $route.current and $route.next when default route is matched', function() { - module(function($routeProvider){ - $routeProvider.when('/foo', {templateUrl: 'foo.html'}); - $routeProvider.otherwise({templateUrl: '404.html'}); - }); - - inject(function($route, $location, $rootScope) { - var currentRoute, nextRoute, - onChangeSpy = jasmine.createSpy('onChange').andCallFake(function(e, next) { - currentRoute = $route.current; - nextRoute = next; - }); - - - // init - $rootScope.$on('$routeChangeStart', onChangeSpy); - expect($route.current).toBeUndefined(); - expect(onChangeSpy).not.toHaveBeenCalled(); - - - // match otherwise route - $location.path('/unknownRoute'); - $rootScope.$digest(); - - expect(currentRoute).toBeUndefined(); - expect(nextRoute.templateUrl).toBe('404.html'); - expect($route.current.templateUrl).toBe('404.html'); - expect(onChangeSpy).toHaveBeenCalled(); - onChangeSpy.reset(); - - // match regular route - $location.path('/foo'); - $rootScope.$digest(); - - expect(currentRoute.templateUrl).toBe('404.html'); - expect(nextRoute.templateUrl).toBe('foo.html'); - expect($route.current.templateUrl).toEqual('foo.html'); - expect(onChangeSpy).toHaveBeenCalled(); - onChangeSpy.reset(); - - // match otherwise route again - $location.path('/anotherUnknownRoute'); - $rootScope.$digest(); - - expect(currentRoute.templateUrl).toBe('foo.html'); - expect(nextRoute.templateUrl).toBe('404.html'); - expect($route.current.templateUrl).toEqual('404.html'); - expect(onChangeSpy).toHaveBeenCalled(); - }); - }); - }); - - - describe('events', function() { - it('should not fire $after/beforeRouteChange during bootstrap (if no route)', function() { - var routeChangeSpy = jasmine.createSpy('route change'); - - module(function($routeProvider) { - $routeProvider.when('/one', {}); // no otherwise defined - }); - - inject(function($rootScope, $route, $location) { - $rootScope.$on('$routeChangeStart', routeChangeSpy); - $rootScope.$on('$routeChangeSuccess', routeChangeSpy); - - $rootScope.$digest(); - expect(routeChangeSpy).not.toHaveBeenCalled(); - - $location.path('/no-route-here'); - $rootScope.$digest(); - expect(routeChangeSpy).not.toHaveBeenCalled(); - }); - }); - - it('should fire $routeChangeStart and resolve promises', function() { - var deferA, - deferB; - - module(function($provide, $routeProvider) { - $provide.factory('b', function($q) { - deferB = $q.defer(); - return deferB.promise; - }); - $routeProvider.when('/path', { templateUrl: 'foo.html', resolve: { - a: ['$q', function($q) { - deferA = $q.defer(); - return deferA.promise; - }], - b: 'b' - } }); - }); - - inject(function($location, $route, $rootScope, $httpBackend) { - var log = ''; - - $httpBackend.expectGET('foo.html').respond('FOO'); - - $location.path('/path'); - $rootScope.$digest(); - expect(log).toEqual(''); - $httpBackend.flush(); - expect(log).toEqual(''); - deferA.resolve(); - $rootScope.$digest(); - expect(log).toEqual(''); - deferB.resolve(); - $rootScope.$digest(); - expect($route.current.locals.$template).toEqual('FOO'); - }); - }); - - - it('should fire $routeChangeError event on resolution error', function() { - var deferA; - - module(function($provide, $routeProvider) { - $routeProvider.when('/path', { template: 'foo', resolve: { - a: function($q) { - deferA = $q.defer(); - return deferA.promise; - } - } }); - }); - - inject(function($location, $route, $rootScope) { - var log = ''; - - $rootScope.$on('$routeChangeStart', function() { log += 'before();'; }); - $rootScope.$on('$routeChangeError', function(e, n, l, reason) { log += 'failed(' + reason + ');'; }); - - $location.path('/path'); - $rootScope.$digest(); - expect(log).toEqual('before();'); - - deferA.reject('MyError'); - $rootScope.$digest(); - expect(log).toEqual('before();failed(MyError);'); - }); - }); - - - it('should fetch templates', function() { - module(function($routeProvider) { - $routeProvider. - when('/r1', { templateUrl: 'r1.html' }). - when('/r2', { templateUrl: 'r2.html' }); - }); - - inject(function($route, $httpBackend, $location, $rootScope) { - var log = ''; - $rootScope.$on('$routeChangeStart', function(e, next) { log += '$before(' + next.templateUrl + ');'}); - $rootScope.$on('$routeChangeSuccess', function(e, next) { log += '$after(' + next.templateUrl + ');'}); - - $httpBackend.expectGET('r1.html').respond('R1'); - $httpBackend.expectGET('r2.html').respond('R2'); - - $location.path('/r1'); - $rootScope.$digest(); - expect(log).toBe('$before(r1.html);'); - - $location.path('/r2'); - $rootScope.$digest(); - expect(log).toBe('$before(r1.html);$before(r2.html);'); - - $httpBackend.flush(); - expect(log).toBe('$before(r1.html);$before(r2.html);$after(r2.html);'); - expect(log).not.toContain('$after(r1.html);'); - }); - }); - - - it('should not update $routeParams until $routeChangeSuccess', function() { - module(function($routeProvider) { - $routeProvider. - when('/r1/:id', { templateUrl: 'r1.html' }). - when('/r2/:id', { templateUrl: 'r2.html' }); - }); - - inject(function($route, $httpBackend, $location, $rootScope, $routeParams) { - var log = ''; - $rootScope.$on('$routeChangeStart', function(e, next) { log += '$before' + toJson($routeParams) + ';'}); - $rootScope.$on('$routeChangeSuccess', function(e, next) { log += '$after' + toJson($routeParams) + ';'}); - - $httpBackend.whenGET('r1.html').respond('R1'); - $httpBackend.whenGET('r2.html').respond('R2'); - - $location.path('/r1/1'); - $rootScope.$digest(); - expect(log).toBe('$before{};'); - $httpBackend.flush(); - expect(log).toBe('$before{};$after{"id":"1"};'); - - log = ''; - - $location.path('/r2/2'); - $rootScope.$digest(); - expect(log).toBe('$before{"id":"1"};'); - $httpBackend.flush(); - expect(log).toBe('$before{"id":"1"};$after{"id":"2"};'); - }); - }); - - - it('should drop in progress route change when new route change occurs', function() { - module(function($routeProvider) { - $routeProvider. - when('/r1', { templateUrl: 'r1.html' }). - when('/r2', { templateUrl: 'r2.html' }); - }); - - inject(function($route, $httpBackend, $location, $rootScope) { - var log = ''; - $rootScope.$on('$routeChangeStart', function(e, next) { log += '$before(' + next.templateUrl + ');'}); - $rootScope.$on('$routeChangeSuccess', function(e, next) { log += '$after(' + next.templateUrl + ');'}); - - $httpBackend.expectGET('r1.html').respond('R1'); - $httpBackend.expectGET('r2.html').respond('R2'); - - $location.path('/r1'); - $rootScope.$digest(); - expect(log).toBe('$before(r1.html);'); - - $location.path('/r2'); - $rootScope.$digest(); - expect(log).toBe('$before(r1.html);$before(r2.html);'); - - $httpBackend.flush(); - expect(log).toBe('$before(r1.html);$before(r2.html);$after(r2.html);'); - expect(log).not.toContain('$after(r1.html);'); - }); - }); - - - it('should drop in progress route change when new route change occurs and old fails', function() { - module(function($routeProvider) { - $routeProvider. - when('/r1', { templateUrl: 'r1.html' }). - when('/r2', { templateUrl: 'r2.html' }); - }); - - inject(function($route, $httpBackend, $location, $rootScope) { - var log = ''; - $rootScope.$on('$routeChangeError', function(e, next, last, error) { - log += '$failed(' + next.templateUrl + ', ' + error.status + ');'; - }); - $rootScope.$on('$routeChangeStart', function(e, next) { log += '$before(' + next.templateUrl + ');'}); - $rootScope.$on('$routeChangeSuccess', function(e, next) { log += '$after(' + next.templateUrl + ');'}); - - $httpBackend.expectGET('r1.html').respond(404, 'R1'); - $httpBackend.expectGET('r2.html').respond('R2'); - - $location.path('/r1'); - $rootScope.$digest(); - expect(log).toBe('$before(r1.html);'); - - $location.path('/r2'); - $rootScope.$digest(); - expect(log).toBe('$before(r1.html);$before(r2.html);'); - - $httpBackend.flush(); - expect(log).toBe('$before(r1.html);$before(r2.html);$after(r2.html);'); - expect(log).not.toContain('$after(r1.html);'); - }); - }); - - - it('should catch local factory errors', function() { - var myError = new Error('MyError'); - module(function($routeProvider, $exceptionHandlerProvider) { - $exceptionHandlerProvider.mode('log'); - $routeProvider.when('/locals', { - resolve: { - a: function($q) { - throw myError; - } - } - }); - }); - - inject(function($location, $route, $rootScope, $exceptionHandler) { - $location.path('/locals'); - $rootScope.$digest(); - expect($exceptionHandler.errors).toEqual([myError]); - }); - }); - }); - - - it('should match route with and without trailing slash', function() { - module(function($routeProvider){ - $routeProvider.when('/foo', {templateUrl: 'foo.html'}); - $routeProvider.when('/bar/', {templateUrl: 'bar.html'}); - }); - - inject(function($route, $location, $rootScope) { - $location.path('/foo'); - $rootScope.$digest(); - expect($location.path()).toBe('/foo'); - expect($route.current.templateUrl).toBe('foo.html'); - - $location.path('/foo/'); - $rootScope.$digest(); - expect($location.path()).toBe('/foo'); - expect($route.current.templateUrl).toBe('foo.html'); - - $location.path('/bar'); - $rootScope.$digest(); - expect($location.path()).toBe('/bar/'); - expect($route.current.templateUrl).toBe('bar.html'); - - $location.path('/bar/'); - $rootScope.$digest(); - expect($location.path()).toBe('/bar/'); - expect($route.current.templateUrl).toBe('bar.html'); - }); - }); - - - describe('redirection', function() { - it('should support redirection via redirectTo property by updating $location', function() { - module(function($routeProvider) { - $routeProvider.when('/', {redirectTo: '/foo'}); - $routeProvider.when('/foo', {templateUrl: 'foo.html'}); - $routeProvider.when('/bar', {templateUrl: 'bar.html'}); - $routeProvider.when('/baz', {redirectTo: '/bar'}); - $routeProvider.otherwise({templateUrl: '404.html'}); - }); - - inject(function($route, $location, $rootScope) { - var onChangeSpy = jasmine.createSpy('onChange'); - - $rootScope.$on('$routeChangeStart', onChangeSpy); - expect($route.current).toBeUndefined(); - expect(onChangeSpy).not.toHaveBeenCalled(); - - $location.path('/'); - $rootScope.$digest(); - expect($location.path()).toBe('/foo'); - expect($route.current.templateUrl).toBe('foo.html'); - expect(onChangeSpy.callCount).toBe(2); - - onChangeSpy.reset(); - $location.path('/baz'); - $rootScope.$digest(); - expect($location.path()).toBe('/bar'); - expect($route.current.templateUrl).toBe('bar.html'); - expect(onChangeSpy.callCount).toBe(2); - }); - }); - - - it('should interpolate route vars in the redirected path from original path', function() { - module(function($routeProvider) { - $routeProvider.when('/foo/:id/foo/:subid/:extraId', {redirectTo: '/bar/:id/:subid/23'}); - $routeProvider.when('/bar/:id/:subid/:subsubid', {templateUrl: 'bar.html'}); - }); - - inject(function($route, $location, $rootScope) { - $location.path('/foo/id1/foo/subid3/gah'); - $rootScope.$digest(); - - expect($location.path()).toEqual('/bar/id1/subid3/23'); - expect($location.search()).toEqual({extraId: 'gah'}); - expect($route.current.templateUrl).toEqual('bar.html'); - }); - }); - - - it('should interpolate route vars in the redirected path from original search', function() { - module(function($routeProvider) { - $routeProvider.when('/bar/:id/:subid/:subsubid', {templateUrl: 'bar.html'}); - $routeProvider.when('/foo/:id/:extra', {redirectTo: '/bar/:id/:subid/99'}); - }); - - inject(function($route, $location, $rootScope) { - $location.path('/foo/id3/eId').search('subid=sid1&appended=true'); - $rootScope.$digest(); - - expect($location.path()).toEqual('/bar/id3/sid1/99'); - expect($location.search()).toEqual({appended: 'true', extra: 'eId'}); - expect($route.current.templateUrl).toEqual('bar.html'); - }); - }); - - - it('should allow custom redirectTo function to be used', function() { - function customRedirectFn(routePathParams, path, search) { - expect(routePathParams).toEqual({id: 'id3'}); - expect(path).toEqual('/foo/id3'); - expect(search).toEqual({ subid: 'sid1', appended: 'true' }); - return '/custom'; - } - - module(function($routeProvider){ - $routeProvider.when('/bar/:id/:subid/:subsubid', {templateUrl: 'bar.html'}); - $routeProvider.when('/foo/:id', {redirectTo: customRedirectFn}); - }); - - inject(function($route, $location, $rootScope) { - $location.path('/foo/id3').search('subid=sid1&appended=true'); - $rootScope.$digest(); - - expect($location.path()).toEqual('/custom'); - }); - }); - - - it('should replace the url when redirecting', function() { - module(function($routeProvider) { - $routeProvider.when('/bar/:id', {templateUrl: 'bar.html'}); - $routeProvider.when('/foo/:id/:extra', {redirectTo: '/bar/:id'}); - }); - inject(function($browser, $route, $location, $rootScope) { - var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough(); - - $location.path('/foo/id3/eId'); - $rootScope.$digest(); - - expect($location.path()).toEqual('/bar/id3'); - expect($browserUrl.mostRecentCall.args) - .toEqual(['http://server/#/bar/id3?extra=eId', true]); - }); - }); - }); - - - describe('reloadOnSearch', function() { - it('should reload a route when reloadOnSearch is enabled and .search() changes', function() { - var reloaded = jasmine.createSpy('route reload'); - - module(function($routeProvider) { - $routeProvider.when('/foo', {controller: noop}); - }); - - inject(function($route, $location, $rootScope, $routeParams) { - $rootScope.$on('$routeChangeStart', reloaded); - $location.path('/foo'); - $rootScope.$digest(); - expect(reloaded).toHaveBeenCalled(); - expect($routeParams).toEqual({}); - reloaded.reset(); - - // trigger reload - $location.search({foo: 'bar'}); - $rootScope.$digest(); - expect(reloaded).toHaveBeenCalled(); - expect($routeParams).toEqual({foo:'bar'}); - }); - }); - - - it('should not reload a route when reloadOnSearch is disabled and only .search() changes', function() { - var routeChange = jasmine.createSpy('route change'), - routeUpdate = jasmine.createSpy('route update'); - - module(function($routeProvider) { - $routeProvider.when('/foo', {controller: noop, reloadOnSearch: false}); - }); - - inject(function($route, $location, $rootScope) { - $rootScope.$on('$routeChangeStart', routeChange); - $rootScope.$on('$routeChangeSuccess', routeChange); - $rootScope.$on('$routeUpdate', routeUpdate); - - expect(routeChange).not.toHaveBeenCalled(); - - $location.path('/foo'); - $rootScope.$digest(); - expect(routeChange).toHaveBeenCalled(); - expect(routeChange.callCount).toBe(2); - expect(routeUpdate).not.toHaveBeenCalled(); - routeChange.reset(); - - // don't trigger reload - $location.search({foo: 'bar'}); - $rootScope.$digest(); - expect(routeChange).not.toHaveBeenCalled(); - expect(routeUpdate).toHaveBeenCalled(); - }); - }); - - - it('should reload reloadOnSearch route when url differs only in route path param', function() { - var routeChange = jasmine.createSpy('route change'); - - module(function($routeProvider) { - $routeProvider.when('/foo/:fooId', {controller: noop, reloadOnSearch: false}); - }); - - inject(function($route, $location, $rootScope) { - $rootScope.$on('$routeChangeStart', routeChange); - $rootScope.$on('$routeChangeSuccess', routeChange); - - expect(routeChange).not.toHaveBeenCalled(); - - $location.path('/foo/aaa'); - $rootScope.$digest(); - expect(routeChange).toHaveBeenCalled(); - expect(routeChange.callCount).toBe(2); - routeChange.reset(); - - $location.path('/foo/bbb'); - $rootScope.$digest(); - expect(routeChange).toHaveBeenCalled(); - expect(routeChange.callCount).toBe(2); - routeChange.reset(); - - $location.search({foo: 'bar'}); - $rootScope.$digest(); - expect(routeChange).not.toHaveBeenCalled(); - }); - }); - - - it('should update params when reloadOnSearch is disabled and .search() changes', function() { - var routeParamsWatcher = jasmine.createSpy('routeParamsWatcher'); - - module(function($routeProvider) { - $routeProvider.when('/foo', {controller: noop}); - $routeProvider.when('/bar/:barId', {controller: noop, reloadOnSearch: false}); - }); - - inject(function($route, $location, $rootScope, $routeParams) { - $rootScope.$watch(function() { - return $routeParams; - }, function(value) { - routeParamsWatcher(value); - }, true); - - expect(routeParamsWatcher).not.toHaveBeenCalled(); - - $location.path('/foo'); - $rootScope.$digest(); - expect(routeParamsWatcher).toHaveBeenCalledWith({}); - routeParamsWatcher.reset(); - - // trigger reload - $location.search({foo: 'bar'}); - $rootScope.$digest(); - expect(routeParamsWatcher).toHaveBeenCalledWith({foo: 'bar'}); - routeParamsWatcher.reset(); - - $location.path('/bar/123').search({}); - $rootScope.$digest(); - expect(routeParamsWatcher).toHaveBeenCalledWith({barId: '123'}); - routeParamsWatcher.reset(); - - // don't trigger reload - $location.search({foo: 'bar'}); - $rootScope.$digest(); - expect(routeParamsWatcher).toHaveBeenCalledWith({barId: '123', foo: 'bar'}); - }); - }); - - - it('should allow using a function as a template', function() { - var customTemplateWatcher = jasmine.createSpy('customTemplateWatcher'); - - function customTemplateFn(routePathParams) { - customTemplateWatcher(routePathParams); - expect(routePathParams).toEqual({id: 'id3'}); - return '

' + routePathParams.id + '

'; - } - - module(function($routeProvider){ - $routeProvider.when('/bar/:id/:subid/:subsubid', {templateUrl: 'bar.html'}); - $routeProvider.when('/foo/:id', {template: customTemplateFn}); - }); - - inject(function($route, $location, $rootScope) { - $location.path('/foo/id3'); - $rootScope.$digest(); - - expect(customTemplateWatcher).toHaveBeenCalledWith({id: 'id3'}); - }); - }); - - - it('should allow using a function as a templateUrl', function() { - var customTemplateUrlWatcher = jasmine.createSpy('customTemplateUrlWatcher'); - - function customTemplateUrlFn(routePathParams) { - customTemplateUrlWatcher(routePathParams); - expect(routePathParams).toEqual({id: 'id3'}); - return 'foo.html'; - } - - module(function($routeProvider){ - $routeProvider.when('/bar/:id/:subid/:subsubid', {templateUrl: 'bar.html'}); - $routeProvider.when('/foo/:id', {templateUrl: customTemplateUrlFn}); - }); - - inject(function($route, $location, $rootScope) { - $location.path('/foo/id3'); - $rootScope.$digest(); - - expect(customTemplateUrlWatcher).toHaveBeenCalledWith({id: 'id3'}); - expect($route.current.loadedTemplateUrl).toEqual('foo.html'); - }); - }); - - - describe('reload', function() { - - it('should reload even if reloadOnSearch is false', function() { - var routeChangeSpy = jasmine.createSpy('route change'); - - module(function($routeProvider) { - $routeProvider.when('/bar/:barId', {controller: noop, reloadOnSearch: false}); - }); - - inject(function($route, $location, $rootScope, $routeParams) { - $rootScope.$on('$routeChangeSuccess', routeChangeSpy); - - $location.path('/bar/123'); - $rootScope.$digest(); - expect($routeParams).toEqual({barId:'123'}); - expect(routeChangeSpy).toHaveBeenCalledOnce(); - routeChangeSpy.reset(); - - $location.path('/bar/123').search('a=b'); - $rootScope.$digest(); - expect($routeParams).toEqual({barId:'123', a:'b'}); - expect(routeChangeSpy).not.toHaveBeenCalled(); - - $route.reload(); - $rootScope.$digest(); - expect($routeParams).toEqual({barId:'123', a:'b'}); - expect(routeChangeSpy).toHaveBeenCalledOnce(); - }); - }); - }); - }); -}); diff --git a/test/ngRoute/directive/ngViewSpec.js b/test/ngRoute/directive/ngViewSpec.js new file mode 100644 index 00000000..6beb3d27 --- /dev/null +++ b/test/ngRoute/directive/ngViewSpec.js @@ -0,0 +1,669 @@ +'use strict'; + +describe('ngView', function() { + var element; + + beforeEach(module('ngRoute')); + + beforeEach(module(function($provide) { + $provide.value('$window', angular.mock.createMockWindow()); + return function($rootScope, $compile, $animator) { + element = $compile('')($rootScope); + $animator.enabled(true); + }; + })); + + + afterEach(function(){ + dealoc(element); + }); + + + it('should do nothing when no routes are defined', + inject(function($rootScope, $compile, $location) { + $location.path('/unknown'); + $rootScope.$digest(); + expect(element.text()).toEqual(''); + })); + + + it('should instantiate controller after compiling the content', function() { + var log = [], controllerScope, + Ctrl = function($scope) { + controllerScope = $scope; + log.push('ctrl-init'); + }; + + module(function($compileProvider, $routeProvider) { + $compileProvider.directive('compileLog', function() { + return { + compile: function() { + log.push('compile'); + } + }; + }); + + $routeProvider.when('/some', {templateUrl: '/tpl.html', controller: Ctrl}); + }); + + inject(function($route, $rootScope, $templateCache, $location) { + $templateCache.put('/tpl.html', [200, '
partial
', {}]); + $location.path('/some'); + $rootScope.$digest(); + + expect(controllerScope.$parent).toBe($rootScope); + expect(controllerScope).toBe($route.current.scope); + expect(log).toEqual(['compile', 'ctrl-init']); + }); + }); + + + it('should instantiate controller with an alias', function() { + var log = [], controllerScope, + Ctrl = function($scope) { + this.name = 'alias'; + controllerScope = $scope; + }; + + module(function($compileProvider, $routeProvider) { + $routeProvider.when('/some', {templateUrl: '/tpl.html', controller: Ctrl, controllerAs: 'ctrl'}); + }); + + inject(function($route, $rootScope, $templateCache, $location) { + $templateCache.put('/tpl.html', [200, '
', {}]); + $location.path('/some'); + $rootScope.$digest(); + + expect(controllerScope.ctrl.name).toBe('alias'); + }); + }); + + + it('should support string controller declaration', function() { + var MyCtrl = jasmine.createSpy('MyCtrl'); + + module(function($controllerProvider, $routeProvider) { + $controllerProvider.register('MyCtrl', ['$scope', MyCtrl]); + $routeProvider.when('/foo', {controller: 'MyCtrl', templateUrl: '/tpl.html'}); + }); + + inject(function($route, $location, $rootScope, $templateCache) { + $templateCache.put('/tpl.html', [200, '
', {}]); + $location.path('/foo'); + $rootScope.$digest(); + + expect($route.current.controller).toBe('MyCtrl'); + expect(MyCtrl).toHaveBeenCalledWith(element.contents().scope()); + }); + }); + + + it('should load content via xhr when route changes', function() { + module(function($routeProvider) { + $routeProvider.when('/foo', {templateUrl: 'myUrl1'}); + $routeProvider.when('/bar', {templateUrl: 'myUrl2'}); + }); + + inject(function($rootScope, $compile, $httpBackend, $location, $route) { + expect(element.text()).toEqual(''); + + $location.path('/foo'); + $httpBackend.expect('GET', 'myUrl1').respond('
{{1+3}}
'); + $rootScope.$digest(); + $httpBackend.flush(); + expect(element.text()).toEqual('4'); + + $location.path('/bar'); + $httpBackend.expect('GET', 'myUrl2').respond('angular is da best'); + $rootScope.$digest(); + $httpBackend.flush(); + expect(element.text()).toEqual('angular is da best'); + }); + }); + + + it('should use inline content route changes', function() { + module(function($routeProvider) { + $routeProvider.when('/foo', {template: '
{{1+3}}
'}); + $routeProvider.when('/bar', {template: 'angular is da best'}); + $routeProvider.when('/blank', {template: ''}); + }); + + inject(function($rootScope, $compile, $location, $route) { + expect(element.text()).toEqual(''); + + $location.path('/foo'); + $rootScope.$digest(); + expect(element.text()).toEqual('4'); + + $location.path('/bar'); + $rootScope.$digest(); + expect(element.text()).toEqual('angular is da best'); + + $location.path('/blank'); + $rootScope.$digest(); + expect(element.text()).toEqual(''); + }); + }); + + + it('should remove all content when location changes to an unknown route', function() { + module(function($routeProvider) { + $routeProvider.when('/foo', {templateUrl: 'myUrl1'}); + }); + + inject(function($rootScope, $compile, $location, $httpBackend, $route) { + $location.path('/foo'); + $httpBackend.expect('GET', 'myUrl1').respond('
{{1+3}}
'); + $rootScope.$digest(); + $httpBackend.flush(); + expect(element.text()).toEqual('4'); + + $location.path('/unknown'); + $rootScope.$digest(); + expect(element.text()).toEqual(''); + }); + }); + + + it('should chain scopes and propagate evals to the child scope', function() { + module(function($routeProvider) { + $routeProvider.when('/foo', {templateUrl: 'myUrl1'}); + }); + + inject(function($rootScope, $compile, $location, $httpBackend, $route) { + $rootScope.parentVar = 'parent'; + + $location.path('/foo'); + $httpBackend.expect('GET', 'myUrl1').respond('
{{parentVar}}
'); + $rootScope.$digest(); + $httpBackend.flush(); + expect(element.text()).toEqual('parent'); + + $rootScope.parentVar = 'new parent'; + $rootScope.$digest(); + expect(element.text()).toEqual('new parent'); + }); + }); + + + it('should be possible to nest ngView in ngInclude', function() { + + module(function($routeProvider) { + $routeProvider.when('/foo', {templateUrl: 'viewPartial.html'}); + }); + + inject(function($httpBackend, $location, $route, $compile, $rootScope) { + $httpBackend.whenGET('includePartial.html').respond('view: '); + $httpBackend.whenGET('viewPartial.html').respond('content'); + $location.path('/foo'); + + var elm = $compile( + '
' + + 'include: ' + + '
')($rootScope); + $rootScope.$digest(); + $httpBackend.flush(); + + expect(elm.text()).toEqual('include: view: content'); + expect($route.current.templateUrl).toEqual('viewPartial.html'); + dealoc(elm) + }); + }); + + + it('should initialize view template after the view controller was initialized even when ' + + 'templates were cached', function() { + //this is a test for a regression that was introduced by making the ng-view cache sync + function ParentCtrl($scope) { + $scope.log.push('parent'); + } + + module(function($routeProvider) { + $routeProvider.when('/foo', {controller: ParentCtrl, templateUrl: 'viewPartial.html'}); + }); + + + inject(function($rootScope, $compile, $location, $httpBackend, $route) { + $rootScope.log = []; + + $rootScope.ChildCtrl = function($scope) { + $scope.log.push('child'); + }; + + $location.path('/foo'); + $httpBackend.expect('GET', 'viewPartial.html'). + respond('
' + + '
' + + '
'); + $rootScope.$apply(); + $httpBackend.flush(); + + expect($rootScope.log).toEqual(['parent', 'init', 'child']); + + $location.path('/'); + $rootScope.$apply(); + expect($rootScope.log).toEqual(['parent', 'init', 'child']); + + $rootScope.log = []; + $location.path('/foo'); + $rootScope.$apply(); + + expect($rootScope.log).toEqual(['parent', 'init', 'child']); + }); + }); + + + it('should discard pending xhr callbacks if a new route is requested before the current ' + + 'finished loading', function() { + // this is a test for a bad race condition that affected feedback + + module(function($routeProvider) { + $routeProvider.when('/foo', {templateUrl: 'myUrl1'}); + $routeProvider.when('/bar', {templateUrl: 'myUrl2'}); + }); + + inject(function($route, $rootScope, $location, $httpBackend) { + expect(element.text()).toEqual(''); + + $location.path('/foo'); + $httpBackend.expect('GET', 'myUrl1').respond('
{{1+3}}
'); + $rootScope.$digest(); + $location.path('/bar'); + $httpBackend.expect('GET', 'myUrl2').respond('
{{1+1}}
'); + $rootScope.$digest(); + $httpBackend.flush(); // now that we have two requests pending, flush! + + expect(element.text()).toEqual('2'); + }); + }); + + + it('should be async even if served from cache', function() { + module(function($routeProvider) { + $routeProvider.when('/foo', {controller: noop, templateUrl: 'myUrl1'}); + }); + + inject(function($route, $rootScope, $location, $templateCache) { + $templateCache.put('myUrl1', [200, 'my partial', {}]); + $location.path('/foo'); + + var called = 0; + // we want to assert only during first watch + $rootScope.$watch(function() { + if (!called++) expect(element.text()).toBe(''); + }); + + $rootScope.$digest(); + expect(element.text()).toBe('my partial'); + }); + }); + + it('should fire $contentLoaded event when content compiled and linked', function() { + var log = []; + var logger = function(name) { + return function() { + log.push(name); + }; + }; + var Ctrl = function($scope) { + $scope.value = 'bound-value'; + log.push('init-ctrl'); + }; + + module(function($routeProvider) { + $routeProvider.when('/foo', {templateUrl: 'tpl.html', controller: Ctrl}); + }); + + inject(function($templateCache, $rootScope, $location) { + $rootScope.$on('$routeChangeStart', logger('$routeChangeStart')); + $rootScope.$on('$routeChangeSuccess', logger('$routeChangeSuccess')); + $rootScope.$on('$viewContentLoaded', logger('$viewContentLoaded')); + + $templateCache.put('tpl.html', [200, '{{value}}', {}]); + $location.path('/foo'); + $rootScope.$digest(); + + expect(element.text()).toBe('bound-value'); + expect(log).toEqual([ + '$routeChangeStart', 'init-ctrl', '$viewContentLoaded', '$routeChangeSuccess' ]); + }); + }); + + it('should destroy previous scope', function() { + module(function($routeProvider) { + $routeProvider.when('/foo', {templateUrl: 'tpl.html'}); + }); + + inject(function($templateCache, $rootScope, $location) { + $templateCache.put('tpl.html', [200, 'partial', {}]); + + expect($rootScope.$$childHead).toBeNull(); + expect($rootScope.$$childTail).toBeNull(); + + $location.path('/foo'); + $rootScope.$digest(); + + expect(element.text()).toBe('partial'); + expect($rootScope.$$childHead).not.toBeNull(); + expect($rootScope.$$childTail).not.toBeNull(); + + $location.path('/non/existing/route'); + $rootScope.$digest(); + + expect(element.text()).toBe(''); + expect($rootScope.$$childHead).toBeNull(); + expect($rootScope.$$childTail).toBeNull(); + }); + }); + + + it('should destroy previous scope if multiple route changes occur before server responds', + function() { + var log = []; + var createCtrl = function(name) { + return function($scope) { + log.push('init-' + name); + $scope.$on('$destroy', function() {log.push('destroy-' + name);}); + }; + }; + + module(function($routeProvider) { + $routeProvider.when('/one', {templateUrl: 'one.html', controller: createCtrl('ctrl1')}); + $routeProvider.when('/two', {templateUrl: 'two.html', controller: createCtrl('ctrl2')}); + }); + + inject(function($httpBackend, $rootScope, $location) { + $httpBackend.whenGET('one.html').respond('content 1'); + $httpBackend.whenGET('two.html').respond('content 2'); + + $location.path('/one'); + $rootScope.$digest(); + $location.path('/two'); + $rootScope.$digest(); + + $httpBackend.flush(); + expect(element.text()).toBe('content 2'); + expect(log).toEqual(['init-ctrl2']); + + $location.path('/non-existing'); + $rootScope.$digest(); + + expect(element.text()).toBe(''); + expect(log).toEqual(['init-ctrl2', 'destroy-ctrl2']); + + expect($rootScope.$$childHead).toBeNull(); + expect($rootScope.$$childTail).toBeNull(); + }); + }); + + + it('should $destroy scope after update and reload', function() { + // this is a regression of bug, where $route doesn't copy scope when only updating + + var log = []; + + function logger(msg) { + return function() { + log.push(msg); + }; + } + + function createController(name) { + return function($scope) { + log.push('init-' + name); + $scope.$on('$destroy', logger('destroy-' + name)); + $scope.$on('$routeUpdate', logger('route-update')); + }; + } + + module(function($routeProvider) { + $routeProvider.when('/bar', {templateUrl: 'tpl.html', controller: createController('bar')}); + $routeProvider.when('/foo', { + templateUrl: 'tpl.html', controller: createController('foo'), reloadOnSearch: false}); + }); + + inject(function($templateCache, $location, $rootScope) { + $templateCache.put('tpl.html', [200, 'partial', {}]); + + $location.url('/foo'); + $rootScope.$digest(); + expect(log).toEqual(['init-foo']); + + $location.search({q: 'some'}); + $rootScope.$digest(); + expect(log).toEqual(['init-foo', 'route-update']); + + $location.url('/bar'); + $rootScope.$digest(); + expect(log).toEqual(['init-foo', 'route-update', 'destroy-foo', 'init-bar']); + }); + }); + + + it('should evaluate onload expression after linking the content', function() { + module(function($routeProvider) { + $routeProvider.when('/foo', {templateUrl: 'tpl.html'}); + }); + + inject(function($templateCache, $location, $rootScope) { + $templateCache.put('tpl.html', [200, '{{1+1}}', {}]); + $rootScope.load = jasmine.createSpy('onload'); + + $location.url('/foo'); + $rootScope.$digest(); + expect($rootScope.load).toHaveBeenCalledOnce(); + }); + }); + + + it('should set $scope and $controllerController on the view', function() { + function MyCtrl($scope) { + $scope.state = 'WORKS'; + $scope.ctrl = this; + } + + module(function($routeProvider) { + $routeProvider.when('/foo', {templateUrl: 'tpl.html', controller: MyCtrl}); + }); + + inject(function($templateCache, $location, $rootScope, $route) { + $templateCache.put('tpl.html', [200, '
{{state}}
', {}]); + + $location.url('/foo'); + $rootScope.$digest(); + expect(element.text()).toEqual('WORKS'); + + var div = element.find('div'); + expect(div.parent()[0].nodeName.toUpperCase()).toBeOneOf('NG:VIEW', 'VIEW'); + + expect(div.scope()).toBe($route.current.scope); + expect(div.scope().hasOwnProperty('state')).toBe(true); + expect(div.scope().state).toEqual('WORKS'); + + expect(div.controller()).toBe($route.current.scope.ctrl); + }); + }); + + it('should not set $scope or $controllerController on top level text elements in the view', function() { + function MyCtrl($scope) {} + + module(function($routeProvider) { + $routeProvider.when('/foo', {templateUrl: 'tpl.html', controller: MyCtrl}); + }); + + inject(function($templateCache, $location, $rootScope, $route) { + $templateCache.put('tpl.html', '
'); + $location.url('/foo'); + $rootScope.$digest(); + + forEach(element.contents(), function(node) { + if ( node.nodeType == 3 /* text node */) { + expect(jqLite(node).scope()).not.toBe($route.current.scope); + expect(jqLite(node).controller()).not.toBeDefined(); + } else { + expect(jqLite(node).scope()).toBe($route.current.scope); + expect(jqLite(node).controller()).toBeDefined(); + } + }); + }); + }); + + describe('ngAnimate ', function() { + var window, vendorPrefix; + var body, element; + + function html(html) { + body.html(html); + element = body.children().eq(0); + return element; + } + + function applyCSS(element, cssProp, cssValue) { + element.css(cssProp, cssValue); + element.css(vendorPrefix + cssProp, cssValue); + } + + beforeEach(function() { + // we need to run animation on attached elements; + body = jqLite(document.body); + }); + + afterEach(function(){ + dealoc(body); + dealoc(element); + }); + + + beforeEach(module(function($provide, $routeProvider) { + $provide.value('$window', window = angular.mock.createMockWindow()); + $routeProvider.when('/foo', {controller: noop, templateUrl: '/foo.html'}); + return function($sniffer, $templateCache, $animator) { + vendorPrefix = '-' + $sniffer.vendorPrefix + '-'; + $templateCache.put('/foo.html', [200, '
data
', {}]); + $animator.enabled(true); + } + })); + + it('should fire off the enter animation + add and remove the css classes', + inject(function($compile, $rootScope, $sniffer, $location) { + element = $compile(html('
'))($rootScope); + + $location.path('/foo'); + $rootScope.$digest(); + + //if we add the custom css stuff here then it will get picked up before the animation takes place + var child = jqLite(element.children()[0]); + applyCSS(child, 'transition', '1s linear all'); + + if ($sniffer.transitions) { + expect(child.attr('class')).toContain('custom-enter'); + window.setTimeout.expect(1).process(); + + expect(child.attr('class')).toContain('custom-enter-active'); + window.setTimeout.expect(1000).process(); + } else { + expect(window.setTimeout.queue).toEqual([]); + } + + expect(child.attr('class')).not.toContain('custom-enter'); + expect(child.attr('class')).not.toContain('custom-enter-active'); + })); + + it('should fire off the leave animation + add and remove the css classes', + inject(function($compile, $rootScope, $sniffer, $location, $templateCache) { + $templateCache.put('/foo.html', [200, '
foo
', {}]); + element = $compile(html('
'))($rootScope); + + $location.path('/foo'); + $rootScope.$digest(); + + //if we add the custom css stuff here then it will get picked up before the animation takes place + var child = jqLite(element.children()[0]); + applyCSS(child, 'transition', '1s linear all'); + + $location.path('/'); + $rootScope.$digest(); + + if ($sniffer.transitions) { + expect(child.attr('class')).toContain('custom-leave'); + window.setTimeout.expect(1).process(); + + expect(child.attr('class')).toContain('custom-leave-active'); + window.setTimeout.expect(1000).process(); + } else { + expect(window.setTimeout.queue).toEqual([]); + } + + expect(child.attr('class')).not.toContain('custom-leave'); + expect(child.attr('class')).not.toContain('custom-leave-active'); + })); + + it('should catch and use the correct duration for animations', + inject(function($compile, $rootScope, $sniffer, $location, $templateCache) { + $templateCache.put('/foo.html', [200, '
foo
', {}]); + element = $compile(html( + '
' + + '
' + ))($rootScope); + + $location.path('/foo'); + $rootScope.$digest(); + + //if we add the custom css stuff here then it will get picked up before the animation takes place + var child = jqLite(element.children()[0]); + applyCSS(child, 'transition', '0.5s linear all'); + + if($sniffer.transitions) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect($sniffer.transitions ? 500 : 0).process(); + } else { + expect(window.setTimeout.queue).toEqual([]); + } + })); + + + it('should not double compile when route changes', function() { + module(function($routeProvider, $animationProvider, $provide) { + $routeProvider.when('/foo', {template: '
{{i}}
'}); + $routeProvider.when('/bar', {template: '
{{i}}
'}); + $animationProvider.register('my-animation-leave', function() { + return { + start: function(element, done) { + done(); + } + }; + }); + }); + + inject(function($rootScope, $compile, $location, $route, $window, $rootElement, $sniffer) { + element = $compile(html(''))($rootScope); + + $location.path('/foo'); + $rootScope.$digest(); + if ($sniffer.transitions) { + $window.setTimeout.expect(1).process(); + $window.setTimeout.expect(0).process(); + } + expect(element.text()).toEqual('12'); + + $location.path('/bar'); + $rootScope.$digest(); + expect(n(element.text())).toEqual('1234'); + if ($sniffer.transitions) { + $window.setTimeout.expect(1).process(); + $window.setTimeout.expect(1).process(); + } else { + $window.setTimeout.expect(1).process(); + } + expect(element.text()).toEqual('34'); + + function n(text) { + return text.replace(/\r\n/m, '').replace(/\r\n/m, ''); + } + }); + }); + }); +}); diff --git a/test/ngRoute/routeParamsSpec.js b/test/ngRoute/routeParamsSpec.js new file mode 100644 index 00000000..1391151c --- /dev/null +++ b/test/ngRoute/routeParamsSpec.js @@ -0,0 +1,48 @@ +'use strict'; + +describe('$routeParams', function() { + + beforeEach(module('ngRoute')); + + + it('should publish the params into a service', function() { + module(function($routeProvider) { + $routeProvider.when('/foo', {}); + $routeProvider.when('/bar/:barId', {}); + }); + + inject(function($rootScope, $route, $location, $routeParams) { + $location.path('/foo').search('a=b'); + $rootScope.$digest(); + expect($routeParams).toEqual({a:'b'}); + + $location.path('/bar/123').search('x=abc'); + $rootScope.$digest(); + expect($routeParams).toEqual({barId:'123', x:'abc'}); + }); + }); + + it('should correctly extract the params when a param name is part of the route', function() { + module(function($routeProvider) { + $routeProvider.when('/bar/:foo/:bar', {}); + }); + + inject(function($rootScope, $route, $location, $routeParams) { + $location.path('/bar/foovalue/barvalue'); + $rootScope.$digest(); + expect($routeParams).toEqual({bar:'barvalue', foo:'foovalue'}); + }); + }); + + it('should support route params not preceded by slashes', function() { + module(function($routeProvider) { + $routeProvider.when('/bar:barId/foo:fooId/', {}); + }); + + inject(function($rootScope, $route, $location, $routeParams) { + $location.path('/barbarvalue/foofoovalue/'); + $rootScope.$digest(); + expect($routeParams).toEqual({barId: 'barvalue', fooId: 'foovalue'}); + }); + }); +}); diff --git a/test/ngRoute/routeSpec.js b/test/ngRoute/routeSpec.js new file mode 100644 index 00000000..300ca2d7 --- /dev/null +++ b/test/ngRoute/routeSpec.js @@ -0,0 +1,976 @@ +'use strict'; + +describe('$route', function() { + var $httpBackend; + + beforeEach(module('ngRoute')); + + beforeEach(module(function() { + return function(_$httpBackend_) { + $httpBackend = _$httpBackend_; + $httpBackend.when('GET', 'Chapter.html').respond('chapter'); + $httpBackend.when('GET', 'test.html').respond('test'); + $httpBackend.when('GET', 'foo.html').respond('foo'); + $httpBackend.when('GET', 'baz.html').respond('baz'); + $httpBackend.when('GET', 'bar.html').respond('bar'); + $httpBackend.when('GET', '404.html').respond('not found'); + }; + })); + + it('should route and fire change event', function() { + var log = '', + lastRoute, + nextRoute; + + module(function($routeProvider) { + $routeProvider.when('/Book/:book/Chapter/:chapter', + {controller: noop, templateUrl: 'Chapter.html'}); + $routeProvider.when('/Blank', {}); + }); + inject(function($route, $location, $rootScope) { + $rootScope.$on('$routeChangeStart', function(event, next, current) { + log += 'before();'; + expect(current).toBe($route.current); + lastRoute = current; + nextRoute = next; + }); + $rootScope.$on('$routeChangeSuccess', function(event, current, last) { + log += 'after();'; + expect(current).toBe($route.current); + expect(lastRoute).toBe(last); + expect(nextRoute).toBe(current); + }); + + $location.path('/Book/Moby/Chapter/Intro').search('p=123'); + $rootScope.$digest(); + $httpBackend.flush(); + expect(log).toEqual('before();after();'); + expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', p:'123'}); + + log = ''; + $location.path('/Blank').search('ignore'); + $rootScope.$digest(); + expect(log).toEqual('before();after();'); + expect($route.current.params).toEqual({ignore:true}); + + log = ''; + $location.path('/NONE'); + $rootScope.$digest(); + expect(log).toEqual('before();after();'); + expect($route.current).toEqual(null); + }); + }); + + it('should route and fire change event when catch-all params are used', function() { + var log = '', + lastRoute, + nextRoute; + + module(function($routeProvider) { + $routeProvider.when('/Book1/:book/Chapter/:chapter/*highlight/edit', + {controller: noop, templateUrl: 'Chapter.html'}); + $routeProvider.when('/Book2/:book/*highlight/Chapter/:chapter', + {controller: noop, templateUrl: 'Chapter.html'}); + $routeProvider.when('/Blank', {}); + }); + inject(function($route, $location, $rootScope) { + $rootScope.$on('$routeChangeStart', function(event, next, current) { + log += 'before();'; + expect(current).toBe($route.current); + lastRoute = current; + nextRoute = next; + }); + $rootScope.$on('$routeChangeSuccess', function(event, current, last) { + log += 'after();'; + expect(current).toBe($route.current); + expect(lastRoute).toBe(last); + expect(nextRoute).toBe(current); + }); + + $location.path('/Book1/Moby/Chapter/Intro/one/edit').search('p=123'); + $rootScope.$digest(); + $httpBackend.flush(); + expect(log).toEqual('before();after();'); + expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', highlight:'one', p:'123'}); + + log = ''; + $location.path('/Blank').search('ignore'); + $rootScope.$digest(); + expect(log).toEqual('before();after();'); + expect($route.current.params).toEqual({ignore:true}); + + log = ''; + $location.path('/Book1/Moby/Chapter/Intro/one/two/edit').search('p=123'); + $rootScope.$digest(); + expect(log).toEqual('before();after();'); + expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', highlight:'one/two', p:'123'}); + + log = ''; + $location.path('/Book2/Moby/one/two/Chapter/Intro').search('p=123'); + $rootScope.$digest(); + expect(log).toEqual('before();after();'); + expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', highlight:'one/two', p:'123'}); + + log = ''; + $location.path('/NONE'); + $rootScope.$digest(); + expect(log).toEqual('before();after();'); + expect($route.current).toEqual(null); + }); + }); + + + it('should route and fire change event correctly whenever the case insensitive flag is utilized', function() { + var log = '', + lastRoute, + nextRoute; + + module(function($routeProvider) { + $routeProvider.when('/Book1/:book/Chapter/:chapter/*highlight/edit', + {controller: noop, templateUrl: 'Chapter.html', caseInsensitiveMatch: true}); + $routeProvider.when('/Book2/:book/*highlight/Chapter/:chapter', + {controller: noop, templateUrl: 'Chapter.html'}); + $routeProvider.when('/Blank', {}); + }); + inject(function($route, $location, $rootScope) { + $rootScope.$on('$routeChangeStart', function(event, next, current) { + log += 'before();'; + expect(current).toBe($route.current); + lastRoute = current; + nextRoute = next; + }); + $rootScope.$on('$routeChangeSuccess', function(event, current, last) { + log += 'after();'; + expect(current).toBe($route.current); + expect(lastRoute).toBe(last); + expect(nextRoute).toBe(current); + }); + + $location.path('/Book1/Moby/Chapter/Intro/one/edit').search('p=123'); + $rootScope.$digest(); + $httpBackend.flush(); + expect(log).toEqual('before();after();'); + expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', highlight:'one', p:'123'}); + + log = ''; + $location.path('/BOOK1/Moby/CHAPTER/Intro/one/EDIT').search('p=123'); + $rootScope.$digest(); + expect(log).toEqual('before();after();'); + expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', highlight:'one', p:'123'}); + + log = ''; + $location.path('/Blank').search('ignore'); + $rootScope.$digest(); + expect(log).toEqual('before();after();'); + expect($route.current.params).toEqual({ignore:true}); + + log = ''; + $location.path('/BLANK'); + $rootScope.$digest(); + expect(log).toEqual('before();after();'); + expect($route.current).toEqual(null); + + log = ''; + $location.path('/Book2/Moby/one/two/Chapter/Intro').search('p=123'); + $rootScope.$digest(); + expect(log).toEqual('before();after();'); + expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', highlight:'one/two', p:'123'}); + + log = ''; + $location.path('/BOOK2/Moby/one/two/CHAPTER/Intro').search('p=123'); + $rootScope.$digest(); + expect(log).toEqual('before();after();'); + expect($route.current).toEqual(null); + }); + }); + + + it('should not change route when location is canceled', function() { + module(function($routeProvider) { + $routeProvider.when('/somePath', {template: 'some path'}); + }); + inject(function($route, $location, $rootScope, $log) { + $rootScope.$on('$locationChangeStart', function(event) { + $log.info('$locationChangeStart'); + event.preventDefault(); + }); + + $rootScope.$on('$beforeRouteChange', function(event) { + throw new Error('Should not get here'); + }); + + $location.path('/somePath'); + $rootScope.$digest(); + + expect($log.info.logs.shift()).toEqual(['$locationChangeStart']); + }); + }); + + + describe('should match a route that contains special chars in the path', function() { + beforeEach(module(function($routeProvider) { + $routeProvider.when('/$test.23/foo*(bar)/:baz', {templateUrl: 'test.html'}); + })); + + it('matches the full path', inject(function($route, $location, $rootScope) { + $location.path('/test'); + $rootScope.$digest(); + expect($route.current).toBeUndefined(); + })); + + it('matches literal .', inject(function($route, $location, $rootScope) { + $location.path('/$testX23/foo*(bar)/222'); + $rootScope.$digest(); + expect($route.current).toBeUndefined(); + })); + + it('matches literal *', inject(function($route, $location, $rootScope) { + $location.path('/$test.23/foooo(bar)/222'); + $rootScope.$digest(); + expect($route.current).toBeUndefined(); + })); + + it('treats backslashes normally', inject(function($route, $location, $rootScope) { + $location.path('/$test.23/foo*\\(bar)/222'); + $rootScope.$digest(); + expect($route.current).toBeUndefined(); + })); + + it('matches a URL with special chars', inject(function($route, $location, $rootScope) { + $location.path('/$test.23/foo*(bar)/222'); + $rootScope.$digest(); + expect($route.current).toBeDefined(); + })); + }); + + + it('should change route even when only search param changes', function() { + module(function($routeProvider) { + $routeProvider.when('/test', {templateUrl: 'test.html'}); + }); + + inject(function($route, $location, $rootScope) { + var callback = jasmine.createSpy('onRouteChange'); + + $rootScope.$on('$routeChangeStart', callback); + $location.path('/test'); + $rootScope.$digest(); + callback.reset(); + + $location.search({any: true}); + $rootScope.$digest(); + + expect(callback).toHaveBeenCalled(); + }); + }); + + + it('should allow routes to be defined with just templates without controllers', function() { + module(function($routeProvider) { + $routeProvider.when('/foo', {templateUrl: 'foo.html'}); + }); + + inject(function($route, $location, $rootScope) { + var onChangeSpy = jasmine.createSpy('onChange'); + + $rootScope.$on('$routeChangeStart', onChangeSpy); + expect($route.current).toBeUndefined(); + expect(onChangeSpy).not.toHaveBeenCalled(); + + $location.path('/foo'); + $rootScope.$digest(); + + expect($route.current.templateUrl).toEqual('foo.html'); + expect($route.current.controller).toBeUndefined(); + expect(onChangeSpy).toHaveBeenCalled(); + }); + }); + + + it('should chain whens and otherwise', function() { + module(function($routeProvider){ + $routeProvider.when('/foo', {templateUrl: 'foo.html'}). + otherwise({templateUrl: 'bar.html'}). + when('/baz', {templateUrl: 'baz.html'}); + }); + + inject(function($route, $location, $rootScope) { + $rootScope.$digest(); + expect($route.current.templateUrl).toBe('bar.html'); + + $location.url('/baz'); + $rootScope.$digest(); + expect($route.current.templateUrl).toBe('baz.html'); + }); + }); + + + describe('otherwise', function() { + + it('should handle unknown routes with "otherwise" route definition', function() { + function NotFoundCtrl() {} + + module(function($routeProvider){ + $routeProvider.when('/foo', {templateUrl: 'foo.html'}); + $routeProvider.otherwise({templateUrl: '404.html', controller: NotFoundCtrl}); + }); + + inject(function($route, $location, $rootScope) { + var onChangeSpy = jasmine.createSpy('onChange'); + + $rootScope.$on('$routeChangeStart', onChangeSpy); + expect($route.current).toBeUndefined(); + expect(onChangeSpy).not.toHaveBeenCalled(); + + $location.path('/unknownRoute'); + $rootScope.$digest(); + + expect($route.current.templateUrl).toBe('404.html'); + expect($route.current.controller).toBe(NotFoundCtrl); + expect(onChangeSpy).toHaveBeenCalled(); + + onChangeSpy.reset(); + $location.path('/foo'); + $rootScope.$digest(); + + expect($route.current.templateUrl).toEqual('foo.html'); + expect($route.current.controller).toBeUndefined(); + expect(onChangeSpy).toHaveBeenCalled(); + }); + }); + + + it('should update $route.current and $route.next when default route is matched', function() { + module(function($routeProvider){ + $routeProvider.when('/foo', {templateUrl: 'foo.html'}); + $routeProvider.otherwise({templateUrl: '404.html'}); + }); + + inject(function($route, $location, $rootScope) { + var currentRoute, nextRoute, + onChangeSpy = jasmine.createSpy('onChange').andCallFake(function(e, next) { + currentRoute = $route.current; + nextRoute = next; + }); + + + // init + $rootScope.$on('$routeChangeStart', onChangeSpy); + expect($route.current).toBeUndefined(); + expect(onChangeSpy).not.toHaveBeenCalled(); + + + // match otherwise route + $location.path('/unknownRoute'); + $rootScope.$digest(); + + expect(currentRoute).toBeUndefined(); + expect(nextRoute.templateUrl).toBe('404.html'); + expect($route.current.templateUrl).toBe('404.html'); + expect(onChangeSpy).toHaveBeenCalled(); + onChangeSpy.reset(); + + // match regular route + $location.path('/foo'); + $rootScope.$digest(); + + expect(currentRoute.templateUrl).toBe('404.html'); + expect(nextRoute.templateUrl).toBe('foo.html'); + expect($route.current.templateUrl).toEqual('foo.html'); + expect(onChangeSpy).toHaveBeenCalled(); + onChangeSpy.reset(); + + // match otherwise route again + $location.path('/anotherUnknownRoute'); + $rootScope.$digest(); + + expect(currentRoute.templateUrl).toBe('foo.html'); + expect(nextRoute.templateUrl).toBe('404.html'); + expect($route.current.templateUrl).toEqual('404.html'); + expect(onChangeSpy).toHaveBeenCalled(); + }); + }); + }); + + + describe('events', function() { + it('should not fire $after/beforeRouteChange during bootstrap (if no route)', function() { + var routeChangeSpy = jasmine.createSpy('route change'); + + module(function($routeProvider) { + $routeProvider.when('/one', {}); // no otherwise defined + }); + + inject(function($rootScope, $route, $location) { + $rootScope.$on('$routeChangeStart', routeChangeSpy); + $rootScope.$on('$routeChangeSuccess', routeChangeSpy); + + $rootScope.$digest(); + expect(routeChangeSpy).not.toHaveBeenCalled(); + + $location.path('/no-route-here'); + $rootScope.$digest(); + expect(routeChangeSpy).not.toHaveBeenCalled(); + }); + }); + + it('should fire $routeChangeStart and resolve promises', function() { + var deferA, + deferB; + + module(function($provide, $routeProvider) { + $provide.factory('b', function($q) { + deferB = $q.defer(); + return deferB.promise; + }); + $routeProvider.when('/path', { templateUrl: 'foo.html', resolve: { + a: ['$q', function($q) { + deferA = $q.defer(); + return deferA.promise; + }], + b: 'b' + } }); + }); + + inject(function($location, $route, $rootScope, $httpBackend) { + var log = ''; + + $httpBackend.expectGET('foo.html').respond('FOO'); + + $location.path('/path'); + $rootScope.$digest(); + expect(log).toEqual(''); + $httpBackend.flush(); + expect(log).toEqual(''); + deferA.resolve(); + $rootScope.$digest(); + expect(log).toEqual(''); + deferB.resolve(); + $rootScope.$digest(); + expect($route.current.locals.$template).toEqual('FOO'); + }); + }); + + + it('should fire $routeChangeError event on resolution error', function() { + var deferA; + + module(function($provide, $routeProvider) { + $routeProvider.when('/path', { template: 'foo', resolve: { + a: function($q) { + deferA = $q.defer(); + return deferA.promise; + } + } }); + }); + + inject(function($location, $route, $rootScope) { + var log = ''; + + $rootScope.$on('$routeChangeStart', function() { log += 'before();'; }); + $rootScope.$on('$routeChangeError', function(e, n, l, reason) { log += 'failed(' + reason + ');'; }); + + $location.path('/path'); + $rootScope.$digest(); + expect(log).toEqual('before();'); + + deferA.reject('MyError'); + $rootScope.$digest(); + expect(log).toEqual('before();failed(MyError);'); + }); + }); + + + it('should fetch templates', function() { + module(function($routeProvider) { + $routeProvider. + when('/r1', { templateUrl: 'r1.html' }). + when('/r2', { templateUrl: 'r2.html' }); + }); + + inject(function($route, $httpBackend, $location, $rootScope) { + var log = ''; + $rootScope.$on('$routeChangeStart', function(e, next) { log += '$before(' + next.templateUrl + ');'}); + $rootScope.$on('$routeChangeSuccess', function(e, next) { log += '$after(' + next.templateUrl + ');'}); + + $httpBackend.expectGET('r1.html').respond('R1'); + $httpBackend.expectGET('r2.html').respond('R2'); + + $location.path('/r1'); + $rootScope.$digest(); + expect(log).toBe('$before(r1.html);'); + + $location.path('/r2'); + $rootScope.$digest(); + expect(log).toBe('$before(r1.html);$before(r2.html);'); + + $httpBackend.flush(); + expect(log).toBe('$before(r1.html);$before(r2.html);$after(r2.html);'); + expect(log).not.toContain('$after(r1.html);'); + }); + }); + + + it('should not update $routeParams until $routeChangeSuccess', function() { + module(function($routeProvider) { + $routeProvider. + when('/r1/:id', { templateUrl: 'r1.html' }). + when('/r2/:id', { templateUrl: 'r2.html' }); + }); + + inject(function($route, $httpBackend, $location, $rootScope, $routeParams) { + var log = ''; + $rootScope.$on('$routeChangeStart', function(e, next) { log += '$before' + toJson($routeParams) + ';'}); + $rootScope.$on('$routeChangeSuccess', function(e, next) { log += '$after' + toJson($routeParams) + ';'}); + + $httpBackend.whenGET('r1.html').respond('R1'); + $httpBackend.whenGET('r2.html').respond('R2'); + + $location.path('/r1/1'); + $rootScope.$digest(); + expect(log).toBe('$before{};'); + $httpBackend.flush(); + expect(log).toBe('$before{};$after{"id":"1"};'); + + log = ''; + + $location.path('/r2/2'); + $rootScope.$digest(); + expect(log).toBe('$before{"id":"1"};'); + $httpBackend.flush(); + expect(log).toBe('$before{"id":"1"};$after{"id":"2"};'); + }); + }); + + + it('should drop in progress route change when new route change occurs', function() { + module(function($routeProvider) { + $routeProvider. + when('/r1', { templateUrl: 'r1.html' }). + when('/r2', { templateUrl: 'r2.html' }); + }); + + inject(function($route, $httpBackend, $location, $rootScope) { + var log = ''; + $rootScope.$on('$routeChangeStart', function(e, next) { log += '$before(' + next.templateUrl + ');'}); + $rootScope.$on('$routeChangeSuccess', function(e, next) { log += '$after(' + next.templateUrl + ');'}); + + $httpBackend.expectGET('r1.html').respond('R1'); + $httpBackend.expectGET('r2.html').respond('R2'); + + $location.path('/r1'); + $rootScope.$digest(); + expect(log).toBe('$before(r1.html);'); + + $location.path('/r2'); + $rootScope.$digest(); + expect(log).toBe('$before(r1.html);$before(r2.html);'); + + $httpBackend.flush(); + expect(log).toBe('$before(r1.html);$before(r2.html);$after(r2.html);'); + expect(log).not.toContain('$after(r1.html);'); + }); + }); + + + it('should drop in progress route change when new route change occurs and old fails', function() { + module(function($routeProvider) { + $routeProvider. + when('/r1', { templateUrl: 'r1.html' }). + when('/r2', { templateUrl: 'r2.html' }); + }); + + inject(function($route, $httpBackend, $location, $rootScope) { + var log = ''; + $rootScope.$on('$routeChangeError', function(e, next, last, error) { + log += '$failed(' + next.templateUrl + ', ' + error.status + ');'; + }); + $rootScope.$on('$routeChangeStart', function(e, next) { log += '$before(' + next.templateUrl + ');'}); + $rootScope.$on('$routeChangeSuccess', function(e, next) { log += '$after(' + next.templateUrl + ');'}); + + $httpBackend.expectGET('r1.html').respond(404, 'R1'); + $httpBackend.expectGET('r2.html').respond('R2'); + + $location.path('/r1'); + $rootScope.$digest(); + expect(log).toBe('$before(r1.html);'); + + $location.path('/r2'); + $rootScope.$digest(); + expect(log).toBe('$before(r1.html);$before(r2.html);'); + + $httpBackend.flush(); + expect(log).toBe('$before(r1.html);$before(r2.html);$after(r2.html);'); + expect(log).not.toContain('$after(r1.html);'); + }); + }); + + + it('should catch local factory errors', function() { + var myError = new Error('MyError'); + module(function($routeProvider, $exceptionHandlerProvider) { + $exceptionHandlerProvider.mode('log'); + $routeProvider.when('/locals', { + resolve: { + a: function($q) { + throw myError; + } + } + }); + }); + + inject(function($location, $route, $rootScope, $exceptionHandler) { + $location.path('/locals'); + $rootScope.$digest(); + expect($exceptionHandler.errors).toEqual([myError]); + }); + }); + }); + + + it('should match route with and without trailing slash', function() { + module(function($routeProvider){ + $routeProvider.when('/foo', {templateUrl: 'foo.html'}); + $routeProvider.when('/bar/', {templateUrl: 'bar.html'}); + }); + + inject(function($route, $location, $rootScope) { + $location.path('/foo'); + $rootScope.$digest(); + expect($location.path()).toBe('/foo'); + expect($route.current.templateUrl).toBe('foo.html'); + + $location.path('/foo/'); + $rootScope.$digest(); + expect($location.path()).toBe('/foo'); + expect($route.current.templateUrl).toBe('foo.html'); + + $location.path('/bar'); + $rootScope.$digest(); + expect($location.path()).toBe('/bar/'); + expect($route.current.templateUrl).toBe('bar.html'); + + $location.path('/bar/'); + $rootScope.$digest(); + expect($location.path()).toBe('/bar/'); + expect($route.current.templateUrl).toBe('bar.html'); + }); + }); + + + describe('redirection', function() { + it('should support redirection via redirectTo property by updating $location', function() { + module(function($routeProvider) { + $routeProvider.when('/', {redirectTo: '/foo'}); + $routeProvider.when('/foo', {templateUrl: 'foo.html'}); + $routeProvider.when('/bar', {templateUrl: 'bar.html'}); + $routeProvider.when('/baz', {redirectTo: '/bar'}); + $routeProvider.otherwise({templateUrl: '404.html'}); + }); + + inject(function($route, $location, $rootScope) { + var onChangeSpy = jasmine.createSpy('onChange'); + + $rootScope.$on('$routeChangeStart', onChangeSpy); + expect($route.current).toBeUndefined(); + expect(onChangeSpy).not.toHaveBeenCalled(); + + $location.path('/'); + $rootScope.$digest(); + expect($location.path()).toBe('/foo'); + expect($route.current.templateUrl).toBe('foo.html'); + expect(onChangeSpy.callCount).toBe(2); + + onChangeSpy.reset(); + $location.path('/baz'); + $rootScope.$digest(); + expect($location.path()).toBe('/bar'); + expect($route.current.templateUrl).toBe('bar.html'); + expect(onChangeSpy.callCount).toBe(2); + }); + }); + + + it('should interpolate route vars in the redirected path from original path', function() { + module(function($routeProvider) { + $routeProvider.when('/foo/:id/foo/:subid/:extraId', {redirectTo: '/bar/:id/:subid/23'}); + $routeProvider.when('/bar/:id/:subid/:subsubid', {templateUrl: 'bar.html'}); + }); + + inject(function($route, $location, $rootScope) { + $location.path('/foo/id1/foo/subid3/gah'); + $rootScope.$digest(); + + expect($location.path()).toEqual('/bar/id1/subid3/23'); + expect($location.search()).toEqual({extraId: 'gah'}); + expect($route.current.templateUrl).toEqual('bar.html'); + }); + }); + + + it('should interpolate route vars in the redirected path from original search', function() { + module(function($routeProvider) { + $routeProvider.when('/bar/:id/:subid/:subsubid', {templateUrl: 'bar.html'}); + $routeProvider.when('/foo/:id/:extra', {redirectTo: '/bar/:id/:subid/99'}); + }); + + inject(function($route, $location, $rootScope) { + $location.path('/foo/id3/eId').search('subid=sid1&appended=true'); + $rootScope.$digest(); + + expect($location.path()).toEqual('/bar/id3/sid1/99'); + expect($location.search()).toEqual({appended: 'true', extra: 'eId'}); + expect($route.current.templateUrl).toEqual('bar.html'); + }); + }); + + + it('should allow custom redirectTo function to be used', function() { + function customRedirectFn(routePathParams, path, search) { + expect(routePathParams).toEqual({id: 'id3'}); + expect(path).toEqual('/foo/id3'); + expect(search).toEqual({ subid: 'sid1', appended: 'true' }); + return '/custom'; + } + + module(function($routeProvider){ + $routeProvider.when('/bar/:id/:subid/:subsubid', {templateUrl: 'bar.html'}); + $routeProvider.when('/foo/:id', {redirectTo: customRedirectFn}); + }); + + inject(function($route, $location, $rootScope) { + $location.path('/foo/id3').search('subid=sid1&appended=true'); + $rootScope.$digest(); + + expect($location.path()).toEqual('/custom'); + }); + }); + + + it('should replace the url when redirecting', function() { + module(function($routeProvider) { + $routeProvider.when('/bar/:id', {templateUrl: 'bar.html'}); + $routeProvider.when('/foo/:id/:extra', {redirectTo: '/bar/:id'}); + }); + inject(function($browser, $route, $location, $rootScope) { + var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough(); + + $location.path('/foo/id3/eId'); + $rootScope.$digest(); + + expect($location.path()).toEqual('/bar/id3'); + expect($browserUrl.mostRecentCall.args) + .toEqual(['http://server/#/bar/id3?extra=eId', true]); + }); + }); + }); + + + describe('reloadOnSearch', function() { + it('should reload a route when reloadOnSearch is enabled and .search() changes', function() { + var reloaded = jasmine.createSpy('route reload'); + + module(function($routeProvider) { + $routeProvider.when('/foo', {controller: noop}); + }); + + inject(function($route, $location, $rootScope, $routeParams) { + $rootScope.$on('$routeChangeStart', reloaded); + $location.path('/foo'); + $rootScope.$digest(); + expect(reloaded).toHaveBeenCalled(); + expect($routeParams).toEqual({}); + reloaded.reset(); + + // trigger reload + $location.search({foo: 'bar'}); + $rootScope.$digest(); + expect(reloaded).toHaveBeenCalled(); + expect($routeParams).toEqual({foo:'bar'}); + }); + }); + + + it('should not reload a route when reloadOnSearch is disabled and only .search() changes', function() { + var routeChange = jasmine.createSpy('route change'), + routeUpdate = jasmine.createSpy('route update'); + + module(function($routeProvider) { + $routeProvider.when('/foo', {controller: noop, reloadOnSearch: false}); + }); + + inject(function($route, $location, $rootScope) { + $rootScope.$on('$routeChangeStart', routeChange); + $rootScope.$on('$routeChangeSuccess', routeChange); + $rootScope.$on('$routeUpdate', routeUpdate); + + expect(routeChange).not.toHaveBeenCalled(); + + $location.path('/foo'); + $rootScope.$digest(); + expect(routeChange).toHaveBeenCalled(); + expect(routeChange.callCount).toBe(2); + expect(routeUpdate).not.toHaveBeenCalled(); + routeChange.reset(); + + // don't trigger reload + $location.search({foo: 'bar'}); + $rootScope.$digest(); + expect(routeChange).not.toHaveBeenCalled(); + expect(routeUpdate).toHaveBeenCalled(); + }); + }); + + + it('should reload reloadOnSearch route when url differs only in route path param', function() { + var routeChange = jasmine.createSpy('route change'); + + module(function($routeProvider) { + $routeProvider.when('/foo/:fooId', {controller: noop, reloadOnSearch: false}); + }); + + inject(function($route, $location, $rootScope) { + $rootScope.$on('$routeChangeStart', routeChange); + $rootScope.$on('$routeChangeSuccess', routeChange); + + expect(routeChange).not.toHaveBeenCalled(); + + $location.path('/foo/aaa'); + $rootScope.$digest(); + expect(routeChange).toHaveBeenCalled(); + expect(routeChange.callCount).toBe(2); + routeChange.reset(); + + $location.path('/foo/bbb'); + $rootScope.$digest(); + expect(routeChange).toHaveBeenCalled(); + expect(routeChange.callCount).toBe(2); + routeChange.reset(); + + $location.search({foo: 'bar'}); + $rootScope.$digest(); + expect(routeChange).not.toHaveBeenCalled(); + }); + }); + + + it('should update params when reloadOnSearch is disabled and .search() changes', function() { + var routeParamsWatcher = jasmine.createSpy('routeParamsWatcher'); + + module(function($routeProvider) { + $routeProvider.when('/foo', {controller: noop}); + $routeProvider.when('/bar/:barId', {controller: noop, reloadOnSearch: false}); + }); + + inject(function($route, $location, $rootScope, $routeParams) { + $rootScope.$watch(function() { + return $routeParams; + }, function(value) { + routeParamsWatcher(value); + }, true); + + expect(routeParamsWatcher).not.toHaveBeenCalled(); + + $location.path('/foo'); + $rootScope.$digest(); + expect(routeParamsWatcher).toHaveBeenCalledWith({}); + routeParamsWatcher.reset(); + + // trigger reload + $location.search({foo: 'bar'}); + $rootScope.$digest(); + expect(routeParamsWatcher).toHaveBeenCalledWith({foo: 'bar'}); + routeParamsWatcher.reset(); + + $location.path('/bar/123').search({}); + $rootScope.$digest(); + expect(routeParamsWatcher).toHaveBeenCalledWith({barId: '123'}); + routeParamsWatcher.reset(); + + // don't trigger reload + $location.search({foo: 'bar'}); + $rootScope.$digest(); + expect(routeParamsWatcher).toHaveBeenCalledWith({barId: '123', foo: 'bar'}); + }); + }); + + + it('should allow using a function as a template', function() { + var customTemplateWatcher = jasmine.createSpy('customTemplateWatcher'); + + function customTemplateFn(routePathParams) { + customTemplateWatcher(routePathParams); + expect(routePathParams).toEqual({id: 'id3'}); + return '

' + routePathParams.id + '

'; + } + + module(function($routeProvider){ + $routeProvider.when('/bar/:id/:subid/:subsubid', {templateUrl: 'bar.html'}); + $routeProvider.when('/foo/:id', {template: customTemplateFn}); + }); + + inject(function($route, $location, $rootScope) { + $location.path('/foo/id3'); + $rootScope.$digest(); + + expect(customTemplateWatcher).toHaveBeenCalledWith({id: 'id3'}); + }); + }); + + + it('should allow using a function as a templateUrl', function() { + var customTemplateUrlWatcher = jasmine.createSpy('customTemplateUrlWatcher'); + + function customTemplateUrlFn(routePathParams) { + customTemplateUrlWatcher(routePathParams); + expect(routePathParams).toEqual({id: 'id3'}); + return 'foo.html'; + } + + module(function($routeProvider){ + $routeProvider.when('/bar/:id/:subid/:subsubid', {templateUrl: 'bar.html'}); + $routeProvider.when('/foo/:id', {templateUrl: customTemplateUrlFn}); + }); + + inject(function($route, $location, $rootScope) { + $location.path('/foo/id3'); + $rootScope.$digest(); + + expect(customTemplateUrlWatcher).toHaveBeenCalledWith({id: 'id3'}); + expect($route.current.loadedTemplateUrl).toEqual('foo.html'); + }); + }); + + + describe('reload', function() { + + it('should reload even if reloadOnSearch is false', function() { + var routeChangeSpy = jasmine.createSpy('route change'); + + module(function($routeProvider) { + $routeProvider.when('/bar/:barId', {controller: angular.noop, reloadOnSearch: false}); + }); + + inject(function($route, $location, $rootScope, $routeParams) { + $rootScope.$on('$routeChangeSuccess', routeChangeSpy); + + $location.path('/bar/123'); + $rootScope.$digest(); + expect($routeParams).toEqual({barId:'123'}); + expect(routeChangeSpy).toHaveBeenCalledOnce(); + routeChangeSpy.reset(); + + $location.path('/bar/123').search('a=b'); + $rootScope.$digest(); + expect($routeParams).toEqual({barId:'123', a:'b'}); + expect(routeChangeSpy).not.toHaveBeenCalled(); + + $route.reload(); + $rootScope.$digest(); + expect($routeParams).toEqual({barId:'123', a:'b'}); + expect(routeChangeSpy).toHaveBeenCalledOnce(); + }); + }); + }); + }); +}); -- cgit v1.2.3