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.
This commit is contained in:
Brian Ford 2015-09-18 15:53:50 -07:00
parent 6e0ca7f39a
commit 5205a9e65f
12 changed files with 547 additions and 724 deletions

View File

@ -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;
}

View File

@ -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

View File

@ -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;
}

View File

@ -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 = '<div ' + dashCase(componentName) + '></div>';
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) {
* </div>
* ```
*/
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();
});
}
}

View File

@ -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('<ng-outlet></ng-outlet>');
$router.config([
{ path: '/', component: Ctrl }
]);
$router.navigateByUrl('/');
$rootScope.$digest();
expect(elt.text()).toBe('howdy');
}));
function compile(template) {
elt = $compile('<div>' + template + '</div>')($rootScope);
$rootScope.$digest();
return elt;
}
});

View File

@ -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);
}));
});

View File

@ -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);
}));
});

View File

@ -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', '<div>hello {{user.name}}</div>');
$controllerProvider.register('UserController', UserController);
registerComponent('userCmp', {
template: '<div>hello {{userCmp.$routeParams.name}}</div>'
});
});
afterEach(function () {
@ -43,7 +39,7 @@ describe('ngOutlet animations', function () {
compile('<div ng-outlet></div>');
$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) {

View File

@ -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', '<div>hello {{user.name}}</div>', function ($routeParams) {
this.name = $routeParams.name;
registerComponent('oneCmp', {
template: '<div>{{oneCmp.number}}</div>',
controller: function () {this.number = 'one'}
});
registerComponent('twoCmp', {
template: '<div><a ng-link="[\'/Two\']">{{twoCmp.number}}</a></div>',
controller: function () {this.number = 'two'}
});
OneController = registerComponent('one', '<div>{{one.number}}</div>', boringController('number', 'one'));
TwoController = registerComponent('two', '<div>{{two.number}}</div>', 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: '<p>hello</p>',
$onActivate: spy
});
$router.config([
{ path: '/a', component: activate }
{ path: '/a', component: 'activateCmp' }
]);
compile('<div>outer { <div ng-outlet></div> }</div>');
@ -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('<div ng-outlet></div>');
$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('<div ng-outlet></div>');
@ -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('<div ng-outlet></div>');
@ -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('<div ng-outlet></div>');
@ -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('<div ng-outlet></div>');
@ -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 { <div ng-outlet></div> }');
@ -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 {<ng-outlet></ng-outlet>}', ReuseCmp);
registerComponent('reuseCmp', {
template: 'reuse {<ng-outlet></ng-outlet>}',
$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 { <div ng-outlet></div> }');
@ -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 {<ng-outlet></ng-outlet>}', NeverReuseCmp);
registerComponent('reuseCmp', {
template: 'reuse {<ng-outlet></ng-outlet>}',
$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 { <div ng-outlet></div> }');
@ -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 { <div ng-outlet></div> }');
@ -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('<div ng-outlet></div>');
$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('<div ng-outlet></div>');
@ -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('<div ng-outlet></div>');
$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 { <div ng-outlet></div> }');
@ -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 { <div ng-outlet></div> }');
@ -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('<div ng-outlet></div>');
@ -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('<div ng-outlet></div>');
@ -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});
}
});

View File

@ -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', '<div>hello {{user.name}}</div>', function ($routeParams) {
this.name = $routeParams.name;
registerComponent('userCmp', {
template: '<div>hello {{userCmp.$routeParams.name}}</div>'
});
registerComponent('oneCmp', {
template: '<div>{{oneCmp.number}}</div>',
controller: function () {this.number = 'one'}
});
registerComponent('twoCmp', {
template: '<div>{{twoCmp.number}}</div>',
controller: function () {this.number = 'two'}
});
OneController = registerComponent('one', '<div>{{one.number}}</div>', boringController('number', 'one'));
TwoController = registerComponent('two', '<div>{{two.number}}</div>', boringController('number', 'two'));
});
it('should work in a simple case', function () {
compile('<ng-outlet></ng-outlet>');
$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', '<div>{{ 2 + 2 }}</div>');
$router.config([
{ path: '/', component: 'noController' }
]);
spyOn(console, 'warn');
compile('<ng-outlet></ng-outlet>');
$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('<ng-outlet></ng-outlet>');
@ -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 { <ng-outlet></ng-outlet> }', 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 { <ng-outlet></ng-outlet> }',
$routeConfig: [
{ path: '/user/:name', component: 'userCmp' }
],
controller: ParentController
});
$router.config([
{ path: '/parent/...', component: ParentController }
{ path: '/parent/...', component: 'parentCmp' }
]);
compile('<ng-outlet></ng-outlet>');
$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', '<div>inner { <div ng-outlet></div> }</div>', [
{ path: '/b', component: OneController }
]);
registerComponent('childCmp', {
template: '<div>inner { <div ng-outlet></div> }</div>',
$routeConfig: [
{ path: '/b', component: 'oneCmp' }
]
});
$router.config([
{ path: '/a/...', component: childComponent }
{ path: '/a/...', component: 'childCmp' }
]);
compile('<div>outer { <div ng-outlet></div> }</div>');
@ -128,40 +113,30 @@ describe('navigation', function () {
});
it('should work with recursive nested outlets', function () {
put('two', '<div>recur { <div ng-outlet></div> }</div>');
// TODO: fix this
xit('should work with recursive nested outlets', function () {
registerComponent('recurCmp', {
template: '<div>recur { <div ng-outlet></div> }</div>',
$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('<div>root { <div ng-outlet></div> }</div>');
$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('<div ng-outlet></div>');
$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('<div ng-outlet></div>');
@ -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', '<div>inner { <div ng-outlet></div> }</div>', [
{ 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: '<div>inner { <div ng-outlet></div> }</div>',
$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('<div ng-outlet></div>');
@ -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('<div ng-outlet></div>');
@ -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('<div ng-outlet></div>');
$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) {

View File

@ -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', '<div>hello {{user.name}}</div>', function ($routeParams) {
this.name = $routeParams.name;
});
OneController = registerComponent('one', '<div>{{one.number}}</div>', boringController('number', 'one'));
TwoController = registerComponent('two', '<div>{{two.number}}</div>', boringController('number', 'two'));
registerComponent('userCmp', '<div>hello {{userCmp.$routeParams.name}}</div>', function () {});
registerComponent('oneCmp', '<div>{{oneCmp.number}}</div>', function () {this.number = 'one'});
registerComponent('twoCmp', '<div><a ng-link="[\'/Two\']">{{twoCmp.number}}</a></div>', function () {this.number = 'two'});
});
it('should allow linking from the parent to the child', function () {
put('one', '<div>{{number}}</div>');
$router.config([
{ path: '/a', component: OneController },
{ path: '/b', component: TwoController, as: 'Two' }
{ path: '/a', component: 'oneCmp' },
{ path: '/b', component: 'twoCmp', as: 'Two' }
]);
compile('<a ng-link="[\'/Two\']">link</a> | outer { <div ng-outlet></div> }');
@ -49,15 +41,13 @@ describe('ngOutlet', function () {
});
it('should allow linking from the child and the parent', function () {
put('one', '<div><a ng-link="[\'/Two\']">{{number}}</a></div>');
$router.config([
{ path: '/a', component: OneController },
{ path: '/b', component: TwoController, as: 'Two' }
{ path: '/a', component: 'oneCmp' },
{ path: '/b', component: 'twoCmp', as: 'Two' }
]);
compile('outer { <div ng-outlet></div> }');
$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', '<div>outer { <div ng-outlet></div> }</div>');
put('one', '<div><a ng-link="[\'/Two\', {param: \'lol\'}]">{{number}}</a></div>');
registerComponent('twoLinkCmp', '<div><a ng-link="[\'/Two\', {param: \'lol\'}]">{{twoLinkCmp.number}}</a></div>', 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('<div ng-outlet></div>');
@ -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', '<div>outer { <div ng-outlet></div> }</div>');
put('one', '<div><a ng-link="[\'/Two\', {param: one.number}]">{{one.number}}</a></div>');
it('should update the href of links with bound params', function () {
registerComponent('twoLinkCmp', '<div><a ng-link="[\'/Two\', {param: twoLinkCmp.number}]">{{twoLinkCmp.number}}</a></div>', 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('<div ng-outlet></div>');
$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('<a href="/two">link</a> | <div ng-outlet></div>');
compile('<a ng-link="[\'/Two\']">link</a> | <div ng-outlet></div>');
$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('<a href="./two">link</a> | <div ng-outlet></div>');
compile('<a ng-link="[\'/Two\']">link</a> | <div ng-outlet></div>');
$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('<a>link</a>');
@ -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) {

View File

@ -16,11 +16,6 @@ function dashCase(str) {
});
}
function boringController (model, value) {
return function () {
this[model] = value;
};
}
function provideHelpers(fn, preInject) {
return function () {