270 lines
9.6 KiB
TypeScript
270 lines
9.6 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google Inc. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
|
|
import {Directive, DoCheck, ElementRef, EventEmitter, Inject, Injector, OnChanges, OnInit, SimpleChange, SimpleChanges, Type} from '@angular/core';
|
|
|
|
import * as angular from '../common/angular1';
|
|
import {$SCOPE} from '../common/constants';
|
|
import {IBindingDestination, IControllerInstance, UpgradeHelper} from '../common/upgrade_helper';
|
|
import {isFunction, strictEquals} from '../common/util';
|
|
|
|
|
|
const CAMEL_CASE = /([A-Z])/g;
|
|
const INITIAL_VALUE = {
|
|
__UNINITIALIZED__: true
|
|
};
|
|
const NOT_SUPPORTED: any = 'NOT_SUPPORTED';
|
|
|
|
|
|
export class UpgradeNg1ComponentAdapterBuilder {
|
|
type: Type<any>;
|
|
inputs: string[] = [];
|
|
inputsRename: string[] = [];
|
|
outputs: string[] = [];
|
|
outputsRename: string[] = [];
|
|
propertyOutputs: string[] = [];
|
|
checkProperties: string[] = [];
|
|
propertyMap: {[name: string]: string} = {};
|
|
directive: angular.IDirective|null = null;
|
|
template: string;
|
|
|
|
constructor(public name: string) {
|
|
const selector =
|
|
name.replace(CAMEL_CASE, (all: string, next: string) => '-' + next.toLowerCase());
|
|
const self = this;
|
|
|
|
// Note: There is a bug in TS 2.4 that prevents us from
|
|
// inlining this into @Directive
|
|
// TODO(tbosch): find or file a bug against TypeScript for this.
|
|
const directive = {selector: selector, inputs: this.inputsRename, outputs: this.outputsRename};
|
|
|
|
@Directive(directive)
|
|
class MyClass {
|
|
directive: angular.IDirective;
|
|
constructor(
|
|
@Inject($SCOPE) scope: angular.IScope, injector: Injector, elementRef: ElementRef) {
|
|
const helper = new UpgradeHelper(injector, name, elementRef, this.directive);
|
|
return new UpgradeNg1ComponentAdapter(
|
|
helper, scope, self.template, self.inputs, self.outputs, self.propertyOutputs,
|
|
self.checkProperties, self.propertyMap) as any;
|
|
}
|
|
ngOnInit() { /* needs to be here for ng2 to properly detect it */
|
|
}
|
|
ngOnChanges() { /* needs to be here for ng2 to properly detect it */
|
|
}
|
|
ngDoCheck() { /* needs to be here for ng2 to properly detect it */
|
|
}
|
|
ngOnDestroy() { /* needs to be here for ng2 to properly detect it */
|
|
}
|
|
}
|
|
this.type = MyClass;
|
|
}
|
|
|
|
extractBindings() {
|
|
const 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.`);
|
|
}
|
|
|
|
const context = (btcIsObject) ? this.directive !.bindToController : this.directive !.scope;
|
|
|
|
if (typeof context == 'object') {
|
|
Object.keys(context).forEach(propName => {
|
|
const definition = context[propName];
|
|
const bindingType = definition.charAt(0);
|
|
const bindingOptions = definition.charAt(1);
|
|
const attrName = definition.substring(bindingOptions === '?' ? 2 : 1) || propName;
|
|
|
|
// QUESTION: What about `=*`? Ignore? Throw? Support?
|
|
|
|
const inputName = `input_${attrName}`;
|
|
const inputNameRename = `${inputName}: ${attrName}`;
|
|
const outputName = `output_${attrName}`;
|
|
const outputNameRename = `${outputName}: ${attrName}`;
|
|
const outputNameRenameChange = `${outputNameRename}Change`;
|
|
|
|
switch (bindingType) {
|
|
case '@':
|
|
case '<':
|
|
this.inputs.push(inputName);
|
|
this.inputsRename.push(inputNameRename);
|
|
this.propertyMap[inputName] = propName;
|
|
break;
|
|
case '=':
|
|
this.inputs.push(inputName);
|
|
this.inputsRename.push(inputNameRename);
|
|
this.propertyMap[inputName] = propName;
|
|
|
|
this.outputs.push(outputName);
|
|
this.outputsRename.push(outputNameRenameChange);
|
|
this.propertyMap[outputName] = propName;
|
|
|
|
this.checkProperties.push(propName);
|
|
this.propertyOutputs.push(outputName);
|
|
break;
|
|
case '&':
|
|
this.outputs.push(outputName);
|
|
this.outputsRename.push(outputNameRename);
|
|
this.propertyMap[outputName] = propName;
|
|
break;
|
|
default:
|
|
let json = JSON.stringify(context);
|
|
throw new Error(
|
|
`Unexpected mapping '${bindingType}' in '${json}' in '${this.name}' directive.`);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Upgrade ng1 components into Angular.
|
|
*/
|
|
static resolve(
|
|
exportedComponents: {[name: string]: UpgradeNg1ComponentAdapterBuilder},
|
|
$injector: angular.IInjectorService): Promise<string[]> {
|
|
const promises = Object.keys(exportedComponents).map(name => {
|
|
const exportedComponent = exportedComponents[name];
|
|
exportedComponent.directive = UpgradeHelper.getDirective($injector, name);
|
|
exportedComponent.extractBindings();
|
|
|
|
return Promise
|
|
.resolve(UpgradeHelper.getTemplate($injector, exportedComponent.directive, true))
|
|
.then(template => exportedComponent.template = template);
|
|
});
|
|
|
|
return Promise.all(promises);
|
|
}
|
|
}
|
|
|
|
class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck {
|
|
private controllerInstance: IControllerInstance|null = null;
|
|
destinationObj: IBindingDestination|null = null;
|
|
checkLastValues: any[] = [];
|
|
private directive: angular.IDirective;
|
|
element: Element;
|
|
$element: any = null;
|
|
componentScope: angular.IScope;
|
|
|
|
constructor(
|
|
private helper: UpgradeHelper, scope: angular.IScope, private template: string,
|
|
private inputs: string[], private outputs: string[], private propOuts: string[],
|
|
private checkProperties: string[], private propertyMap: {[key: string]: string}) {
|
|
this.directive = helper.directive;
|
|
this.element = helper.element;
|
|
this.$element = helper.$element;
|
|
this.componentScope = scope.$new(!!this.directive.scope);
|
|
|
|
const controllerType = this.directive.controller;
|
|
|
|
if (this.directive.bindToController && controllerType) {
|
|
this.controllerInstance = this.helper.buildController(controllerType, this.componentScope);
|
|
this.destinationObj = this.controllerInstance;
|
|
} else {
|
|
this.destinationObj = this.componentScope;
|
|
}
|
|
|
|
for (let i = 0; i < inputs.length; i++) {
|
|
(this as any)[inputs[i]] = null;
|
|
}
|
|
for (let j = 0; j < outputs.length; j++) {
|
|
const emitter = (this as any)[outputs[j]] = new EventEmitter<any>();
|
|
this.setComponentProperty(
|
|
outputs[j], (emitter => (value: any) => emitter.emit(value))(emitter));
|
|
}
|
|
for (let k = 0; k < propOuts.length; k++) {
|
|
this.checkLastValues.push(INITIAL_VALUE);
|
|
}
|
|
}
|
|
|
|
ngOnInit() {
|
|
// Collect contents, insert and compile template
|
|
const attachChildNodes: angular.ILinkFn|undefined = this.helper.prepareTransclusion();
|
|
const linkFn = this.helper.compileTemplate(this.template);
|
|
|
|
// Instantiate controller (if not already done so)
|
|
const controllerType = this.directive.controller;
|
|
const bindToController = this.directive.bindToController;
|
|
if (controllerType && !bindToController) {
|
|
this.controllerInstance = this.helper.buildController(controllerType, this.componentScope);
|
|
}
|
|
|
|
// Require other controllers
|
|
const requiredControllers =
|
|
this.helper.resolveAndBindRequiredControllers(this.controllerInstance);
|
|
|
|
// Hook: $onInit
|
|
if (this.controllerInstance && isFunction(this.controllerInstance.$onInit)) {
|
|
this.controllerInstance.$onInit();
|
|
}
|
|
|
|
// Linking
|
|
const link = this.directive.link;
|
|
const preLink = (typeof link == 'object') && (link as angular.IDirectivePrePost).pre;
|
|
const postLink = (typeof link == 'object') ? (link as angular.IDirectivePrePost).post : link;
|
|
const attrs: angular.IAttributes = NOT_SUPPORTED;
|
|
const transcludeFn: angular.ITranscludeFunction = NOT_SUPPORTED;
|
|
if (preLink) {
|
|
preLink(this.componentScope, this.$element, attrs, requiredControllers, transcludeFn);
|
|
}
|
|
|
|
linkFn(this.componentScope, null !, {parentBoundTranscludeFn: attachChildNodes});
|
|
|
|
if (postLink) {
|
|
postLink(this.componentScope, this.$element, attrs, requiredControllers, transcludeFn);
|
|
}
|
|
|
|
// Hook: $postLink
|
|
if (this.controllerInstance && isFunction(this.controllerInstance.$postLink)) {
|
|
this.controllerInstance.$postLink();
|
|
}
|
|
}
|
|
|
|
ngOnChanges(changes: SimpleChanges) {
|
|
const ng1Changes: any = {};
|
|
Object.keys(changes).forEach(name => {
|
|
const change: SimpleChange = changes[name];
|
|
this.setComponentProperty(name, change.currentValue);
|
|
ng1Changes[this.propertyMap[name]] = change;
|
|
});
|
|
|
|
if (isFunction(this.destinationObj !.$onChanges)) {
|
|
this.destinationObj !.$onChanges !(ng1Changes);
|
|
}
|
|
}
|
|
|
|
ngDoCheck() {
|
|
const destinationObj = this.destinationObj;
|
|
const lastValues = this.checkLastValues;
|
|
const checkProperties = this.checkProperties;
|
|
const propOuts = this.propOuts;
|
|
checkProperties.forEach((propName, i) => {
|
|
const value = destinationObj ![propName];
|
|
const last = lastValues[i];
|
|
if (!strictEquals(last, value)) {
|
|
const eventEmitter: EventEmitter<any> = (this as any)[propOuts[i]];
|
|
eventEmitter.emit(lastValues[i] = value);
|
|
}
|
|
});
|
|
|
|
if (this.controllerInstance && isFunction(this.controllerInstance.$doCheck)) {
|
|
this.controllerInstance.$doCheck();
|
|
}
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
if (this.controllerInstance && isFunction(this.controllerInstance.$onDestroy)) {
|
|
this.controllerInstance.$onDestroy();
|
|
}
|
|
}
|
|
|
|
setComponentProperty(name: string, value: any) {
|
|
this.destinationObj ![this.propertyMap[name]] = value;
|
|
}
|
|
}
|