feat(ivy): support ng-container as a child of an already inserted view (#25227)
PR Close #25227
This commit is contained in:
parent
28c7a4efbc
commit
c2c12e52fe
|
@ -706,7 +706,7 @@ export function element(
|
||||||
* @param attrs Set of attributes to be used when matching directives.
|
* @param attrs Set of attributes to be used when matching directives.
|
||||||
* @param localRefs A set of local reference bindings on the element.
|
* @param localRefs A set of local reference bindings on the element.
|
||||||
*
|
*
|
||||||
* Even if this instruction accepts a set of attributes no actual attribute values are propoagted to
|
* Even if this instruction accepts a set of attributes no actual attribute values are propagated to
|
||||||
* the DOM (as a comment node can't have attributes). Attributes are here only for directive
|
* the DOM (as a comment node can't have attributes). Attributes are here only for directive
|
||||||
* matching purposes and setting initial inputs of directives.
|
* matching purposes and setting initial inputs of directives.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -56,6 +56,15 @@ export function getParentLNode(node: LNode): LElementNode|LElementContainerNode|
|
||||||
return readElementValue(parent ? node.view[parent.index] : node.view[HOST_NODE]);
|
return readElementValue(parent ? node.view[parent.index] : node.view[HOST_NODE]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves render parent LElementNode for a given view.
|
||||||
|
* Might be null if a view is not yet attatched to any container.
|
||||||
|
*/
|
||||||
|
function getRenderParent(viewNode: LViewNode): LElementNode|null {
|
||||||
|
const container = getParentLNode(viewNode);
|
||||||
|
return container ? container.data[RENDER_PARENT] : null;
|
||||||
|
}
|
||||||
|
|
||||||
const enum WalkLNodeTreeAction {
|
const enum WalkLNodeTreeAction {
|
||||||
/** node insert in the native environment */
|
/** node insert in the native environment */
|
||||||
Insert = 0,
|
Insert = 0,
|
||||||
|
@ -565,6 +574,20 @@ export function canInsertNativeNode(parent: LNode, currentView: LViewData): bool
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a native node before another native node for a given parent using {@link Renderer3}.
|
||||||
|
* This is a utility function that can be used when native nodes were determined - it abstracts an
|
||||||
|
* actual renderer being used.
|
||||||
|
*/
|
||||||
|
function nativeInsertBefore(
|
||||||
|
renderer: Renderer3, parent: RElement, child: RNode, beforeNode: RNode | null): void {
|
||||||
|
if (isProceduralRenderer(renderer)) {
|
||||||
|
renderer.insertBefore(parent, child, beforeNode);
|
||||||
|
} else {
|
||||||
|
parent.insertBefore(child, beforeNode, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Appends the `child` element to the `parent`.
|
* Appends the `child` element to the `parent`.
|
||||||
*
|
*
|
||||||
|
@ -585,15 +608,16 @@ export function appendChild(parent: LNode, child: RNode | null, currentView: LVi
|
||||||
const index = views.indexOf(parent as LViewNode);
|
const index = views.indexOf(parent as LViewNode);
|
||||||
const beforeNode =
|
const beforeNode =
|
||||||
index + 1 < views.length ? (getChildLNode(views[index + 1]) !).native : container.native;
|
index + 1 < views.length ? (getChildLNode(views[index + 1]) !).native : container.native;
|
||||||
isProceduralRenderer(renderer) ?
|
nativeInsertBefore(renderer, renderParent !.native, child, beforeNode);
|
||||||
renderer.insertBefore(renderParent !.native, child, beforeNode) :
|
|
||||||
renderParent !.native.insertBefore(child, beforeNode, true);
|
|
||||||
} else if (parent.tNode.type === TNodeType.ElementContainer) {
|
} else if (parent.tNode.type === TNodeType.ElementContainer) {
|
||||||
const beforeNode = parent.native;
|
const beforeNode = parent.native;
|
||||||
const renderParent = getParentLNode(parent) as LElementNode;
|
const grandParent = getParentLNode(parent) as LElementNode | LViewNode;
|
||||||
isProceduralRenderer(renderer) ?
|
if (grandParent.tNode.type === TNodeType.View) {
|
||||||
renderer.insertBefore(renderParent !.native, child, beforeNode) :
|
const renderParent = getRenderParent(grandParent as LViewNode);
|
||||||
renderParent !.native.insertBefore(child, beforeNode, true);
|
nativeInsertBefore(renderer, renderParent !.native, child, beforeNode);
|
||||||
|
} else {
|
||||||
|
nativeInsertBefore(renderer, (grandParent as LElementNode).native, child, beforeNode);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
isProceduralRenderer(renderer) ? renderer.appendChild(parent.native !as RElement, child) :
|
isProceduralRenderer(renderer) ? renderer.appendChild(parent.native !as RElement, child) :
|
||||||
parent.native !.appendChild(child);
|
parent.native !.appendChild(child);
|
||||||
|
|
|
@ -176,6 +176,9 @@
|
||||||
{
|
{
|
||||||
"name": "getRenderFlags"
|
"name": "getRenderFlags"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "getRenderParent"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "getRootView"
|
"name": "getRootView"
|
||||||
},
|
},
|
||||||
|
@ -200,6 +203,9 @@
|
||||||
{
|
{
|
||||||
"name": "namespaceHTML"
|
"name": "namespaceHTML"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "nativeInsertBefore"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "readElementValue"
|
"name": "readElementValue"
|
||||||
},
|
},
|
||||||
|
|
|
@ -590,6 +590,9 @@
|
||||||
{
|
{
|
||||||
"name": "getRenderFlags"
|
"name": "getRenderFlags"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "getRenderParent"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "getRenderer"
|
"name": "getRenderer"
|
||||||
},
|
},
|
||||||
|
@ -728,6 +731,9 @@
|
||||||
{
|
{
|
||||||
"name": "namespaceHTML"
|
"name": "namespaceHTML"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "nativeInsertBefore"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "nextContext"
|
"name": "nextContext"
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,7 +16,8 @@ import {HEADER_OFFSET} from '../../src/render3/interfaces/view';
|
||||||
import {sanitizeUrl} from '../../src/sanitization/sanitization';
|
import {sanitizeUrl} from '../../src/sanitization/sanitization';
|
||||||
import {Sanitizer, SecurityContext} from '../../src/sanitization/security';
|
import {Sanitizer, SecurityContext} from '../../src/sanitization/security';
|
||||||
|
|
||||||
import {ComponentFixture, TemplateFixture, containerEl, renderToHtml} from './render_util';
|
import {NgIf} from './common_with_def';
|
||||||
|
import {ComponentFixture, TemplateFixture, containerEl, createComponent, renderToHtml} from './render_util';
|
||||||
|
|
||||||
describe('render3 integration test', () => {
|
describe('render3 integration test', () => {
|
||||||
|
|
||||||
|
@ -445,6 +446,154 @@ describe('render3 integration test', () => {
|
||||||
expect(fixture.html).toEqual('<div>before|Greetings<span></span>|after</div>');
|
expect(fixture.html).toEqual('<div>before|Greetings<span></span>|after</div>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should add and remove DOM nodes when ng-container is a child of a regular element', () => {
|
||||||
|
/**
|
||||||
|
* {% if (value) { %}
|
||||||
|
* <div>
|
||||||
|
* <ng-container>content</ng-container>
|
||||||
|
* </div>
|
||||||
|
* {% } %}
|
||||||
|
*/
|
||||||
|
const TestCmpt = createComponent('test-cmpt', function(rf: RenderFlags, ctx: {value: any}) {
|
||||||
|
if (rf & RenderFlags.Create) {
|
||||||
|
container(0);
|
||||||
|
}
|
||||||
|
if (rf & RenderFlags.Update) {
|
||||||
|
containerRefreshStart(0);
|
||||||
|
if (ctx.value) {
|
||||||
|
let rf1 = embeddedViewStart(0);
|
||||||
|
{
|
||||||
|
if (rf1 & RenderFlags.Create) {
|
||||||
|
elementStart(0, 'div');
|
||||||
|
{
|
||||||
|
elementContainerStart(1);
|
||||||
|
{ text(2, 'content'); }
|
||||||
|
elementContainerEnd();
|
||||||
|
}
|
||||||
|
elementEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
embeddedViewEnd();
|
||||||
|
}
|
||||||
|
containerRefreshEnd();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fixture = new ComponentFixture(TestCmpt);
|
||||||
|
expect(fixture.html).toEqual('');
|
||||||
|
|
||||||
|
fixture.component.value = true;
|
||||||
|
fixture.update();
|
||||||
|
expect(fixture.html).toEqual('<div>content</div>');
|
||||||
|
|
||||||
|
fixture.component.value = false;
|
||||||
|
fixture.update();
|
||||||
|
expect(fixture.html).toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add and remove DOM nodes when ng-container is a child of an embedded view (JS block)',
|
||||||
|
() => {
|
||||||
|
/**
|
||||||
|
* {% if (value) { %}
|
||||||
|
* <ng-container>content</ng-container>
|
||||||
|
* {% } %}
|
||||||
|
*/
|
||||||
|
const TestCmpt =
|
||||||
|
createComponent('test-cmpt', function(rf: RenderFlags, ctx: {value: any}) {
|
||||||
|
if (rf & RenderFlags.Create) {
|
||||||
|
container(0);
|
||||||
|
}
|
||||||
|
if (rf & RenderFlags.Update) {
|
||||||
|
containerRefreshStart(0);
|
||||||
|
if (ctx.value) {
|
||||||
|
let rf1 = embeddedViewStart(0);
|
||||||
|
{
|
||||||
|
if (rf1 & RenderFlags.Create) {
|
||||||
|
elementContainerStart(0);
|
||||||
|
{ text(1, 'content'); }
|
||||||
|
elementContainerEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
embeddedViewEnd();
|
||||||
|
}
|
||||||
|
containerRefreshEnd();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fixture = new ComponentFixture(TestCmpt);
|
||||||
|
expect(fixture.html).toEqual('');
|
||||||
|
|
||||||
|
fixture.component.value = true;
|
||||||
|
fixture.update();
|
||||||
|
expect(fixture.html).toEqual('content');
|
||||||
|
|
||||||
|
fixture.component.value = false;
|
||||||
|
fixture.update();
|
||||||
|
expect(fixture.html).toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add and remove DOM nodes when ng-container is a child of an embedded view (ViewContainerRef)',
|
||||||
|
() => {
|
||||||
|
|
||||||
|
function ngIfTemplate(rf: RenderFlags, ctx: any) {
|
||||||
|
if (rf & RenderFlags.Create) {
|
||||||
|
elementContainerStart(0);
|
||||||
|
{ text(1, 'content'); }
|
||||||
|
elementContainerEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <ng-container *ngIf="value">content</ng-container>
|
||||||
|
*/
|
||||||
|
// equivalent to:
|
||||||
|
/**
|
||||||
|
* <ng-template [ngIf]="value">
|
||||||
|
* <ng-container>
|
||||||
|
* content
|
||||||
|
* </ng-container>
|
||||||
|
* </ng-template>
|
||||||
|
*/
|
||||||
|
const TestCmpt =
|
||||||
|
createComponent('test-cmpt', function(rf: RenderFlags, ctx: {value: any}) {
|
||||||
|
if (rf & RenderFlags.Create) {
|
||||||
|
container(0, ngIfTemplate, null, [AttributeMarker.SelectOnly, 'ngIf']);
|
||||||
|
}
|
||||||
|
if (rf & RenderFlags.Update) {
|
||||||
|
elementProperty(0, 'ngIf', bind(ctx.value));
|
||||||
|
}
|
||||||
|
}, [NgIf]);
|
||||||
|
|
||||||
|
const fixture = new ComponentFixture(TestCmpt);
|
||||||
|
expect(fixture.html).toEqual('');
|
||||||
|
|
||||||
|
fixture.component.value = true;
|
||||||
|
fixture.update();
|
||||||
|
expect(fixture.html).toEqual('content');
|
||||||
|
|
||||||
|
fixture.component.value = false;
|
||||||
|
fixture.update();
|
||||||
|
expect(fixture.html).toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render at the component view root', () => {
|
||||||
|
/**
|
||||||
|
* <ng-container>component template</ng-container>
|
||||||
|
*/
|
||||||
|
const TestCmpt = createComponent('test-cmpt', function(rf: RenderFlags) {
|
||||||
|
if (rf & RenderFlags.Create) {
|
||||||
|
elementContainerStart(0);
|
||||||
|
{ text(1, 'component template'); }
|
||||||
|
elementContainerEnd();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function App() { element(0, 'test-cmpt'); }
|
||||||
|
|
||||||
|
const fixture = new TemplateFixture(App, () => {}, [TestCmpt]);
|
||||||
|
expect(fixture.html).toEqual('<test-cmpt>component template</test-cmpt>');
|
||||||
|
});
|
||||||
|
|
||||||
it('should support directives and inject ElementRef', () => {
|
it('should support directives and inject ElementRef', () => {
|
||||||
|
|
||||||
class Directive {
|
class Directive {
|
||||||
|
|
Loading…
Reference in New Issue