Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ivy): implement rootNodes getter on ViewRef #27095

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 17 additions & 19 deletions packages/common/test/directives/ng_component_outlet_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `<ng-template>projected foo</ng-template>${TEST_CMP_TEMPLATE}`;
TestBed.overrideComponent(TestComponent, {set: {template: template}})
.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]});
it('should render projectable nodes, if supplied', async(() => {
const template = `<ng-template>projected foo</ng-template>${TEST_CMP_TEMPLATE}`;
TestBed.overrideComponent(TestComponent, {set: {template: template}})
.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]});

TestBed
.overrideComponent(InjectedComponent, {set: {template: `<ng-content></ng-content>`}})
.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]});
TestBed.overrideComponent(InjectedComponent, {set: {template: `<ng-content></ng-content>`}})
.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(() => {
Expand Down
29 changes: 25 additions & 4 deletions packages/core/src/render3/view_ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -38,8 +40,13 @@ export class ViewRef<T> implements viewEngine_EmbeddedViewRef<T>, 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;
Expand Down Expand Up @@ -269,3 +276,17 @@ export class RootViewRef<T> extends ViewRef<T> {

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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,9 @@
{
"name": "cleanUpView"
},
{
"name": "collectNativeNodes"
},
{
"name": "componentRefresh"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,9 @@
{
"name": "cleanUpView"
},
{
"name": "collectNativeNodes"
},
{
"name": "compileNgModuleFactory"
},
Expand Down
3 changes: 3 additions & 0 deletions packages/core/test/bundling/todo/bundle.golden_symbols.json
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,9 @@
{
"name": "cleanUpView"
},
{
"name": "collectNativeNodes"
},
{
"name": "componentRefresh"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1277,6 +1277,9 @@
{
"name": "cleanUpView"
},
{
"name": "collectNativeNodes"
},
{
"name": "compileNgModuleFactory"
},
Expand Down
1 change: 0 additions & 1 deletion packages/core/test/render3/common_integration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
178 changes: 178 additions & 0 deletions packages/core/test/render3/template_ref_spec.ts
Original file line number Diff line number Diff line change
@@ -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');
}
}

/*
<ng-template tplRef>
<div></div>
some text
<span></span>
</ng-template>
*/
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;

/*
<ng-template tplRef></ng-template>
*/
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));
}
}

/*
<ng-template tplRef><ng-template [ngIf]="true">text</ng-template></ng-template>
*/
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 <ng-template [ngIf]="true">
expect(viewRef.rootNodes.length).toBe(1);
expect(viewRef.rootNodes[0].nodeType).toBe(8);
});


/**
* Contrary to containers (<ng-template>) we _do_ descend into element containers
* (<ng-container):
* https://stackblitz.com/edit/angular-yovmmp?file=src/app/app.component.ts
*/
it('should descend into element containers when retrieving root nodes', () => {
let directiveWithTplRef: DirectiveWithTplRef;

function embeddedTemplate(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementContainerStart(0);
{ text(1, 'text'); }
elementContainerEnd();
}
}

/*
<ng-template tplRef><ng-container>text</ng-container></ng-template>
*/
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 <ng-container>
expect(viewRef.rootNodes[1].nodeType).toBe(3); // a text node
});
});
});