fix(ivy): queries should match elements inside ng-container with the descendants: false option (#35384)
Before this change content queries with the `descendants: false` option, as implemented in ivy, would not descendinto `<ng-container>` elements. This behaviour was different from the way the View Engine worked. This change alligns ngIvy and VE behaviours when it comes to queries and the `<ng-container>` elements and fixes a common bugs where a query target was placed inside the `<ng-container>` element with a * directive on it. Before: ```html <needs-target> <ng-container *ngIf="condition"> <div #target>...</div> <!-- this node would NOT match --> </ng-container> </needs-target> ``` After: ```html <needs-target> <ng-container *ngIf="condition"> <div #target>...</div> <!-- this node WILL match --> </ng-container> </needs-target> ``` Fixes #34768 PR Close #35384
This commit is contained in:

committed by
Alex Rickabaugh

parent
5fbfe6996a
commit
3f4e02b8c7
@ -690,7 +690,14 @@ describe('query logic', () => {
|
||||
expect(fixture.componentInstance.contentChildren.length).toBe(0);
|
||||
});
|
||||
|
||||
describe('descendants', () => {
|
||||
describe('descendants: false (default)', () => {
|
||||
|
||||
/**
|
||||
* A helper function to check if a given object looks like ElementRef. It is used in place of
|
||||
* the `instanceof ElementRef` check since ivy returns a type that looks like ElementRef (have
|
||||
* the same properties but doesn't pass the instanceof ElementRef test)
|
||||
*/
|
||||
function isElementRefLike(result: any): boolean { return result.nativeElement != null; }
|
||||
|
||||
it('should match directives on elements that used to be wrapped by a required parent in HTML parser',
|
||||
() => {
|
||||
@ -715,8 +722,246 @@ describe('query logic', () => {
|
||||
fixture.detectChanges();
|
||||
expect(cmptWithQuery.myDefs.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should match elements with local refs inside <ng-container>', () => {
|
||||
|
||||
@Component({selector: 'needs-target', template: ``})
|
||||
class NeedsTarget {
|
||||
@ContentChildren('target') targets !: QueryList<ElementRef>;
|
||||
}
|
||||
@Component({
|
||||
selector: 'test-cmpt',
|
||||
template: `
|
||||
<needs-target>
|
||||
<ng-container>
|
||||
<tr #target></tr>
|
||||
</ng-container>
|
||||
</needs-target>
|
||||
`,
|
||||
})
|
||||
class TestCmpt {
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [TestCmpt, NeedsTarget]});
|
||||
const fixture = TestBed.createComponent(TestCmpt);
|
||||
const cmptWithQuery = fixture.debugElement.children[0].injector.get(NeedsTarget);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(cmptWithQuery.targets.length).toBe(1);
|
||||
expect(isElementRefLike(cmptWithQuery.targets.first)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should match elements with local refs inside nested <ng-container>', () => {
|
||||
|
||||
@Component({selector: 'needs-target', template: ``})
|
||||
class NeedsTarget {
|
||||
@ContentChildren('target') targets !: QueryList<ElementRef>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'test-cmpt',
|
||||
template: `
|
||||
<needs-target>
|
||||
<ng-container>
|
||||
<ng-container>
|
||||
<ng-container>
|
||||
<tr #target></tr>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</needs-target>
|
||||
`,
|
||||
})
|
||||
class TestCmpt {
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [TestCmpt, NeedsTarget]});
|
||||
const fixture = TestBed.createComponent(TestCmpt);
|
||||
const cmptWithQuery = fixture.debugElement.children[0].injector.get(NeedsTarget);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(cmptWithQuery.targets.length).toBe(1);
|
||||
expect(isElementRefLike(cmptWithQuery.targets.first)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should match directives inside <ng-container>', () => {
|
||||
@Directive({selector: '[targetDir]'})
|
||||
class TargetDir {
|
||||
}
|
||||
|
||||
@Component({selector: 'needs-target', template: ``})
|
||||
class NeedsTarget {
|
||||
@ContentChildren(TargetDir) targets !: QueryList<HTMLElement>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'test-cmpt',
|
||||
template: `
|
||||
<needs-target>
|
||||
<ng-container>
|
||||
<tr targetDir></tr>
|
||||
</ng-container>
|
||||
</needs-target>
|
||||
`,
|
||||
})
|
||||
class TestCmpt {
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [TestCmpt, NeedsTarget, TargetDir]});
|
||||
const fixture = TestBed.createComponent(TestCmpt);
|
||||
const cmptWithQuery = fixture.debugElement.children[0].injector.get(NeedsTarget);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(cmptWithQuery.targets.length).toBe(1);
|
||||
expect(cmptWithQuery.targets.first).toBeAnInstanceOf(TargetDir);
|
||||
});
|
||||
|
||||
it('should match directives inside nested <ng-container>', () => {
|
||||
@Directive({selector: '[targetDir]'})
|
||||
class TargetDir {
|
||||
}
|
||||
|
||||
@Component({selector: 'needs-target', template: ``})
|
||||
class NeedsTarget {
|
||||
@ContentChildren(TargetDir) targets !: QueryList<HTMLElement>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'test-cmpt',
|
||||
template: `
|
||||
<needs-target>
|
||||
<ng-container>
|
||||
<ng-container>
|
||||
<ng-container>
|
||||
<tr targetDir></tr>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</needs-target>
|
||||
`,
|
||||
})
|
||||
class TestCmpt {
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [TestCmpt, NeedsTarget, TargetDir]});
|
||||
const fixture = TestBed.createComponent(TestCmpt);
|
||||
const cmptWithQuery = fixture.debugElement.children[0].injector.get(NeedsTarget);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(cmptWithQuery.targets.length).toBe(1);
|
||||
expect(cmptWithQuery.targets.first).toBeAnInstanceOf(TargetDir);
|
||||
});
|
||||
|
||||
it('should cross child ng-container when query is declared on ng-container', () => {
|
||||
@Directive({selector: '[targetDir]'})
|
||||
class TargetDir {
|
||||
}
|
||||
|
||||
@Directive({selector: '[needs-target]'})
|
||||
class NeedsTarget {
|
||||
@ContentChildren(TargetDir) targets !: QueryList<HTMLElement>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'test-cmpt',
|
||||
template: `
|
||||
<ng-container targetDir>
|
||||
<ng-container needs-target>
|
||||
<ng-container>
|
||||
<tr targetDir></tr>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
`,
|
||||
})
|
||||
class TestCmpt {
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [TestCmpt, NeedsTarget, TargetDir]});
|
||||
const fixture = TestBed.createComponent(TestCmpt);
|
||||
const cmptWithQuery = fixture.debugElement.children[0].injector.get(NeedsTarget);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(cmptWithQuery.targets.length).toBe(1);
|
||||
expect(cmptWithQuery.targets.first).toBeAnInstanceOf(TargetDir);
|
||||
});
|
||||
|
||||
it('should match nodes when using structural directives (*syntax) on <ng-container>', () => {
|
||||
@Directive({selector: '[targetDir]'})
|
||||
class TargetDir {
|
||||
}
|
||||
|
||||
@Component({selector: 'needs-target', template: ``})
|
||||
class NeedsTarget {
|
||||
@ContentChildren(TargetDir) dirTargets !: QueryList<TargetDir>;
|
||||
@ContentChildren('target') localRefsTargets !: QueryList<ElementRef>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'test-cmpt',
|
||||
template: `
|
||||
<needs-target>
|
||||
<ng-container *ngIf="true">
|
||||
<div targetDir></div>
|
||||
<div #target></div>
|
||||
</ng-container>
|
||||
</needs-target>
|
||||
`,
|
||||
})
|
||||
class TestCmpt {
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [TestCmpt, NeedsTarget, TargetDir]});
|
||||
const fixture = TestBed.createComponent(TestCmpt);
|
||||
const cmptWithQuery = fixture.debugElement.children[0].injector.get(NeedsTarget);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(cmptWithQuery.dirTargets.length).toBe(1);
|
||||
expect(cmptWithQuery.dirTargets.first).toBeAnInstanceOf(TargetDir);
|
||||
expect(cmptWithQuery.localRefsTargets.length).toBe(1);
|
||||
expect(isElementRefLike(cmptWithQuery.localRefsTargets.first)).toBeTruthy();
|
||||
});
|
||||
|
||||
onlyInIvy(
|
||||
'VE uses injectors hierarchy to determine if node matches, ivy uses elements as written in a template')
|
||||
.it('should match directives on <ng-container> when crossing nested <ng-container>', () => {
|
||||
@Directive({selector: '[targetDir]'})
|
||||
class TargetDir {
|
||||
}
|
||||
|
||||
@Component({selector: 'needs-target', template: ``})
|
||||
class NeedsTarget {
|
||||
@ContentChildren(TargetDir) targets !: QueryList<HTMLElement>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'test-cmpt',
|
||||
template: `
|
||||
<needs-target>
|
||||
<ng-container>
|
||||
<ng-container targetDir>
|
||||
<ng-container targetDir>
|
||||
<tr targetDir></tr>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</needs-target>
|
||||
`,
|
||||
})
|
||||
class TestCmpt {
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [TestCmpt, NeedsTarget, TargetDir]});
|
||||
const fixture = TestBed.createComponent(TestCmpt);
|
||||
const cmptWithQuery = fixture.debugElement.children[0].injector.get(NeedsTarget);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(cmptWithQuery.targets.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
describe('observable interface', () => {
|
||||
|
||||
it('should allow observing changes to query list', () => {
|
||||
|
Reference in New Issue
Block a user