From 34b61be68280835203949d243a1bf0f8be4b88e4 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Tue, 9 May 2017 20:36:49 +0300 Subject: [PATCH] fix(upgrade): fix transclusion on upgraded components Previously, only simple, single-slot transclusion worked on upgraded components. This commit fixes/adds support for the following: - Multi-slot transclusion. - Using fallback content when no transclusion content is provided. - Destroy unused scope (when using fallback content). Fixes #13271 --- packages/upgrade/src/common/angular1.ts | 6 +- packages/upgrade/src/common/util.ts | 8 + .../upgrade/src/static/upgrade_component.ts | 135 +++-- packages/upgrade/test/common/test_helpers.ts | 5 +- .../integration/upgrade_component_spec.ts | 511 ++++++++++++++++-- 5 files changed, 535 insertions(+), 130 deletions(-) diff --git a/packages/upgrade/src/common/angular1.ts b/packages/upgrade/src/common/angular1.ts index 3e45a23cafdcd2..57bfeb9d40f234 100644 --- a/packages/upgrade/src/common/angular1.ts +++ b/packages/upgrade/src/common/angular1.ts @@ -33,6 +33,7 @@ export interface ICompileService { } export interface ILinkFn { (scope: IScope, cloneAttachFn?: ICloneAttachFunction, options?: ILinkFnOptions): IAugmentedJQuery; + $$slots?: {[slotName: string]: ILinkFn}; } export interface ILinkFnOptions { parentBoundTranscludeFn?: Function; @@ -75,9 +76,10 @@ export interface IDirective { templateUrl?: string|Function; templateNamespace?: string; terminal?: boolean; - transclude?: boolean|'element'|{[key: string]: string}; + transclude?: DirectiveTranscludeProperty; } export type DirectiveRequireProperty = SingleOrListOrMap; +export type DirectiveTranscludeProperty = boolean | 'element' | {[key: string]: string}; export interface IDirectiveCompileFn { (templateElement: IAugmentedJQuery, templateAttributes: IAttributes, transclude: ITranscludeFunction): IDirectivePrePost; @@ -97,7 +99,7 @@ export interface IComponent { require?: DirectiveRequireProperty; template?: string|Function; templateUrl?: string|Function; - transclude?: boolean; + transclude?: DirectiveTranscludeProperty; } export interface IAttributes { $observe(attr: string, fn: (v: string) => void): void; } export interface ITranscludeFunction { diff --git a/packages/upgrade/src/common/util.ts b/packages/upgrade/src/common/util.ts index cc1d1a086c2c33..22180802f11056 100644 --- a/packages/upgrade/src/common/util.ts +++ b/packages/upgrade/src/common/util.ts @@ -9,6 +9,9 @@ import {Type} from '@angular/core'; import * as angular from './angular1'; +const DIRECTIVE_PREFIX_REGEXP = /^(?:x|data)[:\-_]/i; +const DIRECTIVE_SPECIAL_CHARS_REGEXP = /[:\-_]+(.)/g; + export function onError(e: any) { // TODO: (misko): We seem to not have a stack trace here! if (console.error) { @@ -24,6 +27,11 @@ export function controllerKey(name: string): string { return '$' + name + 'Controller'; } +export function directiveNormalize(name: string): string { + return name.replace(DIRECTIVE_PREFIX_REGEXP, '') + .replace(DIRECTIVE_SPECIAL_CHARS_REGEXP, (_, letter) => letter.toUpperCase()); +} + export function getAttributesAsArray(node: Node): [string, string][] { const attributes = node.attributes; let asArray: [string, string][] = undefined !; diff --git a/packages/upgrade/src/static/upgrade_component.ts b/packages/upgrade/src/static/upgrade_component.ts index 23fd55657e4920..4b3b86b860e748 100644 --- a/packages/upgrade/src/static/upgrade_component.ts +++ b/packages/upgrade/src/static/upgrade_component.ts @@ -9,7 +9,7 @@ import {DoCheck, ElementRef, EventEmitter, Injector, OnChanges, OnDestroy, OnInit, SimpleChanges, ɵlooseIdentical as looseIdentical} from '@angular/core'; import * as angular from '../common/angular1'; import {$COMPILE, $CONTROLLER, $HTTP_BACKEND, $INJECTOR, $SCOPE, $TEMPLATE_CACHE} from '../common/constants'; -import {controllerKey} from '../common/util'; +import {controllerKey, directiveNormalize} from '../common/util'; const REQUIRE_PREFIX_RE = /^(\^\^?)?(\?)?(\^\^?)?/; const NOT_SUPPORTED: any = 'NOT_SUPPORTED'; @@ -143,9 +143,9 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy { } ngOnInit() { - console.log('ngOnInit'); - const attachChildNodes: angular.ILinkFn | undefined = this.prepareTransclusion(this.directive.transclude) // Collect contents, insert and compile template + const attachChildNodes: angular.ILinkFn|undefined = + this.prepareTransclusion(this.directive.transclude); const linkFn = this.compileTemplate(this.directive); // Instantiate controller @@ -216,73 +216,6 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy { } } - private prepareTransclusion(transclude: any = false): angular.ILinkFn | undefined { - let childTranscludeFn: angular.ILinkFn | undefined; - - if (transclude) { - const slots = Object.create(null); - let $template: angular.IAugmentedJQuery | Node[]; - - if (typeof transclude !== 'object') { - $template = this.extractChildNodes(this.element); - } else { - $template = []; - - const slotMap = Object.create(null); - const filledSlots = Object.create(null); - - // Parse the element selectors. - Object.keys(transclude).forEach(slotName => { - let selector = transclude[slotName]; - const optional = selector.charAt(0) === '?'; - selector = optional ? selector.substring(1) : selector; - - slotMap[selector] = slotName; - slots[slotName] = null; // `null`: Defined but not yet filled. - filledSlots[slotName] = optional; // Consider optional slots as filled. - }); - - - // Add the matching elements into their slot. - Array.prototype.forEach.call(this.$element.contents !(), (node: Element) => { - console.log(node); - const slotName = slotMap[directiveNormalize(node.nodeName.toLowerCase())]; - if (slotName) { - filledSlots[slotName] = true; - slots[slotName] = slots[slotName] || []; - slots[slotName].push(node); - } else { - $template.push(node); - } - }); - - console.log(slots, filledSlots); - - // Check for required slots that were not filled. - Object.keys(filledSlots).forEach(slotName => { - if (!filledSlots[slotName]) { - throw new Error(`Required transclusion slot '${slotName}' on directive: ${this.name}`); - } - }); - - Object.keys(slots) - .filter(slotName => slots[slotName]) - .forEach(slotName => { - const slot = slots[slotName]; - slots[slotName] = (scope: any, cloneAttach: any) => cloneAttach !(angular.element(slot), scope); - }); - } - - this.$element.empty !(); - - // default slot transclude fn - childTranscludeFn = (scope, cloneAttach) => cloneAttach !(angular.element($template as any)); - (childTranscludeFn as any).$$slots = slots; - - return childTranscludeFn; - } - } - ngOnChanges(changes: SimpleChanges) { if (!this.bindingDestination) { this.pendingChanges = changes; @@ -399,6 +332,66 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy { return bindings; } + private prepareTransclusion(transclude: angular.DirectiveTranscludeProperty = false): + angular.ILinkFn|undefined { + const contentChildNodes = this.extractChildNodes(this.element); + let $template = contentChildNodes; + let attachChildrenFn: angular.ILinkFn|undefined = (scope, cloneAttach) => + cloneAttach !($template, scope); + + if (transclude) { + const slots = Object.create(null); + + if (typeof transclude === 'object') { + $template = []; + + const slotMap = Object.create(null); + const filledSlots = Object.create(null); + + // Parse the element selectors. + Object.keys(transclude).forEach(slotName => { + let selector = transclude[slotName]; + const optional = selector.charAt(0) === '?'; + selector = optional ? selector.substring(1) : selector; + + slotMap[selector] = slotName; + slots[slotName] = null; // `null`: Defined but not yet filled. + filledSlots[slotName] = optional; // Consider optional slots as filled. + }); + + // Add the matching elements into their slot. + contentChildNodes.forEach(node => { + const slotName = slotMap[directiveNormalize(node.nodeName.toLowerCase())]; + if (slotName) { + filledSlots[slotName] = true; + slots[slotName] = slots[slotName] || []; + slots[slotName].push(node); + } else { + $template.push(node); + } + }); + + // Check for required slots that were not filled. + Object.keys(filledSlots).forEach(slotName => { + if (!filledSlots[slotName]) { + throw new Error(`Required transclusion slot '${slotName}' on directive: ${this.name}`); + } + }); + + Object.keys(slots).filter(slotName => slots[slotName]).forEach(slotName => { + const nodes = slots[slotName]; + slots[slotName] = (scope: angular.IScope, cloneAttach: angular.ICloneAttachFunction) => + cloneAttach !(nodes, scope); + }); + } + + // Attach `$$slots` to default slot transclude fn. + attachChildrenFn.$$slots = slots; + } + + return attachChildrenFn; + } + private extractChildNodes(element: Element): Node[] { const childNodes: Node[] = []; let childNode: Node|null; @@ -543,5 +536,3 @@ function isFunction(value: any): value is Function { function isMap(value: angular.SingleOrListOrMap): value is {[key: string]: T} { return value && !Array.isArray(value) && typeof value === 'object'; } - -function directiveNormalize(id: string) { return id; } diff --git a/packages/upgrade/test/common/test_helpers.ts b/packages/upgrade/test/common/test_helpers.ts index 037f59238c3299..827998ca76a470 100644 --- a/packages/upgrade/test/common/test_helpers.ts +++ b/packages/upgrade/test/common/test_helpers.ts @@ -20,9 +20,10 @@ export function html(html: string): Element { return div; } -export function multiTrim(text: string | null | undefined): string { +export function multiTrim(text: string | null | undefined, allSpace = false): string { if (typeof text == 'string') { - return text.replace(/\n/g, '').replace(/\s\s+/g, ' ').trim(); + const repl = allSpace ? '' : ' '; + return text.replace(/\n/g, '').replace(/\s+/g, repl).trim(); } throw new Error('Argument can not be undefined.'); } diff --git a/packages/upgrade/test/static/integration/upgrade_component_spec.ts b/packages/upgrade/test/static/integration/upgrade_component_spec.ts index ad76086311547b..988aa5ca8e1c3b 100644 --- a/packages/upgrade/test/static/integration/upgrade_component_spec.ts +++ b/packages/upgrade/test/static/integration/upgrade_component_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, Directive, ElementRef, EventEmitter, Inject, Injector, Input, NO_ERRORS_SCHEMA, NgModule, Output, SimpleChanges, destroyPlatform} from '@angular/core'; +import {Component, Directive, ElementRef, ErrorHandler, EventEmitter, Inject, Injector, Input, NO_ERRORS_SCHEMA, NgModule, Output, SimpleChanges, destroyPlatform} from '@angular/core'; import {async, fakeAsync, tick} from '@angular/core/testing'; import {BrowserModule} from '@angular/platform-browser'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; @@ -1862,69 +1862,472 @@ export function main() { }); }); - fdescribe('transclusion', () => { - it('should support multizone transclusion', async(() => { - // Define `ng1Component` - const ng1Component: angular.IComponent = { - transclude: { x: 'x', y: 'y' } as any, - // template: 'pre(
(original)
)post' - template: 'pre(
(original X)
(mid {{ 1 + 2 }})
(original Y)
(original default)
)post' - }; + describe('transclusion', () => { + it('should support single-slot transclusion', async(() => { + let ng2ComponentAInstance: Ng2ComponentA; + let ng2ComponentBInstance: Ng2ComponentB; - // Define `Ng1ComponentFacade` - @Directive({selector: 'ng1'}) - class Ng1ComponentFacade extends UpgradeComponent { - constructor(elementRef: ElementRef, injector: Injector) { - super('ng1', elementRef, injector); + // Define `ng1Component` + const ng1Component: + angular.IComponent = {template: 'ng1(
)', transclude: true}; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1'}) + class Ng1ComponentFacade extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } } - } - let component: any; + // Define `Ng2Component` + @Component({ + selector: 'ng2A', + template: 'ng2A({{ value }} | )' + }) + class Ng2ComponentA { + value = 'foo'; + showB = false; + constructor() { ng2ComponentAInstance = this; } + } - @Component({selector: 'app', template: '
(trans-X)
(trans-default {{ 1 + 2 }})(trans-Y)
'}) - class AppComponent { - constructor() { - component = this; + @Component({selector: 'ng2B', template: 'ng2B({{ value }})'}) + class Ng2ComponentB { + value = 'bar'; + constructor() { ng2ComponentBInstance = this; } } - value = true; - } - // Define `ng1Module` - const ng1Module = angular.module('ng1', []) - .component('ng1', ng1Component) - .directive('app', downgradeComponent({component: AppComponent})); + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2A', downgradeComponent({component: Ng2ComponentA})); - const element = html(``); + // Define `Ng2Module` + @NgModule({ + imports: [BrowserModule, UpgradeModule], + declarations: [Ng1ComponentFacade, Ng2ComponentA, Ng2ComponentB], + entryComponents: [Ng2ComponentA] + }) + class Ng2Module { + ngDoBootstrap() {} + } - // Define `Ng2Module` - @NgModule({ - declarations: [Ng1ComponentFacade, AppComponent], - entryComponents: [AppComponent], - imports: [BrowserModule, UpgradeModule], - schemas: [NO_ERRORS_SCHEMA], - }) - class Ng2Module { - constructor(private upgrade: UpgradeModule) {} - ngDoBootstrap() { - this.upgrade.bootstrap(element, [ng1Module.name]); + // Bootstrap + const element = html(``); + + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => { + expect(multiTrim(element.textContent)).toBe('ng2A(ng1(foo | ))'); + + ng2ComponentAInstance.value = 'baz'; + ng2ComponentAInstance.showB = true; + $digest(adapter); + + expect(multiTrim(element.textContent)).toBe('ng2A(ng1(baz | ng2B(bar)))'); + + ng2ComponentBInstance.value = 'qux'; + $digest(adapter); + + expect(multiTrim(element.textContent)).toBe('ng2A(ng1(baz | ng2B(qux)))'); + }); + })); + + it('should support single-slot transclusion with fallback content', async(() => { + let ng1ControllerInstances: any[] = []; + let ng2ComponentInstance: Ng2Component; + + // Define `ng1Component` + const ng1Component: angular.IComponent = { + template: 'ng1(
{{ $ctrl.value }}
)', + transclude: true, + controller: + class {value = 'from-ng1'; constructor() { ng1ControllerInstances.push(this); }} + }; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1'}) + class Ng1ComponentFacade extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } } - } - console.log('bootstrapping'); - platformBrowserDynamic().bootstrapModule(Ng2Module).then((ref) => { - console.log(element); - component.value = false; - $digest(ref.injector.get(UpgradeModule)); - console.log(element); - component.value = true; - $digest(ref.injector.get(UpgradeModule)); - console.log(element); - }).then(() => { - expect(element.textContent).toEqual(''); - }).catch((err) => { - console.log(err); - }); - })); + // Define `Ng2Component` + @Component({selector: 'ng2', template: 'ng2({{ value }} | )'}) + class Ng2Component { + value = 'from-ng2'; + constructor() { ng2ComponentInstance = this; } + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2', downgradeComponent({component: Ng2Component})); + + // Define `Ng2Module` + @NgModule({ + imports: [BrowserModule, UpgradeModule], + declarations: [Ng1ComponentFacade, Ng2Component], + entryComponents: [Ng2Component] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => { + expect(multiTrim(element.textContent)).toBe('ng2(ng1(from-ng2) | ng1(from-ng1))'); + + ng1ControllerInstances.forEach(ctrl => ctrl.value = 'ng1-foo'); + ng2ComponentInstance.value = 'ng2-bar'; + $digest(adapter); + + expect(multiTrim(element.textContent)).toBe('ng2(ng1(ng2-bar) | ng1(ng1-foo))'); + }); + })); + + it('should support multi-slot transclusion', async(() => { + let ng2ComponentInstance: Ng2Component; + + // Define `ng1Component` + const ng1Component: angular.IComponent = { + template: + 'ng1(x(
) | y(
))', + transclude: {slotX: 'contentX', slotY: 'contentY'} + }; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1'}) + class Ng1ComponentFacade extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({ + selector: 'ng2', + template: ` + ng2( + + {{ x }}1 + {{ y }}1 + {{ x }}2 + {{ y }}2 + + )` + }) + class Ng2Component { + x = 'foo'; + y = 'bar'; + constructor() { ng2ComponentInstance = this; } + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2', downgradeComponent({component: Ng2Component})); + + // Define `Ng2Module` + @NgModule({ + imports: [BrowserModule, UpgradeModule], + declarations: [Ng1ComponentFacade, Ng2Component], + entryComponents: [Ng2Component], + schemas: [NO_ERRORS_SCHEMA] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => { + expect(multiTrim(element.textContent, true)).toBe('ng2(ng1(x(foo1foo2)|y(bar1bar2)))'); + + ng2ComponentInstance.x = 'baz'; + ng2ComponentInstance.y = 'qux'; + $digest(adapter); + + expect(multiTrim(element.textContent, true)).toBe('ng2(ng1(x(baz1baz2)|y(qux1qux2)))'); + }); + })); + + it('should support default slot (with fallback content)', async(() => { + let ng1ControllerInstances: any[] = []; + let ng2ComponentInstance: Ng2Component; + + // Define `ng1Component` + const ng1Component: angular.IComponent = { + template: 'ng1(default(
fallback-{{ $ctrl.value }}
))', + transclude: {slotX: 'contentX', slotY: 'contentY'}, + controller: + class {value = 'ng1'; constructor() { ng1ControllerInstances.push(this); }} + }; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1'}) + class Ng1ComponentFacade extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({ + selector: 'ng2', + template: ` + ng2( + + ({{ x }}) + ignored x + {{ x }}-{{ y }} + ignored y + ({{ y }}) + | + + ignored xignored y + )` + }) + class Ng2Component { + x = 'foo'; + y = 'bar'; + constructor() { ng2ComponentInstance = this; } + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2', downgradeComponent({component: Ng2Component})); + + // Define `Ng2Module` + @NgModule({ + imports: [BrowserModule, UpgradeModule], + declarations: [Ng1ComponentFacade, Ng2Component], + entryComponents: [Ng2Component], + schemas: [NO_ERRORS_SCHEMA] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => { + expect(multiTrim(element.textContent, true)) + .toBe('ng2(ng1(default((foo)foo-bar(bar)))|ng1(default(fallback-ng1)))'); + + ng1ControllerInstances.forEach(ctrl => ctrl.value = 'ng1-plus'); + ng2ComponentInstance.x = 'baz'; + ng2ComponentInstance.y = 'qux'; + $digest(adapter); + + expect(multiTrim(element.textContent, true)) + .toBe('ng2(ng1(default((baz)baz-qux(qux)))|ng1(default(fallback-ng1-plus)))'); + }); + })); + + it('should support optional transclusion slots (with fallback content)', async(() => { + let ng1ControllerInstances: any[] = []; + let ng2ComponentInstance: Ng2Component; + + // Define `ng1Component` + const ng1Component: angular.IComponent = { + template: ` + ng1( + x(
{{ $ctrl.x }}
) | + y(
{{ $ctrl.y }}
) + )`, + transclude: {slotX: '?contentX', slotY: '?contentY'}, + controller: class { + x = 'ng1X'; y = 'ng1Y'; constructor() { ng1ControllerInstances.push(this); } + } + }; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1'}) + class Ng1ComponentFacade extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({ + selector: 'ng2', + template: ` + ng2( + {{ x }} | + {{ y }} + )` + }) + class Ng2Component { + x = 'ng2X'; + y = 'ng2Y'; + constructor() { ng2ComponentInstance = this; } + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2', downgradeComponent({component: Ng2Component})); + + // Define `Ng2Module` + @NgModule({ + imports: [BrowserModule, UpgradeModule], + declarations: [Ng1ComponentFacade, Ng2Component], + entryComponents: [Ng2Component], + schemas: [NO_ERRORS_SCHEMA] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => { + expect(multiTrim(element.textContent, true)) + .toBe('ng2(ng1(x(ng2X)|y(ng1Y))|ng1(x(ng1X)|y(ng2Y)))'); + + ng1ControllerInstances.forEach(ctrl => { + ctrl.x = 'ng1X-foo'; + ctrl.y = 'ng1Y-bar'; + }); + ng2ComponentInstance.x = 'ng2X-baz'; + ng2ComponentInstance.y = 'ng2Y-qux'; + $digest(adapter); + + expect(multiTrim(element.textContent, true)) + .toBe('ng2(ng1(x(ng2X-baz)|y(ng1Y-bar))|ng1(x(ng1X-foo)|y(ng2Y-qux)))'); + }); + })); + + it('should throw if a non-optional slot is not filled', async(() => { + let errorMessage: string; + + // Define `ng1Component` + const ng1Component: angular.IComponent = { + template: '', + transclude: {slotX: '?contentX', slotY: 'contentY'} + }; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1'}) + class Ng1ComponentFacade extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({selector: 'ng2', template: ''}) + class Ng2Component { + } + + // Define `ng1Module` + const ng1Module = + angular.module('ng1Module', []) + .value('$exceptionHandler', (error: Error) => errorMessage = error.message) + .component('ng1', ng1Component) + .directive('ng2', downgradeComponent({component: Ng2Component})); + + // Define `Ng2Module` + @NgModule({ + imports: [BrowserModule, UpgradeModule], + declarations: [Ng1ComponentFacade, Ng2Component], + entryComponents: [Ng2Component] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => { + expect(errorMessage) + .toContain('Required transclusion slot \'slotY\' on directive: ng1'); + }); + })); + + it('should support structural directives in transcluded content', async(() => { + let ng2ComponentInstance: Ng2Component; + + // Define `ng1Component` + const ng1Component: angular.IComponent = { + template: + 'ng1(x(
) | default(
))', + transclude: {slotX: 'contentX'} + }; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1'}) + class Ng1ComponentFacade extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + // Define `Ng2Component` + @Component({ + selector: 'ng2', + template: ` + ng2( + +
{{ x }}1
+
{{ y }}1
+
{{ x }}2
+
{{ y }}2
+
+ )` + }) + class Ng2Component { + x = 'foo'; + y = 'bar'; + show = true; + constructor() { ng2ComponentInstance = this; } + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1Module', []) + .component('ng1', ng1Component) + .directive('ng2', downgradeComponent({component: Ng2Component})); + + // Define `Ng2Module` + @NgModule({ + imports: [BrowserModule, UpgradeModule], + declarations: [Ng1ComponentFacade, Ng2Component], + entryComponents: [Ng2Component], + schemas: [NO_ERRORS_SCHEMA] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + // Bootstrap + const element = html(``); + + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => { + expect(multiTrim(element.textContent, true)).toBe('ng2(ng1(x(foo1)|default(bar2)))'); + + ng2ComponentInstance.x = 'baz'; + ng2ComponentInstance.y = 'qux'; + ng2ComponentInstance.show = false; + $digest(adapter); + + expect(multiTrim(element.textContent, true)).toBe('ng2(ng1(x(baz2)|default(qux1)))'); + + ng2ComponentInstance.show = true; + $digest(adapter); + + expect(multiTrim(element.textContent, true)).toBe('ng2(ng1(x(baz1)|default(qux2)))'); + }); + })); }); describe('lifecycle hooks', () => {