From 97f1dcba313d96cdbad72eef09a9957a5cdf08d9 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 8 May 2017 12:35:23 +0100 Subject: [PATCH] fix(upgrade): WIP fix multislot transclusion on upgraded components --- .../upgrade/src/static/upgrade_component.ts | 75 ++++++++++++++++++- .../integration/upgrade_component_spec.ts | 65 ++++++++++++++++ 2 files changed, 136 insertions(+), 4 deletions(-) diff --git a/packages/upgrade/src/static/upgrade_component.ts b/packages/upgrade/src/static/upgrade_component.ts index 94065b0f8a07af..23fd55657e4920 100644 --- a/packages/upgrade/src/static/upgrade_component.ts +++ b/packages/upgrade/src/static/upgrade_component.ts @@ -143,8 +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 contentChildNodes = this.extractChildNodes(this.element); const linkFn = this.compileTemplate(this.directive); // Instantiate controller @@ -203,8 +204,6 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy { preLink(this.$componentScope, this.$element, attrs, requiredControllers, transcludeFn); } - const attachChildNodes: angular.ILinkFn = (scope, cloneAttach) => - cloneAttach !(contentChildNodes); linkFn(this.$componentScope, null !, {parentBoundTranscludeFn: attachChildNodes}); if (postLink) { @@ -217,6 +216,73 @@ 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; @@ -465,7 +531,6 @@ export class UpgradeComponent implements OnInit, OnChanges, DoCheck, OnDestroy { } } - function getOrCall(property: Function | T): T { return isFunction(property) ? property() : property; } @@ -478,3 +543,5 @@ 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/static/integration/upgrade_component_spec.ts b/packages/upgrade/test/static/integration/upgrade_component_spec.ts index 707a48197e03e1..ad76086311547b 100644 --- a/packages/upgrade/test/static/integration/upgrade_component_spec.ts +++ b/packages/upgrade/test/static/integration/upgrade_component_spec.ts @@ -1862,6 +1862,71 @@ 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' + }; + + // Define `Ng1ComponentFacade` + @Directive({selector: 'ng1'}) + class Ng1ComponentFacade extends UpgradeComponent { + constructor(elementRef: ElementRef, injector: Injector) { + super('ng1', elementRef, injector); + } + } + + let component: any; + + @Component({selector: 'app', template: '
(trans-X)
(trans-default {{ 1 + 2 }})(trans-Y)
'}) + class AppComponent { + constructor() { + component = this; + } + value = true; + } + + // Define `ng1Module` + const ng1Module = angular.module('ng1', []) + .component('ng1', ng1Component) + .directive('app', downgradeComponent({component: AppComponent})); + + const element = html(``); + + // 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]); + } + } + + 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); + }); + })); + }); + describe('lifecycle hooks', () => { it('should call `$onChanges()` on binding destination (prototype)', fakeAsync(() => { const scopeOnChanges = jasmine.createSpy('scopeOnChanges');