From 3567e811752ebdaa69a8e69cf1352f14947c84c4 Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Mon, 29 Oct 2018 21:55:58 -0700 Subject: [PATCH] fix(ivy): restore missing match operation in View and Content Queries (#26847) PR Close #26847 --- packages/core/src/render3/query.ts | 57 ++++-- packages/core/test/render3/query_spec.ts | 223 ++++++++++++++++++++++- 2 files changed, 256 insertions(+), 24 deletions(-) diff --git a/packages/core/src/render3/query.ts b/packages/core/src/render3/query.ts index 3731756743..54e90eb411 100644 --- a/packages/core/src/render3/query.ts +++ b/packages/core/src/render3/query.ts @@ -269,7 +269,7 @@ function getIdxOfMatchingDirective(tNode: TNode, currentView: LViewData, type: T } // TODO: "read" should be an AbstractType (FW-486) -function queryRead(tNode: TNode, currentView: LViewData, read: any): any { +function queryByReadToken(read: any, tNode: TNode, currentView: LViewData): any { const factoryFn = (read as any)[NG_ELEMENT_ID]; if (typeof factoryFn === 'function') { return factoryFn(); @@ -282,7 +282,7 @@ function queryRead(tNode: TNode, currentView: LViewData, read: any): any { return null; } -function queryReadByTNodeType(tNode: TNode, currentView: LViewData): any { +function queryByTNodeType(tNode: TNode, currentView: LViewData): any { if (tNode.type === TNodeType.Element || tNode.type === TNodeType.ElementContainer) { return createElementRef(ViewEngine_ElementRef, tNode, currentView); } @@ -292,37 +292,54 @@ function queryReadByTNodeType(tNode: TNode, currentView: LViewData): any { return null; } +function queryByTemplateRef( + templateRefToken: ViewEngine_TemplateRef, tNode: TNode, currentView: LViewData, + read: any): any { + const templateRefResult = (templateRefToken as any)[NG_ELEMENT_ID](); + if (read) { + return templateRefResult ? queryByReadToken(read, tNode, currentView) : null; + } + return templateRefResult; +} + +function queryRead(tNode: TNode, currentView: LViewData, read: any, matchingIdx: number): any { + if (read) { + return queryByReadToken(read, tNode, currentView); + } + if (matchingIdx > -1) { + return currentView[matchingIdx]; + } + // if read token and / or strategy is not specified, + // detect it using appropriate tNode type + return queryByTNodeType(tNode, currentView); +} + function add( query: LQuery| null, tNode: TElementNode | TContainerNode | TElementContainerNode) { const currentView = getViewData(); while (query) { const predicate = query.predicate; - const type = predicate.type; + const type = predicate.type as any; if (type) { - // if read token and / or strategy is not specified, use type as read token - const result = queryRead(tNode, currentView, predicate.read || type); + let result = null; + if (type === ViewEngine_TemplateRef) { + result = queryByTemplateRef(type, tNode, currentView, predicate.read); + } else { + const matchingIdx = getIdxOfMatchingDirective(tNode, currentView, type); + if (matchingIdx !== null) { + result = queryRead(tNode, currentView, predicate.read, matchingIdx); + } + } if (result !== null) { addMatch(query, result); } } else { const selector = predicate.selector !; for (let i = 0; i < selector.length; i++) { - const directiveIdx = getIdxOfMatchingSelector(tNode, selector[i]); - if (directiveIdx !== null) { - let result: any = null; - if (predicate.read) { - result = queryRead(tNode, currentView, predicate.read); - } else { - if (directiveIdx > -1) { - result = currentView[directiveIdx]; - } else { - // if read token and / or strategy is not specified, - // detect it using appropriate tNode type - result = queryReadByTNodeType(tNode, currentView); - } - } - + const matchingIdx = getIdxOfMatchingSelector(tNode, selector[i]); + if (matchingIdx !== null) { + const result = queryRead(tNode, currentView, predicate.read, matchingIdx); if (result !== null) { addMatch(query, result); } diff --git a/packages/core/test/render3/query_spec.ts b/packages/core/test/render3/query_spec.ts index 762756138a..3e819a7a0b 100644 --- a/packages/core/test/render3/query_spec.ts +++ b/packages/core/test/render3/query_spec.ts @@ -13,7 +13,7 @@ import {EventEmitter} from '../..'; import {AttributeMarker, QueryList, defineComponent, defineDirective, detectChanges} from '../../src/render3/index'; import {getNativeByIndex} from '../../src/render3/util'; -import {bind, container, containerRefreshEnd, containerRefreshStart, directiveInject, element, elementContainerEnd, elementContainerStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, load, loadQueryList, reference, registerContentQuery, template} from '../../src/render3/instructions'; +import {bind, container, containerRefreshEnd, containerRefreshStart, directiveInject, element, elementContainerEnd, elementContainerStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, load, loadQueryList, reference, registerContentQuery, template, text} from '../../src/render3/instructions'; import {RenderFlags} from '../../src/render3/interfaces/definition'; import {query, queryRefresh} from '../../src/render3/query'; import {getViewData} from '../../src/render3/state'; @@ -949,7 +949,7 @@ describe('query', () => { expect(qList.last).toBe(childInstance); }); - it('should not add results to query if a requested token cant be read', () => { + it('should not add results to selector-based query if a requested token cant be read', () => { const Child = createDirective('child'); /** @@ -976,11 +976,226 @@ describe('query', () => { } }); - const cmptInstance = renderComponent(Cmpt); - const qList = (cmptInstance.query as QueryList); + const {component} = new ComponentFixture(Cmpt); + const qList = component.query; expect(qList.length).toBe(0); }); + it('should not add results to directive-based query if requested token cant be read', () => { + const Child = createDirective('child'); + const OtherChild = createDirective('otherchild'); + + /** + *
+ * class Cmpt { + * @ViewChildren(Child, {read: OtherChild}) query; + * } + */ + const Cmpt = createComponent( + 'cmpt', + function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + element(1, 'div', ['child', '']); + } + }, + 2, 0, [Child, OtherChild], [], + function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + query(0, Child, false, OtherChild); + } + if (rf & RenderFlags.Update) { + let tmp: any; + queryRefresh(tmp = load>(0)) && (ctx.query = tmp as QueryList); + } + }); + + const {component} = new ComponentFixture(Cmpt); + const qList = component.query; + expect(qList.length).toBe(0); + }); + + it('should not add results to directive-based query if only read token matches', () => { + const Child = createDirective('child'); + const OtherChild = createDirective('otherchild'); + + /** + *
+ * class Cmpt { + * @ViewChildren(OtherChild, {read: Child}) query; + * } + */ + const Cmpt = createComponent( + 'cmpt', + function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + element(1, 'div', ['child', '']); + } + }, + 2, 0, [Child, OtherChild], [], + function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + query(0, OtherChild, false, Child); + } + if (rf & RenderFlags.Update) { + let tmp: any; + queryRefresh(tmp = load>(0)) && (ctx.query = tmp as QueryList); + } + }); + + const {component} = new ComponentFixture(Cmpt); + const qList = component.query; + expect(qList.length).toBe(0); + }); + + it('should not add results to TemplateRef-based query if only read token matches', () => { + /** + *
+ * class Cmpt { + * @ViewChildren(TemplateRef, {read: ElementRef}) query; + * } + */ + const Cmpt = createComponent( + 'cmpt', + function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + element(1, 'div'); + } + }, + 2, 0, [], [], + function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + query(0, TemplateRef as any, false, ElementRef); + } + if (rf & RenderFlags.Update) { + let tmp: any; + queryRefresh(tmp = load>(0)) && (ctx.query = tmp as QueryList); + } + }); + + const {component} = new ComponentFixture(Cmpt); + const qList = component.query; + expect(qList.length).toBe(0); + }); + + it('should match using string selector and directive as a read argument', () => { + const Child = createDirective('child'); + + /** + *
+ * class Cmpt { + * @ViewChildren('foo', {read: Child}) query; + * } + */ + const Cmpt = createComponent( + 'cmpt', + function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + element(1, 'div', ['child', ''], ['foo', '']); + } + }, + 3, 0, [Child], [], + function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + query(0, ['foo'], false, Child); + } + if (rf & RenderFlags.Update) { + let tmp: any; + queryRefresh(tmp = load>(0)) && (ctx.query = tmp as QueryList); + } + }); + + const {component} = new ComponentFixture(Cmpt); + const qList = component.query; + expect(qList.length).toBe(1); + expect(qList.first instanceof Child).toBeTruthy(); + }); + + it('should not add results to the query in case no match found (via TemplateRef)', () => { + const Child = createDirective('child'); + + /** + *
+ * class Cmpt { + * @ViewChildren(TemplateRef) query; + * } + */ + const Cmpt = createComponent( + 'cmpt', + function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + element(1, 'div', ['child', '']); + } + }, + 2, 0, [Child], [], + function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + query(0, TemplateRef as any, false); + } + if (rf & RenderFlags.Update) { + let tmp: any; + queryRefresh(tmp = load>(0)) && (ctx.query = tmp as QueryList); + } + }); + + const {component} = new ComponentFixture(Cmpt); + const qList = component.query; + expect(qList.length).toBe(0); + }); + + it('should query templates if the type is TemplateRef (and respect "read" option)', () => { + function Cmpt_Template_1(rf: RenderFlags, ctx1: any) { + if (rf & RenderFlags.Create) { + elementStart(0, 'div'); + text(1, 'Test'); + elementEnd(); + } + } + /** + *
Test
+ *
Test
+ *
Test
+ * class Cmpt { + * @ViewChildren(TemplateRef) tmplQuery; + * @ViewChildren(TemplateRef, {read: ElementRef}) elemQuery; + * } + */ + const Cmpt = createComponent( + 'cmpt', + function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + template(2, Cmpt_Template_1, 2, 0, null, null, ['foo', ''], templateRefExtractor); + template(3, Cmpt_Template_1, 2, 0, null, null, ['bar', ''], templateRefExtractor); + template(4, Cmpt_Template_1, 2, 0, null, null, ['baz', ''], templateRefExtractor); + } + }, + 5, 0, [], [], + function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + query(0, TemplateRef as any, false); + query(1, TemplateRef as any, false, ElementRef); + } + if (rf & RenderFlags.Update) { + let tmp: any; + queryRefresh(tmp = load>(0)) && + (ctx.tmplQuery = tmp as QueryList); + queryRefresh(tmp = load>(1)) && + (ctx.elemQuery = tmp as QueryList); + } + }); + + const {component} = new ComponentFixture(Cmpt); + + // check template-based query set + const tmplQList = component.tmplQuery; + expect(tmplQList.length).toBe(3); + expect(isTemplateRef(tmplQList.first)).toBeTruthy(); + + // check element-based query set + const elemQList = component.elemQuery; + expect(elemQList.length).toBe(3); + expect(isElementRef(elemQList.first)).toBeTruthy(); + }); + }); });