Peter Bacon Darwin d86be245b8 fix(angular1-router): add support for using the component helper
In Angular 1.5 there is a new helper method for creating component directives.
See https://docs.angularjs.org/guide/component for more information about components.

These kind of directives only match the `E` element form and the previously component
router only created HTML that matched directives that matched the `A` attribute form.

This commit changes the `<ng-outlet>` directive so that it generates custom HTML
elements rather divs with custom attributes to trigger the relevant component to
appear in the DOM.

Going forward, Angular 1.5 users are encouraged to create their router components
using the following style:

```
myModule.componnet('component-name', {
  // component definition object
});
```

Closes angular/angular.js#13860
Closes #6076
Closes #5278

BREAKING CHANGE:

The component router now creates custom element HTML rather than custom attribute
HTML, in order to create a new component. So rather than

```html
<div custom-component></div>
```

it now creates

```html
<custom-component></custom-component>
```

If you defined you router components using the `directive()` helper and
specified the `restrict` properties such that element matching was not allowed,
e.g. `restrict: 'A'` then these components will no longer be instantiated
by the component router and the outlet will be empty.

The fix is to include `E` in the `restrict` property.

`restrict: 'EA'`

Note that this does not affect directives that did not specify the `restrict`
property as the default for this property is already `EA`.
2016-02-08 16:18:13 -08:00

299 lines
9.0 KiB
TypeScript

