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): support ng-container inside another ng-container #25346

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
17 changes: 15 additions & 2 deletions packages/core/src/render3/node_manipulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ function walkLNodeTree(
nextNode = head ? (componentHost.data as LViewData)[PARENT] ![head.index] : null;
} else {
// Otherwise look at the first child
nextNode = getChildLNode(node as LViewNode);
nextNode = getChildLNode(node as LViewNode | LElementContainerNode);
}

if (nextNode === null) {
Expand Down Expand Up @@ -532,6 +532,16 @@ function canInsertNativeChildOfElement(parent: LElementNode, currentView: LViewD
return false;
}

/**
* We might delay insertion of children for a given view if it is disconnected.
* This might happen for 2 main reason:
* - view is not inserted into any container (view was created but not iserted yet)
* - view is inserted into a container but the container itself is not inserted into the DOM
* (container might be part of projection or child of a view that is not inserted yet).
*
* In other words we can insert children of a given view this view was inserted into a container and
* the container itself has it render parent determined.
*/
function canInsertNativeChildOfView(parent: LViewNode): boolean {
ngDevMode && assertNodeType(parent, TNodeType.View);

Expand Down Expand Up @@ -635,7 +645,10 @@ export function appendChild(parent: LNode, child: RNode | null, currentView: LVi
nativeInsertBefore(renderer, renderParent !.native, child, beforeNode);
} else if (parent.tNode.type === TNodeType.ElementContainer) {
const beforeNode = parent.native;
const grandParent = getParentLNode(parent) as LElementNode | LViewNode;
let grandParent = getParentLNode(parent as LElementContainerNode);
while (grandParent.tNode.type === TNodeType.ElementContainer) {
grandParent = getParentLNode(grandParent as LElementContainerNode);
}
if (grandParent.tNode.type === TNodeType.View) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the canInsertNativeNode counterpart, we account for grandParent being null here. Is that relevant here too?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, since we've already checked (in the canInsertNativeNode) that it can be inserted so there should be a render parent somewhere above.

const renderParent = getRenderParent(grandParent as LViewNode);
nativeInsertBefore(renderer, renderParent !.native, child, beforeNode);
Expand Down
102 changes: 102 additions & 0 deletions packages/core/test/render3/integration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,108 @@ describe('render3 integration test', () => {
expect(fixture.html).toEqual('<test-cmpt>component template</test-cmpt>');
});

it('should render inside another ng-container', () => {
/**
* <ng-container>
* <ng-container>
* <ng-container>
* content
* </ng-container>
* </ng-container>
* </ng-container>
*/
const TestCmpt = createComponent('test-cmpt', function(rf: RenderFlags) {
if (rf & RenderFlags.Create) {
elementContainerStart(0);
{
elementContainerStart(1);
{
elementContainerStart(2);
{ text(3, 'content'); }
elementContainerEnd();
}
elementContainerEnd();
}
elementContainerEnd();
}
});

function App() { element(0, 'test-cmpt'); }

const fixture = new TemplateFixture(App, () => {}, [TestCmpt]);
expect(fixture.html).toEqual('<test-cmpt>content</test-cmpt>');
});

it('should render inside another ng-container at the root of a delayed view', () => {

class TestDirective {
constructor(private _tplRef: TemplateRef<any>, private _vcRef: ViewContainerRef) {}

createAndInsert() { this._vcRef.insert(this._tplRef.createEmbeddedView({})); }

clear() { this._vcRef.clear(); }

static ngDirectiveDef = defineDirective({
type: TestDirective,
selectors: [['', 'testDirective', '']],
factory: () => new TestDirective(injectTemplateRef(), injectViewContainerRef()),
});
}

let testDirective: TestDirective;

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

`<ng-template testDirective>
<ng-container>
<ng-container>
<ng-container>
content
</ng-container>
</ng-container>
</ng-container>
</ng-template>`;
const TestCmpt = createComponent('test-cmpt', function(rf: RenderFlags) {
if (rf & RenderFlags.Create) {
container(0, embeddedTemplate, null, [AttributeMarker.SelectOnly, 'testDirective']);
}
if (rf & RenderFlags.Update) {
testDirective = loadDirective<TestDirective>(0);
}
}, [TestDirective]);

function App() { element(0, 'test-cmpt'); }

const fixture = new ComponentFixture(TestCmpt);
expect(fixture.html).toEqual('');

testDirective !.createAndInsert();
fixture.update();
expect(fixture.html).toEqual('content');

testDirective !.createAndInsert();
fixture.update();
expect(fixture.html).toEqual('contentcontent');

testDirective !.clear();
fixture.update();
expect(fixture.html).toEqual('');
});

it('should support directives and inject ElementRef', () => {

class Directive {
Expand Down