2015-10-04 09:33:20 -07:00
|
|
|
import {
|
|
|
|
Directive,
|
|
|
|
DoCheck,
|
|
|
|
ElementRef,
|
|
|
|
EventEmitter,
|
|
|
|
Inject,
|
|
|
|
OnChanges,
|
|
|
|
SimpleChange,
|
|
|
|
Type
|
2015-12-09 17:05:49 +01:00
|
|
|
} from 'angular2/core';
|
2015-10-11 11:18:11 -07:00
|
|
|
import {
|
|
|
|
NG1_COMPILE,
|
|
|
|
NG1_SCOPE,
|
|
|
|
NG1_HTTP_BACKEND,
|
|
|
|
NG1_TEMPLATE_CACHE,
|
|
|
|
NG1_CONTROLLER
|
|
|
|
} from './constants';
|
|
|
|
import {controllerKey} from './util';
|
2015-10-26 20:17:46 -07:00
|
|
|
import * as angular from './angular_js';
|
2015-10-04 09:33:20 -07:00
|
|
|
|
|
|
|
const CAMEL_CASE = /([A-Z])/g;
|
|
|
|
const INITIAL_VALUE = {
|
|
|
|
__UNINITIALIZED__: true
|
|
|
|
};
|
2015-10-11 11:18:11 -07:00
|
|
|
const NOT_SUPPORTED: any = 'NOT_SUPPORTED';
|
2015-10-04 09:33:20 -07:00
|
|
|
|
|
|
|
|
2015-10-12 21:32:41 -07:00
|
|
|
export class UpgradeNg1ComponentAdapterBuilder {
|
2015-10-04 09:33:20 -07:00
|
|
|
type: Type;
|
|
|
|
inputs: string[] = [];
|
|
|
|
inputsRename: string[] = [];
|
|
|
|
outputs: string[] = [];
|
|
|
|
outputsRename: string[] = [];
|
|
|
|
propertyOutputs: string[] = [];
|
|
|
|
checkProperties: string[] = [];
|
|
|
|
propertyMap: {[name: string]: string} = {};
|
2015-10-11 11:18:11 -07:00
|
|
|
linkFn: angular.ILinkFn = null;
|
|
|
|
directive: angular.IDirective = null;
|
|
|
|
$controller: angular.IControllerService = null;
|
2015-10-04 09:33:20 -07:00
|
|
|
|
|
|
|
constructor(public name: string) {
|
|
|
|
var selector = name.replace(CAMEL_CASE, (all, next: string) => '-' + next.toLowerCase());
|
|
|
|
var self = this;
|
|
|
|
this.type =
|
|
|
|
Directive({selector: selector, inputs: this.inputsRename, outputs: this.outputsRename})
|
|
|
|
.Class({
|
|
|
|
constructor: [
|
|
|
|
new Inject(NG1_SCOPE),
|
|
|
|
ElementRef,
|
2015-10-11 11:18:11 -07:00
|
|
|
function(scope: angular.IScope, elementRef: ElementRef) {
|
|
|
|
return new UpgradeNg1ComponentAdapter(
|
|
|
|
self.linkFn, scope, self.directive, elementRef, self.$controller, self.inputs,
|
|
|
|
self.outputs, self.propertyOutputs, self.checkProperties, self.propertyMap);
|
2015-10-04 09:33:20 -07:00
|
|
|
}
|
|
|
|
],
|
refactor(lifecycle): prefix lifecycle methods with "ng"
BREAKING CHANGE:
Previously, components that would implement lifecycle interfaces would include methods
like "onChanges" or "afterViewInit." Given that components were at risk of using such
names without realizing that Angular would call the methods at different points of
the component lifecycle. This change adds an "ng" prefix to all lifecycle hook methods,
far reducing the risk of an accidental name collision.
To fix, just rename these methods:
* onInit
* onDestroy
* doCheck
* onChanges
* afterContentInit
* afterContentChecked
* afterViewInit
* afterViewChecked
* _Router Hooks_
* onActivate
* onReuse
* onDeactivate
* canReuse
* canDeactivate
To:
* ngOnInit,
* ngOnDestroy,
* ngDoCheck,
* ngOnChanges,
* ngAfterContentInit,
* ngAfterContentChecked,
* ngAfterViewInit,
* ngAfterViewChecked
* _Router Hooks_
* routerOnActivate
* routerOnReuse
* routerOnDeactivate
* routerCanReuse
* routerCanDeactivate
The names of lifecycle interfaces and enums have not changed, though interfaces
have been updated to reflect the new method names.
Closes #5036
2015-11-16 17:04:36 -08:00
|
|
|
ngOnChanges: function() { /* needs to be here for ng2 to properly detect it */ },
|
|
|
|
ngDoCheck: function() { /* needs to be here for ng2 to properly detect it */ }
|
2015-10-04 09:33:20 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2015-10-26 20:17:46 -07:00
|
|
|
extractDirective(injector: angular.IInjectorService): angular.IDirective {
|
2015-10-04 09:33:20 -07:00
|
|
|
var directives: angular.IDirective[] = injector.get(this.name + 'Directive');
|
|
|
|
if (directives.length > 1) {
|
|
|
|
throw new Error('Only support single directive definition for: ' + this.name);
|
|
|
|
}
|
|
|
|
var directive = directives[0];
|
2015-10-11 11:18:11 -07:00
|
|
|
if (directive.replace) this.notSupported('replace');
|
|
|
|
if (directive.terminal) this.notSupported('terminal');
|
|
|
|
var link = directive.link;
|
|
|
|
if (typeof link == 'object') {
|
|
|
|
if ((<angular.IDirectivePrePost>link).post) this.notSupported('link.post');
|
|
|
|
}
|
|
|
|
return directive;
|
|
|
|
}
|
|
|
|
|
|
|
|
private notSupported(feature: string) {
|
|
|
|
throw new Error(`Upgraded directive '${this.name}' does not support '${feature}'.`);
|
|
|
|
}
|
|
|
|
|
|
|
|
extractBindings() {
|
2015-11-13 18:55:40 +01:00
|
|
|
var btcIsObject = typeof this.directive.bindToController === 'object';
|
|
|
|
if (btcIsObject && Object.keys(this.directive.scope).length) {
|
|
|
|
throw new Error(
|
|
|
|
`Binding definitions on scope and controller at the same time are not supported.`);
|
|
|
|
}
|
|
|
|
|
|
|
|
var context = (btcIsObject) ? this.directive.bindToController : this.directive.scope;
|
|
|
|
|
|
|
|
if (typeof context == 'object') {
|
|
|
|
for (var name in context) {
|
|
|
|
if ((<any>context).hasOwnProperty(name)) {
|
|
|
|
var localName = context[name];
|
2015-10-04 09:33:20 -07:00
|
|
|
var type = localName.charAt(0);
|
|
|
|
localName = localName.substr(1) || name;
|
|
|
|
var outputName = 'output_' + name;
|
|
|
|
var outputNameRename = outputName + ': ' + name;
|
2015-10-10 19:56:22 -07:00
|
|
|
var outputNameRenameChange = outputName + ': ' + name + 'Change';
|
2015-10-04 09:33:20 -07:00
|
|
|
var inputName = 'input_' + name;
|
|
|
|
var inputNameRename = inputName + ': ' + name;
|
|
|
|
switch (type) {
|
|
|
|
case '=':
|
|
|
|
this.propertyOutputs.push(outputName);
|
|
|
|
this.checkProperties.push(localName);
|
|
|
|
this.outputs.push(outputName);
|
2015-10-10 19:56:22 -07:00
|
|
|
this.outputsRename.push(outputNameRenameChange);
|
2015-10-04 09:33:20 -07:00
|
|
|
this.propertyMap[outputName] = localName;
|
|
|
|
// don't break; let it fall through to '@'
|
|
|
|
case '@':
|
|
|
|
this.inputs.push(inputName);
|
|
|
|
this.inputsRename.push(inputNameRename);
|
|
|
|
this.propertyMap[inputName] = localName;
|
|
|
|
break;
|
|
|
|
case '&':
|
|
|
|
this.outputs.push(outputName);
|
|
|
|
this.outputsRename.push(outputNameRename);
|
|
|
|
this.propertyMap[outputName] = localName;
|
|
|
|
break;
|
|
|
|
default:
|
2015-11-13 18:55:40 +01:00
|
|
|
var json = JSON.stringify(context);
|
2015-10-04 09:33:20 -07:00
|
|
|
throw new Error(
|
|
|
|
`Unexpected mapping '${type}' in '${json}' in '${this.name}' directive.`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-10-11 11:18:11 -07:00
|
|
|
compileTemplate(compile: angular.ICompileService, templateCache: angular.ITemplateCacheService,
|
|
|
|
httpBackend: angular.IHttpBackendService): Promise<any> {
|
2015-12-10 13:23:47 +01:00
|
|
|
if (this.directive.template !== undefined) {
|
2015-10-11 11:18:11 -07:00
|
|
|
this.linkFn = compileHtml(this.directive.template);
|
|
|
|
} else if (this.directive.templateUrl) {
|
|
|
|
var url = this.directive.templateUrl;
|
|
|
|
var html = templateCache.get(url);
|
|
|
|
if (html !== undefined) {
|
|
|
|
this.linkFn = compileHtml(html);
|
|
|
|
} else {
|
|
|
|
return new Promise((resolve, err) => {
|
|
|
|
httpBackend('GET', url, null, (status, response) => {
|
|
|
|
if (status == 200) {
|
|
|
|
resolve(this.linkFn = compileHtml(templateCache.put(url, response)));
|
|
|
|
} else {
|
|
|
|
err(`GET ${url} returned ${status}: ${response}`);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new Error(`Directive '${this.name}' is not a component, it is missing template.`);
|
|
|
|
}
|
|
|
|
return null;
|
2015-10-26 20:17:46 -07:00
|
|
|
function compileHtml(html): angular.ILinkFn {
|
2015-10-11 11:18:11 -07:00
|
|
|
var div = document.createElement('div');
|
|
|
|
div.innerHTML = html;
|
|
|
|
return compile(div.childNodes);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-10-12 21:32:41 -07:00
|
|
|
static resolve(exportedComponents: {[name: string]: UpgradeNg1ComponentAdapterBuilder},
|
2015-10-26 20:17:46 -07:00
|
|
|
injector: angular.IInjectorService): Promise<any> {
|
2015-10-11 11:18:11 -07:00
|
|
|
var promises = [];
|
|
|
|
var compile: angular.ICompileService = injector.get(NG1_COMPILE);
|
|
|
|
var templateCache: angular.ITemplateCacheService = injector.get(NG1_TEMPLATE_CACHE);
|
|
|
|
var httpBackend: angular.IHttpBackendService = injector.get(NG1_HTTP_BACKEND);
|
|
|
|
var $controller: angular.IControllerService = injector.get(NG1_CONTROLLER);
|
2015-10-04 09:33:20 -07:00
|
|
|
for (var name in exportedComponents) {
|
2015-10-09 21:19:00 -07:00
|
|
|
if ((<any>exportedComponents).hasOwnProperty(name)) {
|
2015-10-04 09:33:20 -07:00
|
|
|
var exportedComponent = exportedComponents[name];
|
2015-10-11 11:18:11 -07:00
|
|
|
exportedComponent.directive = exportedComponent.extractDirective(injector);
|
|
|
|
exportedComponent.$controller = $controller;
|
|
|
|
exportedComponent.extractBindings();
|
|
|
|
var promise = exportedComponent.compileTemplate(compile, templateCache, httpBackend);
|
2015-10-26 20:17:46 -07:00
|
|
|
if (promise) promises.push(promise);
|
2015-10-04 09:33:20 -07:00
|
|
|
}
|
|
|
|
}
|
2015-10-11 11:18:11 -07:00
|
|
|
return Promise.all(promises);
|
2015-10-04 09:33:20 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-10-12 21:32:41 -07:00
|
|
|
class UpgradeNg1ComponentAdapter implements OnChanges, DoCheck {
|
2015-10-11 11:18:11 -07:00
|
|
|
destinationObj: any = null;
|
2015-10-04 09:33:20 -07:00
|
|
|
checkLastValues: any[] = [];
|
|
|
|
|
2015-10-11 11:18:11 -07:00
|
|
|
constructor(linkFn: angular.ILinkFn, scope: angular.IScope, private directive: angular.IDirective,
|
|
|
|
elementRef: ElementRef, $controller: angular.IControllerService,
|
2015-10-04 09:33:20 -07:00
|
|
|
private inputs: string[], private outputs: string[], private propOuts: string[],
|
|
|
|
private checkProperties: string[], private propertyMap: {[key: string]: string}) {
|
2015-10-09 21:31:42 -07:00
|
|
|
var element: Element = elementRef.nativeElement;
|
|
|
|
var childNodes: Node[] = [];
|
|
|
|
var childNode;
|
|
|
|
while (childNode = element.firstChild) {
|
|
|
|
element.removeChild(childNode);
|
|
|
|
childNodes.push(childNode);
|
|
|
|
}
|
2015-10-11 11:18:11 -07:00
|
|
|
var componentScope = scope.$new(!!directive.scope);
|
|
|
|
var $element = angular.element(element);
|
|
|
|
var controllerType = directive.controller;
|
|
|
|
var controller: any = null;
|
|
|
|
if (controllerType) {
|
|
|
|
var locals = {$scope: componentScope, $element: $element};
|
|
|
|
controller = $controller(controllerType, locals, null, directive.controllerAs);
|
|
|
|
$element.data(controllerKey(directive.name), controller);
|
|
|
|
}
|
|
|
|
var link = directive.link;
|
|
|
|
if (typeof link == 'object') link = (<angular.IDirectivePrePost>link).pre;
|
|
|
|
if (link) {
|
|
|
|
var attrs: angular.IAttributes = NOT_SUPPORTED;
|
|
|
|
var transcludeFn: angular.ITranscludeFunction = NOT_SUPPORTED;
|
|
|
|
var linkController = this.resolveRequired($element, directive.require);
|
|
|
|
(<angular.IDirectiveLinkFn>directive.link)(componentScope, $element, attrs, linkController,
|
|
|
|
transcludeFn);
|
|
|
|
}
|
|
|
|
this.destinationObj = directive.bindToController && controller ? controller : componentScope;
|
|
|
|
|
|
|
|
linkFn(componentScope, (clonedElement: Node[], scope: angular.IScope) => {
|
|
|
|
for (var i = 0, ii = clonedElement.length; i < ii; i++) {
|
|
|
|
element.appendChild(clonedElement[i]);
|
|
|
|
}
|
2015-10-26 20:17:46 -07:00
|
|
|
}, {parentBoundTranscludeFn: (scope, cloneAttach) => { cloneAttach(childNodes); }});
|
2015-10-04 09:33:20 -07:00
|
|
|
|
|
|
|
for (var i = 0; i < inputs.length; i++) {
|
|
|
|
this[inputs[i]] = null;
|
|
|
|
}
|
|
|
|
for (var j = 0; j < outputs.length; j++) {
|
|
|
|
var emitter = this[outputs[j]] = new EventEmitter();
|
2015-11-15 23:58:59 -08:00
|
|
|
this.setComponentProperty(outputs[j], ((emitter) => (value) => emitter.emit(value))(emitter));
|
2015-10-04 09:33:20 -07:00
|
|
|
}
|
|
|
|
for (var k = 0; k < propOuts.length; k++) {
|
|
|
|
this[propOuts[k]] = new EventEmitter();
|
|
|
|
this.checkLastValues.push(INITIAL_VALUE);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
refactor(lifecycle): prefix lifecycle methods with "ng"
BREAKING CHANGE:
Previously, components that would implement lifecycle interfaces would include methods
like "onChanges" or "afterViewInit." Given that components were at risk of using such
names without realizing that Angular would call the methods at different points of
the component lifecycle. This change adds an "ng" prefix to all lifecycle hook methods,
far reducing the risk of an accidental name collision.
To fix, just rename these methods:
* onInit
* onDestroy
* doCheck
* onChanges
* afterContentInit
* afterContentChecked
* afterViewInit
* afterViewChecked
* _Router Hooks_
* onActivate
* onReuse
* onDeactivate
* canReuse
* canDeactivate
To:
* ngOnInit,
* ngOnDestroy,
* ngDoCheck,
* ngOnChanges,
* ngAfterContentInit,
* ngAfterContentChecked,
* ngAfterViewInit,
* ngAfterViewChecked
* _Router Hooks_
* routerOnActivate
* routerOnReuse
* routerOnDeactivate
* routerCanReuse
* routerCanDeactivate
The names of lifecycle interfaces and enums have not changed, though interfaces
have been updated to reflect the new method names.
Closes #5036
2015-11-16 17:04:36 -08:00
|
|
|
ngOnChanges(changes: {[name: string]: SimpleChange}) {
|
2015-10-04 09:33:20 -07:00
|
|
|
for (var name in changes) {
|
2015-10-26 20:17:46 -07:00
|
|
|
if ((<Object>changes).hasOwnProperty(name)) {
|
2015-10-04 09:33:20 -07:00
|
|
|
var change: SimpleChange = changes[name];
|
|
|
|
this.setComponentProperty(name, change.currentValue);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
refactor(lifecycle): prefix lifecycle methods with "ng"
BREAKING CHANGE:
Previously, components that would implement lifecycle interfaces would include methods
like "onChanges" or "afterViewInit." Given that components were at risk of using such
names without realizing that Angular would call the methods at different points of
the component lifecycle. This change adds an "ng" prefix to all lifecycle hook methods,
far reducing the risk of an accidental name collision.
To fix, just rename these methods:
* onInit
* onDestroy
* doCheck
* onChanges
* afterContentInit
* afterContentChecked
* afterViewInit
* afterViewChecked
* _Router Hooks_
* onActivate
* onReuse
* onDeactivate
* canReuse
* canDeactivate
To:
* ngOnInit,
* ngOnDestroy,
* ngDoCheck,
* ngOnChanges,
* ngAfterContentInit,
* ngAfterContentChecked,
* ngAfterViewInit,
* ngAfterViewChecked
* _Router Hooks_
* routerOnActivate
* routerOnReuse
* routerOnDeactivate
* routerCanReuse
* routerCanDeactivate
The names of lifecycle interfaces and enums have not changed, though interfaces
have been updated to reflect the new method names.
Closes #5036
2015-11-16 17:04:36 -08:00
|
|
|
ngDoCheck(): number {
|
2015-10-04 09:33:20 -07:00
|
|
|
var count = 0;
|
2015-10-11 11:18:11 -07:00
|
|
|
var destinationObj = this.destinationObj;
|
2015-10-04 09:33:20 -07:00
|
|
|
var lastValues = this.checkLastValues;
|
|
|
|
var checkProperties = this.checkProperties;
|
|
|
|
for (var i = 0; i < checkProperties.length; i++) {
|
2015-10-11 11:18:11 -07:00
|
|
|
var value = destinationObj[checkProperties[i]];
|
2015-10-04 09:33:20 -07:00
|
|
|
var last = lastValues[i];
|
|
|
|
if (value !== last) {
|
|
|
|
if (typeof value == 'number' && isNaN(value) && typeof last == 'number' && isNaN(last)) {
|
|
|
|
// ignore because NaN != NaN
|
|
|
|
} else {
|
2015-10-24 18:48:43 -07:00
|
|
|
var eventEmitter: EventEmitter<any> = this[this.propOuts[i]];
|
2015-11-15 23:58:59 -08:00
|
|
|
eventEmitter.emit(lastValues[i] = value);
|
2015-10-04 09:33:20 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return count;
|
|
|
|
}
|
|
|
|
|
|
|
|
setComponentProperty(name: string, value: any) {
|
2015-10-11 11:18:11 -07:00
|
|
|
this.destinationObj[this.propertyMap[name]] = value;
|
|
|
|
}
|
|
|
|
|
|
|
|
private resolveRequired($element: angular.IAugmentedJQuery, require: string | string[]): any {
|
|
|
|
if (!require) {
|
|
|
|
return undefined;
|
|
|
|
} else if (typeof require == 'string') {
|
|
|
|
var name: string = <string>require;
|
|
|
|
var isOptional = false;
|
|
|
|
var startParent = false;
|
|
|
|
var searchParents = false;
|
|
|
|
var ch: string;
|
|
|
|
if (name.charAt(0) == '?') {
|
|
|
|
isOptional = true;
|
|
|
|
name = name.substr(1);
|
|
|
|
}
|
|
|
|
if (name.charAt(0) == '^') {
|
|
|
|
searchParents = true;
|
|
|
|
name = name.substr(1);
|
|
|
|
}
|
|
|
|
if (name.charAt(0) == '^') {
|
|
|
|
startParent = true;
|
|
|
|
name = name.substr(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
var key = controllerKey(name);
|
|
|
|
if (startParent) $element = $element.parent();
|
|
|
|
var dep = searchParents ? $element.inheritedData(key) : $element.data(key);
|
|
|
|
if (!dep && !isOptional) {
|
|
|
|
throw new Error(`Can not locate '${require}' in '${this.directive.name}'.`);
|
|
|
|
}
|
|
|
|
return dep;
|
|
|
|
} else if (require instanceof Array) {
|
|
|
|
var deps = [];
|
|
|
|
for (var i = 0; i < require.length; i++) {
|
|
|
|
deps.push(this.resolveRequired($element, require[i]));
|
|
|
|
}
|
|
|
|
return deps;
|
|
|
|
}
|
|
|
|
throw new Error(
|
|
|
|
`Directive '${this.directive.name}' require syntax unrecognized: ${this.directive.require}`);
|
2015-10-04 09:33:20 -07:00
|
|
|
}
|
|
|
|
}
|