angular/packages/upgrade/src/common/downgrade_component_adapter.ts
2017-12-12 11:56:06 -08:00

291 lines
11 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 {ApplicationRef, ChangeDetectorRef, ComponentFactory, ComponentRef, EventEmitter, Injector, OnChanges, SimpleChange, SimpleChanges, StaticProvider, Testability, TestabilityRegistry, Type} from '@angular/core';
import * as angular from './angular1';
import {PropertyBinding} from './component_info';
import {$SCOPE} from './constants';
import {getAttributesAsArray, getComponentName, hookupNgModel, strictEquals} from './util';
const INITIAL_VALUE = {
__UNINITIALIZED__: true
};
export class DowngradeComponentAdapter {
private implementsOnChanges = false;
private inputChangeCount: number = 0;
private inputChanges: SimpleChanges = {};
private componentScope: angular.IScope;
private componentRef: ComponentRef<any>;
private component: any;
private changeDetector: ChangeDetectorRef;
constructor(
private element: angular.IAugmentedJQuery, private attrs: angular.IAttributes,
private scope: angular.IScope, private ngModel: angular.INgModelController,
private parentInjector: Injector, private $injector: angular.IInjectorService,
private $compile: angular.ICompileService, private $parse: angular.IParseService,
private componentFactory: ComponentFactory<any>,
private wrapCallback: <T>(cb: () => T) => () => T) {
this.componentScope = scope.$new();
}
compileContents(): Node[][] {
const compiledProjectableNodes: Node[][] = [];
const projectableNodes: Node[][] = this.groupProjectableNodes();
const linkFns = projectableNodes.map(nodes => this.$compile(nodes));
this.element.empty !();
linkFns.forEach(linkFn => {
linkFn(this.scope, (clone: Node[]) => {
compiledProjectableNodes.push(clone);
this.element.append !(clone);
});
});
return compiledProjectableNodes;
}
createComponent(projectableNodes: Node[][]) {
const providers: StaticProvider[] = [{provide: $SCOPE, useValue: this.componentScope}];
const childInjector = Injector.create(
{providers: providers, parent: this.parentInjector, name: 'DowngradeComponentAdapter'});
this.componentRef =
this.componentFactory.create(childInjector, projectableNodes, this.element[0]);
this.changeDetector = this.componentRef.changeDetectorRef;
this.component = this.componentRef.instance;
// testability hook is commonly added during component bootstrap in
// packages/core/src/application_ref.bootstrap()
// in downgraded application, component creation will take place here as well as adding the
// testability hook.
const testability = this.componentRef.injector.get(Testability, null);
if (testability) {
this.componentRef.injector.get(TestabilityRegistry)
.registerApplication(this.componentRef.location.nativeElement, testability);
}
hookupNgModel(this.ngModel, this.component);
}
setupInputs(needsNgZone: boolean, propagateDigest = true): void {
const attrs = this.attrs;
const inputs = this.componentFactory.inputs || [];
for (let i = 0; i < inputs.length; i++) {
const input = new PropertyBinding(inputs[i].propName, inputs[i].templateName);
let expr: string|null = null;
if (attrs.hasOwnProperty(input.attr)) {
const observeFn = (prop => {
let prevValue = INITIAL_VALUE;
return (currValue: any) => {
// Initially, both `$observe()` and `$watch()` will call this function.
if (!strictEquals(prevValue, currValue)) {
if (prevValue === INITIAL_VALUE) {
prevValue = currValue;
}
this.updateInput(prop, prevValue, currValue);
prevValue = currValue;
}
};
})(input.prop);
attrs.$observe(input.attr, observeFn);
// Use `$watch()` (in addition to `$observe()`) in order to initialize the input in time
// for `ngOnChanges()`. This is necessary if we are already in a `$digest`, which means that
// `ngOnChanges()` (which is called by a watcher) will run before the `$observe()` callback.
let unwatch: Function|null = this.componentScope.$watch(() => {
unwatch !();
unwatch = null;
observeFn(attrs[input.attr]);
});
} else if (attrs.hasOwnProperty(input.bindAttr)) {
expr = attrs[input.bindAttr];
} else if (attrs.hasOwnProperty(input.bracketAttr)) {
expr = attrs[input.bracketAttr];
} else if (attrs.hasOwnProperty(input.bindonAttr)) {
expr = attrs[input.bindonAttr];
} else if (attrs.hasOwnProperty(input.bracketParenAttr)) {
expr = attrs[input.bracketParenAttr];
}
if (expr != null) {
const watchFn =
(prop => (currValue: any, prevValue: any) =>
this.updateInput(prop, prevValue, currValue))(input.prop);
this.componentScope.$watch(expr, watchFn);
}
}
// Invoke `ngOnChanges()` and Change Detection (when necessary)
const detectChanges = () => this.changeDetector.detectChanges();
const prototype = this.componentFactory.componentType.prototype;
this.implementsOnChanges = !!(prototype && (<OnChanges>prototype).ngOnChanges);
this.componentScope.$watch(() => this.inputChangeCount, this.wrapCallback(() => {
// Invoke `ngOnChanges()`
if (this.implementsOnChanges) {
const inputChanges = this.inputChanges;
this.inputChanges = {};
(<OnChanges>this.component).ngOnChanges(inputChanges !);
}
// If opted out of propagating digests, invoke change detection when inputs change.
if (!propagateDigest) {
detectChanges();
}
}));
// If not opted out of propagating digests, invoke change detection on every digest
if (propagateDigest) {
this.componentScope.$watch(this.wrapCallback(detectChanges));
}
// If necessary, attach the view so that it will be dirty-checked.
// (Allow time for the initial input values to be set and `ngOnChanges()` to be called.)
if (needsNgZone || !propagateDigest) {
let unwatch: Function|null = this.componentScope.$watch(() => {
unwatch !();
unwatch = null;
const appRef = this.parentInjector.get<ApplicationRef>(ApplicationRef);
appRef.attachView(this.componentRef.hostView);
});
}
}
setupOutputs() {
const attrs = this.attrs;
const outputs = this.componentFactory.outputs || [];
for (let j = 0; j < outputs.length; j++) {
const output = new PropertyBinding(outputs[j].propName, outputs[j].templateName);
let expr: string|null = null;
let assignExpr = false;
const bindonAttr = output.bindonAttr.substring(0, output.bindonAttr.length - 6);
const bracketParenAttr =
`[(${output.bracketParenAttr.substring(2, output.bracketParenAttr.length - 8)})]`;
if (attrs.hasOwnProperty(output.onAttr)) {
expr = attrs[output.onAttr];
} else if (attrs.hasOwnProperty(output.parenAttr)) {
expr = attrs[output.parenAttr];
} else if (attrs.hasOwnProperty(bindonAttr)) {
expr = attrs[bindonAttr];
assignExpr = true;
} else if (attrs.hasOwnProperty(bracketParenAttr)) {
expr = attrs[bracketParenAttr];
assignExpr = true;
}
if (expr != null && assignExpr != null) {
const getter = this.$parse(expr);
const setter = getter.assign;
if (assignExpr && !setter) {
throw new Error(`Expression '${expr}' is not assignable!`);
}
const emitter = this.component[output.prop] as EventEmitter<any>;
if (emitter) {
emitter.subscribe({
next: assignExpr ? (v: any) => setter !(this.scope, v) :
(v: any) => getter(this.scope, {'$event': v})
});
} else {
throw new Error(
`Missing emitter '${output.prop}' on component '${getComponentName(this.componentFactory.componentType)}'!`);
}
}
}
}
registerCleanup() {
const destroyComponentRef = this.wrapCallback(() => this.componentRef.destroy());
this.element.on !('$destroy', () => {
this.componentScope.$destroy();
this.componentRef.injector.get(TestabilityRegistry)
.unregisterApplication(this.componentRef.location.nativeElement);
destroyComponentRef();
});
}
getInjector(): Injector { return this.componentRef.injector; }
private updateInput(prop: string, prevValue: any, currValue: any) {
if (this.implementsOnChanges) {
this.inputChanges[prop] = new SimpleChange(prevValue, currValue, prevValue === currValue);
}
this.inputChangeCount++;
this.component[prop] = currValue;
}
groupProjectableNodes() {
let ngContentSelectors = this.componentFactory.ngContentSelectors;
return groupNodesBySelector(ngContentSelectors, this.element.contents !());
}
}
/**
* Group a set of DOM nodes into `ngContent` groups, based on the given content selectors.
*/
export function groupNodesBySelector(ngContentSelectors: string[], nodes: Node[]): Node[][] {
const projectableNodes: Node[][] = [];
let wildcardNgContentIndex: number;
for (let i = 0, ii = ngContentSelectors.length; i < ii; ++i) {
projectableNodes[i] = [];
}
for (let j = 0, jj = nodes.length; j < jj; ++j) {
const node = nodes[j];
const ngContentIndex = findMatchingNgContentIndex(node, ngContentSelectors);
if (ngContentIndex != null) {
projectableNodes[ngContentIndex].push(node);
}
}
return projectableNodes;
}
function findMatchingNgContentIndex(element: any, ngContentSelectors: string[]): number|null {
const ngContentIndices: number[] = [];
let wildcardNgContentIndex: number = -1;
for (let i = 0; i < ngContentSelectors.length; i++) {
const selector = ngContentSelectors[i];
if (selector === '*') {
wildcardNgContentIndex = i;
} else {
if (matchesSelector(element, selector)) {
ngContentIndices.push(i);
}
}
}
ngContentIndices.sort();
if (wildcardNgContentIndex !== -1) {
ngContentIndices.push(wildcardNgContentIndex);
}
return ngContentIndices.length ? ngContentIndices[0] : null;
}
let _matches: (this: any, selector: string) => boolean;
function matchesSelector(el: any, selector: string): boolean {
if (!_matches) {
const elProto = <any>Element.prototype;
_matches = elProto.matches || elProto.matchesSelector || elProto.mozMatchesSelector ||
elProto.msMatchesSelector || elProto.oMatchesSelector || elProto.webkitMatchesSelector;
}
return el.nodeType === Node.ELEMENT_NODE ? _matches.call(el, selector) : false;
}