Skip to content

Commit ce52424

Browse files
fix(ivy): implement rootNodes getter on ViewRef (angular#27095)
PR Close angular#27095
1 parent 1c9e526 commit ce52424

File tree

8 files changed

+232
-24
lines changed

8 files changed

+232
-24
lines changed

packages/common/test/directives/ng_component_outlet_spec.ts

+17-19
Original file line numberDiff line numberDiff line change
@@ -120,31 +120,29 @@ describe('insert/remove', () => {
120120
expect(cmpRef.instance.testToken).toBeNull();
121121
}));
122122

123-
fixmeIvy('can not pass projectable nodes') &&
124-
it('should render projectable nodes, if supplied', async(() => {
125-
const template = `<ng-template>projected foo</ng-template>${TEST_CMP_TEMPLATE}`;
126-
TestBed.overrideComponent(TestComponent, {set: {template: template}})
127-
.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]});
123+
it('should render projectable nodes, if supplied', async(() => {
124+
const template = `<ng-template>projected foo</ng-template>${TEST_CMP_TEMPLATE}`;
125+
TestBed.overrideComponent(TestComponent, {set: {template: template}})
126+
.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]});
128127

129-
TestBed
130-
.overrideComponent(InjectedComponent, {set: {template: `<ng-content></ng-content>`}})
131-
.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]});
128+
TestBed.overrideComponent(InjectedComponent, {set: {template: `<ng-content></ng-content>`}})
129+
.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]});
132130

133-
let fixture = TestBed.createComponent(TestComponent);
131+
let fixture = TestBed.createComponent(TestComponent);
134132

135-
fixture.detectChanges();
136-
expect(fixture.nativeElement).toHaveText('');
133+
fixture.detectChanges();
134+
expect(fixture.nativeElement).toHaveText('');
137135

138-
fixture.componentInstance.currentComponent = InjectedComponent;
139-
fixture.componentInstance.projectables =
140-
[fixture.componentInstance.vcRef
141-
.createEmbeddedView(fixture.componentInstance.tplRefs.first)
142-
.rootNodes];
136+
fixture.componentInstance.currentComponent = InjectedComponent;
137+
fixture.componentInstance.projectables =
138+
[fixture.componentInstance.vcRef
139+
.createEmbeddedView(fixture.componentInstance.tplRefs.first)
140+
.rootNodes];
143141

144142

145-
fixture.detectChanges();
146-
expect(fixture.nativeElement).toHaveText('projected foo');
147-
}));
143+
fixture.detectChanges();
144+
expect(fixture.nativeElement).toHaveText('projected foo');
145+
}));
148146

