refactor(upgrade): use shared code in `downgradeNg2Component()` (#14037)

This unified the implementations of dynamic's `downgradeNg2Component()` and
static's `downgradeComponent()`.
This commit is contained in:
Georgios Kalpakas 2017-01-19 13:04:24 +02:00 committed by Miško Hevery
parent 1367cd9569
commit ea63676970
16 changed files with 247 additions and 515 deletions

View File

@ -309,23 +309,12 @@ export class PlatformRef_ extends PlatformRef {
}
private _bootstrapModuleWithZone<M>(
moduleType: Type<M>, compilerOptions: CompilerOptions|CompilerOptions[] = [], ngZone: NgZone,
componentFactoryCallback?: any): Promise<NgModuleRef<M>> {
moduleType: Type<M>, compilerOptions: CompilerOptions|CompilerOptions[] = [],
ngZone: NgZone): Promise<NgModuleRef<M>> {
const compilerFactory: CompilerFactory = this.injector.get(CompilerFactory);
const compiler = compilerFactory.createCompiler(
Array.isArray(compilerOptions) ? compilerOptions : [compilerOptions]);
// ugly internal api hack: generate host component factories for all declared components and
// pass the factories into the callback - this is used by UpdateAdapter to get hold of all
// factories.
if (componentFactoryCallback) {
return compiler.compileModuleAndAllComponentsAsync(moduleType)
.then(({ngModuleFactory, componentFactories}) => {
componentFactoryCallback(componentFactories);
return this._bootstrapModuleFactoryWithZone(ngModuleFactory, ngZone);
});
}
return compiler.compileModuleAsync(moduleType)
.then((moduleFactory) => this._bootstrapModuleFactoryWithZone(moduleFactory, ngZone));
}

View File

@ -21,7 +21,7 @@ export const $TEMPLATE_REQUEST = '$templateRequest';
export const $$TESTABILITY = '$$testability';
export const COMPILER_KEY = '$$angularCompiler';
export const COMPONENT_FACTORY_REF_MAP_KEY = '$$angularComponentFactoryRefMap';
export const GROUP_PROJECTABLE_NODES_KEY = '$$angularGroupProjectableNodes';
export const INJECTOR_KEY = '$$angularInjector';
export const NG_ZONE_KEY = '$$angularNgZone';

View File

@ -0,0 +1,20 @@
/**
* @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 {Type} from '@angular/core';
import * as angular from './angular1';
export class ContentProjectionHelper {
groupProjectableNodes($injector: angular.IInjectorService, component: Type<any>, nodes: Node[]):
Node[][] {
// By default, do not support multi-slot projection,
// as `upgrade/static` does not support it yet.
return [nodes];
}
}

View File

@ -11,7 +11,7 @@ import {ComponentFactory, ComponentFactoryResolver, Injector, Type} from '@angul
import * as angular from './angular1';
import {$COMPILE, $INJECTOR, $PARSE, INJECTOR_KEY, REQUIRE_INJECTOR, REQUIRE_NG_MODEL} from './constants';
import {DowngradeComponentAdapter} from './downgrade_component_adapter';
import {controllerKey} from './util';
import {controllerKey, getComponentName} from './util';
let downgradeCount = 0;
@ -86,7 +86,8 @@ export function downgradeComponent(info: /* ComponentInfo */ {
// triggered by `UpgradeNg1ComponentAdapterBuilder`, before the Angular templates have
// been compiled.
const parentInjector: Injector | ParentInjectorPromise = required[0] || $injector.get(INJECTOR_KEY);
const parentInjector: Injector|ParentInjectorPromise =
required[0] || $injector.get(INJECTOR_KEY);
const ngModel: angular.INgModelController = required[1];
const downgradeFn = (injector: Injector) => {
@ -96,13 +97,14 @@ export function downgradeComponent(info: /* ComponentInfo */ {
componentFactoryResolver.resolveComponentFactory(info.component);
if (!componentFactory) {
throw new Error('Expecting ComponentFactory for: ' + info.component);
throw new Error('Expecting ComponentFactory for: ' + getComponentName(info.component));
}
const id = idPrefix + (idCount++);
const injectorPromise = new ParentInjectorPromise(element);
const facade = new DowngradeComponentAdapter(
id, info, element, attrs, scope, ngModel, injector, $compile, $parse, componentFactory);
id, info, element, attrs, scope, ngModel, injector, $injector, $compile, $parse,
componentFactory);
const projectableNodes = facade.compileContents();
facade.createComponent(projectableNodes);

View File

@ -11,7 +11,8 @@ import {ChangeDetectorRef, ComponentFactory, ComponentRef, EventEmitter, Injecto
import * as angular from './angular1';
import {ComponentInfo, PropertyBinding} from './component_info';
import {$SCOPE} from './constants';
import {hookupNgModel} from './util';
import {ContentProjectionHelper} from './content_projection_helper';
import {getComponentName, hookupNgModel} from './util';
const INITIAL_VALUE = {
__UNINITIALIZED__: true
@ -29,24 +30,32 @@ export class DowngradeComponentAdapter {
private id: string, private info: ComponentInfo, private element: angular.IAugmentedJQuery,
private attrs: angular.IAttributes, private scope: angular.IScope,
private ngModel: angular.INgModelController, private parentInjector: Injector,
private $compile: angular.ICompileService, private $parse: angular.IParseService,
private componentFactory: ComponentFactory<any>) {
private $injector: angular.IInjectorService, private $compile: angular.ICompileService,
private $parse: angular.IParseService, private componentFactory: ComponentFactory<any>) {
(this.element[0] as any).id = id;
this.componentScope = scope.$new();
}
compileContents(): Node[][] {
const projectableNodes: Node[][] = [];
const linkFn = this.$compile(this.element.contents());
const compiledProjectableNodes: Node[][] = [];
// The projected content has to be grouped, before it is compiled.
const projectionHelper: ContentProjectionHelper =
this.parentInjector.get(ContentProjectionHelper);
const projectableNodes: Node[][] = projectionHelper.groupProjectableNodes(
this.$injector, this.info.component, this.element.contents());
const linkFns = projectableNodes.map(nodes => this.$compile(nodes));
this.element.empty();
linkFn(this.scope, (clone: Node[]) => {
projectableNodes.push(clone);
this.element.append(clone);
linkFns.forEach(linkFn => {
linkFn(this.scope, (clone: Node[]) => {
compiledProjectableNodes.push(clone);
this.element.append(clone);
});
});
return projectableNodes;
return compiledProjectableNodes;
}
createComponent(projectableNodes: Node[][]) {
@ -162,7 +171,7 @@ export class DowngradeComponentAdapter {
});
} else {
throw new Error(
`Missing emitter '${output.prop}' on component '${this.info.component}'!`);
`Missing emitter '${output.prop}' on component '${getComponentName(this.info.component)}'!`);
}
}
}

View File

@ -6,7 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as angular from './angular_js';
import {Type} from '@angular/core';
import * as angular from './angular1';
export function onError(e: any) {
// TODO: (misko): We seem to not have a stack trace here!
@ -36,6 +37,11 @@ export function getAttributesAsArray(node: Node): [string, string][] {
return asArray || [];
}
export function getComponentName(component: Type<any>): string {
// Return the name of the component or the first line of its stringified version.
return (component as any).overriddenName || component.name || component.toString().split('\n')[0];
}
export class Deferred<R> {
promise: Promise<R>;
resolve: (value?: R|PromiseLike<R>) => void;
@ -50,8 +56,9 @@ export class Deferred<R> {
}
/**
* @return true if the passed-in component implements the subset of
* ControlValueAccessor needed for AngularJS ng-model compatibility.
* @return Whether the passed-in component implements the subset of the
* `ControlValueAccessor` interface needed for AngularJS `ng-model`
* compatibility.
*/
function supportsNgModel(component: any) {
return typeof component.writeValue === 'function' &&
@ -59,8 +66,8 @@ function supportsNgModel(component: any) {
}
/**
* Glue the AngularJS ngModelController if it exists to the component if it
* implements the needed subset of ControlValueAccessor.
* Glue the AngularJS `NgModelController` (if it exists) to the component
* (if it implements the needed subset of the `ControlValueAccessor` interface).
*/
export function hookupNgModel(ngModel: angular.INgModelController, component: any) {
if (ngModel && supportsNgModel(component)) {

View File

@ -0,0 +1,70 @@
/**
* @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 {CssSelector, SelectorMatcher, createElementCssSelector} from '@angular/compiler';
import {Compiler, Type} from '@angular/core';
import * as angular from '../common/angular1';
import {COMPILER_KEY} from '../common/constants';
import {ContentProjectionHelper} from '../common/content_projection_helper';
import {getAttributesAsArray, getComponentName} from '../common/util';
export class DynamicContentProjectionHelper extends ContentProjectionHelper {
groupProjectableNodes($injector: angular.IInjectorService, component: Type<any>, nodes: Node[]):
Node[][] {
const ng2Compiler = $injector.get(COMPILER_KEY) as Compiler;
const ngContentSelectors = ng2Compiler.getNgContentSelectors(component);
if (!ngContentSelectors) {
throw new Error('Expecting ngContentSelectors for: ' + getComponentName(component));
}
return this.groupNodesBySelector(ngContentSelectors, nodes);
}
/**
* Group a set of DOM nodes into `ngContent` groups, based on the given content selectors.
*/
groupNodesBySelector(ngContentSelectors: string[], nodes: Node[]): Node[][] {
const projectableNodes: Node[][] = [];
let matcher = new SelectorMatcher();
let wildcardNgContentIndex: number;
for (let i = 0, ii = ngContentSelectors.length; i < ii; ++i) {
projectableNodes[i] = [];
const selector = ngContentSelectors[i];
if (selector === '*') {
wildcardNgContentIndex = i;
} else {
matcher.addSelectables(CssSelector.parse(selector), i);
}
}
for (let j = 0, jj = nodes.length; j < jj; ++j) {
const ngContentIndices: number[] = [];
const node = nodes[j];
const selector =
createElementCssSelector(node.nodeName.toLowerCase(), getAttributesAsArray(node));
matcher.match(selector, (_, index) => ngContentIndices.push(index));
ngContentIndices.sort();
if (wildcardNgContentIndex !== undefined) {
ngContentIndices.push(wildcardNgContentIndex);
}
if (ngContentIndices.length) {
projectableNodes[ngContentIndices[0]].push(node);
}
}
return projectableNodes;
}
}

View File

@ -1,159 +0,0 @@
/**
* @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 {ChangeDetectorRef, ComponentFactory, ComponentRef, EventEmitter, Injector, OnChanges, ReflectiveInjector, SimpleChange, SimpleChanges} from '@angular/core';
import * as angular from '../common/angular1';
import {$SCOPE} from '../common/constants';
import {ComponentInfo} from './metadata';
import {hookupNgModel} from './util';
const INITIAL_VALUE = {
__UNINITIALIZED__: true
};
export class DowngradeNg2ComponentAdapter {
component: any = null;
inputChangeCount: number = 0;
inputChanges: SimpleChanges = null;
componentRef: ComponentRef<any> = null;
changeDetector: ChangeDetectorRef = null;
componentScope: angular.IScope;
constructor(
private info: ComponentInfo, private element: angular.IAugmentedJQuery,
private attrs: angular.IAttributes, private scope: angular.IScope,
private ngModel: angular.INgModelController, private parentInjector: Injector,
private parse: angular.IParseService, private componentFactory: ComponentFactory<any>) {
this.componentScope = scope.$new();
}
bootstrapNg2(projectableNodes: Node[][]) {
const childInjector = ReflectiveInjector.resolveAndCreate(
[{provide: $SCOPE, useValue: this.componentScope}], this.parentInjector);
this.componentRef =
this.componentFactory.create(childInjector, projectableNodes, this.element[0]);
this.changeDetector = this.componentRef.changeDetectorRef;
this.component = this.componentRef.instance;
hookupNgModel(this.ngModel, this.component);
}
setupInputs(): void {
const attrs = this.attrs;
const inputs = this.info.inputs || [];
for (let i = 0; i < inputs.length; i++) {
const input = inputs[i];
let expr: any /** TODO #9100 */ = null;
if (attrs.hasOwnProperty(input.attr)) {
const observeFn = ((prop: any /** TODO #9100 */) => {
let prevValue = INITIAL_VALUE;
return (value: any /** TODO #9100 */) => {
if (this.inputChanges !== null) {
this.inputChangeCount++;
this.inputChanges[prop] = new SimpleChange(
value, prevValue === INITIAL_VALUE ? value : prevValue,
prevValue === INITIAL_VALUE);
prevValue = value;
}
this.component[prop] = value;
};
})(input.prop);
attrs.$observe(input.attr, observeFn);
} else if (attrs.hasOwnProperty(input.bindAttr)) {
expr = (attrs as any /** TODO #9100 */)[input.bindAttr];
} else if (attrs.hasOwnProperty(input.bracketAttr)) {
expr = (attrs as any /** TODO #9100 */)[input.bracketAttr];
} else if (attrs.hasOwnProperty(input.bindonAttr)) {
expr = (attrs as any /** TODO #9100 */)[input.bindonAttr];
} else if (attrs.hasOwnProperty(input.bracketParenAttr)) {
expr = (attrs as any /** TODO #9100 */)[input.bracketParenAttr];
}
if (expr != null) {
const watchFn =
((prop: any /** TODO #9100 */) => (
value: any /** TODO #9100 */, prevValue: any /** TODO #9100 */) => {
if (this.inputChanges != null) {
this.inputChangeCount++;
this.inputChanges[prop] = new SimpleChange(prevValue, value, prevValue === value);
}
this.component[prop] = value;
})(input.prop);
this.componentScope.$watch(expr, watchFn);
}
}
const prototype = this.info.type.prototype;
if (prototype && (<OnChanges>prototype).ngOnChanges) {
// Detect: OnChanges interface
this.inputChanges = {};
this.componentScope.$watch(() => this.inputChangeCount, () => {
const inputChanges = this.inputChanges;
this.inputChanges = {};
(<OnChanges>this.component).ngOnChanges(inputChanges);
});
}
this.componentScope.$watch(() => this.changeDetector && this.changeDetector.detectChanges());
}
setupOutputs() {
const attrs = this.attrs;
const outputs = this.info.outputs || [];
for (let j = 0; j < outputs.length; j++) {
const output = outputs[j];
let expr: any /** TODO #9100 */ = null;
let assignExpr = false;
const bindonAttr =
output.bindonAttr ? output.bindonAttr.substring(0, output.bindonAttr.length - 6) : null;
const bracketParenAttr = output.bracketParenAttr ?
`[(${output.bracketParenAttr.substring(2, output.bracketParenAttr.length - 8)})]` :
null;
if (attrs.hasOwnProperty(output.onAttr)) {
expr = (attrs as any /** TODO #9100 */)[output.onAttr];
} else if (attrs.hasOwnProperty(output.parenAttr)) {
expr = (attrs as any /** TODO #9100 */)[output.parenAttr];
} else if (attrs.hasOwnProperty(bindonAttr)) {
expr = (attrs as any /** TODO #9100 */)[bindonAttr];
assignExpr = true;
} else if (attrs.hasOwnProperty(bracketParenAttr)) {
expr = (attrs as any /** TODO #9100 */)[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 ?
((setter: any) => (v: any /** TODO #9100 */) => setter(this.scope, v))(setter) :
((getter: any) => (v: any /** TODO #9100 */) =>
getter(this.scope, {$event: v}))(getter)
});
} else {
throw new Error(`Missing emitter '${output.prop}' on component '${this.info.selector}'!`);
}
}
}
}
registerCleanup() {
this.element.bind('$destroy', () => {
this.componentScope.$destroy();
this.componentRef.destroy();
});
}
}

View File

@ -1,40 +0,0 @@
/**
* @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 {DirectiveResolver} from '@angular/compiler';
import {Directive, Type} from '@angular/core';
import {PropertyBinding} from '../common/component_info';
const COMPONENT_SELECTOR = /^[\w|-]*$/;
const SKEWER_CASE = /-(\w)/g;
const directiveResolver = new DirectiveResolver();
export interface ComponentInfo {
type: Type<any>;
selector: string;
inputs?: PropertyBinding[];
outputs?: PropertyBinding[];
}
export function getComponentInfo(type: Type<any>): ComponentInfo {
const resolvedMetadata: Directive = directiveResolver.resolve(type);
const selector = resolvedMetadata.selector;
return {
type,
selector,
inputs: parseFields(resolvedMetadata.inputs),
outputs: parseFields(resolvedMetadata.outputs)
};
}
export function parseFields(bindings: string[]): PropertyBinding[] {
return (bindings || []).map(binding => new PropertyBinding(binding));
}

View File

@ -6,17 +6,19 @@
* found in the LICENSE file at https://angular.io/license
*/
import {CssSelector, SelectorMatcher, createElementCssSelector} from '@angular/compiler';
import {Compiler, CompilerOptions, ComponentFactory, Injector, NgModule, NgModuleRef, NgZone, Provider, Testability, Type} from '@angular/core';
import {DirectiveResolver} from '@angular/compiler';
import {Compiler, CompilerOptions, Directive, Injector, NgModule, NgModuleRef, NgZone, Provider, Testability, Type} from '@angular/core';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import * as angular from '../common/angular1';
import {$$TESTABILITY, $COMPILE, $INJECTOR, $PARSE, $ROOT_SCOPE, COMPILER_KEY, COMPONENT_FACTORY_REF_MAP_KEY, INJECTOR_KEY, NG_ZONE_KEY, REQUIRE_INJECTOR, REQUIRE_NG_MODEL} from '../common/constants';
import {ComponentInfo} from '../common/component_info';
import {$$TESTABILITY, $COMPILE, $INJECTOR, $ROOT_SCOPE, COMPILER_KEY, INJECTOR_KEY, NG_ZONE_KEY} from '../common/constants';
import {ContentProjectionHelper} from '../common/content_projection_helper';
import {downgradeComponent} from '../common/downgrade_component';
import {downgradeInjectable} from '../common/downgrade_injectable';
import {Deferred, controllerKey, getAttributesAsArray, onError} from '../common/util';
import {Deferred, controllerKey, onError} from '../common/util';
import {DowngradeNg2ComponentAdapter} from './downgrade_ng2_adapter';
import {ComponentInfo, getComponentInfo} from './metadata';
import {DynamicContentProjectionHelper} from './content_projection_helper';
import {UpgradeNg1ComponentAdapterBuilder} from './upgrade_ng1_adapter';
let upgradeCount: number = 0;
@ -102,7 +104,8 @@ let upgradeCount: number = 0;
*/
export class UpgradeAdapter {
private idPrefix: string = `NG2_UPGRADE_${upgradeCount++}_`;
private upgradedComponents: Type<any>[] = [];
private directiveResolver: DirectiveResolver = new DirectiveResolver();
private downgradedComponents: Type<any>[] = [];
/**
* An internal map of ng1 components which need to up upgraded to ng2.
*
@ -184,10 +187,13 @@ export class UpgradeAdapter {
* });
* ```
*/
downgradeNg2Component(type: Type<any>): Function {
this.upgradedComponents.push(type);
const info: ComponentInfo = getComponentInfo(type);
return ng1ComponentDirective(info, `${this.idPrefix}${info.selector}_c`);
downgradeNg2Component(component: Type<any>): Function {
this.downgradedComponents.push(component);
const metadata: Directive = this.directiveResolver.resolve(component);
const info: ComponentInfo = {component, inputs: metadata.inputs, outputs: metadata.outputs};
return downgradeComponent(info);
}
/**
@ -490,7 +496,6 @@ export class UpgradeAdapter {
let original$applyFn: Function;
let rootScopePrototype: any;
let rootScope: angular.IRootScopeService;
const componentFactoryRefMap: ComponentFactoryRefMap = {};
const upgradeAdapter = this;
const ng1Module = this.ng1Module = angular.module(this.idPrefix, modules);
const platformRef = platformBrowserDynamic();
@ -499,7 +504,6 @@ export class UpgradeAdapter {
this.ng2BootstrapDeferred = new Deferred();
ng1Module.factory(INJECTOR_KEY, () => this.moduleRef.injector.get(Injector))
.constant(NG_ZONE_KEY, this.ngZone)
.constant(COMPONENT_FACTORY_REF_MAP_KEY, componentFactoryRefMap)
.factory(COMPILER_KEY, () => this.moduleRef.injector.get(Compiler))
.config([
'$provide', '$injector',
@ -557,25 +561,18 @@ export class UpgradeAdapter {
providers: [
{provide: $INJECTOR, useFactory: () => ng1Injector},
{provide: $COMPILE, useFactory: () => ng1Injector.get($COMPILE)},
{provide: ContentProjectionHelper, useClass: DynamicContentProjectionHelper},
this.upgradedProviders
],
imports: [this.ng2AppModule]
imports: [this.ng2AppModule],
entryComponents: this.downgradedComponents
}).Class({
constructor: function DynamicNgUpgradeModule() {},
ngDoBootstrap: function() {}
});
(platformRef as any)
._bootstrapModuleWithZone(
DynamicNgUpgradeModule, this.compilerOptions, this.ngZone,
(componentFactories: ComponentFactory<any>[]) => {
componentFactories.forEach((componentFactory) => {
const type: Type<any> = componentFactory.componentType;
if (this.upgradedComponents.indexOf(type) !== -1) {
componentFactoryRefMap[getComponentInfo(type).selector] =
componentFactory;
}
});
})
DynamicNgUpgradeModule, this.compilerOptions, this.ngZone)
.then((ref: NgModuleRef<any>) => {
this.moduleRef = ref;
this.ngZone.run(() => {
@ -603,10 +600,6 @@ export class UpgradeAdapter {
}
}
interface ComponentFactoryRefMap {
[selector: string]: ComponentFactory<any>;
}
/**
* Synchronous promise-like object to wrap parent injectors,
* to preserve the synchronous nature of AngularJS's $compile.
@ -644,88 +637,6 @@ class ParentInjectorPromise {
}
function ng1ComponentDirective(info: ComponentInfo, idPrefix: string): Function {
(<any>directiveFactory).$inject = [$INJECTOR, $COMPILE, COMPONENT_FACTORY_REF_MAP_KEY, $PARSE];
function directiveFactory(
ng1Injector: angular.IInjectorService, ng1Compile: angular.ICompileService,
componentFactoryRefMap: ComponentFactoryRefMap,
parse: angular.IParseService): angular.IDirective {
let idCount = 0;
let dashSelector = info.selector.replace(/[A-Z]/g, char => '-' + char.toLowerCase());
return {
restrict: 'E',
terminal: true,
require: [REQUIRE_INJECTOR, REQUIRE_NG_MODEL],
compile: (templateElement: angular.IAugmentedJQuery, templateAttributes: angular.IAttributes,
transclude: angular.ITranscludeFunction) => {
// We might have compile the contents lazily, because this might have been triggered by the
// UpgradeNg1ComponentAdapterBuilder, when the ng2 templates have not been compiled yet
return {
post: (scope: angular.IScope, element: angular.IAugmentedJQuery,
attrs: angular.IAttributes, required: any[],
transclude: angular.ITranscludeFunction): void => {
let id = idPrefix + (idCount++);
(<any>element[0]).id = id;
let parentInjector: Injector | ParentInjectorPromise = required[0];
let injectorPromise = new ParentInjectorPromise(element);
const ngModel: angular.INgModelController = required[1];
const ng2Compiler = ng1Injector.get(COMPILER_KEY) as Compiler;
const ngContentSelectors = ng2Compiler.getNgContentSelectors(info.type);
const linkFns = compileProjectedNodes(templateElement, ngContentSelectors);
const componentFactory: ComponentFactory<any> = componentFactoryRefMap[info.selector];
if (!componentFactory)
throw new Error('Expecting ComponentFactory for: ' + info.selector);
element.empty();
let projectableNodes = linkFns.map(link => {
let projectedClone: Node[];
link(scope, (clone: Node[]) => {
projectedClone = clone;
element.append(clone);
});
return projectedClone;
});
parentInjector = parentInjector || ng1Injector.get(INJECTOR_KEY);
if (parentInjector instanceof ParentInjectorPromise) {
parentInjector.then((resolvedInjector: Injector) => downgrade(resolvedInjector));
} else {
downgrade(parentInjector);
}
function downgrade(injector: Injector) {
const facade = new DowngradeNg2ComponentAdapter(
info, element, attrs, scope, ngModel, injector, parse, componentFactory);
facade.bootstrapNg2(projectableNodes);
facade.setupInputs();
facade.setupOutputs();
facade.registerCleanup();
injectorPromise.resolve(facade.componentRef.injector);
}
}
};
}
};
function compileProjectedNodes(
templateElement: angular.IAugmentedJQuery,
ngContentSelectors: string[]): angular.ILinkFn[] {
if (!ngContentSelectors)
throw new Error('Expecting ngContentSelectors for: ' + info.selector);
// We have to sort the projected content before we compile it, hence the terminal: true
let projectableTemplateNodes =
sortProjectableNodes(ngContentSelectors, templateElement.contents());
return projectableTemplateNodes.map(nodes => ng1Compile(nodes));
}
}
return directiveFactory;
}
/**
* Use `UpgradeAdapterRef` to control a hybrid AngularJS / Angular application.
*
@ -766,36 +677,3 @@ export class UpgradeAdapterRef {
this.ng2ModuleRef.destroy();
}
}
/**
* Sort a set of DOM nodes that into groups based on the given content selectors
*/
export function sortProjectableNodes(ngContentSelectors: string[], childNodes: Node[]): Node[][] {
let projectableNodes: Node[][] = [];
let matcher = new SelectorMatcher();
let wildcardNgContentIndex: number;
for (let i = 0, ii = ngContentSelectors.length; i < ii; i++) {
projectableNodes[i] = [];
if (ngContentSelectors[i] === '*') {
wildcardNgContentIndex = i;
} else {
matcher.addSelectables(CssSelector.parse(ngContentSelectors[i]), i);
}
}
for (let node of childNodes) {
let ngContentIndices: number[] = [];
let selector =
createElementCssSelector(node.nodeName.toLowerCase(), getAttributesAsArray(node));
matcher.match(
selector, (selector, ngContentIndex) => { ngContentIndices.push(ngContentIndex); });
ngContentIndices.sort();
if (wildcardNgContentIndex !== undefined) {
ngContentIndices.push(wildcardNgContentIndex);
}
if (ngContentIndices.length > 0) {
projectableNodes[ngContentIndices[0]].push(node);
}
}
return projectableNodes;
}

View File

@ -10,6 +10,7 @@ import {Injector, NgModule, NgZone, Testability} from '@angular/core';
import * as angular from '../common/angular1';
import {$$TESTABILITY, $DELEGATE, $INJECTOR, $PROVIDE, $ROOT_SCOPE, INJECTOR_KEY, UPGRADE_MODULE_NAME} from '../common/constants';
import {ContentProjectionHelper} from '../common/content_projection_helper';
import {controllerKey} from '../common/util';
import {angular1Providers, setTempInjectorRef} from './angular1_providers';
@ -129,7 +130,7 @@ import {angular1Providers, setTempInjectorRef} from './angular1_providers';
*
* @experimental
*/
@NgModule({providers: angular1Providers})
@NgModule({providers: [angular1Providers, ContentProjectionHelper]})
export class UpgradeModule {
/**
* The AngularJS `$injector` for the upgrade application.

View File

@ -0,0 +1,85 @@
/**
* @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 {DynamicContentProjectionHelper} from '@angular/upgrade/src/dynamic/content_projection_helper';
import {nodes} from './test_helpers';
export function main() {
describe('groupNodesBySelector', () => {
let groupNodesBySelector: (ngContentSelectors: string[], nodes: Node[]) => Node[][];
beforeEach(() => {
const projectionHelper = new DynamicContentProjectionHelper();
groupNodesBySelector = projectionHelper.groupNodesBySelector.bind(projectionHelper);
});
it('should return an array of node collections for each selector', () => {
const contentNodes = nodes(
'<div class="x"><span>div-1 content</span></div>' +
'<input type="number" name="myNum">' +
'<input type="date" name="myDate">' +
'<span>span content</span>' +
'<div class="x"><span>div-2 content</span></div>');
const selectors = ['input[type=date]', 'span', '.x'];
const projectableNodes = groupNodesBySelector(selectors, contentNodes);
expect(projectableNodes[0]).toEqual(nodes('<input type="date" name="myDate">'));
expect(projectableNodes[1]).toEqual(nodes('<span>span content</span>'));
expect(projectableNodes[2])
.toEqual(nodes(
'<div class="x"><span>div-1 content</span></div>' +
'<div class="x"><span>div-2 content</span></div>'));
});
it('should collect up unmatched nodes for the wildcard selector', () => {
const contentNodes = nodes(
'<div class="x"><span>div-1 content</span></div>' +
'<input type="number" name="myNum">' +
'<input type="date" name="myDate">' +
'<span>span content</span>' +
'<div class="x"><span>div-2 content</span></div>');
const selectors = ['.x', '*', 'input[type=date]'];
const projectableNodes = groupNodesBySelector(selectors, contentNodes);
expect(projectableNodes[0])
.toEqual(nodes(
'<div class="x"><span>div-1 content</span></div>' +
'<div class="x"><span>div-2 content</span></div>'));
expect(projectableNodes[1])
.toEqual(nodes(
'<input type="number" name="myNum">' +
'<span>span content</span>'));
expect(projectableNodes[2]).toEqual(nodes('<input type="date" name="myDate">'));
});
it('should return an array of empty arrays if there are no nodes passed in', () => {
const selectors = ['.x', '*', 'input[type=date]'];
const projectableNodes = groupNodesBySelector(selectors, []);
expect(projectableNodes).toEqual([[], [], []]);
});
it('should return an empty array for each selector that does not match', () => {
const contentNodes = nodes(
'<div class="x"><span>div-1 content</span></div>' +
'<input type="number" name="myNum">' +
'<input type="date" name="myDate">' +
'<span>span content</span>' +
'<div class="x"><span>div-2 content</span></div>');
const noSelectorNodes = groupNodesBySelector([], contentNodes);
expect(noSelectorNodes).toEqual([]);
const noMatchSelectorNodes = groupNodesBySelector(['.not-there'], contentNodes);
expect(noMatchSelectorNodes).toEqual([[]]);
});
});
}

View File

@ -1,65 +0,0 @@
/**
* @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 {Component} from '@angular/core';
import {getComponentInfo, parseFields} from '@angular/upgrade/src/dynamic/metadata';
export function main() {
describe('upgrade metadata', () => {
it('should extract component selector', () => {
expect(getComponentInfo(ElementNameComponent).selector).toBe('element-name-dashed');
});
describe('errors', () => {
it('should throw on missing selector', () => {
expect(() => getComponentInfo(NoAnnotationComponent))
.toThrowError('No Directive annotation found on NoAnnotationComponent');
});
});
describe('parseFields', () => {
it('should process nulls', () => { expect(parseFields(null)).toEqual([]); });
it('should process values', () => {
expect(parseFields([' name ', ' prop : attr '])).toEqual([
jasmine.objectContaining({
prop: 'name',
attr: 'name',
bracketAttr: '[name]',
parenAttr: '(name)',
bracketParenAttr: '[(name)]',
onAttr: 'onName',
bindAttr: 'bindName',
bindonAttr: 'bindonName'
}),
jasmine.objectContaining({
prop: 'prop',
attr: 'attr',
bracketAttr: '[attr]',
parenAttr: '(attr)',
bracketParenAttr: '[(attr)]',
onAttr: 'onAttr',
bindAttr: 'bindAttr',
bindonAttr: 'bindonAttr'
})
]);
});
});
});
}
@Component({selector: 'element-name-dashed', template: ``})
class ElementNameComponent {
}
@Component({selector: '[attr-name]', template: ``})
class AttributeNameComponent {
}
class NoAnnotationComponent {}

View File

@ -7,3 +7,9 @@
*/
export * from '../common/test_helpers';
export function nodes(html: string) {
const div = document.createElement('div');
div.innerHTML = html.trim();
return Array.prototype.slice.call(div.childNodes);
}

View File

@ -11,7 +11,7 @@ import {async, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import * as angular from '@angular/upgrade/src/common/angular1';
import {UpgradeAdapter, UpgradeAdapterRef, sortProjectableNodes} from '@angular/upgrade/src/dynamic/upgrade_adapter';
import {UpgradeAdapter, UpgradeAdapterRef} from '@angular/upgrade/src/dynamic/upgrade_adapter';
import {html, multiTrim} from './test_helpers';
export function main() {
@ -95,9 +95,7 @@ export function main() {
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
adapter.bootstrap(element, ['ng1']).ready((ref) => {
expect((platformRef as any)._bootstrapModuleWithZone)
.toHaveBeenCalledWith(
jasmine.any(Function), {providers: []}, jasmine.any(Object),
jasmine.any(Function));
.toHaveBeenCalledWith(jasmine.any(Function), {providers: []}, jasmine.any(Object));
ref.dispose();
});
}));
@ -1876,73 +1874,4 @@ export function main() {
}));
});
});
describe('sortProjectableNodes', () => {
it('should return an array of node collections for each selector', () => {
const contentNodes = nodes(
'<div class="x"><span>div-1 content</span></div>' +
'<input type="number" name="myNum">' +
'<input type="date" name="myDate">' +
'<span>span content</span>' +
'<div class="x"><span>div-2 content</span></div>');
const selectors = ['input[type=date]', 'span', '.x'];
const projectableNodes = sortProjectableNodes(selectors, contentNodes);
expect(projectableNodes[0]).toEqual(nodes('<input type="date" name="myDate">'));
expect(projectableNodes[1]).toEqual(nodes('<span>span content</span>'));
expect(projectableNodes[2])
.toEqual(nodes(
'<div class="x"><span>div-1 content</span></div>' +
'<div class="x"><span>div-2 content</span></div>'));
});
it('should collect up unmatched nodes for the wildcard selector', () => {
const contentNodes = nodes(
'<div class="x"><span>div-1 content</span></div>' +
'<input type="number" name="myNum">' +
'<input type="date" name="myDate">' +
'<span>span content</span>' +
'<div class="x"><span>div-2 content</span></div>');
const selectors = ['.x', '*', 'input[type=date]'];
const projectableNodes = sortProjectableNodes(selectors, contentNodes);
expect(projectableNodes[0])
.toEqual(nodes(
'<div class="x"><span>div-1 content</span></div>' +
'<div class="x"><span>div-2 content</span></div>'));
expect(projectableNodes[1])
.toEqual(nodes(
'<input type="number" name="myNum">' +
'<span>span content</span>'));
expect(projectableNodes[2]).toEqual(nodes('<input type="date" name="myDate">'));
});
it('should return an array of empty arrays if there are no nodes passed in', () => {
const selectors = ['.x', '*', 'input[type=date]'];
const projectableNodes = sortProjectableNodes(selectors, []);
expect(projectableNodes).toEqual([[], [], []]);
});
it('should return an empty array for each selector that does not match', () => {
const contentNodes = nodes(
'<div class="x"><span>div-1 content</span></div>' +
'<input type="number" name="myNum">' +
'<input type="date" name="myDate">' +
'<span>span content</span>' +
'<div class="x"><span>div-2 content</span></div>');
const noSelectorNodes = sortProjectableNodes([], contentNodes);
expect(noSelectorNodes).toEqual([]);
const noMatchSelectorNodes = sortProjectableNodes(['.not-there'], contentNodes);
expect(noMatchSelectorNodes).toEqual([[]]);
});
});
}
function nodes(html: string) {
const element = document.createElement('div');
element.innerHTML = html;
return Array.prototype.slice.call(element.childNodes);

View File

@ -2,7 +2,7 @@
export declare class UpgradeAdapter {
constructor(ng2AppModule: Type<any>, compilerOptions?: CompilerOptions);
bootstrap(element: Element, modules?: any[], config?: angular.IAngularBootstrapConfig): UpgradeAdapterRef;
downgradeNg2Component(type: Type<any>): Function;
downgradeNg2Component(component: Type<any>): Function;
downgradeNg2Provider(token: any): Function;
registerForNg1Tests(modules?: string[]): UpgradeAdapterRef;
upgradeNg1Component(name: string): Type<any>;