From ea636769709a58276d6ea00f32a583c264d76a5d Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Thu, 19 Jan 2017 13:04:24 +0200 Subject: [PATCH] refactor(upgrade): use shared code in `downgradeNg2Component()` (#14037) This unified the implementations of dynamic's `downgradeNg2Component()` and static's `downgradeComponent()`. --- modules/@angular/core/src/application_ref.ts | 15 +- .../@angular/upgrade/src/common/constants.ts | 2 +- .../src/common/content_projection_helper.ts | 20 +++ .../upgrade/src/common/downgrade_component.ts | 10 +- .../src/common/downgrade_component_adapter.ts | 29 ++-- modules/@angular/upgrade/src/common/util.ts | 17 +- .../src/dynamic/content_projection_helper.ts | 70 ++++++++ .../src/dynamic/downgrade_ng2_adapter.ts | 159 ----------------- .../@angular/upgrade/src/dynamic/metadata.ts | 40 ----- .../upgrade/src/dynamic/upgrade_adapter.ts | 164 +++--------------- .../upgrade/src/static/upgrade_module.ts | 3 +- .../dynamic/group_projectable_nodes_spec.ts | 85 +++++++++ .../upgrade/test/dynamic/metadata_spec.ts | 65 ------- .../upgrade/test/dynamic/test_helpers.ts | 6 + .../upgrade/test/dynamic/upgrade_spec.ts | 75 +------- tools/public_api_guard/upgrade/index.d.ts | 2 +- 16 files changed, 247 insertions(+), 515 deletions(-) create mode 100644 modules/@angular/upgrade/src/common/content_projection_helper.ts create mode 100644 modules/@angular/upgrade/src/dynamic/content_projection_helper.ts delete mode 100644 modules/@angular/upgrade/src/dynamic/downgrade_ng2_adapter.ts delete mode 100644 modules/@angular/upgrade/src/dynamic/metadata.ts create mode 100644 modules/@angular/upgrade/test/dynamic/group_projectable_nodes_spec.ts delete mode 100644 modules/@angular/upgrade/test/dynamic/metadata_spec.ts diff --git a/modules/@angular/core/src/application_ref.ts b/modules/@angular/core/src/application_ref.ts index f89a6611a7..13c0e98425 100644 --- a/modules/@angular/core/src/application_ref.ts +++ b/modules/@angular/core/src/application_ref.ts @@ -309,23 +309,12 @@ export class PlatformRef_ extends PlatformRef { } private _bootstrapModuleWithZone( - moduleType: Type, compilerOptions: CompilerOptions|CompilerOptions[] = [], ngZone: NgZone, - componentFactoryCallback?: any): Promise> { + moduleType: Type, compilerOptions: CompilerOptions|CompilerOptions[] = [], + ngZone: NgZone): Promise> { 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)); } diff --git a/modules/@angular/upgrade/src/common/constants.ts b/modules/@angular/upgrade/src/common/constants.ts index 3ef2802f5a..87e60f3a19 100644 --- a/modules/@angular/upgrade/src/common/constants.ts +++ b/modules/@angular/upgrade/src/common/constants.ts @@ -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'; diff --git a/modules/@angular/upgrade/src/common/content_projection_helper.ts b/modules/@angular/upgrade/src/common/content_projection_helper.ts new file mode 100644 index 0000000000..df9eb42960 --- /dev/null +++ b/modules/@angular/upgrade/src/common/content_projection_helper.ts @@ -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, nodes: Node[]): + Node[][] { + // By default, do not support multi-slot projection, + // as `upgrade/static` does not support it yet. + return [nodes]; + } +} diff --git a/modules/@angular/upgrade/src/common/downgrade_component.ts b/modules/@angular/upgrade/src/common/downgrade_component.ts index 4815ce1b5e..388b3bcefb 100644 --- a/modules/@angular/upgrade/src/common/downgrade_component.ts +++ b/modules/@angular/upgrade/src/common/downgrade_component.ts @@ -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); diff --git a/modules/@angular/upgrade/src/common/downgrade_component_adapter.ts b/modules/@angular/upgrade/src/common/downgrade_component_adapter.ts index ceaeb9b189..c6549e8923 100644 --- a/modules/@angular/upgrade/src/common/downgrade_component_adapter.ts +++ b/modules/@angular/upgrade/src/common/downgrade_component_adapter.ts @@ -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) { + private $injector: angular.IInjectorService, private $compile: angular.ICompileService, + private $parse: angular.IParseService, private componentFactory: ComponentFactory) { (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)}'!`); } } } diff --git a/modules/@angular/upgrade/src/common/util.ts b/modules/@angular/upgrade/src/common/util.ts index 8a038dd959..ae42dc14b8 100644 --- a/modules/@angular/upgrade/src/common/util.ts +++ b/modules/@angular/upgrade/src/common/util.ts @@ -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): 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 { promise: Promise; resolve: (value?: R|PromiseLike) => void; @@ -50,8 +56,9 @@ export class Deferred { } /** - * @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)) { diff --git a/modules/@angular/upgrade/src/dynamic/content_projection_helper.ts b/modules/@angular/upgrade/src/dynamic/content_projection_helper.ts new file mode 100644 index 0000000000..338cdef363 --- /dev/null +++ b/modules/@angular/upgrade/src/dynamic/content_projection_helper.ts @@ -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, 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; + } +} diff --git a/modules/@angular/upgrade/src/dynamic/downgrade_ng2_adapter.ts b/modules/@angular/upgrade/src/dynamic/downgrade_ng2_adapter.ts deleted file mode 100644 index 1151340177..0000000000 --- a/modules/@angular/upgrade/src/dynamic/downgrade_ng2_adapter.ts +++ /dev/null @@ -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 = 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) { - 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 && (prototype).ngOnChanges) { - // Detect: OnChanges interface - this.inputChanges = {}; - this.componentScope.$watch(() => this.inputChangeCount, () => { - const inputChanges = this.inputChanges; - this.inputChanges = {}; - (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; - 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(); - }); - } -} diff --git a/modules/@angular/upgrade/src/dynamic/metadata.ts b/modules/@angular/upgrade/src/dynamic/metadata.ts deleted file mode 100644 index e842051f55..0000000000 --- a/modules/@angular/upgrade/src/dynamic/metadata.ts +++ /dev/null @@ -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; - selector: string; - inputs?: PropertyBinding[]; - outputs?: PropertyBinding[]; -} - -export function getComponentInfo(type: Type): 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)); -} diff --git a/modules/@angular/upgrade/src/dynamic/upgrade_adapter.ts b/modules/@angular/upgrade/src/dynamic/upgrade_adapter.ts index ac32f5b53c..106ac05516 100644 --- a/modules/@angular/upgrade/src/dynamic/upgrade_adapter.ts +++ b/modules/@angular/upgrade/src/dynamic/upgrade_adapter.ts @@ -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[] = []; + private directiveResolver: DirectiveResolver = new DirectiveResolver(); + private downgradedComponents: Type[] = []; /** * An internal map of ng1 components which need to up upgraded to ng2. * @@ -184,10 +187,13 @@ export class UpgradeAdapter { * }); * ``` */ - downgradeNg2Component(type: Type): Function { - this.upgradedComponents.push(type); - const info: ComponentInfo = getComponentInfo(type); - return ng1ComponentDirective(info, `${this.idPrefix}${info.selector}_c`); + downgradeNg2Component(component: Type): 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[]) => { - componentFactories.forEach((componentFactory) => { - const type: Type = componentFactory.componentType; - if (this.upgradedComponents.indexOf(type) !== -1) { - componentFactoryRefMap[getComponentInfo(type).selector] = - componentFactory; - } - }); - }) + DynamicNgUpgradeModule, this.compilerOptions, this.ngZone) .then((ref: NgModuleRef) => { this.moduleRef = ref; this.ngZone.run(() => { @@ -603,10 +600,6 @@ export class UpgradeAdapter { } } -interface ComponentFactoryRefMap { - [selector: string]: ComponentFactory; -} - /** * 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 { - (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++); - (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 = 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; -} diff --git a/modules/@angular/upgrade/src/static/upgrade_module.ts b/modules/@angular/upgrade/src/static/upgrade_module.ts index 80bc6ea50f..5a9c128629 100644 --- a/modules/@angular/upgrade/src/static/upgrade_module.ts +++ b/modules/@angular/upgrade/src/static/upgrade_module.ts @@ -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. diff --git a/modules/@angular/upgrade/test/dynamic/group_projectable_nodes_spec.ts b/modules/@angular/upgrade/test/dynamic/group_projectable_nodes_spec.ts new file mode 100644 index 0000000000..4c7389e1f3 --- /dev/null +++ b/modules/@angular/upgrade/test/dynamic/group_projectable_nodes_spec.ts @@ -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-1 content
' + + '' + + '' + + 'span content' + + '
div-2 content
'); + + const selectors = ['input[type=date]', 'span', '.x']; + const projectableNodes = groupNodesBySelector(selectors, contentNodes); + + expect(projectableNodes[0]).toEqual(nodes('')); + expect(projectableNodes[1]).toEqual(nodes('span content')); + expect(projectableNodes[2]) + .toEqual(nodes( + '
div-1 content
' + + '
div-2 content
')); + }); + + it('should collect up unmatched nodes for the wildcard selector', () => { + const contentNodes = nodes( + '
div-1 content
' + + '' + + '' + + 'span content' + + '
div-2 content
'); + + const selectors = ['.x', '*', 'input[type=date]']; + const projectableNodes = groupNodesBySelector(selectors, contentNodes); + + expect(projectableNodes[0]) + .toEqual(nodes( + '
div-1 content
' + + '
div-2 content
')); + expect(projectableNodes[1]) + .toEqual(nodes( + '' + + 'span content')); + expect(projectableNodes[2]).toEqual(nodes('')); + }); + + 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-1 content
' + + '' + + '' + + 'span content' + + '
div-2 content
'); + + const noSelectorNodes = groupNodesBySelector([], contentNodes); + expect(noSelectorNodes).toEqual([]); + + const noMatchSelectorNodes = groupNodesBySelector(['.not-there'], contentNodes); + expect(noMatchSelectorNodes).toEqual([[]]); + }); + }); +} diff --git a/modules/@angular/upgrade/test/dynamic/metadata_spec.ts b/modules/@angular/upgrade/test/dynamic/metadata_spec.ts deleted file mode 100644 index 5975d8b559..0000000000 --- a/modules/@angular/upgrade/test/dynamic/metadata_spec.ts +++ /dev/null @@ -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 {} diff --git a/modules/@angular/upgrade/test/dynamic/test_helpers.ts b/modules/@angular/upgrade/test/dynamic/test_helpers.ts index d1676d7569..a1ac576d64 100644 --- a/modules/@angular/upgrade/test/dynamic/test_helpers.ts +++ b/modules/@angular/upgrade/test/dynamic/test_helpers.ts @@ -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); +} diff --git a/modules/@angular/upgrade/test/dynamic/upgrade_spec.ts b/modules/@angular/upgrade/test/dynamic/upgrade_spec.ts index a5e66adaf4..f5d58ee899 100644 --- a/modules/@angular/upgrade/test/dynamic/upgrade_spec.ts +++ b/modules/@angular/upgrade/test/dynamic/upgrade_spec.ts @@ -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-1 content
' + - '' + - '' + - 'span content' + - '
div-2 content
'); - - const selectors = ['input[type=date]', 'span', '.x']; - const projectableNodes = sortProjectableNodes(selectors, contentNodes); - - expect(projectableNodes[0]).toEqual(nodes('')); - expect(projectableNodes[1]).toEqual(nodes('span content')); - expect(projectableNodes[2]) - .toEqual(nodes( - '
div-1 content
' + - '
div-2 content
')); - }); - - it('should collect up unmatched nodes for the wildcard selector', () => { - const contentNodes = nodes( - '
div-1 content
' + - '' + - '' + - 'span content' + - '
div-2 content
'); - - const selectors = ['.x', '*', 'input[type=date]']; - const projectableNodes = sortProjectableNodes(selectors, contentNodes); - - expect(projectableNodes[0]) - .toEqual(nodes( - '
div-1 content
' + - '
div-2 content
')); - expect(projectableNodes[1]) - .toEqual(nodes( - '' + - 'span content')); - expect(projectableNodes[2]).toEqual(nodes('')); - }); - - 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-1 content
' + - '' + - '' + - 'span content' + - '
div-2 content
'); - - 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); \ No newline at end of file diff --git a/tools/public_api_guard/upgrade/index.d.ts b/tools/public_api_guard/upgrade/index.d.ts index 8ac4511a85..e8469a35ef 100644 --- a/tools/public_api_guard/upgrade/index.d.ts +++ b/tools/public_api_guard/upgrade/index.d.ts @@ -2,7 +2,7 @@ export declare class UpgradeAdapter { constructor(ng2AppModule: Type, compilerOptions?: CompilerOptions); bootstrap(element: Element, modules?: any[], config?: angular.IAngularBootstrapConfig): UpgradeAdapterRef; - downgradeNg2Component(type: Type): Function; + downgradeNg2Component(component: Type): Function; downgradeNg2Provider(token: any): Function; registerForNg1Tests(modules?: string[]): UpgradeAdapterRef; upgradeNg1Component(name: string): Type;