fix(ivy): ng-container with ViewContainerRef creates two comments (#30611)

With Ivy, injecting a `ViewContainerRef` for a `<ng-container>` element
results in two comments generated in the DOM. One comment as host
element for the `ElementContainer` and one for the generated `LContainer`
which is needed for the created `ViewContainerRef`.

This is problematic as developers expect the same anchor element for
the `LContainer` and the `ElementContainer` in order to be able to move
the host element of `<ng-container>` without leaving the actual
`LContainer` anchor element at the original location.

This worked differently in View Engine and various applications might
depend on the behavior where the `ViewContainerRef` shares the anchor
comment node with the host comment node of the `<ng-container>`. For
example `CdkTable` from `@angular/cdk` moves around the host element of
a `<ng-container>` and also expects embedded views to be inserted next
to the moved `<ng-container>` host element.

See: f8be5329f8/src/cdk/table/table.ts (L999-L1016)

Resolves FW-1341

PR Close #30611
This commit is contained in:
Paul Gschwendtner
2019-05-22 13:21:46 +02:00
committed by Jason Aden
parent e20b92ba37
commit fa6cbb3ffe
3 changed files with 69 additions and 7 deletions

View File

@ -937,14 +937,14 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
fixture.detectChanges();
expect(q.query.length).toEqual(1);
expect(toHtml(fixture.nativeElement))
.toEqual(`<div-query><!--ng-container-->Contenu<!--container--></div-query>`);
.toEqual(`<div-query>Contenu<!--ng-container--></div-query>`);
// Disable ng-if
fixture.componentInstance.visible = false;
fixture.detectChanges();
expect(q.query.length).toEqual(0);
expect(toHtml(fixture.nativeElement))
.toEqual(`<div-query><!--ng-container-->Contenu<!--container--></div-query>`);
.toEqual(`<div-query>Contenu<!--ng-container--></div-query>`);
});
});
});

View File

@ -61,6 +61,59 @@ describe('ViewContainerRef', () => {
expect(fixture.componentInstance.foo).toBeAnInstanceOf(TemplateRef);
});
it('should use comment node of host ng-container as insertion marker', () => {
@Component({template: 'hello'})
class HelloComp {
}
@NgModule({entryComponents: [HelloComp], declarations: [HelloComp]})
class HelloCompModule {
}
@Component({
template: `
<ng-container vcref></ng-container>
`
})
class TestComp {
@ViewChild(VCRefDirective, {static: true}) vcRefDir !: VCRefDirective;
}
TestBed.configureTestingModule(
{declarations: [TestComp, VCRefDirective], imports: [HelloCompModule]});
const fixture = TestBed.createComponent(TestComp);
const {vcref, cfr, elementRef} = fixture.componentInstance.vcRefDir;
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toMatch(/<!--(ng-container)?-->/, 'Expected only one comment node to be generated.');
const testParent = document.createElement('div');
testParent.appendChild(elementRef.nativeElement);
expect(testParent.textContent).toBe('');
expect(testParent.childNodes.length).toBe(1);
expect(testParent.childNodes[0].nodeType).toBe(Node.COMMENT_NODE);
// Add a test component to the view container ref to ensure that
// the "ng-container" comment was used as marker for the insertion.
vcref.createComponent(cfr.resolveComponentFactory(HelloComp));
fixture.detectChanges();
expect(testParent.textContent).toBe('hello');
expect(testParent.childNodes.length).toBe(2);
// With Ivy, views are inserted before the container comment marker.
if (ivyEnabled) {
expect(testParent.childNodes[0].nodeType).toBe(Node.ELEMENT_NODE);
expect(testParent.childNodes[0].textContent).toBe('hello');
expect(testParent.childNodes[1].nodeType).toBe(Node.COMMENT_NODE);
} else {
expect(testParent.childNodes[0].nodeType).toBe(Node.COMMENT_NODE);
expect(testParent.childNodes[1].nodeType).toBe(Node.ELEMENT_NODE);
expect(testParent.childNodes[1].textContent).toBe('hello');
}
});
});
describe('insert', () => {
@ -1668,7 +1721,9 @@ class VCRefDirective {
// Injecting the ViewContainerRef to create a dynamic container in which
// embedded views will be created
constructor(public vcref: ViewContainerRef, public cfr: ComponentFactoryResolver) {}
constructor(
public vcref: ViewContainerRef, public cfr: ComponentFactoryResolver,
public elementRef: ElementRef) {}
createView(s: string, index?: number): EmbeddedViewRef<any> {
if (!this.tplRef) {