149147
fixmeIvy('Runtime compiler is not loaded') &&
150148
it('should resolve components from other modules, if supplied', async(() => {

packages/core/src/render3/view_ref.ts

+25-4
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_co
1212
import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEngine_InternalViewRef} from '../linker/view_ref';
1313

1414
import {checkNoChanges, checkNoChangesInRootView, detectChanges, detectChangesInRootView, markViewDirty, storeCleanupFn, viewAttached} from './instructions';
15-
import {TViewNode} from './interfaces/node';
16-
import {FLAGS, LViewData, LViewFlags, PARENT} from './interfaces/view';
15+
import {TNode, TNodeType, TViewNode} from './interfaces/node';
16+
import {FLAGS, HOST, HOST_NODE, LViewData, LViewFlags, PARENT} from './interfaces/view';
1717
import {destroyLView} from './node_manipulation';
1818
import {getRendererFactory} from './state';
19+
import {getNativeByTNode} from './util';
20+
1921

2022

2123
// Needed due to tsickle downleveling where multiple `implements` with classes creates
@@ -38,8 +40,13 @@ export class ViewRef<T> implements viewEngine_EmbeddedViewRef<T>, viewEngine_Int
3840
*/
3941
_tViewNode: TViewNode|null = null;
4042

41-
// TODO(issue/24571): remove '!'.
42-
rootNodes !: any[];
43+
get rootNodes(): any[] {
44+
if (this._view[HOST] == null) {
45+
const tView = this._view[HOST_NODE] as TViewNode;
46+
return collectNativeNodes(this._view, tView, []);
47+
}
48+
return [];
49+
}
4350

4451
constructor(_view: LViewData, private _context: T|null, private _componentIndex: number) {
4552
this._view = _view;
@@ -269,3 +276,17 @@ export class RootViewRef<T> extends ViewRef<T> {
269276

270277
checkNoChanges(): void { checkNoChangesInRootView(this._view); }
271278
}
279+
280+
function collectNativeNodes(lView: LViewData, parentTNode: TNode, result: any[]): any[] {
281+
let tNodeChild = parentTNode.child;
282+
283+
while (tNodeChild) {
284+
result.push(getNativeByTNode(tNodeChild, lView));
285+
if (tNodeChild.type === TNodeType.ElementContainer) {
286+
collectNativeNodes(lView, tNodeChild, result);
287+
}
288+
tNodeChild = tNodeChild.next;
289+
}
290+
291+
return result;
292+
}

packages/core/test/bundling/animation_world/bundle.golden_symbols.json

+3
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,9 @@
371371
{
372372
"name": "cleanUpView"
373373
},
374+
{
375+
"name": "collectNativeNodes"
376+
},
374377
{
375378
"name": "componentRefresh"
376379
},

packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json

+3
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,9 @@
611611
{
612612
"name": "cleanUpView"
613613
},
614+
{
615+
"name": "collectNativeNodes"
616+
},
614617
{
615618
"name": "compileNgModuleFactory"
616619
},

packages/core/test/bundling/todo/bundle.golden_symbols.json

+3
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,9 @@
428428
{
429429
"name": "cleanUpView"
430430
},
431+
{
432+
"name": "collectNativeNodes"
433+
},
431434
{
432435
"name": "componentRefresh"
433436
},

packages/core/test/bundling/todo_r2/bundle.golden_symbols.json

+3
Original file line numberDiff line numberDiff line change
@@ -1280,6 +1280,9 @@
12801280
{
12811281
"name": "cleanUpView"
12821282
},
1283+
{
1284+
"name": "collectNativeNodes"
1285+
},
12831286
{
12841287
"name": "compileNgModuleFactory"
12851288
},

packages/core/test/render3/common_integration_spec.ts

-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
*/
88

99
import {NgForOfContext} from '@angular/common';
10-
import {ElementRef, TemplateRef} from '@angular/core';
1110

1211
import {AttributeMarker, defineComponent, templateRefExtractor} from '../../src/render3/index';
1312
import {bind, template, elementEnd, elementProperty, elementStart, interpolation1, interpolation2, interpolation3, interpolationV, listener, load, nextContext, text, textBinding, elementContainerStart, elementContainerEnd, reference} from '../../src/render3/instructions';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {TemplateRef} from '@angular/core';
10+
11+
import {ComponentFixture, createComponent, getDirectiveOnNode} from './render_util';
12+
import {bind, directiveInject, element, elementContainerStart, elementContainerEnd, elementProperty, template, text} from '../../src/render3/instructions';
13+
import {RenderFlags, defineDirective, AttributeMarker} from '../../src/render3/index';
14+
15+
import {NgIf} from './common_with_def';
16+
17+
describe('TemplateRef', () => {
18+
19+
describe('rootNodes', () => {
20+
21+
class DirectiveWithTplRef {
22+
static ngDirectiveDef = defineDirective({
23+
type: DirectiveWithTplRef,
24+
selectors: [['', 'tplRef', '']],
25+
factory: () => new DirectiveWithTplRef(directiveInject(TemplateRef as any))
26+
});
27+
28+
// injecting a ViewContainerRef to create a dynamic container in which embedded views will be
29+
// created
30+
constructor(public tplRef: TemplateRef<{}>) {}
31+
}
32+
33+
it('should return root render nodes for an embedded view instance', () => {
34+
let directiveWithTplRef: DirectiveWithTplRef;
35+
36+
function embeddedTemplate(rf: RenderFlags, ctx: any) {
37+
if (rf & RenderFlags.Create) {
38+
element(0, 'div');
39+
text(1, 'some text');
40+
element(2, 'span');
41+
}
42+
}
43+
44+
/*
45+
<ng-template tplRef>
46+
<div></div>
47+
some text
48+
<span></span>
49+
</ng-template>
50+
*/
51+
const AppComponent = createComponent('app-cmp', function(rf: RenderFlags, ctx: any) {
52+
if (rf & RenderFlags.Create) {
53+
template(0, embeddedTemplate, 3, 0, null, ['tplRef', '']);
54+
directiveWithTplRef = getDirectiveOnNode(0, 0);
55+
}
56+
}, 1, 0, [DirectiveWithTplRef]);
57+
58+
59+
const fixture = new ComponentFixture(AppComponent);
60+
expect(directiveWithTplRef !).toBeDefined();
61+
62+
const viewRef = directiveWithTplRef !.tplRef.createEmbeddedView({});
63+
expect(viewRef.rootNodes.length).toBe(3);
64+
});
65+
66+
/**
67+
* This is different as compared to the view engine implementation which returns a comment node
68+
* in this case:
69+
* https://stackblitz.com/edit/angular-uiqry6?file=src/app/app.component.ts
70+
*
71+
* Returning a comment node for a template ref with no nodes is wrong and should be fixed in
72+
* ivy.
73+
*/
74+
it('should return an empty array for embedded view with no nodes', () => {
75+
let directiveWithTplRef: DirectiveWithTplRef;
76+
77+
/*
78+
<ng-template tplRef></ng-template>
79+
*/
80+
const AppComponent = createComponent('app-cmp', function(rf: RenderFlags, ctx: any) {
81+
if (rf & RenderFlags.Create) {
82+
template(0, () => {}, 0, 0, null, ['tplRef', '']);
83+
directiveWithTplRef = getDirectiveOnNode(0, 0);
84+
}
85+
}, 1, 0, [DirectiveWithTplRef]);
86+
87+
88+
const fixture = new ComponentFixture(AppComponent);
89+
expect(directiveWithTplRef !).toBeDefined();
90+
91+
const viewRef = directiveWithTplRef !.tplRef.createEmbeddedView({});
92+
expect(viewRef.rootNodes.length).toBe(0);
93+
});
94+
95+
/**
96+
* This is somehow surprising but the current view engine don't descend into containers when
97+
* getting root nodes of an embedded view:
98+
* https://stackblitz.com/edit/angular-z8zev7?file=src/app/app.component.ts
99+
*/
100+
it('should not descend into containers when retrieving root nodes', () => {
101+
let directiveWithTplRef: DirectiveWithTplRef;
102+
103+
function ngIfTemplate(rf: RenderFlags, ctx: any) {
104+
if (rf & RenderFlags.Create) {
105+
text(0, 'text');
106+
}
107+
}
108+
109+
function embeddedTemplate(rf: RenderFlags, ctx: any) {
110+
if (rf & RenderFlags.Create) {
111+
template(0, ngIfTemplate, 1, 0, null, [AttributeMarker.SelectOnly, 'ngIf']);
112+
}
113+
if (rf & RenderFlags.Update) {
114+
elementProperty(0, 'ngIf', bind(ctx.showing));
115+
}
116+
}
117+
118+
/*
119+
<ng-template tplRef><ng-template [ngIf]="true">text</ng-template></ng-template>
120+
*/
121+
const AppComponent = createComponent('app-cmp', function(rf: RenderFlags, ctx: any) {
122+
if (rf & RenderFlags.Create) {
123+
template(0, embeddedTemplate, 1, 1, null, ['tplRef', '']);
124+
directiveWithTplRef = getDirectiveOnNode(0, 0);
125+
}
126+
}, 1, 0, [DirectiveWithTplRef, NgIf]);
127+
128+
129+
const fixture = new ComponentFixture(AppComponent);
130+
expect(directiveWithTplRef !).toBeDefined();
131+
132+
const viewRef = directiveWithTplRef !.tplRef.createEmbeddedView({});
133+
134+
// assert that we've got a comment node (only!) corresponding to <ng-template [ngIf]="true">
135+
expect(viewRef.rootNodes.length).toBe(1);
136+
expect(viewRef.rootNodes[0].nodeType).toBe(8);
137+
});
138+
139+
140+
/**
141+
* Contrary to containers (<ng-template>) we _do_ descend into element containers
142+
* (<ng-container):
143+
* https://stackblitz.com/edit/angular-yovmmp?file=src/app/app.component.ts
144+
*/
145+
it('should descend into element containers when retrieving root nodes', () => {
146+
let directiveWithTplRef: DirectiveWithTplRef;
147+
148+
function embeddedTemplate(rf: RenderFlags, ctx: any) {
149+
if (rf & RenderFlags.Create) {
150+
elementContainerStart(0);
151+
{ text(1, 'text'); }
152+
elementContainerEnd();
153+
}
154+
}
155+
156+
/*
157+
<ng-template tplRef><ng-container>text</ng-container></ng-template>
158+
*/
159+
const AppComponent = createComponent('app-cmp', function(rf: RenderFlags, ctx: any) {
160+
if (rf & RenderFlags.Create) {
161+
template(0, embeddedTemplate, 2, 0, null, ['tplRef', '']);
162+
directiveWithTplRef = getDirectiveOnNode(0, 0);
163+
}
164+
}, 1, 0, [DirectiveWithTplRef]);
165+
166+
167+
const fixture = new ComponentFixture(AppComponent);
168+
expect(directiveWithTplRef !).toBeDefined();
169+
170+
const viewRef = directiveWithTplRef !.tplRef.createEmbeddedView({});
171+
172+
expect(viewRef.rootNodes.length).toBe(2);
173+
expect(viewRef.rootNodes[0].nodeType)
174+
.toBe(8); // a comment node (only!) corresponding to <ng-container>
175+
expect(viewRef.rootNodes[1].nodeType).toBe(3); // a text node
176+
});
177+
});
178+
});

0 commit comments

Comments
 (0)