From 5205a9e65f32e4a98608c17c77a3bdaa372dfe32 Mon Sep 17 00:00:00 2001 From: Brian Ford Date: Fri, 18 Sep 2015 15:53:50 -0700 Subject: [PATCH] refactor(angular_1_router): use directives for route targets BREAKING CHANGE: Previously, route configuration took a controller constructor function as the value of `component` in a route definition: ``` $route.config([ { route: '/', component: MyController } ]) ``` Based on the name of the controller, we used to use a componentMapper service to determine what template to pair with each controller, how to bind the instance to the $scope. To make the 1.x router more semantically alligned with Angular 2, we now route to a directive. Thus a route configuration takes a normalized directive name: ``` $route.config([ { route: '/', component: 'myDirective' } ]) ``` BREAKING CHANGE: In order to avoid name collisions, lifecycle hooks are now prefixed with `$`. Before: ``` MyController.prototype.onActivate = ... ``` After: ``` MyController.prototype.$onActivate = ... ``` Same for `$canActivate` (which now lives on the directive factory function), `$canDeactivate`, `$canReuse`, and `$onDeactivate` hooks. --- modules/angular1_router/build.js | 56 +--- modules/angular1_router/lib/facades.es5 | 20 +- .../angular1_router/src/module_template.js | 66 ++++ modules/angular1_router/src/ng_outlet.js | 280 +++++----------- .../test/component_mapper_spec.js | 77 ----- .../test/controller_introspector_spec.js | 38 --- .../test/directive_introspector_spec.js | 38 +++ .../animation_spec.js} | 58 ++-- .../test/integration/lifecycle_hook_spec.js | 307 +++++++++--------- .../test/integration/navigation_spec.js | 205 +++++------- modules/angular1_router/test/ng_link_spec.js | 121 +++---- modules/angular1_router/test/util.es5.js | 5 - 12 files changed, 547 insertions(+), 724 deletions(-) create mode 100644 modules/angular1_router/src/module_template.js delete mode 100644 modules/angular1_router/test/component_mapper_spec.js delete mode 100644 modules/angular1_router/test/controller_introspector_spec.js create mode 100644 modules/angular1_router/test/directive_introspector_spec.js rename modules/angular1_router/test/{ng_outlet_animation_spec.js => integration/animation_spec.js} (57%) diff --git a/modules/angular1_router/build.js b/modules/angular1_router/build.js index e907bf3547..a2daececab 100644 --- a/modules/angular1_router/build.js +++ b/modules/angular1_router/build.js @@ -22,12 +22,12 @@ var PRELUDE = '(function(){\n'; var POSTLUDE = '\n}());\n'; var FACADES = fs.readFileSync(__dirname + '/lib/facades.es5', 'utf8'); var DIRECTIVES = fs.readFileSync(__dirname + '/src/ng_outlet.js', 'utf8'); +var moduleTemplate = fs.readFileSync(__dirname + '/src/module_template.js', 'utf8'); + function main() { var ES6_SHIM = fs.readFileSync(__dirname + '/../../node_modules/es6-shim/es6-shim.js', 'utf8'); var dir = __dirname + '/../angular2/src/router/'; - var out = ''; - var sharedCode = ''; files.forEach(function (file) { var moduleName = 'router/' + file.replace(/\.ts$/, ''); @@ -35,57 +35,9 @@ function main() { sharedCode += transform(moduleName, fs.readFileSync(dir + file, 'utf8')); }); - out += "angular.module('ngComponentRouter')"; - out += angularFactory('$router', ['$q', '$location', '$$controllerIntrospector', - '$browser', '$rootScope', '$injector'], [ - FACADES, - "var exports = {Injectable: function () {}};", - "var require = function () {return exports;};", - sharedCode, - "var RouteConfig = exports.RouteConfig;", - "angular.annotations = {RouteConfig: RouteConfig, CanActivate: exports.CanActivate};", - "angular.stringifyInstruction = exports.stringifyInstruction;", - "var RouteRegistry = exports.RouteRegistry;", - "var RootRouter = exports.RootRouter;", - //TODO: move this code into a templated JS file - "var registry = new RouteRegistry();", - "var location = new Location();", + var out = moduleTemplate.replace('//{{FACADES}}', FACADES).replace('//{{SHARED_CODE}}', sharedCode); - "$$controllerIntrospector(function (name, constructor) {", - "if (constructor.$canActivate) {", - "constructor.annotations = constructor.annotations || [];", - "constructor.annotations.push(new angular.annotations.CanActivate(function (instruction) {", - "return $injector.invoke(constructor.$canActivate, constructor, {", - "$routeParams: instruction.component ? instruction.component.params : instruction.params", - "});", - "}));", - "}", - "if (constructor.$routeConfig) {", - "constructor.annotations = constructor.annotations || [];", - "constructor.annotations.push(new angular.annotations.RouteConfig(constructor.$routeConfig));", - "}", - "if (constructor.annotations) {", - "constructor.annotations.forEach(function(annotation) {", - "if (annotation instanceof RouteConfig) {", - "annotation.configs.forEach(function (config) {", - "registry.config(constructor, config);", - "});", - "}", - "});", - "}", - "});", - - "var router = new RootRouter(registry, location, new Object());", - "$rootScope.$watch(function () { return $location.path(); }, function (path) {", - "if (router.lastNavigationAttempt !== path) {", - "router.navigateByUrl(path);", - "}", - "});", - - "return router;" - ].join('\n')); - - return PRELUDE + ES6_SHIM + DIRECTIVES + out + POSTLUDE; + return PRELUDE + DIRECTIVES + out + POSTLUDE; } diff --git a/modules/angular1_router/lib/facades.es5 b/modules/angular1_router/lib/facades.es5 index cdac8bfa52..34166736a1 100644 --- a/modules/angular1_router/lib/facades.es5 +++ b/modules/angular1_router/lib/facades.es5 @@ -32,6 +32,10 @@ function isArray(obj) { return Array.isArray(obj); } +function getTypeNameForDebugging (fn) { + return fn.name || 'Root'; +} + var PromiseWrapper = { resolve: function (reason) { return $q.when(reason); @@ -251,8 +255,8 @@ var StringWrapper = { return s.replace(from, replace); }, - startsWith: function(s, start) { - return s.startsWith(start); + startsWith: function(s, start) { + return s.substr(0, start.length) === start; }, replaceAllMapped: function(s, from, cb) { @@ -272,14 +276,18 @@ var StringWrapper = { //TODO: implement? // I think it's too heavy to ask 1.x users to bring in Rx for the router... -function EventEmitter() { - -} +function EventEmitter() {} var BaseException = Error; var ObservableWrapper = { - callNext: function(){} + callNext: function(ob, val) { + ob.fn(val); + }, + + subscribe: function(ob, fn) { + ob.fn = fn; + } }; // TODO: https://github.com/angular/angular.js/blob/master/src/ng/browser.js#L227-L265 diff --git a/modules/angular1_router/src/module_template.js b/modules/angular1_router/src/module_template.js new file mode 100644 index 0000000000..cc94806c5c --- /dev/null +++ b/modules/angular1_router/src/module_template.js @@ -0,0 +1,66 @@ + +angular.module('ngComponentRouter'). + value('$route', null). // can be overloaded with ngRouteShim + factory('$router', ['$q', '$location', '$$directiveIntrospector', '$browser', '$rootScope', '$injector', '$route', routerFactory]); + +function routerFactory($q, $location, $$directiveIntrospector, $browser, $rootScope, $injector) { + + // When this file is processed, the line below is replaced with + // the contents of `../lib/facades.es5`. + //{{FACADES}} + + var exports = {Injectable: function () {}}; + var require = function () {return exports;}; + + // When this file is processed, the line below is replaced with + // the contents of the compiled TypeScript classes. + //{{SHARED_CODE}} + + //TODO: this is a hack to replace the exiting implementation at run-time + exports.getCanActivateHook = function (directiveName) { + var factory = $$directiveIntrospector.getTypeByName(directiveName); + return factory && factory.$canActivate && function (next, prev) { + return $injector.invoke(factory.$canActivate, null, { + $nextInstruction: next, + $prevInstruction: prev + }); + }; + }; + + // This hack removes assertions about the type of the "component" + // property in a route config + exports.assertComponentExists = function () {}; + + angular.stringifyInstruction = exports.stringifyInstruction; + + var RouteRegistry = exports.RouteRegistry; + var RootRouter = exports.RootRouter; + + var registry = new RouteRegistry(); + var location = new Location(); + + $$directiveIntrospector(function (name, factory) { + if (angular.isArray(factory.$routeConfig)) { + factory.$routeConfig.forEach(function (config) { + registry.config(name, config); + }); + } + }); + + // Because Angular 1 has no notion of a root component, we use an object with unique identity + // to represent this. + var ROOT_COMPONENT_OBJECT = new Object(); + + var router = new RootRouter(registry, location, ROOT_COMPONENT_OBJECT); + $rootScope.$watch(function () { return $location.path(); }, function (path) { + if (router.lastNavigationAttempt !== path) { + router.navigateByUrl(path); + } + }); + + router.subscribe(function () { + $rootScope.$broadcast('$routeChangeSuccess', {}); + }); + + return router; +} diff --git a/modules/angular1_router/src/ng_outlet.js b/modules/angular1_router/src/ng_outlet.js index 8a3e790300..535205043c 100644 --- a/modules/angular1_router/src/ng_outlet.js +++ b/modules/angular1_router/src/ng_outlet.js @@ -4,69 +4,63 @@ * A module for adding new a routing system Angular 1. */ angular.module('ngComponentRouter', []) - .factory('$componentMapper', $componentMapperFactory) .directive('ngOutlet', ngOutletDirective) .directive('ngOutlet', ngOutletFillContentDirective) - .directive('ngLink', ngLinkDirective) - .directive('a', anchorLinkDirective); // TODO: make the anchor link feature configurable + .directive('ngLink', ngLinkDirective); /* * A module for inspecting controller constructors */ angular.module('ng') - .provider('$$controllerIntrospector', $$controllerIntrospectorProvider) - .config(controllerProviderDecorator); + .provider('$$directiveIntrospector', $$directiveIntrospectorProvider) + .config(compilerProviderDecorator); /* - * decorates with routing info + * decorates $compileProvider so that we have access to routing metadata */ -function controllerProviderDecorator($controllerProvider, $$controllerIntrospectorProvider) { - var register = $controllerProvider.register; - $controllerProvider.register = function (name, ctrl) { - $$controllerIntrospectorProvider.register(name, ctrl); - return register.apply(this, arguments); +function compilerProviderDecorator($compileProvider, $$directiveIntrospectorProvider) { + var directive = $compileProvider.directive; + $compileProvider.directive = function (name, factory) { + $$directiveIntrospectorProvider.register(name, factory); + return directive.apply(this, arguments); }; } -// TODO: decorate $controller ? + /* * private service that holds route mappings for each controller */ -function $$controllerIntrospectorProvider() { - var controllers = []; - var controllersByName = {}; - var onControllerRegistered = null; +function $$directiveIntrospectorProvider() { + var directiveBuffer = []; + var directiveFactoriesByName = {}; + var onDirectiveRegistered = null; return { - register: function (name, constructor) { - if (angular.isArray(constructor)) { - constructor = constructor[constructor.length - 1]; + register: function (name, factory) { + if (angular.isArray(factory)) { + factory = factory[factory.length - 1]; } - controllersByName[name] = constructor; - constructor.$$controllerName = name; - if (onControllerRegistered) { - onControllerRegistered(name, constructor); + directiveFactoriesByName[name] = factory; + if (onDirectiveRegistered) { + onDirectiveRegistered(name, factory); } else { - controllers.push({name: name, constructor: constructor}); + directiveBuffer.push({name: name, factory: factory}); } }, - $get: ['$componentMapper', function ($componentMapper) { + $get: function () { var fn = function (newOnControllerRegistered) { - onControllerRegistered = function (name, constructor) { - name = $componentMapper.component(name); - return newOnControllerRegistered(name, constructor); - }; - while (controllers.length > 0) { - var rule = controllers.pop(); - onControllerRegistered(rule.name, rule.constructor); + onDirectiveRegistered = newOnControllerRegistered; + while (directiveBuffer.length > 0) { + var directive = directiveBuffer.pop(); + onDirectiveRegistered(directive.name, directive.factory); } }; fn.getTypeByName = function (name) { - return controllersByName[name]; + return directiveFactoriesByName[name]; }; return fn; - }] + } }; } @@ -85,7 +79,7 @@ function $$controllerIntrospectorProvider() { * * The value for the `ngOutlet` attribute is optional. */ -function ngOutletDirective($animate, $q, $router, $componentMapper, $controller, $templateRequest) { +function ngOutletDirective($animate, $q, $router) { var rootRouter = $router; return { @@ -105,10 +99,12 @@ function ngOutletDirective($animate, $q, $router, $componentMapper, $controller, myCtrl = ctrls[1], router = (parentCtrl && parentCtrl.$$router) || rootRouter; + myCtrl.$$currentComponent = null; + var childRouter, + currentController, currentInstruction, currentScope, - currentController, currentElement, previousLeaveAnimation; @@ -136,8 +132,8 @@ function ngOutletDirective($animate, $q, $router, $componentMapper, $controller, var next = $q.when(true); var previousInstruction = currentInstruction; currentInstruction = instruction; - if (currentController.onReuse) { - next = $q.when(currentController.onReuse(currentInstruction, previousInstruction)); + if (currentController && currentController.$onReuse) { + next = $q.when(currentController.$onReuse(currentInstruction, previousInstruction)); } return next; @@ -147,8 +143,8 @@ function ngOutletDirective($animate, $q, $router, $componentMapper, $controller, if (!currentInstruction || currentInstruction.componentType !== nextInstruction.componentType) { result = false; - } else if (currentController.canReuse) { - result = currentController.canReuse(nextInstruction, currentInstruction); + } else if (currentController && currentController.$canReuse) { + result = currentController.$canReuse(nextInstruction, currentInstruction); } else { result = nextInstruction === currentInstruction || angular.equals(nextInstruction.params, currentInstruction.params); @@ -156,60 +152,59 @@ function ngOutletDirective($animate, $q, $router, $componentMapper, $controller, return $q.when(result); }, canDeactivate: function (instruction) { - if (currentInstruction && currentController && currentController.canDeactivate) { - return $q.when(currentController.canDeactivate(instruction, currentInstruction)); + if (currentController && currentController.$canDeactivate) { + return $q.when(currentController.$canDeactivate(instruction, currentInstruction)); } return $q.when(true); }, deactivate: function (instruction) { - if (currentController && currentController.onDeactivate) { - return $q.when(currentController.onDeactivate(instruction, currentInstruction)); + if (currentController && currentController.$onDeactivate) { + return $q.when(currentController.$onDeactivate(instruction, currentInstruction)); } return $q.when(); }, activate: function (instruction) { var previousInstruction = currentInstruction; currentInstruction = instruction; - childRouter = router.childRouter(instruction.componentType); - var controllerConstructor, componentName; - controllerConstructor = instruction.componentType; - componentName = $componentMapper.component(controllerConstructor.$$controllerName); + var componentName = myCtrl.$$componentName = instruction.componentType; - var componentTemplateUrl = $componentMapper.template(componentName); - return $templateRequest(componentTemplateUrl).then(function (templateHtml) { - myCtrl.$$router = childRouter; - myCtrl.$$template = templateHtml; - }).then(function () { - var newScope = scope.$new(); - var locals = { - $scope: newScope, - $router: childRouter, - $routeParams: (instruction.params || {}) - }; + if (typeof componentName != 'string') { + throw new Error('Component is not a string for ' + instruction.urlPath); + } - // todo(shahata): controllerConstructor is not minify friendly - currentController = $controller(controllerConstructor, locals); + myCtrl.$$routeParams = instruction.params; - var clone = $transclude(newScope, function (clone) { - $animate.enter(clone, null, currentElement || $element); - cleanupLastView(); - }); + myCtrl.$$template = '
'; - var controllerAs = $componentMapper.controllerAs(componentName) || componentName; - newScope[controllerAs] = currentController; - currentElement = clone; - currentScope = newScope; + myCtrl.$$router = router.childRouter(instruction.componentType); - if (currentController.onActivate) { - return currentController.onActivate(instruction, previousInstruction); - } + var newScope = scope.$new(); + + var clone = $transclude(newScope, function (clone) { + $animate.enter(clone, null, currentElement || $element); + cleanupLastView(); }); + + + currentElement = clone; + currentScope = newScope; + + // TODO: prefer the other directive retrieving the controller + // by debug mode + currentController = currentElement.children().eq(0).controller(componentName); + + if (currentController && currentController.$onActivate) { + return currentController.$onActivate(instruction, previousInstruction); + } + return $q.when(); } }); } } - +/** + * This directive is responsible for compiling the contents of ng-outlet + */ function ngOutletFillContentDirective($compile) { return { restrict: 'EA', @@ -220,6 +215,15 @@ function ngOutletFillContentDirective($compile) { $element.html(template); var link = $compile($element.contents()); link(scope); + + // TODO: move to primary directive + var componentInstance = scope[ctrl.$$componentName]; + if (componentInstance) { + ctrl.$$currentComponent = componentInstance; + + componentInstance.$router = ctrl.$$router; + componentInstance.$routeParams = ctrl.$$routeParams; + } } }; } @@ -249,7 +253,7 @@ function ngOutletFillContentDirective($compile) { * * ``` */ -function ngLinkDirective($router, $location, $parse) { +function ngLinkDirective($router, $parse) { var rootRouter = $router; return { @@ -264,10 +268,12 @@ function ngLinkDirective($router, $location, $parse) { return; } + var instruction = null; var link = attrs.ngLink || ''; function getLink(params) { - return './' + angular.stringifyInstruction(router.generate(params)); + instruction = router.generate(params); + return './' + angular.stringifyInstruction(instruction); } var routeParamsGetter = $parse(link); @@ -282,128 +288,16 @@ function ngLinkDirective($router, $location, $parse) { elt.attr('href', getLink(params)); }, true); } - } -} - -function anchorLinkDirective($router) { - return { - restrict: 'E', - link: function (scope, element) { - // If the linked element is not an anchor tag anymore, do nothing - if (element[0].nodeName.toLowerCase() !== 'a') { + elt.on('click', function (event) { + if (event.which !== 1 || !instruction) { return; } - // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute. - var hrefAttrName = Object.prototype.toString.call(element.prop('href')) === '[object SVGAnimatedString]' ? - 'xlink:href' : 'href'; - - element.on('click', function (event) { - if (event.which !== 1) { - return; - } - - var href = element.attr(hrefAttrName); - if (href && $router.recognize(href)) { - $router.navigateByUrl(href); - event.preventDefault(); - } - }); - } - }; -} - -/** - * @name $componentMapperFactory - * @description - * - * This lets you configure conventions for what controllers are named and where to load templates from. - * - * The default behavior is to dasherize and serve from `./components`. A component called `myWidget` - * uses a controller named `MyWidgetController` and a template loaded from `./components/my-widget/my-widget.html`. - * - * A component is: - * - a controller - * - a template - * - an optional router - * - * This service makes it easy to group all of them into a single concept. - */ -function $componentMapperFactory() { - - var DEFAULT_SUFFIX = 'Controller'; - - var componentToCtrl = function componentToCtrlDefault(name) { - return name[0].toUpperCase() + name.substr(1) + DEFAULT_SUFFIX; - }; - - var componentToTemplate = function componentToTemplateDefault(name) { - var dashName = dashCase(name); - return './components/' + dashName + '/' + dashName + '.html'; - }; - - var ctrlToComponent = function ctrlToComponentDefault(name) { - return name[0].toLowerCase() + name.substr(1, name.length - DEFAULT_SUFFIX.length - 1); - }; - - var componentToControllerAs = function componentToControllerAsDefault(name) { - return name; - }; - - return { - controllerName: function (name) { - return componentToCtrl(name); - }, - - controllerAs: function (name) { - return componentToControllerAs(name); - }, - - template: function (name) { - return componentToTemplate(name); - }, - - component: function (name) { - return ctrlToComponent(name); - }, - - /** - * @name $componentMapper#setCtrlNameMapping - * @description takes a function for mapping component names to component controller names - */ - setCtrlNameMapping: function (newFn) { - componentToCtrl = newFn; - return this; - }, - - /** - * @name $componentMapper#setCtrlAsMapping - * @description takes a function for mapping component names to controllerAs name in the template - */ - setCtrlAsMapping: function (newFn) { - componentToControllerAs = newFn; - return this; - }, - - /** - * @name $componentMapper#setComponentFromCtrlMapping - * @description takes a function for mapping component controller names to component names - */ - setComponentFromCtrlMapping: function (newFn) { - ctrlToComponent = newFn; - return this; - }, - - /** - * @name $componentMapper#setTemplateMapping - * @description takes a function for mapping component names to component template URLs - */ - setTemplateMapping: function (newFn) { - componentToTemplate = newFn; - return this; - } - }; + $router.navigateByInstruction(instruction); + event.preventDefault(); + }); + } } diff --git a/modules/angular1_router/test/component_mapper_spec.js b/modules/angular1_router/test/component_mapper_spec.js deleted file mode 100644 index 8a8f5538bb..0000000000 --- a/modules/angular1_router/test/component_mapper_spec.js +++ /dev/null @@ -1,77 +0,0 @@ -'use strict'; - -describe('$componentMapper', function () { - var elt, - $compile, - $rootScope, - $router, - $templateCache; - - function Ctrl() { - this.message = 'howdy'; - } - - beforeEach(function() { - module('ng'); - module('ngComponentRouter'); - module(function ($controllerProvider) { - $controllerProvider.register('myComponentController', Ctrl); - }); - }); - - it('should convert a component name to a controller name', inject(function ($componentMapper) { - expect($componentMapper.controllerName('foo')).toBe('FooController'); - })); - - it('should convert a controller name to a component name', inject(function ($componentMapper) { - expect($componentMapper.component('FooController')).toBe('foo'); - })); - - it('should convert a component name to a template URL', inject(function ($componentMapper) { - expect($componentMapper.template('foo')).toBe('./components/foo/foo.html'); - })); - - it('should work with a controller constructor fn and a template url', inject(function ($componentMapper) { - var routes = {}; - $componentMapper.setCtrlNameMapping(function (name) { - return routes[name].controller; - }); - $componentMapper.setTemplateMapping(function (name) { - return routes[name].templateUrl; - }); - $componentMapper.setCtrlAsMapping(function (name) { - return 'ctrl'; - }); - - routes.myComponent = { - controller: Ctrl, - templateUrl: '/foo' - }; - - inject(function(_$compile_, _$rootScope_, _$router_, _$templateCache_) { - $compile = _$compile_; - $rootScope = _$rootScope_; - $router = _$router_; - $templateCache = _$templateCache_; - }); - - $templateCache.put('/foo', [200, '{{ctrl.message}}', {}]); - - compile(''); - - $router.config([ - { path: '/', component: Ctrl } - ]); - - $router.navigateByUrl('/'); - $rootScope.$digest(); - - expect(elt.text()).toBe('howdy'); - })); - - function compile(template) { - elt = $compile('
' + template + '
')($rootScope); - $rootScope.$digest(); - return elt; - } -}); diff --git a/modules/angular1_router/test/controller_introspector_spec.js b/modules/angular1_router/test/controller_introspector_spec.js deleted file mode 100644 index fa3d8e34da..0000000000 --- a/modules/angular1_router/test/controller_introspector_spec.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -describe('$$controllerIntrospector', function () { - - var $controllerProvider; - - beforeEach(function() { - module('ng'); - module('ngComponentRouter'); - module(function(_$controllerProvider_) { - $controllerProvider = _$controllerProvider_; - }); - }); - - it('should call the introspector function whenever a controller is registered', inject(function ($$controllerIntrospector) { - var spy = jasmine.createSpy(); - $$controllerIntrospector(spy); - function Ctrl(){} - $controllerProvider.register('SomeController', Ctrl); - - expect(spy).toHaveBeenCalledWith('some', Ctrl); - })); - - it('should call the introspector function whenever a controller is registered with array annotations', inject(function ($$controllerIntrospector) { - var spy = jasmine.createSpy(); - $$controllerIntrospector(spy); - function Ctrl(foo){} - $controllerProvider.register('SomeController', ['foo', Ctrl]); - - expect(spy).toHaveBeenCalledWith('some', Ctrl); - })); - - it('should retrieve a constructor', inject(function ($$controllerIntrospector) { - function Ctrl(foo){} - $controllerProvider.register('SomeController', ['foo', Ctrl]); - expect($$controllerIntrospector.getTypeByName('SomeController')).toBe(Ctrl); - })); -}); diff --git a/modules/angular1_router/test/directive_introspector_spec.js b/modules/angular1_router/test/directive_introspector_spec.js new file mode 100644 index 0000000000..5126643d66 --- /dev/null +++ b/modules/angular1_router/test/directive_introspector_spec.js @@ -0,0 +1,38 @@ +'use strict'; + +describe('$$directiveIntrospector', function () { + + var $compileProvider; + + beforeEach(function() { + module('ng'); + module('ngComponentRouter'); + module(function(_$compileProvider_) { + $compileProvider = _$compileProvider_; + }); + }); + + it('should call the introspector function whenever a directive factory is registered', inject(function ($$directiveIntrospector) { + var spy = jasmine.createSpy(); + $$directiveIntrospector(spy); + function myDir(){} + $compileProvider.directive('myDir', myDir); + + expect(spy).toHaveBeenCalledWith('myDir', myDir); + })); + + it('should call the introspector function whenever a directive factory is registered with array annotations', inject(function ($$directiveIntrospector) { + var spy = jasmine.createSpy(); + $$directiveIntrospector(spy); + function myDir(){} + $compileProvider.directive('myDir', ['foo', myDir]); + + expect(spy).toHaveBeenCalledWith('myDir', myDir); + })); + + it('should retrieve a factory based on directive name', inject(function ($$directiveIntrospector) { + function myDir(){} + $compileProvider.directive('myDir', ['foo', myDir]); + expect($$directiveIntrospector.getTypeByName('myDir')).toBe(myDir); + })); +}); diff --git a/modules/angular1_router/test/ng_outlet_animation_spec.js b/modules/angular1_router/test/integration/animation_spec.js similarity index 57% rename from modules/angular1_router/test/ng_outlet_animation_spec.js rename to modules/angular1_router/test/integration/animation_spec.js index ad60c74e32..1ad02c6fb9 100644 --- a/modules/angular1_router/test/ng_outlet_animation_spec.js +++ b/modules/angular1_router/test/integration/animation_spec.js @@ -2,35 +2,31 @@ describe('ngOutlet animations', function () { var elt, - $animate, - $compile, - $rootScope, - $router, - $templateCache, - $controllerProvider; - - function UserController($routeParams) { - this.name = $routeParams.name; - } + $animate, + $compile, + $rootScope, + $router, + $compileProvider; beforeEach(function () { + module('ng'); module('ngAnimate'); module('ngAnimateMock'); module('ngComponentRouter'); - module(function (_$controllerProvider_) { - $controllerProvider = _$controllerProvider_; + module(function (_$compileProvider_) { + $compileProvider = _$compileProvider_; }); - inject(function (_$animate_, _$compile_, _$rootScope_, _$router_, _$templateCache_) { + inject(function (_$animate_, _$compile_, _$rootScope_, _$router_) { $animate = _$animate_; $compile = _$compile_; $rootScope = _$rootScope_; $router = _$router_; - $templateCache = _$templateCache_; }); - put('user', '
hello {{user.name}}
'); - $controllerProvider.register('UserController', UserController); + registerComponent('userCmp', { + template: '
hello {{userCmp.$routeParams.name}}
' + }); }); afterEach(function () { @@ -43,7 +39,7 @@ describe('ngOutlet animations', function () { compile('
'); $router.config([ - { path: '/user/:name', component: UserController } + { path: '/user/:name', component: 'userCmp' } ]); $router.navigateByUrl('/user/brian'); @@ -70,8 +66,32 @@ describe('ngOutlet animations', function () { expect(item.element.text()).toBe('hello brian'); }); - function put(name, template) { - $templateCache.put(componentTemplatePath(name), [200, template, {}]); + + function registerComponent(name, options) { + var controller = options.controller || function () {}; + + ['$onActivate', '$onDeactivate', '$onReuse', '$canReuse', '$canDeactivate'].forEach(function (hookName) { + if (options[hookName]) { + controller.prototype[hookName] = options[hookName]; + } + }); + + function factory() { + return { + template: options.template || '', + controllerAs: name, + controller: controller + }; + } + + if (options.$canActivate) { + factory.$canActivate = options.$canActivate; + } + if (options.$routeConfig) { + factory.$routeConfig = options.$routeConfig; + } + + $compileProvider.directive(name, factory); } function compile(template) { diff --git a/modules/angular1_router/test/integration/lifecycle_hook_spec.js b/modules/angular1_router/test/integration/lifecycle_hook_spec.js index 3470d54b1b..61ee0d531e 100644 --- a/modules/angular1_router/test/integration/lifecycle_hook_spec.js +++ b/modules/angular1_router/test/integration/lifecycle_hook_spec.js @@ -1,53 +1,45 @@ 'use strict'; -describe('ngOutlet', function () { - +describe('Navigation lifecycle', function () { var elt, - $compile, - $rootScope, - $router, - $templateCache, - $controllerProvider, - $componentMapperProvider; - - var OneController, TwoController, UserController; - - function instructionFor(componentType) { - return jasmine.objectContaining({componentType: componentType}); - } - + $compile, + $rootScope, + $router, + $compileProvider; beforeEach(function () { module('ng'); module('ngComponentRouter'); - module(function (_$controllerProvider_, _$componentMapperProvider_) { - $controllerProvider = _$controllerProvider_; - $componentMapperProvider = _$componentMapperProvider_; + module(function (_$compileProvider_) { + $compileProvider = _$compileProvider_; }); - inject(function (_$compile_, _$rootScope_, _$router_, _$templateCache_) { + inject(function (_$compile_, _$rootScope_, _$router_) { $compile = _$compile_; $rootScope = _$rootScope_; $router = _$router_; - $templateCache = _$templateCache_; }); - UserController = registerComponent('user', '
hello {{user.name}}
', function ($routeParams) { - this.name = $routeParams.name; + registerComponent('oneCmp', { + template: '
{{oneCmp.number}}
', + controller: function () {this.number = 'one'} + }); + registerComponent('twoCmp', { + template: '
{{twoCmp.number}}
', + controller: function () {this.number = 'two'} }); - OneController = registerComponent('one', '
{{one.number}}
', boringController('number', 'one')); - TwoController = registerComponent('two', '
{{two.number}}
', boringController('number', 'two')); }); it('should run the activate hook of controllers', function () { var spy = jasmine.createSpy('activate'); - var activate = registerComponent('activate', '', { - onActivate: spy + registerComponent('activateCmp', { + template: '

hello

', + $onActivate: spy }); $router.config([ - { path: '/a', component: activate } + { path: '/a', component: 'activateCmp' } ]); compile('
outer {
}
'); @@ -60,31 +52,32 @@ describe('ngOutlet', function () { it('should pass instruction into the activate hook of a controller', function () { var spy = jasmine.createSpy('activate'); - var UserController = registerComponent('user', '', { - onActivate: spy + registerComponent('userCmp', { + $onActivate: spy }); $router.config([ - { path: '/user/:name', component: UserController } + { path: '/user/:name', component: 'userCmp' } ]); compile('
'); $router.navigateByUrl('/user/brian'); $rootScope.$digest(); - expect(spy).toHaveBeenCalledWith(instructionFor(UserController), undefined); + expect(spy).toHaveBeenCalledWith(instructionFor('userCmp'), undefined); }); it('should pass previous instruction into the activate hook of a controller', function () { var spy = jasmine.createSpy('activate'); - var activate = registerComponent('activate', '', { - onActivate: spy + var activate = registerComponent('activateCmp', { + template: 'hi', + $onActivate: spy }); $router.config([ - { path: '/user/:name', component: OneController }, - { path: '/post/:id', component: activate } + { path: '/user/:name', component: 'oneCmp' }, + { path: '/post/:id', component: 'activateCmp' } ]); compile('
'); @@ -92,19 +85,21 @@ describe('ngOutlet', function () { $rootScope.$digest(); $router.navigateByUrl('/post/123'); $rootScope.$digest(); - expect(spy).toHaveBeenCalledWith(instructionFor(activate), - instructionFor(OneController)); + expect(spy).toHaveBeenCalledWith(instructionFor('activateCmp'), + instructionFor('oneCmp')); }); it('should inject $scope into the controller constructor', function () { - var injectedScope; - var UserController = registerComponent('user', '', function ($scope) { - injectedScope = $scope; + registerComponent('userCmp', { + template: '', + controller: function ($scope) { + injectedScope = $scope; + } }); $router.config([ - { path: '/user', component: UserController } + { path: '/user', component: 'userCmp' } ]); compile('
'); @@ -117,13 +112,13 @@ describe('ngOutlet', function () { it('should run the deactivate hook of controllers', function () { var spy = jasmine.createSpy('deactivate'); - var deactivate = registerComponent('deactivate', '', { - onDeactivate: spy + registerComponent('deactivateCmp', { + $onDeactivate: spy }); $router.config([ - { path: '/a', component: deactivate }, - { path: '/b', component: OneController } + { path: '/a', component: 'deactivateCmp' }, + { path: '/b', component: 'oneCmp' } ]); compile('
'); @@ -137,13 +132,13 @@ describe('ngOutlet', function () { it('should pass instructions into the deactivate hook of controllers', function () { var spy = jasmine.createSpy('deactivate'); - var deactivate = registerComponent('deactivate', '', { - onDeactivate: spy + registerComponent('deactivateCmp', { + $onDeactivate: spy }); $router.config([ - { path: '/user/:name', component: deactivate }, - { path: '/post/:id', component: OneController } + { path: '/user/:name', component: 'deactivateCmp' }, + { path: '/post/:id', component: 'oneCmp' } ]); compile('
'); @@ -151,29 +146,29 @@ describe('ngOutlet', function () { $rootScope.$digest(); $router.navigateByUrl('/post/123'); $rootScope.$digest(); - expect(spy).toHaveBeenCalledWith(instructionFor(OneController), - instructionFor(deactivate)); + expect(spy).toHaveBeenCalledWith(instructionFor('oneCmp'), + instructionFor('deactivateCmp')); }); it('should run the deactivate hook before the activate hook', function () { var log = []; - var activate = registerComponent('activate', '', { - onActivate: function () { + registerComponent('activateCmp', { + $onActivate: function () { log.push('activate'); } }); - var deactivate = registerComponent('deactivate', '', { - onDeactivate: function () { + registerComponent('deactivateCmp', { + $onDeactivate: function () { log.push('deactivate'); } }); $router.config([ - { path: '/a', component: deactivate }, - { path: '/b', component: activate } + { path: '/a', component: 'deactivateCmp' }, + { path: '/b', component: 'activateCmp' } ]); compile('outer {
}'); @@ -185,25 +180,32 @@ describe('ngOutlet', function () { expect(log).toEqual(['deactivate', 'activate']); }); - it('should reuse a component when the canReuse hook returns true', function () { var log = []; var cmpInstanceCount = 0; function ReuseCmp() { cmpInstanceCount++; - this.canReuse = function () { - return true; - }; - this.onReuse = function (next, prev) { - log.push('reuse: ' + prev.urlPath + ' -> ' + next.urlPath); - }; } - ReuseCmp.$routeConfig = [{path: '/a', component: OneController}, {path: '/b', component: TwoController}]; - registerComponent('reuse', 'reuse {}', ReuseCmp); + + registerComponent('reuseCmp', { + template: 'reuse {}', + $routeConfig: [ + {path: '/a', component: 'oneCmp'}, + {path: '/b', component: 'twoCmp'} + ], + controller: ReuseCmp, + $canReuse: function () { + return true; + }, + $onReuse: function (next, prev) { + log.push('reuse: ' + prev.urlPath + ' -> ' + next.urlPath); + } + }); $router.config([ - { path: '/on-reuse/:number/...', component: ReuseCmp } + { path: '/on-reuse/:number/...', component: 'reuseCmp' }, + { path: '/two', component: 'twoCmp', as: 'Two'} ]); compile('outer {
}'); @@ -227,18 +229,25 @@ describe('ngOutlet', function () { function NeverReuseCmp() { cmpInstanceCount++; - this.canReuse = function () { - return false; - }; - this.onReuse = function (next, prev) { - log.push('reuse: ' + prev.urlPath + ' -> ' + next.urlPath); - }; } - NeverReuseCmp.$routeConfig = [{path: '/a', component: OneController}, {path: '/b', component: TwoController}]; - registerComponent('reuse', 'reuse {}', NeverReuseCmp); + registerComponent('reuseCmp', { + template: 'reuse {}', + $routeConfig: [ + {path: '/a', component: 'oneCmp'}, + {path: '/b', component: 'twoCmp'} + ], + controller: NeverReuseCmp, + $canReuse: function () { + return false; + }, + $onReuse: function (next, prev) { + log.push('reuse: ' + prev.urlPath + ' -> ' + next.urlPath); + } + }); $router.config([ - { path: '/never-reuse/:number/...', component: NeverReuseCmp } + { path: '/never-reuse/:number/...', component: 'reuseCmp' }, + { path: '/two', component: 'twoCmp', as: 'Two'} ]); compile('outer {
}'); @@ -256,17 +265,17 @@ describe('ngOutlet', function () { }); + // TODO: need to solve getting ahold of canActivate hook it('should not activate a component when canActivate returns false', function () { + var canActivateSpy = jasmine.createSpy('canActivate').and.returnValue(false); var spy = jasmine.createSpy('activate'); - var activate = registerComponent('activate', '', { - canActivate: function () { - return false; - }, - onActivate: spy + registerComponent('activateCmp', { + $canActivate: canActivateSpy, + $onActivate: spy }); $router.config([ - { path: '/a', component: activate } + { path: '/a', component: 'activateCmp' } ]); compile('outer {
}'); @@ -279,38 +288,40 @@ describe('ngOutlet', function () { it('should activate a component when canActivate returns true', function () { - var spy = jasmine.createSpy('activate'); - var activate = registerComponent('activate', 'hi', { - canActivate: function () { - return true; - }, - onActivate: spy + var activateSpy = jasmine.createSpy('activate'); + var canActivateSpy = jasmine.createSpy('canActivate').and.returnValue(true); + registerComponent('activateCmp', { + template: 'hi', + $canActivate: canActivateSpy, + $onActivate: activateSpy }); $router.config([ - { path: '/a', component: activate } + { path: '/a', component: 'activateCmp' } ]); compile('
'); $router.navigateByUrl('/a'); $rootScope.$digest(); - expect(spy).toHaveBeenCalled(); + expect(canActivateSpy).toHaveBeenCalled(); + expect(activateSpy).toHaveBeenCalled(); expect(elt.text()).toBe('hi'); }); it('should activate a component when canActivate returns a resolved promise', inject(function ($q) { var spy = jasmine.createSpy('activate'); - var activate = registerComponent('activate', 'hi', { - canActivate: function () { + registerComponent('activateCmp', { + template: 'hi', + $canActivate: function () { return $q.when(true); }, - onActivate: spy + $onActivate: spy }); $router.config([ - { path: '/a', component: activate } + { path: '/a', component: 'activateCmp' } ]); compile('
'); @@ -324,33 +335,38 @@ describe('ngOutlet', function () { it('should inject into the canActivate hook of controllers', inject(function ($http) { var spy = jasmine.createSpy('canActivate').and.returnValue(true); - var activate = registerComponent('activate', '', { - canActivate: spy + registerComponent('activateCmp', { + $canActivate: spy }); - spy.$inject = ['$routeParams', '$http']; + spy.$inject = ['$nextInstruction', '$http']; $router.config([ - { path: '/user/:name', component: activate } + { path: '/user/:name', component: 'activateCmp' } ]); compile('
'); $router.navigateByUrl('/user/brian'); $rootScope.$digest(); - expect(spy).toHaveBeenCalledWith({name: 'brian'}, $http); + + expect(spy).toHaveBeenCalled(); + var args = spy.calls.mostRecent().args; + expect(args[0].params).toEqual({name: 'brian'}); + expect(args[1]).toBe($http); })); it('should not navigate when canDeactivate returns false', function () { - var activate = registerComponent('activate', 'hi', { - canDeactivate: function () { + registerComponent('activateCmp', { + template: 'hi', + $canDeactivate: function () { return false; } }); $router.config([ - { path: '/a', component: activate }, - { path: '/b', component: OneController } + { path: '/a', component: 'activateCmp' }, + { path: '/b', component: 'oneCmp' } ]); compile('outer {
}'); @@ -365,15 +381,16 @@ describe('ngOutlet', function () { it('should navigate when canDeactivate returns true', function () { - var activate = registerComponent('activate', 'hi', { - canDeactivate: function () { + registerComponent('activateCmp', { + template: 'hi', + $canDeactivate: function () { return true; } }); $router.config([ - { path: '/a', component: activate }, - { path: '/b', component: OneController } + { path: '/a', component: 'activateCmp' }, + { path: '/b', component: 'oneCmp' } ]); compile('outer {
}'); @@ -389,15 +406,16 @@ describe('ngOutlet', function () { it('should activate a component when canActivate returns true', function () { var spy = jasmine.createSpy('activate'); - var activate = registerComponent('activate', 'hi', { - canActivate: function () { + registerComponent('activateCmp', { + template: 'hi', + $canActivate: function () { return true; }, - onActivate: spy + $onActivate: spy }); $router.config([ - { path: '/a', component: activate } + { path: '/a', component: 'activateCmp' } ]); compile('
'); @@ -411,13 +429,13 @@ describe('ngOutlet', function () { it('should pass instructions into the canDeactivate hook of controllers', function () { var spy = jasmine.createSpy('canDeactivate').and.returnValue(true); - var deactivate = registerComponent('deactivate', '', { - canDeactivate: spy + registerComponent('deactivateCmp', { + $canDeactivate: spy }); $router.config([ - { path: '/user/:name', component: deactivate }, - { path: '/post/:id', component: OneController } + { path: '/user/:name', component: 'deactivateCmp' }, + { path: '/post/:id', component: 'oneCmp' } ]); compile('
'); @@ -425,43 +443,36 @@ describe('ngOutlet', function () { $rootScope.$digest(); $router.navigateByUrl('/post/123'); $rootScope.$digest(); - expect(spy).toHaveBeenCalledWith(instructionFor(OneController), - instructionFor(deactivate)); + expect(spy).toHaveBeenCalledWith(instructionFor('oneCmp'), + instructionFor('deactivateCmp')); }); - function registerComponent(name, template, config) { - var Ctrl; - if (!template) { - template = ''; - } - if (!config) { - Ctrl = function () {}; - } else if (angular.isArray(config)) { - Ctrl = function () {}; - Ctrl.annotations = [new angular.annotations.RouteConfig(config)]; - } else if (typeof config === 'function') { - Ctrl = config; - } else { - Ctrl = function () {}; - if (config.canActivate) { - Ctrl.$canActivate = config.canActivate; - delete config.canActivate; + + function registerComponent(name, options) { + var controller = options.controller || function () {}; + + ['$onActivate', '$onDeactivate', '$onReuse', '$canReuse', '$canDeactivate'].forEach(function (hookName) { + if (options[hookName]) { + controller.prototype[hookName] = options[hookName]; } - Ctrl.prototype = config; + }); + + function factory() { + return { + template: options.template || '', + controllerAs: name, + controller: controller + }; } - $controllerProvider.register(componentControllerName(name), Ctrl); - put(name, template); - return Ctrl; - } - function boringController(model, value) { - return function () { - this[model] = value; - }; - } + if (options.$canActivate) { + factory.$canActivate = options.$canActivate; + } + if (options.$routeConfig) { + factory.$routeConfig = options.$routeConfig; + } - function put(name, template) { - $templateCache.put(componentTemplatePath(name), [200, template, {}]); + $compileProvider.directive(name, factory); } function compile(template) { @@ -469,4 +480,8 @@ describe('ngOutlet', function () { $rootScope.$digest(); return elt; } + + function instructionFor(componentType) { + return jasmine.objectContaining({componentType: componentType}); + } }); diff --git a/modules/angular1_router/test/integration/navigation_spec.js b/modules/angular1_router/test/integration/navigation_spec.js index 51632a11eb..1a82f197c6 100644 --- a/modules/angular1_router/test/integration/navigation_spec.js +++ b/modules/angular1_router/test/integration/navigation_spec.js @@ -6,41 +6,39 @@ describe('navigation', function () { $compile, $rootScope, $router, - $templateCache, - $controllerProvider, - $componentMapperProvider; - - var OneController, TwoController, UserController; - + $compileProvider; beforeEach(function () { module('ng'); module('ngComponentRouter'); - module(function (_$controllerProvider_, _$componentMapperProvider_) { - $controllerProvider = _$controllerProvider_; - $componentMapperProvider = _$componentMapperProvider_; + module(function (_$compileProvider_) { + $compileProvider = _$compileProvider_; }); - inject(function (_$compile_, _$rootScope_, _$router_, _$templateCache_) { + inject(function (_$compile_, _$rootScope_, _$router_) { $compile = _$compile_; $rootScope = _$rootScope_; $router = _$router_; - $templateCache = _$templateCache_; }); - UserController = registerComponent('user', '
hello {{user.name}}
', function ($routeParams) { - this.name = $routeParams.name; + registerComponent('userCmp', { + template: '
hello {{userCmp.$routeParams.name}}
' + }); + registerComponent('oneCmp', { + template: '
{{oneCmp.number}}
', + controller: function () {this.number = 'one'} + }); + registerComponent('twoCmp', { + template: '
{{twoCmp.number}}
', + controller: function () {this.number = 'two'} }); - OneController = registerComponent('one', '
{{one.number}}
', boringController('number', 'one')); - TwoController = registerComponent('two', '
{{two.number}}
', boringController('number', 'two')); }); - it('should work in a simple case', function () { compile(''); $router.config([ - { path: '/', component: OneController } + { path: '/', component: 'oneCmp' } ]); $router.navigateByUrl('/'); @@ -49,26 +47,9 @@ describe('navigation', function () { expect(elt.text()).toBe('one'); }); - - // See https://github.com/angular/router/issues/105 - xit('should warn when instantiating a component with no controller', function () { - put('noController', '
{{ 2 + 2 }}
'); - $router.config([ - { path: '/', component: 'noController' } - ]); - - spyOn(console, 'warn'); - compile(''); - $router.navigateByUrl('/'); - - expect(console.warn).toHaveBeenCalledWith('Could not find controller for', 'NoControllerController'); - expect(elt.text()).toBe('4'); - }); - - it('should navigate between components with different parameters', function () { $router.config([ - { path: '/user/:name', component: UserController } + { path: '/user/:name', component: 'userCmp' } ]); compile(''); @@ -82,42 +63,46 @@ describe('navigation', function () { }); - it('should not reactivate a parent when navigating between child components with different parameters', function () { - var spy = jasmine.createSpy('onActivate'); - function ParentController() {} - ParentController.$routeConfig = [ - { path: '/user/:name', component: UserController } - ]; - ParentController.prototype.onActivate = spy; - - registerComponent('parent', 'parent { }', ParentController); + it('should reuse a parent when navigating between child components with different parameters', function () { + var instanceCount = 0; + function ParentController() { + instanceCount += 1; + } + registerComponent('parentCmp', { + template: 'parent { }', + $routeConfig: [ + { path: '/user/:name', component: 'userCmp' } + ], + controller: ParentController + }); $router.config([ - { path: '/parent/...', component: ParentController } + { path: '/parent/...', component: 'parentCmp' } ]); compile(''); $router.navigateByUrl('/parent/user/brian'); $rootScope.$digest(); - expect(spy).toHaveBeenCalled(); + expect(instanceCount).toBe(1); expect(elt.text()).toBe('parent { hello brian }'); - spy.calls.reset(); - $router.navigateByUrl('/parent/user/igor'); $rootScope.$digest(); - expect(spy).not.toHaveBeenCalled(); + expect(instanceCount).toBe(1); expect(elt.text()).toBe('parent { hello igor }'); }); it('should work with nested outlets', function () { - var childComponent = registerComponent('childComponent', '
inner {
}
', [ - { path: '/b', component: OneController } - ]); + registerComponent('childCmp', { + template: '
inner {
}
', + $routeConfig: [ + { path: '/b', component: 'oneCmp' } + ] + }); $router.config([ - { path: '/a/...', component: childComponent } + { path: '/a/...', component: 'childCmp' } ]); compile('
outer {
}
'); @@ -128,40 +113,30 @@ describe('navigation', function () { }); - it('should work with recursive nested outlets', function () { - put('two', '
recur {
}
'); + // TODO: fix this + xit('should work with recursive nested outlets', function () { + registerComponent('recurCmp', { + template: '
recur {
}
', + $routeConfig: [ + { path: '/recur', component: 'recurCmp' }, + { path: '/end', component: 'oneCmp' } + ]}); + $router.config([ - { path: '/recur', component: TwoController }, - { path: '/', component: OneController } + { path: '/recur', component: 'recurCmp' }, + { path: '/', component: 'oneCmp' } ]); compile('
root {
}
'); - $router.navigateByUrl('/'); + $router.navigateByUrl('/recur/recur/end'); $rootScope.$digest(); expect(elt.text()).toBe('root { one }'); }); - it('should inject $scope into the controller constructor', function () { - var injectedScope; - var UserController = registerComponent('user', '', function ($scope) { - injectedScope = $scope; - }); - - $router.config([ - { path: '/user', component: UserController } - ]); - compile('
'); - - $router.navigateByUrl('/user'); - $rootScope.$digest(); - - expect(injectedScope).toBeDefined(); - }); - it('should change location path', inject(function ($location) { $router.config([ - { path: '/user', component: UserController } + { path: '/user', component: 'userCmp' } ]); compile('
'); @@ -178,7 +153,7 @@ describe('navigation', function () { $router.config([ { path: '/', redirectTo: '/user' }, - { path: '/user', component: UserController } + { path: '/user', component: 'userCmp' } ]); $router.navigateByUrl('/'); @@ -189,16 +164,19 @@ describe('navigation', function () { it('should change location to the canonical route with nested components', inject(function ($location) { - var childRouter = registerComponent('childRouter', '
inner {
}
', [ - { path: '/old-child', redirectTo: '/new-child' }, - { path: '/new-child', component: OneController}, - { path: '/old-child-two', redirectTo: '/new-child-two' }, - { path: '/new-child-two', component: TwoController} - ]); + registerComponent('childRouter', { + template: '
inner {
}
', + $routeConfig: [ + { path: '/old-child', redirectTo: '/new-child' }, + { path: '/new-child', component: 'oneCmp'}, + { path: '/old-child-two', redirectTo: '/new-child-two' }, + { path: '/new-child-two', component: 'twoCmp'} + ] + }); $router.config([ { path: '/old-parent', redirectTo: '/new-parent' }, - { path: '/new-parent/...', component: childRouter } + { path: '/new-parent/...', component: 'childRouter' } ]); compile('
'); @@ -219,7 +197,7 @@ describe('navigation', function () { it('should navigate when the location path changes', inject(function ($location) { $router.config([ - { path: '/one', component: OneController } + { path: '/one', component: 'oneCmp' } ]); compile('
'); @@ -232,18 +210,18 @@ describe('navigation', function () { it('should expose a "navigating" property on $router', inject(function ($q) { var defer; - var pendingActivate = registerComponent('pendingActivate', '', { - onActivate: function () { + registerComponent('pendingActivate', { + $canActivate: function () { defer = $q.defer(); return defer.promise; } }); $router.config([ - { path: '/pendingActivate', component: pendingActivate } + { path: '/pending-activate', component: 'pendingActivate' } ]); compile('
'); - $router.navigateByUrl('/pendingActivate'); + $router.navigateByUrl('/pending-activate'); $rootScope.$digest(); expect($router.navigating).toBe(true); defer.resolve(); @@ -251,40 +229,31 @@ describe('navigation', function () { expect($router.navigating).toBe(false); })); + function registerComponent(name, options) { + var controller = options.controller || function () {}; - function registerComponent(name, template, config) { - var Ctrl; - if (!template) { - template = ''; - } - if (!config) { - Ctrl = function () {}; - } else if (angular.isArray(config)) { - Ctrl = function () {}; - Ctrl.annotations = [new angular.annotations.RouteConfig(config)]; - } else if (typeof config === 'function') { - Ctrl = config; - } else { - Ctrl = function () {}; - if (config.canActivate) { - Ctrl.$canActivate = config.canActivate; - delete config.canActivate; + ['$onActivate', '$onDeactivate', '$onReuse', '$canReuse', '$canDeactivate'].forEach(function (hookName) { + if (options[hookName]) { + controller.prototype[hookName] = options[hookName]; } - Ctrl.prototype = config; + }); + + function factory() { + return { + template: options.template || '', + controllerAs: name, + controller: controller + }; } - $controllerProvider.register(componentControllerName(name), Ctrl); - put(name, template); - return Ctrl; - } - function boringController(model, value) { - return function () { - this[model] = value; - }; - } + if (options.$canActivate) { + factory.$canActivate = options.$canActivate; + } + if (options.$routeConfig) { + factory.$routeConfig = options.$routeConfig; + } - function put(name, template) { - $templateCache.put(componentTemplatePath(name), [200, template, {}]); + $compileProvider.directive(name, factory); } function compile(template) { diff --git a/modules/angular1_router/test/ng_link_spec.js b/modules/angular1_router/test/ng_link_spec.js index 213db2f26b..c1633c305f 100644 --- a/modules/angular1_router/test/ng_link_spec.js +++ b/modules/angular1_router/test/ng_link_spec.js @@ -1,44 +1,36 @@ 'use strict'; -describe('ngOutlet', function () { +describe('ngLink', function () { var elt, - $compile, - $rootScope, - $router, - $templateCache, - $controllerProvider; - - var OneController, TwoController, UserController; + $compile, + $rootScope, + $router, + $compileProvider; beforeEach(function () { module('ng'); module('ngComponentRouter'); - module(function (_$controllerProvider_) { - $controllerProvider = _$controllerProvider_; + module(function (_$compileProvider_) { + $compileProvider = _$compileProvider_; }); - inject(function (_$compile_, _$rootScope_, _$router_, _$templateCache_) { + inject(function (_$compile_, _$rootScope_, _$router_) { $compile = _$compile_; $rootScope = _$rootScope_; $router = _$router_; - $templateCache = _$templateCache_; }); - UserController = registerComponent('user', '
hello {{user.name}}
', function ($routeParams) { - this.name = $routeParams.name; - }); - OneController = registerComponent('one', '
{{one.number}}
', boringController('number', 'one')); - TwoController = registerComponent('two', '
{{two.number}}
', boringController('number', 'two')); + registerComponent('userCmp', '
hello {{userCmp.$routeParams.name}}
', function () {}); + registerComponent('oneCmp', '
{{oneCmp.number}}
', function () {this.number = 'one'}); + registerComponent('twoCmp', '
{{twoCmp.number}}
', function () {this.number = 'two'}); }); it('should allow linking from the parent to the child', function () { - put('one', '
{{number}}
'); - $router.config([ - { path: '/a', component: OneController }, - { path: '/b', component: TwoController, as: 'Two' } + { path: '/a', component: 'oneCmp' }, + { path: '/b', component: 'twoCmp', as: 'Two' } ]); compile('link | outer {
}'); @@ -49,15 +41,13 @@ describe('ngOutlet', function () { }); it('should allow linking from the child and the parent', function () { - put('one', '
{{number}}
'); - $router.config([ - { path: '/a', component: OneController }, - { path: '/b', component: TwoController, as: 'Two' } + { path: '/a', component: 'oneCmp' }, + { path: '/b', component: 'twoCmp', as: 'Two' } ]); compile('outer {
}'); - $router.navigateByUrl('/a'); + $router.navigateByUrl('/b'); $rootScope.$digest(); expect(elt.find('a').attr('href')).toBe('./b'); @@ -65,12 +55,11 @@ describe('ngOutlet', function () { it('should allow params in routerLink directive', function () { - put('router', '
outer {
}
'); - put('one', '
{{number}}
'); + registerComponent('twoLinkCmp', '
{{twoLinkCmp.number}}
', function () {this.number = 'two'}); $router.config([ - { path: '/a', component: OneController }, - { path: '/b/:param', component: TwoController, as: 'Two' } + { path: '/a', component: 'twoLinkCmp' }, + { path: '/b/:param', component: 'twoCmp', as: 'Two' } ]); compile('
'); @@ -80,33 +69,34 @@ describe('ngOutlet', function () { expect(elt.find('a').attr('href')).toBe('./b/lol'); }); - // TODO: test dynamic links - it('should update the href of links with bound params', function () { - put('router', '
outer {
}
'); - put('one', '
{{one.number}}
'); + it('should update the href of links with bound params', function () { + registerComponent('twoLinkCmp', '
{{twoLinkCmp.number}}
', function () {this.number = 'param'}); $router.config([ - { path: '/a', component: OneController }, - { path: '/b/:param', component: TwoController, as: 'Two' } + { path: '/a', component: 'twoLinkCmp' }, + { path: '/b/:param', component: 'twoCmp', as: 'Two' } ]); compile('
'); $router.navigateByUrl('/a'); $rootScope.$digest(); - expect(elt.find('a').attr('href')).toBe('./b/one'); + expect(elt.find('a').attr('href')).toBe('./b/param'); }); it('should navigate on left-mouse click when a link url matches a route', function () { $router.config([ - { path: '/', component: OneController }, - { path: '/two', component: TwoController } + { path: '/', component: 'oneCmp' }, + { path: '/two', component: 'twoCmp', as: 'Two'} ]); - compile('link |
'); + compile('link |
'); $rootScope.$digest(); expect(elt.text()).toBe('link | one'); + + expect(elt.find('a').attr('href')).toBe('./two'); + elt.find('a')[0].click(); $rootScope.$digest(); @@ -116,11 +106,11 @@ describe('ngOutlet', function () { it('should not navigate on non-left mouse click when a link url matches a route', inject(function ($router) { $router.config([ - { path: '/', component: OneController }, - { path: '/two', component: TwoController } + { path: '/', component: 'oneCmp' }, + { path: '/two', component: 'twoCmp', as: 'Two'} ]); - compile('link |
'); + compile('link |
'); $rootScope.$digest(); expect(elt.text()).toBe('link | one'); elt.find('a').triggerHandler({ type: 'click', which: 3 }); @@ -133,8 +123,8 @@ describe('ngOutlet', function () { // See https://github.com/angular/router/issues/206 it('should not navigate a link without an href', function () { $router.config([ - { path: '/', component: OneController }, - { path: '/two', component: TwoController } + { path: '/', component: 'oneCmp' }, + { path: '/two', component: 'twoCmp', as: 'Two'} ]); expect(function () { compile('link'); @@ -147,38 +137,29 @@ describe('ngOutlet', function () { function registerComponent(name, template, config) { - var Ctrl; + var controller = function () {}; + + function factory() { + return { + template: template, + controllerAs: name, + controller: controller + }; + } + if (!template) { template = ''; } - if (!config) { - Ctrl = function () {}; - } else if (angular.isArray(config)) { - Ctrl = function () {}; - Ctrl.annotations = [new angular.annotations.RouteConfig(config)]; + if (angular.isArray(config)) { + factory.annotations = [new angular.annotations.RouteConfig(config)]; } else if (typeof config === 'function') { - Ctrl = config; - } else { - Ctrl = function () {}; + controller = config; + } else if (typeof config === 'object') { if (config.canActivate) { - Ctrl.$canActivate = config.canActivate; - delete config.canActivate; + controller.$canActivate = config.canActivate; } - Ctrl.prototype = config; } - $controllerProvider.register(componentControllerName(name), Ctrl); - put(name, template); - return Ctrl; - } - - function boringController(model, value) { - return function () { - this[model] = value; - }; - } - - function put(name, template) { - $templateCache.put(componentTemplatePath(name), [200, template, {}]); + $compileProvider.directive(name, factory); } function compile(template) { diff --git a/modules/angular1_router/test/util.es5.js b/modules/angular1_router/test/util.es5.js index c12ed8d632..31568d74b2 100644 --- a/modules/angular1_router/test/util.es5.js +++ b/modules/angular1_router/test/util.es5.js @@ -16,11 +16,6 @@ function dashCase(str) { }); } -function boringController (model, value) { - return function () { - this[model] = value; - }; -} function provideHelpers(fn, preInject) { return function () {