From ce5242462b6ebbca06f208fb32829653eeee3738 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Wed, 14 Nov 2018 10:36:10 +0100 Subject: [PATCH] fix(ivy): implement rootNodes getter on ViewRef (#27095) PR Close #27095 --- .../directives/ng_component_outlet_spec.ts | 36 ++-- packages/core/src/render3/view_ref.ts | 29 ++- .../bundle.golden_symbols.json | 3 + .../hello_world_r2/bundle.golden_symbols.json | 3 + .../bundling/todo/bundle.golden_symbols.json | 3 + .../todo_r2/bundle.golden_symbols.json | 3 + .../test/render3/common_integration_spec.ts | 1 - .../core/test/render3/template_ref_spec.ts | 178 ++++++++++++++++++ 8 files changed, 232 insertions(+), 24 deletions(-) create mode 100644 packages/core/test/render3/template_ref_spec.ts diff --git a/packages/common/test/directives/ng_component_outlet_spec.ts b/packages/common/test/directives/ng_component_outlet_spec.ts index 4b59d8b4d1..dcc3193dd0 100644 --- a/packages/common/test/directives/ng_component_outlet_spec.ts +++ b/packages/common/test/directives/ng_component_outlet_spec.ts @@ -120,31 +120,29 @@ describe('insert/remove', () => { expect(cmpRef.instance.testToken).toBeNull(); })); - fixmeIvy('can not pass projectable nodes') && - it('should render projectable nodes, if supplied', async(() => { - const template = `projected foo${TEST_CMP_TEMPLATE}`; - TestBed.overrideComponent(TestComponent, {set: {template: template}}) - .configureTestingModule({schemas: [NO_ERRORS_SCHEMA]}); + it('should render projectable nodes, if supplied', async(() => { + const template = `projected foo${TEST_CMP_TEMPLATE}`; + TestBed.overrideComponent(TestComponent, {set: {template: template}}) + .configureTestingModule({schemas: [NO_ERRORS_SCHEMA]}); - TestBed - .overrideComponent(InjectedComponent, {set: {template: ``}}) - .configureTestingModule({schemas: [NO_ERRORS_SCHEMA]}); + TestBed.overrideComponent(InjectedComponent, {set: {template: ``}}) + .configureTestingModule({schemas: [NO_ERRORS_SCHEMA]}); - let fixture = TestBed.createComponent(TestComponent); + let fixture = TestBed.createComponent(TestComponent); - fixture.detectChanges(); - expect(fixture.nativeElement).toHaveText(''); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText(''); - fixture.componentInstance.currentComponent = InjectedComponent; - fixture.componentInstance.projectables = - [fixture.componentInstance.vcRef - .createEmbeddedView(fixture.componentInstance.tplRefs.first) - .rootNodes]; + fixture.componentInstance.currentComponent = InjectedComponent; + fixture.componentInstance.projectables = + [fixture.componentInstance.vcRef + .createEmbeddedView(fixture.componentInstance.tplRefs.first) + .rootNodes]; - fixture.detectChanges(); - expect(fixture.nativeElement).toHaveText('projected foo'); - })); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText('projected foo'); + })); fixmeIvy('Runtime compiler is not loaded') && it('should resolve components from other modules, if supplied', async(() => { diff --git a/packages/core/src/render3/view_ref.ts b/packages/core/src/render3/view_ref.ts index e47b32fbcf..ec1066dab0 100644 --- a/packages/core/src/render3/view_ref.ts +++ b/packages/core/src/render3/view_ref.ts @@ -12,10 +12,12 @@ import {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_co import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEngine_InternalViewRef} from '../linker/view_ref'; import {checkNoChanges, checkNoChangesInRootView, detectChanges, detectChangesInRootView, markViewDirty, storeCleanupFn, viewAttached} from './instructions'; -import {TViewNode} from './interfaces/node'; -import {FLAGS, LViewData, LViewFlags, PARENT} from './interfaces/view'; +import {TNode, TNodeType, TViewNode} from './interfaces/node'; +import {FLAGS, HOST, HOST_NODE, LViewData, LViewFlags, PARENT} from './interfaces/view'; import {destroyLView} from './node_manipulation'; import {getRendererFactory} from './state'; +import {getNativeByTNode} from './util'; + // Needed due to tsickle downleveling where multiple `implements` with classes creates @@ -38,8 +40,13 @@ export class ViewRef implements viewEngine_EmbeddedViewRef, viewEngine_Int */ _tViewNode: TViewNode|null = null; - // TODO(issue/24571): remove '!'. - rootNodes !: any[]; + get rootNodes(): any[] { + if (this._view[HOST] == null) { + const tView = this._view[HOST_NODE] as TViewNode; + return collectNativeNodes(this._view, tView, []); + } + return []; + } constructor(_view: LViewData, private _context: T|null, private _componentIndex: number) { this._view = _view; @@ -269,3 +276,17 @@ export class RootViewRef extends ViewRef { checkNoChanges(): void { checkNoChangesInRootView(this._view); } } + +function collectNativeNodes(lView: LViewData, parentTNode: TNode, result: any[]): any[] { + let tNodeChild = parentTNode.child; + + while (tNodeChild) { + result.push(getNativeByTNode(tNodeChild, lView)); + if (tNodeChild.type === TNodeType.ElementContainer) { + collectNativeNodes(lView, tNodeChild, result); + } + tNodeChild = tNodeChild.next; + } + + return result; +} \ No newline at end of file diff --git a/packages/core/test/bundling/animation_world/bundle.golden_symbols.json b/packages/core/test/bundling/animation_world/bundle.golden_symbols.json index ac48a8cfd4..01ee1e9f0a 100644 --- a/packages/core/test/bundling/animation_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/animation_world/bundle.golden_symbols.json @@ -371,6 +371,9 @@ { "name": "cleanUpView" }, + { + "name": "collectNativeNodes" + }, { "name": "componentRefresh" }, diff --git a/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json index 79e0992226..d5bba26b9a 100644 --- a/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json @@ -611,6 +611,9 @@ { "name": "cleanUpView" }, + { + "name": "collectNativeNodes" + }, { "name": "compileNgModuleFactory" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 8d17e05d2b..8857a5ca90 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -428,6 +428,9 @@ { "name": "cleanUpView" }, + { + "name": "collectNativeNodes" + }, { "name": "componentRefresh" }, diff --git a/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json b/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json index 26914e538d..e5630d71fc 100644 --- a/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json @@ -1280,6 +1280,9 @@ { "name": "cleanUpView" }, + { + "name": "collectNativeNodes" + }, { "name": "compileNgModuleFactory" }, diff --git a/packages/core/test/render3/common_integration_spec.ts b/packages/core/test/render3/common_integration_spec.ts index 55a5c5495e..177cb16fd4 100644 --- a/packages/core/test/render3/common_integration_spec.ts +++ b/packages/core/test/render3/common_integration_spec.ts @@ -7,7 +7,6 @@ */ import {NgForOfContext} from '@angular/common'; -import {ElementRef, TemplateRef} from '@angular/core'; import {AttributeMarker, defineComponent, templateRefExtractor} from '../../src/render3/index'; import {bind, template, elementEnd, elementProperty, elementStart, interpolation1, interpolation2, interpolation3, interpolationV, listener, load, nextContext, text, textBinding, elementContainerStart, elementContainerEnd, reference} from '../../src/render3/instructions'; diff --git a/packages/core/test/render3/template_ref_spec.ts b/packages/core/test/render3/template_ref_spec.ts new file mode 100644 index 0000000000..a2fe6883ae --- /dev/null +++ b/packages/core/test/render3/template_ref_spec.ts @@ -0,0 +1,178 @@ +/** + * @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 {TemplateRef} from '@angular/core'; + +import {ComponentFixture, createComponent, getDirectiveOnNode} from './render_util'; +import {bind, directiveInject, element, elementContainerStart, elementContainerEnd, elementProperty, template, text} from '../../src/render3/instructions'; +import {RenderFlags, defineDirective, AttributeMarker} from '../../src/render3/index'; + +import {NgIf} from './common_with_def'; + +describe('TemplateRef', () => { + + describe('rootNodes', () => { + + class DirectiveWithTplRef { + static ngDirectiveDef = defineDirective({ + type: DirectiveWithTplRef, + selectors: [['', 'tplRef', '']], + factory: () => new DirectiveWithTplRef(directiveInject(TemplateRef as any)) + }); + + // injecting a ViewContainerRef to create a dynamic container in which embedded views will be + // created + constructor(public tplRef: TemplateRef<{}>) {} + } + + it('should return root render nodes for an embedded view instance', () => { + let directiveWithTplRef: DirectiveWithTplRef; + + function embeddedTemplate(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + element(0, 'div'); + text(1, 'some text'); + element(2, 'span'); + } + } + + /* + +
+ some text + +
+ */ + const AppComponent = createComponent('app-cmp', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + template(0, embeddedTemplate, 3, 0, null, ['tplRef', '']); + directiveWithTplRef = getDirectiveOnNode(0, 0); + } + }, 1, 0, [DirectiveWithTplRef]); + + + const fixture = new ComponentFixture(AppComponent); + expect(directiveWithTplRef !).toBeDefined(); + + const viewRef = directiveWithTplRef !.tplRef.createEmbeddedView({}); + expect(viewRef.rootNodes.length).toBe(3); + }); + + /** + * This is different as compared to the view engine implementation which returns a comment node + * in this case: + * https://stackblitz.com/edit/angular-uiqry6?file=src/app/app.component.ts + * + * Returning a comment node for a template ref with no nodes is wrong and should be fixed in + * ivy. + */ + it('should return an empty array for embedded view with no nodes', () => { + let directiveWithTplRef: DirectiveWithTplRef; + + /* + + */ + const AppComponent = createComponent('app-cmp', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + template(0, () => {}, 0, 0, null, ['tplRef', '']); + directiveWithTplRef = getDirectiveOnNode(0, 0); + } + }, 1, 0, [DirectiveWithTplRef]); + + + const fixture = new ComponentFixture(AppComponent); + expect(directiveWithTplRef !).toBeDefined(); + + const viewRef = directiveWithTplRef !.tplRef.createEmbeddedView({}); + expect(viewRef.rootNodes.length).toBe(0); + }); + + /** + * This is somehow surprising but the current view engine don't descend into containers when + * getting root nodes of an embedded view: + * https://stackblitz.com/edit/angular-z8zev7?file=src/app/app.component.ts + */ + it('should not descend into containers when retrieving root nodes', () => { + let directiveWithTplRef: DirectiveWithTplRef; + + function ngIfTemplate(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + text(0, 'text'); + } + } + + function embeddedTemplate(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + template(0, ngIfTemplate, 1, 0, null, [AttributeMarker.SelectOnly, 'ngIf']); + } + if (rf & RenderFlags.Update) { + elementProperty(0, 'ngIf', bind(ctx.showing)); + } + } + + /* + text + */ + const AppComponent = createComponent('app-cmp', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + template(0, embeddedTemplate, 1, 1, null, ['tplRef', '']); + directiveWithTplRef = getDirectiveOnNode(0, 0); + } + }, 1, 0, [DirectiveWithTplRef, NgIf]); + + + const fixture = new ComponentFixture(AppComponent); + expect(directiveWithTplRef !).toBeDefined(); + + const viewRef = directiveWithTplRef !.tplRef.createEmbeddedView({}); + + // assert that we've got a comment node (only!) corresponding to + expect(viewRef.rootNodes.length).toBe(1); + expect(viewRef.rootNodes[0].nodeType).toBe(8); + }); + + + /** + * Contrary to containers () we _do_ descend into element containers + * ( { + let directiveWithTplRef: DirectiveWithTplRef; + + function embeddedTemplate(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + elementContainerStart(0); + { text(1, 'text'); } + elementContainerEnd(); + } + } + + /* + text + */ + const AppComponent = createComponent('app-cmp', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + template(0, embeddedTemplate, 2, 0, null, ['tplRef', '']); + directiveWithTplRef = getDirectiveOnNode(0, 0); + } + }, 1, 0, [DirectiveWithTplRef]); + + + const fixture = new ComponentFixture(AppComponent); + expect(directiveWithTplRef !).toBeDefined(); + + const viewRef = directiveWithTplRef !.tplRef.createEmbeddedView({}); + + expect(viewRef.rootNodes.length).toBe(2); + expect(viewRef.rootNodes[0].nodeType) + .toBe(8); // a comment node (only!) corresponding to + expect(viewRef.rootNodes[1].nodeType).toBe(3); // a text node + }); + }); +}); \ No newline at end of file