///<reference path="../typings/angularjs/angular.d.ts"/>
/*
* decorates $compileProvider so that we have access to routing metadata
*/
function compilerProviderDecorator($compileProvider,
$$directiveIntrospectorProvider: DirectiveIntrospectorProvider) {
let directive = $compileProvider.directive;
$compileProvider.directive = function(name: string, factory: Function) {
$$directiveIntrospectorProvider.register(name, factory);
return directive.apply(this, arguments);
};
}
/*
* private service that holds route mappings for each controller
*/
class DirectiveIntrospectorProvider {
private directiveBuffer: any[] = [];
private directiveFactoriesByName: {[name: string]: Function} = {};
private onDirectiveRegistered: (name: string, factory: Function) => any = null;
register(name: string, factory: Function) {
if (angular.isArray(factory)) {
factory = factory[factory.length - 1];
}
this.directiveFactoriesByName[name] = factory;
if (this.onDirectiveRegistered) {
this.onDirectiveRegistered(name, factory);
} else {
this.directiveBuffer.push({name: name, factory: factory});
}
}
$get() {
let fn: any = newOnControllerRegistered => {
this.onDirectiveRegistered = newOnControllerRegistered;
while (this.directiveBuffer.length > 0) {
let directive = this.directiveBuffer.pop();
this.onDirectiveRegistered(directive.name, directive.factory);
}
};
fn.getTypeByName = name => this.directiveFactoriesByName[name];
return fn;
}
}
/**
* @name ngOutlet
*
* @description
* An ngOutlet is where resolved content goes.
*
* ## Use
*
* ```html
* <div ng-outlet="name"></div>
* ```
*
* The value for the `ngOutlet` attribute is optional.
*/
function ngOutletDirective($animate, $q: ng.IQService, $router) {
let rootRouter = $router;
return {
restrict: 'AE',
transclude: 'element',
terminal: true,
priority: 400,
require: ['?^^ngOutlet', 'ngOutlet'],
link: outletLink,
controller: class {},
controllerAs: '$$ngOutlet'
};
function outletLink(scope, element, attrs, ctrls, $transclude) {
class Outlet {
constructor(private controller, private router) {}
private currentController;
private currentInstruction;
private currentScope;
private currentElement;
private previousLeaveAnimation;
private cleanupLastView() {
if (this.previousLeaveAnimation) {
$animate.cancel(this.previousLeaveAnimation);
this.previousLeaveAnimation = null;
}
if (this.currentScope) {
this.currentScope.$destroy();
this.currentScope = null;
}
if (this.currentElement) {
this.previousLeaveAnimation = $animate.leave(this.currentElement);
this.previousLeaveAnimation.then(() => this.previousLeaveAnimation = null);
this.currentElement = null;
}
}
reuse(instruction) {
let next = $q.when(true);
let previousInstruction = this.currentInstruction;
this.currentInstruction = instruction;
if (this.currentController && this.currentController.$routerOnReuse) {
next = $q.when(
this.currentController.$routerOnReuse(this.currentInstruction, previousInstruction));
}
return next;
}
routerCanReuse(nextInstruction) {
let result;
if (!this.currentInstruction ||
this.currentInstruction.componentType !== nextInstruction.componentType) {
result = false;
} else if (this.currentController && this.currentController.$routerCanReuse) {
result = this.currentController.$routerCanReuse(nextInstruction, this.currentInstruction);
} else {
result = nextInstruction === this.currentInstruction ||
angular.equals(nextInstruction.params, this.currentInstruction.params);
}
return $q.when(result);
}
routerCanDeactivate(instruction) {
if (this.currentController && this.currentController.$routerCanDeactivate) {
return $q.when(
this.currentController.$routerCanDeactivate(instruction, this.currentInstruction));
}
return $q.when(true);
}
deactivate(instruction) {
if (this.currentController && this.currentController.$routerOnDeactivate) {
return $q.when(
this.currentController.$routerOnDeactivate(instruction, this.currentInstruction));
}
return $q.when();
}
activate(instruction) {
let previousInstruction = this.currentInstruction;
this.currentInstruction = instruction;
let componentName = this.controller.$$componentName = instruction.componentType;
if (typeof componentName !== 'string') {
throw new Error('Component is not a string for ' + instruction.urlPath);
}
this.controller.$$routeParams = instruction.params;
this.controller.$$template =
'<' + dashCase(componentName) + '></' + dashCase(componentName) + '>';
this.controller.$$router = this.router.childRouter(instruction.componentType);
let newScope = scope.$new();
let clone = $transclude(newScope, clone => {
$animate.enter(clone, null, this.currentElement || element);
this.cleanupLastView();
});
this.currentElement = clone;
this.currentScope = newScope;
// TODO: prefer the other directive retrieving the controller
// by debug mode
this.currentController = this.currentElement.children().eq(0).controller(componentName);
if (this.currentController && this.currentController.$routerOnActivate) {
return this.currentController.$routerOnActivate(instruction, previousInstruction);
}
return $q.when();
}
}
let parentCtrl = ctrls[0], myCtrl = ctrls[1],
router = (parentCtrl && parentCtrl.$$router) || rootRouter;
myCtrl.$$currentComponent = null;
router.registerPrimaryOutlet(new Outlet(myCtrl, router));
}
}
/**
* This directive is responsible for compiling the contents of ng-outlet
*/
function ngOutletFillContentDirective($compile) {
return {
restrict: 'EA',
priority: -400,
require: 'ngOutlet',
link: (scope, element, attrs, ctrl) => {
let template = ctrl.$$template;
element.html(template);
let link = $compile(element.contents());
link(scope);
// TODO: move to primary directive
let componentInstance = scope[ctrl.$$componentName];
if (componentInstance) {
ctrl.$$currentComponent = componentInstance;
componentInstance.$router = ctrl.$$router;
componentInstance.$routeParams = ctrl.$$routeParams;
}
}
};
}
/**
* @name ngLink
* @description
* Lets you link to different parts of the app, and automatically generates hrefs.
*
* ## Use
* The directive uses a simple syntax: `ng-link="componentName({ param: paramValue })"`
*
* ### Example
*
* ```js
* angular.module('myApp', ['ngComponentRouter'])
* .controller('AppController', ['$router', function($router) {
* $router.config({ path: '/user/:id', component: 'user' });
* this.user = { name: 'Brian', id: 123 };
* });
* ```
*
* ```html
* <div ng-controller="AppController as app">
* <a ng-link="user({id: app.user.id})">{{app.user.name}}</a>
* </div>
* ```
*/
function ngLinkDirective($router, $parse) {
let rootRouter = $router;
return {require: '?^^ngOutlet', restrict: 'A', link: ngLinkDirectiveLinkFn};
function ngLinkDirectiveLinkFn(scope, element, attrs, ctrl) {
let router = (ctrl && ctrl.$$router) || rootRouter;
if (!router) {
return;
}
let instruction = null;
let link = attrs.ngLink || '';
function getLink(params) {
instruction = router.generate(params);
return './' + angular.stringifyInstruction(instruction);
}
let routeParamsGetter = $parse(link);
// we can avoid adding a watcher if it's a literal
if (routeParamsGetter.constant) {
let params = routeParamsGetter();
element.attr('href', getLink(params));
} else {
scope.$watch(() => routeParamsGetter(scope), params => element.attr('href', getLink(params)),
true);
}
element.on('click', event => {
if (event.which !== 1 || !instruction) {
return;
}
$router.navigateByInstruction(instruction);
event.preventDefault();
});
}
}
function dashCase(str: string): string {
return str.replace(/[A-Z]/g, match => '-' + match.toLowerCase());
}
/*
* A module for adding new a routing system Angular 1.
*/
angular.module('ngComponentRouter', [])
.directive('ngOutlet', ngOutletDirective)
.directive('ngOutlet', ngOutletFillContentDirective)
.directive('ngLink', ngLinkDirective);
/*
* A module for inspecting controller constructors
*/
angular.module('ng')
.provider('$$directiveIntrospector', DirectiveIntrospectorProvider)
.config(compilerProviderDecorator);