From d91a86aac6a086ecb1673057a7ff7d0b5f88b0d2 Mon Sep 17 00:00:00 2001 From: Eudes Petonnet-Vincent Date: Wed, 2 Nov 2016 22:38:00 +0000 Subject: [PATCH] fix(upgrade): fix downgrade content projection and injector inheritance - Full support for content projection in downgraded Angular 2 components. In particular, this enables multi-slot projection and other features on . - Correctly wire up hierarchical injectors for downgraded Angular 2 components: downgraded components inherit the injector of the first other downgraded Angular 2 component they find up the DOM tree. Closes #6629, #7727, #8729, #9643, #9649, #12675 --- modules/@angular/compiler/src/jit/compiler.ts | 13 + modules/@angular/compiler/testing/index.ts | 4 + modules/@angular/core/src/linker/compiler.ts | 8 + modules/@angular/upgrade/src/angular_js.ts | 2 +- modules/@angular/upgrade/src/constants.ts | 2 +- .../upgrade/src/downgrade_ng2_adapter.ts | 23 +- .../@angular/upgrade/src/upgrade_adapter.ts | 456 ++++++++++++------ modules/@angular/upgrade/src/util.ts | 17 +- modules/@angular/upgrade/test/upgrade_spec.ts | 128 ++++- tools/public_api_guard/core/index.d.ts | 1 + 10 files changed, 470 insertions(+), 184 deletions(-) diff --git a/modules/@angular/compiler/src/jit/compiler.ts b/modules/@angular/compiler/src/jit/compiler.ts index ed2c56af4a..159f05b6b4 100644 --- a/modules/@angular/compiler/src/jit/compiler.ts +++ b/modules/@angular/compiler/src/jit/compiler.ts @@ -70,6 +70,14 @@ export class JitCompiler implements Compiler { return this._compileModuleAndAllComponents(moduleType, false).asyncResult; } + getNgContentSelectors(component: Type): string[] { + const template = this._compiledTemplateCache.get(component); + if (!template) { + throw new Error(`The component ${stringify(component)} is not yet compiled!`); + } + return template.compMeta.template.ngContentSelectors; + } + private _compileModuleAndComponents(moduleType: Type, isSync: boolean): SyncAsyncResult> { const loadingPromise = this._loadModules(moduleType, isSync); @@ -408,6 +416,11 @@ class ModuleBoundCompiler implements Compiler { return this._delegate.compileModuleAndAllComponentsAsync(moduleType); } + getNgContentSelectors(component: Type): string[] { + return this._delegate.getNgContentSelectors(component); + } + + /** * Clears all caches */ diff --git a/modules/@angular/compiler/testing/index.ts b/modules/@angular/compiler/testing/index.ts index 57749c3c35..1824bbdc91 100644 --- a/modules/@angular/compiler/testing/index.ts +++ b/modules/@angular/compiler/testing/index.ts @@ -70,6 +70,10 @@ export class TestingCompilerImpl implements TestingCompiler { return this._compiler.compileModuleAndAllComponentsAsync(moduleType); } + getNgContentSelectors(component: Type): string[] { + return this._compiler.getNgContentSelectors(component); + } + overrideModule(ngModule: Type, override: MetadataOverride): void { const oldMetadata = this._moduleResolver.resolve(ngModule, false); this._moduleResolver.setNgModule( diff --git a/modules/@angular/core/src/linker/compiler.ts b/modules/@angular/core/src/linker/compiler.ts index ba3618047f..856053ad7f 100644 --- a/modules/@angular/core/src/linker/compiler.ts +++ b/modules/@angular/core/src/linker/compiler.ts @@ -82,6 +82,14 @@ export class Compiler { throw _throwError(); } + /** + * Exposes the CSS-style selectors that have been used in `ngContent` directives within + * the template of the given component. + * This is used by the `upgrade` library to compile the appropriate transclude content + * in the Angular 1 wrapper component. + */ + getNgContentSelectors(component: Type): string[] { throw _throwError(); } + /** * Clears all caches. */ diff --git a/modules/@angular/upgrade/src/angular_js.ts b/modules/@angular/upgrade/src/angular_js.ts index a9bf40a8d0..5abbeb44bf 100644 --- a/modules/@angular/upgrade/src/angular_js.ts +++ b/modules/@angular/upgrade/src/angular_js.ts @@ -27,7 +27,7 @@ export interface IModule { run(a: IInjectable): IModule; } export interface ICompileService { - (element: Element|NodeList|string, transclude?: Function): ILinkFn; + (element: Element|NodeList|Node[]|string, transclude?: Function): ILinkFn; } export interface ILinkFn { (scope: IScope, cloneAttachFn?: ICloneAttachFunction, options?: ILinkFnOptions): IAugmentedJQuery; diff --git a/modules/@angular/upgrade/src/constants.ts b/modules/@angular/upgrade/src/constants.ts index 36cb81e838..09c36a5009 100644 --- a/modules/@angular/upgrade/src/constants.ts +++ b/modules/@angular/upgrade/src/constants.ts @@ -21,4 +21,4 @@ export const NG1_INJECTOR = '$injector'; export const NG1_PARSE = '$parse'; export const NG1_TEMPLATE_CACHE = '$templateCache'; export const NG1_TESTABILITY = '$$testability'; -export const REQUIRE_INJECTOR = '?^' + NG2_INJECTOR; +export const REQUIRE_INJECTOR = '?^^' + NG2_INJECTOR; diff --git a/modules/@angular/upgrade/src/downgrade_ng2_adapter.ts b/modules/@angular/upgrade/src/downgrade_ng2_adapter.ts index 1f6296df8f..a991132f9e 100644 --- a/modules/@angular/upgrade/src/downgrade_ng2_adapter.ts +++ b/modules/@angular/upgrade/src/downgrade_ng2_adapter.ts @@ -23,26 +23,21 @@ export class DowngradeNg2ComponentAdapter { componentRef: ComponentRef = null; changeDetector: ChangeDetectorRef = null; componentScope: angular.IScope; - childNodes: Node[]; - contentInsertionPoint: Node = null; constructor( - private id: string, private info: ComponentInfo, private element: angular.IAugmentedJQuery, + private info: ComponentInfo, private element: angular.IAugmentedJQuery, private attrs: angular.IAttributes, private scope: angular.IScope, private parentInjector: Injector, private parse: angular.IParseService, private componentFactory: ComponentFactory) { - (this.element[0]).id = id; this.componentScope = scope.$new(); - this.childNodes = element.contents(); } - bootstrapNg2() { + bootstrapNg2(projectableNodes: Node[][]) { const childInjector = ReflectiveInjector.resolveAndCreate( [{provide: NG1_SCOPE, useValue: this.componentScope}], this.parentInjector); - this.contentInsertionPoint = document.createComment('ng1 insertion point'); - this.componentRef = this.componentFactory.create( - childInjector, [[this.contentInsertionPoint]], this.element[0]); + this.componentRef = + this.componentFactory.create(childInjector, projectableNodes, this.element[0]); this.changeDetector = this.componentRef.changeDetectorRef; this.component = this.componentRef.instance; } @@ -103,16 +98,6 @@ export class DowngradeNg2ComponentAdapter { this.componentScope.$watch(() => this.changeDetector && this.changeDetector.detectChanges()); } - projectContent() { - const childNodes = this.childNodes; - const parent = this.contentInsertionPoint.parentNode; - if (parent) { - for (let i = 0, ii = childNodes.length; i < ii; i++) { - parent.insertBefore(childNodes[i], this.contentInsertionPoint); - } - } - } - setupOutputs() { const attrs = this.attrs; const outputs = this.info.outputs || []; diff --git a/modules/@angular/upgrade/src/upgrade_adapter.ts b/modules/@angular/upgrade/src/upgrade_adapter.ts index 12d8110837..bfd99592f5 100644 --- a/modules/@angular/upgrade/src/upgrade_adapter.ts +++ b/modules/@angular/upgrade/src/upgrade_adapter.ts @@ -6,15 +6,17 @@ * 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 {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import * as angular from './angular_js'; import {NG1_COMPILE, NG1_INJECTOR, NG1_PARSE, NG1_ROOT_SCOPE, NG1_TESTABILITY, NG2_COMPILER, NG2_COMPONENT_FACTORY_REF_MAP, NG2_INJECTOR, NG2_ZONE, REQUIRE_INJECTOR} from './constants'; import {DowngradeNg2ComponentAdapter} from './downgrade_ng2_adapter'; +import {isPresent} from './facade/lang'; import {ComponentInfo, getComponentInfo} from './metadata'; import {UpgradeNg1ComponentAdapterBuilder} from './upgrade_ng1_adapter'; -import {controllerKey, onError, Deferred} from './util'; +import {Deferred, controllerKey, getAttributesAsArray, onError} from './util'; let upgradeCount: number = 0; @@ -58,8 +60,8 @@ let upgradeCount: number = 0; * ### Example * * ``` - * var adapter = new UpgradeAdapter(forwardRef(() => MyNg2Module), myCompilerOptions); - * var module = angular.module('myExample', []); + * const adapter = new UpgradeAdapter(forwardRef(() => MyNg2Module), myCompilerOptions); + * const module = angular.module('myExample', []); * module.directive('ng2Comp', adapter.downgradeNg2Component(Ng2Component)); * * module.directive('ng1Hello', function() { @@ -98,9 +100,7 @@ let upgradeCount: number = 0; * @stable */ export class UpgradeAdapter { - /* @internal */ private idPrefix: string = `NG2_UPGRADE_${upgradeCount++}_`; - /* @internal */ private upgradedComponents: Type[] = []; /** * An internal map of ng1 components which need to up upgraded to ng2. @@ -111,8 +111,11 @@ export class UpgradeAdapter { * @internal */ private ng1ComponentsToBeUpgraded: {[name: string]: UpgradeNg1ComponentAdapterBuilder} = {}; - /* @internal */ private providers: Provider[] = []; + private ngZone: NgZone; + private ng1Module: angular.IModule; + private moduleRef: NgModuleRef = null; + private ng2BootstrapDeferred: Deferred; constructor(private ng2AppModule: Type, private compilerOptions?: CompilerOptions) { if (!ng2AppModule) { @@ -149,8 +152,8 @@ export class UpgradeAdapter { * ### Example * * ``` - * var adapter = new UpgradeAdapter(forwardRef(() => MyNg2Module)); - * var module = angular.module('myExample', []); + * const adapter = new UpgradeAdapter(forwardRef(() => MyNg2Module)); + * const module = angular.module('myExample', []); * module.directive('greet', adapter.downgradeNg2Component(Greeter)); * * @Component({ @@ -227,8 +230,8 @@ export class UpgradeAdapter { * ### Example * * ``` - * var adapter = new UpgradeAdapter(forwardRef(() => MyNg2Module)); - * var module = angular.module('myExample', []); + * const adapter = new UpgradeAdapter(forwardRef(() => MyNg2Module)); + * const module = angular.module('myExample', []); * * module.directive('greet', function() { * return { @@ -278,8 +281,8 @@ export class UpgradeAdapter { * ### Example * * ``` - * var adapter = new UpgradeAdapter(); - * var module = angular.module('myExample', []); + * const adapter = new UpgradeAdapter(); + * const module = angular.module('myExample', []); * module.directive('ng2', adapter.downgradeNg2Component(Ng2)); * * module.directive('ng1', function() { @@ -314,126 +317,21 @@ export class UpgradeAdapter { */ bootstrap(element: Element, modules?: any[], config?: angular.IAngularBootstrapConfig): UpgradeAdapterRef { - const ngZone = - new NgZone({enableLongStackTrace: Zone.hasOwnProperty('longStackTraceZoneSpec')}); + this.declareNg1Module(modules); + const upgrade = new UpgradeAdapterRef(); - let ng1Injector: angular.IInjectorService = null; - let moduleRef: NgModuleRef = null; - const delayApplyExps: Function[] = []; - let original$applyFn: Function; - let rootScopePrototype: any; - let rootScope: angular.IRootScopeService; - const componentFactoryRefMap: ComponentFactoryRefMap = {}; - const ng1Module = angular.module(this.idPrefix, modules); - let ng1BootstrapPromise: Promise; - const ng2BootstrapDeferred = new Deferred(); - ng1Module.factory(NG2_INJECTOR, () => moduleRef.injector.get(Injector)) - .value(NG2_ZONE, ngZone) - .factory(NG2_COMPILER, () => moduleRef.injector.get(Compiler)) - .value(NG2_COMPONENT_FACTORY_REF_MAP, componentFactoryRefMap) - .config([ - '$provide', '$injector', - (provide: angular.IProvideService, ng1Injector: angular.IInjectorService) => { - provide.decorator(NG1_ROOT_SCOPE, [ - '$delegate', - function(rootScopeDelegate: angular.IRootScopeService) { - // Capture the root apply so that we can delay first call to $apply until we - // bootstrap Angular 2 and then we replay and restore the $apply. - rootScopePrototype = rootScopeDelegate.constructor.prototype; - if (rootScopePrototype.hasOwnProperty('$apply')) { - original$applyFn = rootScopePrototype.$apply; - rootScopePrototype.$apply = (exp: any) => delayApplyExps.push(exp); - } else { - throw new Error('Failed to find \'$apply\' on \'$rootScope\'!'); - } - return rootScope = rootScopeDelegate; - } - ]); - if (ng1Injector.has(NG1_TESTABILITY)) { - provide.decorator(NG1_TESTABILITY, [ - '$delegate', - function(testabilityDelegate: angular.ITestabilityService) { - - const originalWhenStable: Function = testabilityDelegate.whenStable; - // Cannot use arrow function below because we need the context - const newWhenStable = function(callback: Function) { - originalWhenStable.call(this, function() { - const ng2Testability: Testability = moduleRef.injector.get(Testability); - if (ng2Testability.isStable()) { - callback.apply(this, arguments); - } else { - ng2Testability.whenStable(newWhenStable.bind(this, callback)); - } - }); - }; - - testabilityDelegate.whenStable = newWhenStable; - return testabilityDelegate; - } - ]); - } - } - ]); - - ng1Module.run([ - '$injector', '$rootScope', - (injector: angular.IInjectorService, rootScope: angular.IRootScopeService) => { - ng1Injector = injector; - UpgradeNg1ComponentAdapterBuilder.resolve(this.ng1ComponentsToBeUpgraded, injector) - .then(() => { - // At this point we have ng1 injector and we have lifted ng1 components into ng2, we - // now can bootstrap ng2. - const DynamicNgUpgradeModule = - NgModule({ - providers: [ - {provide: NG1_INJECTOR, useFactory: () => ng1Injector}, - {provide: NG1_COMPILE, useFactory: () => ng1Injector.get(NG1_COMPILE)}, - this.providers - ], - imports: [this.ng2AppModule] - }).Class({ - constructor: function DynamicNgUpgradeModule() {}, - ngDoBootstrap: function() {} - }); - - (platformBrowserDynamic() as any) - ._bootstrapModuleWithZone( - DynamicNgUpgradeModule, this.compilerOptions, ngZone, - (componentFactories: ComponentFactory[]) => { - componentFactories.forEach((componentFactory: ComponentFactory) => { - const type: Type = componentFactory.componentType; - if (this.upgradedComponents.indexOf(type) !== -1) { - componentFactoryRefMap[getComponentInfo(type).selector] = - componentFactory; - } - }); - }) - .then((ref: NgModuleRef) => { - moduleRef = ref; - angular.element(element).data( - controllerKey(NG2_INJECTOR), moduleRef.injector); - ngZone.onMicrotaskEmpty.subscribe({ - next: (_: any) => ngZone.runOutsideAngular(() => rootScope.$evalAsync()) - }); - }) - .then(ng2BootstrapDeferred.resolve, ng2BootstrapDeferred.reject); - }) - .catch(ng2BootstrapDeferred.reject); - } - ]); // Make sure resumeBootstrap() only exists if the current bootstrap is deferred const windowAngular = (window as any /** TODO #???? */)['angular']; windowAngular.resumeBootstrap = undefined; - ngZone.run(() => { angular.bootstrap(element, [this.idPrefix], config); }); - ng1BootstrapPromise = new Promise((resolve) => { + this.ngZone.run(() => { angular.bootstrap(element, [this.ng1Module.name], config); }); + const ng1BootstrapPromise = new Promise((resolve) => { if (windowAngular.resumeBootstrap) { const originalResumeBootstrap: () => void = windowAngular.resumeBootstrap; windowAngular.resumeBootstrap = function() { - let args = arguments; windowAngular.resumeBootstrap = originalResumeBootstrap; - ngZone.run(() => { windowAngular.resumeBootstrap.apply(this, args); }); + windowAngular.resumeBootstrap.apply(this, arguments); resolve(); }; } else { @@ -441,17 +339,10 @@ export class UpgradeAdapter { } }); - Promise.all([ng1BootstrapPromise, ng2BootstrapDeferred.promise]).then(() => { - moduleRef.injector.get(NgZone).run(() => { - if (rootScopePrototype) { - rootScopePrototype.$apply = original$applyFn; // restore original $apply - while (delayApplyExps.length) { - rootScope.$apply(delayApplyExps.shift()); - } - (upgrade)._bootstrapDone(moduleRef, ng1Injector); - rootScopePrototype = null; - } - }); + Promise.all([this.ng2BootstrapDeferred.promise, ng1BootstrapPromise]).then(([ng1Injector]) => { + angular.element(element).data(controllerKey(NG2_INJECTOR), this.moduleRef.injector); + this.moduleRef.injector.get(NgZone).run( + () => { (upgrade)._bootstrapDone(this.moduleRef, ng1Injector); }); }, onError); return upgrade; } @@ -473,16 +364,16 @@ export class UpgradeAdapter { * } * } * - * var module = angular.module('myExample', []); + * const module = angular.module('myExample', []); * module.service('server', Server); * module.service('login', Login); * - * var adapter = new UpgradeAdapter(); + * const adapter = new UpgradeAdapter(); * adapter.upgradeNg1Provider('server'); * adapter.upgradeNg1Provider('login', {asToken: Login}); * * adapter.bootstrap(document.body, ['myExample']).ready((ref) => { - * var example: Example = ref.ng2Injector.get(Example); + * const example: Example = ref.ng2Injector.get(Example); * }); * * ``` @@ -506,13 +397,13 @@ export class UpgradeAdapter { * class Example { * } * - * var adapter = new UpgradeAdapter(); + * const adapter = new UpgradeAdapter(); * - * var module = angular.module('myExample', []); + * const module = angular.module('myExample', []); * module.factory('example', adapter.downgradeNg2Provider(Example)); * * adapter.bootstrap(document.body, ['myExample']).ready((ref) => { - * var example: Example = ref.ng1Injector.get('example'); + * const example: Example = ref.ng1Injector.get('example'); * }); * * ``` @@ -522,42 +413,258 @@ export class UpgradeAdapter { (factory).$inject = [NG2_INJECTOR]; return factory; } + + + /** + * Declare the Angular 1 upgrade module for this adapter without bootstrapping the whole + * hybrid application. + * + * This method is automatically called by `bootstrap()`. + * + * @param modules The Angular 1 modules that this upgrade module should depend upon. + * @returns The Angular 1 upgrade module that is declared by this method + * + * ### Example + * + * ``` + * const upgradeAdapter = new UpgradeAdapter(); + * upgradeAdapter.declareNg1Module(['heroApp']); + * ``` + */ + private declareNg1Module(modules: string[] = []): angular.IModule { + const delayApplyExps: Function[] = []; + 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(); + + this.ngZone = new NgZone({enableLongStackTrace: Zone.hasOwnProperty('longStackTraceZoneSpec')}); + this.ng2BootstrapDeferred = new Deferred(); + ng1Module.factory(NG2_INJECTOR, () => this.moduleRef.injector.get(Injector)) + .constant(NG2_ZONE, this.ngZone) + .constant(NG2_COMPONENT_FACTORY_REF_MAP, componentFactoryRefMap) + .factory(NG2_COMPILER, () => this.moduleRef.injector.get(Compiler)) + .config([ + '$provide', '$injector', + (provide: angular.IProvideService, ng1Injector: angular.IInjectorService) => { + provide.decorator(NG1_ROOT_SCOPE, [ + '$delegate', + function(rootScopeDelegate: angular.IRootScopeService) { + // Capture the root apply so that we can delay first call to $apply until we + // bootstrap Angular 2 and then we replay and restore the $apply. + rootScopePrototype = rootScopeDelegate.constructor.prototype; + if (rootScopePrototype.hasOwnProperty('$apply')) { + original$applyFn = rootScopePrototype.$apply; + rootScopePrototype.$apply = (exp: any) => delayApplyExps.push(exp); + } else { + throw new Error('Failed to find \'$apply\' on \'$rootScope\'!'); + } + return rootScope = rootScopeDelegate; + } + ]); + if (ng1Injector.has(NG1_TESTABILITY)) { + provide.decorator(NG1_TESTABILITY, [ + '$delegate', + function(testabilityDelegate: angular.ITestabilityService) { + const originalWhenStable: Function = testabilityDelegate.whenStable; + // Cannot use arrow function below because we need the context + const newWhenStable = function(callback: Function) { + originalWhenStable.call(this, function() { + const ng2Testability: Testability = + upgradeAdapter.moduleRef.injector.get(Testability); + if (ng2Testability.isStable()) { + callback.apply(this, arguments); + } else { + ng2Testability.whenStable(newWhenStable.bind(this, callback)); + } + }); + }; + + testabilityDelegate.whenStable = newWhenStable; + return testabilityDelegate; + } + ]); + } + } + ]); + + ng1Module.run([ + '$injector', '$rootScope', + (ng1Injector: angular.IInjectorService, rootScope: angular.IRootScopeService) => { + UpgradeNg1ComponentAdapterBuilder.resolve(this.ng1ComponentsToBeUpgraded, ng1Injector) + .then(() => { + // At this point we have ng1 injector and we have lifted ng1 components into ng2, we + // now can bootstrap ng2. + const DynamicNgUpgradeModule = + NgModule({ + providers: [ + {provide: NG1_INJECTOR, useFactory: () => ng1Injector}, + {provide: NG1_COMPILE, useFactory: () => ng1Injector.get(NG1_COMPILE)}, + this.providers + ], + imports: [this.ng2AppModule] + }).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; + } + }); + }) + .then((ref: NgModuleRef) => { + this.moduleRef = ref; + let subscription = this.ngZone.onMicrotaskEmpty.subscribe({ + next: (_: any) => this.ngZone.runOutsideAngular(() => rootScope.$evalAsync()) + }); + rootScope.$on('$destroy', () => { subscription.unsubscribe(); }); + this.ngZone.run(() => { + if (rootScopePrototype) { + rootScopePrototype.$apply = original$applyFn; // restore original $apply + while (delayApplyExps.length) { + rootScope.$apply(delayApplyExps.shift()); + } + rootScopePrototype = null; + } + }); + }) + .then(() => this.ng2BootstrapDeferred.resolve(ng1Injector), onError); + }) + .catch((e) => this.ng2BootstrapDeferred.reject(e)); + } + ]); + + return ng1Module; + } } interface ComponentFactoryRefMap { [selector: string]: ComponentFactory; } +/** + * Synchronous promise-like object to wrap parent injectors, + * to preserve the synchronous nature of AngularJS v1's $compile. + */ +class ParentInjectorPromise { + private injector: Injector; + private callbacks: ((injector: Injector) => any)[] = []; + + constructor(private element: angular.IAugmentedJQuery) { + // store the promise on the element + element.data(controllerKey(NG2_INJECTOR), this); + } + + then(callback: (injector: Injector) => any) { + if (this.injector) { + callback(this.injector); + } else { + this.callbacks.push(callback); + } + } + + resolve(injector: Injector) { + this.injector = injector; + + // reset the element data to point to the real injector + this.element.data(controllerKey(NG2_INJECTOR), injector); + + // clean out the element to prevent memory leaks + this.element = null; + + // run all the queued callbacks + this.callbacks.forEach((callback) => callback(injector)); + this.callbacks.length = 0; + } +} + + function ng1ComponentDirective(info: ComponentInfo, idPrefix: string): Function { - (directiveFactory).$inject = [NG1_INJECTOR, NG2_COMPONENT_FACTORY_REF_MAP, NG1_PARSE]; + (directiveFactory).$inject = + [NG1_INJECTOR, NG1_COMPILE, NG2_COMPONENT_FACTORY_REF_MAP, NG1_PARSE]; function directiveFactory( - ng1Injector: angular.IInjectorService, componentFactoryRefMap: ComponentFactoryRefMap, + 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, - link: { - post: (scope: angular.IScope, element: angular.IAugmentedJQuery, attrs: angular.IAttributes, - parentInjector: any, transclude: angular.ITranscludeFunction): void => { - const componentFactory: ComponentFactory = componentFactoryRefMap[info.selector]; - if (!componentFactory) - throw new Error('Expecting ComponentFactory for: ' + info.selector); + 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, parentInjector: Injector | ParentInjectorPromise, + transclude: angular.ITranscludeFunction): void => { + let id = idPrefix + (idCount++); + (element[0]).id = id; - if (parentInjector === null) { - parentInjector = ng1Injector.get(NG2_INJECTOR); + let injectorPromise = new ParentInjectorPromise(element); + + const ng2Compiler = ng1Injector.get(NG2_COMPILER) 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(NG2_INJECTOR); + + 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, injector, parse, componentFactory); + facade.setupInputs(); + facade.bootstrapNg2(projectableNodes); + facade.setupOutputs(); + facade.registerCleanup(); + injectorPromise.resolve(facade.componentRef.injector); + } } - const facade = new DowngradeNg2ComponentAdapter( - idPrefix + (idCount++), info, element, attrs, scope, parentInjector, parse, - componentFactory); - facade.setupInputs(); - facade.bootstrapNg2(); - facade.projectContent(); - facade.setupOutputs(); - facade.registerCleanup(); - } + }; } }; + + 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; } @@ -602,3 +709,36 @@ 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/util.ts b/modules/@angular/upgrade/src/util.ts index a3231a4fa4..fb577f8cae 100644 --- a/modules/@angular/upgrade/src/util.ts +++ b/modules/@angular/upgrade/src/util.ts @@ -6,8 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ - - export function onError(e: any) { // TODO: (misko): We seem to not have a stack trace here! if (console.error) { @@ -23,6 +21,19 @@ export function controllerKey(name: string): string { return '$' + name + 'Controller'; } +export function getAttributesAsArray(node: Node): string[][] { + const attributes = node.attributes; + let asArray: string[][]; + if (attributes) { + let attrLen = attributes.length; + asArray = new Array(attrLen); + for (let i = 0; i < attrLen; i++) { + asArray[i] = [attributes[i].nodeName, attributes[i].nodeValue]; + } + } + return asArray || []; +} + export class Deferred { promise: Promise; resolve: (value?: R|PromiseLike) => void; @@ -34,4 +45,4 @@ export class Deferred { this.reject = rej; }); } -} \ No newline at end of file +} diff --git a/modules/@angular/upgrade/test/upgrade_spec.ts b/modules/@angular/upgrade/test/upgrade_spec.ts index 6f4354b1be..e2c91329d7 100644 --- a/modules/@angular/upgrade/test/upgrade_spec.ts +++ b/modules/@angular/upgrade/test/upgrade_spec.ts @@ -10,8 +10,8 @@ import {ChangeDetectorRef, Class, Component, EventEmitter, NO_ERRORS_SCHEMA, NgM import {async, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing'; import {BrowserModule} from '@angular/platform-browser'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; -import {UpgradeAdapter} from '@angular/upgrade'; import * as angular from '@angular/upgrade/src/angular_js'; +import {UpgradeAdapter, sortProjectableNodes} from '@angular/upgrade/src/upgrade_adapter'; export function main() { describe('adapter: ng1 to ng2', () => { @@ -178,7 +178,7 @@ export function main() { adapter.bootstrap(element, ['ng1']).ready((ref) => { expect(document.body.textContent).toEqual('1A;2A;ng1a;2B;ng1b;2C;1C;'); // https://github.com/angular/angular.js/issues/12983 - expect(log).toEqual(['1A', '1B', '1C', '2A', '2B', '2C', 'ng1a', 'ng1b']); + expect(log).toEqual(['1A', '1C', '2A', '2B', '2C', 'ng1a', 'ng1b']); ref.dispose(); }); })); @@ -359,6 +359,33 @@ export function main() { ref.dispose(); }); })); + + it('should support multi-slot projection', async(() => { + const ng1Module = angular.module('ng1', []); + + const Ng2 = Component({ + selector: 'ng2', + template: '2a()' + + '2b()' + }).Class({constructor: function() {}}); + + const Ng2Module = NgModule({declarations: [Ng2], imports: [BrowserModule]}).Class({ + constructor: function() {} + }); + + // The ng-if on one of the projected children is here to make sure + // the correct slot is targeted even with structural directives in play. + const element = html( + '
1a
1b
'); + + const adapter: UpgradeAdapter = new UpgradeAdapter(Ng2Module); + ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); + adapter.bootstrap(element, ['ng1']).ready((ref) => { + expect(document.body.textContent).toEqual('2a(1a)2b(1b)'); + ref.dispose(); + }); + })); }); describe('upgrade ng1 component', () => { @@ -1128,6 +1155,33 @@ export function main() { ref.dispose(); }); })); + + it('should respect hierarchical dependency injection for ng2', async(() => { + const ng1Module = angular.module('ng1', []); + + const Ng2Parent = Component({ + selector: 'ng2-parent', + template: `ng2-parent()` + }).Class({constructor: function() {}}); + const Ng2Child = Component({selector: 'ng2-child', template: `ng2-child`}).Class({ + constructor: [Ng2Parent, function(parent: any) {}] + }); + + const Ng2Module = + NgModule({declarations: [Ng2Parent, Ng2Child], imports: [BrowserModule]}).Class({ + constructor: function() {} + }); + + const element = html(''); + + const adapter: UpgradeAdapter = new UpgradeAdapter(Ng2Module); + ng1Module.directive('ng2Parent', adapter.downgradeNg2Component(Ng2Parent)) + .directive('ng2Child', adapter.downgradeNg2Component(Ng2Child)); + adapter.bootstrap(element, ['ng1']).ready((ref) => { + expect(document.body.textContent).toEqual('ng2-parent(ng2-child)'); + ref.dispose(); + }); + })); }); describe('testability', () => { @@ -1241,6 +1295,70 @@ 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 multiTrim(text: string): string { @@ -1257,3 +1375,9 @@ function html(html: string): Element { return body; } + +function nodes(html: string) { + const element = document.createElement('div'); + element.innerHTML = html; + return Array.prototype.slice.call(element.childNodes); +} diff --git a/tools/public_api_guard/core/index.d.ts b/tools/public_api_guard/core/index.d.ts index b1d8260ebb..b32d844f9b 100644 --- a/tools/public_api_guard/core/index.d.ts +++ b/tools/public_api_guard/core/index.d.ts @@ -219,6 +219,7 @@ export declare class Compiler { compileModuleAndAllComponentsSync(moduleType: Type): ModuleWithComponentFactories; compileModuleAsync(moduleType: Type): Promise>; compileModuleSync(moduleType: Type): NgModuleFactory; + getNgContentSelectors(component: Type): string[]; } /** @experimental */