diff --git a/aio/scripts/_payload-limits.json b/aio/scripts/_payload-limits.json index ca3527e224..c3abfed8ea 100755 --- a/aio/scripts/_payload-limits.json +++ b/aio/scripts/_payload-limits.json @@ -26,4 +26,4 @@ } } } -} +} \ No newline at end of file diff --git a/integration/_payload-limits.json b/integration/_payload-limits.json index a801d1bf30..8441e070c9 100644 --- a/integration/_payload-limits.json +++ b/integration/_payload-limits.json @@ -64,4 +64,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/core/src/render3/query.ts b/packages/core/src/render3/query.ts index ebbef19a4a..10ad1d0256 100644 --- a/packages/core/src/render3/query.ts +++ b/packages/core/src/render3/query.ts @@ -188,7 +188,23 @@ class TQuery_ implements TQuery { private isApplyingToNode(tNode: TNode): boolean { if (this._appliesToNextNode && this.metadata.descendants === false) { - return this._declarationNodeIndex === (tNode.parent ? tNode.parent.index : -1); + const declarationNodeIdx = this._declarationNodeIndex; + let parent = tNode.parent; + // Determine if a given TNode is a "direct" child of a node on which a content query was + // declared (only direct children of query's host node can match with the descendants: false + // option). There are 3 main use-case / conditions to consider here: + // - : here parent node is a query + // host node; + // - : + // here parent node is null; + // - : here we need + // to go past `` to determine parent node (but we shouldn't traverse + // up past the query's host node!). + while (parent !== null && parent.type === TNodeType.ElementContainer && + parent.index !== declarationNodeIdx) { + parent = parent.parent; + } + return declarationNodeIdx === (parent !== null ? parent.index : -1); } return this._appliesToNextNode; } diff --git a/packages/core/test/acceptance/query_spec.ts b/packages/core/test/acceptance/query_spec.ts index 323b5604c9..9a65692489 100644 --- a/packages/core/test/acceptance/query_spec.ts +++ b/packages/core/test/acceptance/query_spec.ts @@ -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 ', () => { + + @Component({selector: 'needs-target', template: ``}) + class NeedsTarget { + @ContentChildren('target') targets !: QueryList; + } + @Component({ + selector: 'test-cmpt', + template: ` + + + + + + `, + }) + 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 ', () => { + + @Component({selector: 'needs-target', template: ``}) + class NeedsTarget { + @ContentChildren('target') targets !: QueryList; + } + + @Component({ + selector: 'test-cmpt', + template: ` + + + + + + + + + + `, + }) + 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 ', () => { + @Directive({selector: '[targetDir]'}) + class TargetDir { + } + + @Component({selector: 'needs-target', template: ``}) + class NeedsTarget { + @ContentChildren(TargetDir) targets !: QueryList; + } + + @Component({ + selector: 'test-cmpt', + template: ` + + + + + + `, + }) + 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 ', () => { + @Directive({selector: '[targetDir]'}) + class TargetDir { + } + + @Component({selector: 'needs-target', template: ``}) + class NeedsTarget { + @ContentChildren(TargetDir) targets !: QueryList; + } + + @Component({ + selector: 'test-cmpt', + template: ` + + + + + + + + + + `, + }) + 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; + } + + @Component({ + selector: 'test-cmpt', + template: ` + + + + + + + + `, + }) + 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 ', () => { + @Directive({selector: '[targetDir]'}) + class TargetDir { + } + + @Component({selector: 'needs-target', template: ``}) + class NeedsTarget { + @ContentChildren(TargetDir) dirTargets !: QueryList; + @ContentChildren('target') localRefsTargets !: QueryList; + } + + @Component({ + selector: 'test-cmpt', + template: ` + + +
+
+
+
+ `, + }) + 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 when crossing nested ', () => { + @Directive({selector: '[targetDir]'}) + class TargetDir { + } + + @Component({selector: 'needs-target', template: ``}) + class NeedsTarget { + @ContentChildren(TargetDir) targets !: QueryList; + } + + @Component({ + selector: 'test-cmpt', + template: ` + + + + + + + + + + `, + }) + 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', () => { diff --git a/packages/core/test/linker/query_integration_spec.ts b/packages/core/test/linker/query_integration_spec.ts index 4870596a4e..4339e63e06 100644 --- a/packages/core/test/linker/query_integration_spec.ts +++ b/packages/core/test/linker/query_integration_spec.ts @@ -361,19 +361,18 @@ describe('Query API', () => { expect(comp.children.length).toBe(1); }); - onlyInIvy( - 'Shallow queries don\'t cross ng-container boundaries in ivy (ng-container is treated as a regular element') - .it('should not cross ng-container boundaries with shallow queries', () => { - const template = ` + + it('should not cross ng-container boundaries with shallow queries', () => { + const template = `
`; - const view = createTestCmpAndDetectChanges(MyComp0, template); + const view = createTestCmpAndDetectChanges(MyComp0, template); - const comp = view.debugElement.children[0].injector.get(NeedsContentChildrenShallow); - expect(comp.children.length).toBe(0); - }); + const comp = view.debugElement.children[0].injector.get(NeedsContentChildrenShallow); + expect(comp.children.length).toBe(1); + }); it('should contain the first descendant content child templateRef', () => { const template = '' + diff --git a/packages/core/test/render3/query_spec.ts b/packages/core/test/render3/query_spec.ts index 01458a7982..bf4f79d5a7 100644 --- a/packages/core/test/render3/query_spec.ts +++ b/packages/core/test/render3/query_spec.ts @@ -569,75 +569,6 @@ describe('query', () => { expect(qList.first.nativeElement).toEqual(elToQuery); }); - /** - * BREAKING CHANGE: this tests asserts different behavior as compared to Renderer2 when it - * comes to descendants: false option and . - * - * Previous behavior: queries with descendants: false would descend into . - * New behavior: queries with descendants: false would NOT descend into . - * - * Reasoning: the Renderer2 behavior is inconsistent and hard to explain to users when it - * comes to descendants: false interpretation (see - * https://github.com/angular/angular/issues/14769#issuecomment-356609267) so we are changing - * it in ngIvy. - * - * In ngIvy implementation queries with the descendants: false option are interpreted as - * "don't descend" into children of a given element when looking for matches. In other words - * only direct children of a given component / directive are checked for matches. This applies - * to both regular elements (ex.
) and grouping elements (, - * )). - * - * Grouping elements (, ) are treated as regular elements since we - * can query for and , so they behave like regular elements from - * this point of view. - */ - it('should not descend into when descendants: false', () => { - let elToQuery; - - /** - * - *
- *
- * class Cmpt { - * @ViewChildren('foo') deep; - * @ViewChildren('foo', {descendants: false}) shallow; - * } - */ - const Cmpt = createComponent( - 'cmpt', - function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementContainerStart(0); - { - ɵɵelement(1, 'div', null, 0); - elToQuery = getNativeByIndex(3, getLView()); - } - ɵɵelementContainerEnd(); - } - }, - 3, 0, [], [], - function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵviewQuery(['foo'], true, ElementRef); - ɵɵviewQuery(['foo'], false, ElementRef); - } - if (rf & RenderFlags.Update) { - let tmp: any; - ɵɵqueryRefresh(tmp = ɵɵloadQuery>()) && - (ctx.deep = tmp as QueryList); - ɵɵqueryRefresh(tmp = ɵɵloadQuery>()) && - (ctx.shallow = tmp as QueryList); - } - }, - [], [], undefined, [['foo', '']]); - - const fixture = new ComponentFixture(Cmpt); - const deepQList = fixture.component.deep; - const shallowQList = fixture.component.shallow; - expect(deepQList.length).toBe(1); - expect(shallowQList.length).toBe(0); - }); - it('should read ViewContainerRef from element nodes when explicitly asked for', () => { /** *