diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index a7c73c9185..c0d357a861 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -127,9 +127,19 @@ let tView: TView; let currentQueries: LQueries|null; +/** + * Query instructions can ask for "current queries" in 2 different cases: + * - when creating view queries (at the root of a component view, before any node is created - in + * this case currentQueries points to view queries) + * - when creating content queries (inb this previousOrParentNode points to a node on which we + * create content queries). + */ export function getCurrentQueries(QueryType: {new (): LQueries}): LQueries { // top level variables should not be exported for performance reasons (PERF_NOTES.md) - return currentQueries || (currentQueries = (previousOrParentNode.queries || new QueryType())); + return currentQueries || + (currentQueries = + (previousOrParentNode.queries && previousOrParentNode.queries.clone() || + new QueryType())); } /** diff --git a/packages/core/src/render3/interfaces/query.ts b/packages/core/src/render3/interfaces/query.ts index 331b3be81f..aa0e40b289 100644 --- a/packages/core/src/render3/interfaces/query.ts +++ b/packages/core/src/render3/interfaces/query.ts @@ -13,7 +13,14 @@ import {LNode} from './node'; /** Used for tracking queries (e.g. ViewChild, ContentChild). */ export interface LQueries { /** - * Used to ask querieis if those should be cloned to the child element. + * Ask queries to prepare copy of itself. This assures that tracking new queries on child nodes + * doesn't mutate list of queries tracked on a parent node. We will clone LQueries before + * constructing content queries. + */ + clone(): LQueries|null; + + /** + * Used to ask queries if those should be cloned to the child element. * * For example in the case of deep queries the `child()` returns * queries for the child node. In case of shallow queries it returns diff --git a/packages/core/src/render3/query.ts b/packages/core/src/render3/query.ts index 1ec3ed22ee..5242b7dd2d 100644 --- a/packages/core/src/render3/query.ts +++ b/packages/core/src/render3/query.ts @@ -94,8 +94,6 @@ export class LQueries_ implements LQueries { track( queryList: viewEngine_QueryList, predicate: Type|string[], descend?: boolean, read?: QueryReadType|Type): void { - // TODO(misko): This is not right. In case of inherited state, a calling track will incorrectly - // mutate parent. if (descend) { this.deep = createQuery(this.deep, queryList, predicate, read != null ? read : null); } else { @@ -103,6 +101,8 @@ export class LQueries_ implements LQueries { } } + clone(): LQueries|null { return this.deep ? new LQueries_(this.deep) : null; } + child(): LQueries|null { if (this.deep === null) { // if we don't have any deep queries then no need to track anything more. diff --git a/packages/core/test/render3/query_spec.ts b/packages/core/test/render3/query_spec.ts index 537f1b0b1d..1a98c2f9a4 100644 --- a/packages/core/test/render3/query_spec.ts +++ b/packages/core/test/render3/query_spec.ts @@ -1752,5 +1752,141 @@ describe('query', () => { expect(viewQList.last.nativeElement.id).toBe('after'); }); + it('should report results to appropriate queries where content queries are nested', () => { + + class QueryDirective { + fooBars: any; + static ngDirectiveDef = defineDirective({ + type: QueryDirective, + selectors: [['', 'query', '']], + exportAs: 'query', + factory: () => { + // @ContentChildren('foo, bar, baz', {descendants: true}) fooBars: + // QueryList; + return [ + new QueryDirective(), query(null, ['foo', 'bar', 'baz'], true, QUERY_READ_FROM_NODE) + ]; + }, + hostBindings: function ContentQueryComponent_HostBindings( + dirIndex: number, elIndex: number) { + let tmp: any; + const instance = loadDirective(dirIndex)[0]; + queryRefresh(tmp = loadDirective(dirIndex)[1]) && + (instance.fooBars = tmp as QueryList); + }, + }); + } + + let outInstance: QueryDirective; + let inInstance: QueryDirective; + + const AppComponent = createComponent( + 'app-component', + /** + *
+ * + *
+ * + *
+ * + *
+ */ + function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + elementStart(0, 'div', [AttributeMarker.SelectOnly, 'query'], ['out', 'query']); + { + element(2, 'span', ['id', 'foo'], ['foo', '']); + elementStart(4, 'div', [AttributeMarker.SelectOnly, 'query'], ['in', 'query']); + { element(6, 'span', ['id', 'bar'], ['bar', '']); } + elementEnd(); + element(8, 'span', ['id', 'baz'], ['baz', '']); + } + elementEnd(); + } + if (rf & RenderFlags.Update) { + outInstance = (load(1))[0] as QueryDirective; + inInstance = (load(5))[0] as QueryDirective; + } + }, + [QueryDirective]); + + const fixture = new ComponentFixture(AppComponent); + expect(outInstance !.fooBars.length).toBe(3); + expect(inInstance !.fooBars.length).toBe(1); + }); + + it('should respect shallow flag on content queries when mixing deep and shallow queries', + () => { + class ShallowQueryDirective { + foos: any; + static ngDirectiveDef = defineDirective({ + type: ShallowQueryDirective, + selectors: [['', 'shallow-query', '']], + exportAs: 'shallow-query', + factory: () => { + // @ContentChildren('foo', {descendants: false}) fooBars: QueryList; + return [ + new ShallowQueryDirective(), query(null, ['foo'], false, QUERY_READ_FROM_NODE) + ]; + }, + hostBindings: function ContentQueryComponent_HostBindings( + dirIndex: number, elIndex: number) { + let tmp: any; + const instance = loadDirective(dirIndex)[0]; + queryRefresh(tmp = loadDirective(dirIndex)[1]) && + (instance.foos = tmp as QueryList); + }, + }); + } + + class DeepQueryDirective { + foos: any; + static ngDirectiveDef = defineDirective({ + type: DeepQueryDirective, + selectors: [['', 'deep-query', '']], + exportAs: 'deep-query', + factory: () => { + // @ContentChildren('foo', {descendants: true}) fooBars: QueryList; + return [new DeepQueryDirective(), query(null, ['foo'], true, QUERY_READ_FROM_NODE)]; + }, + hostBindings: function ContentQueryComponent_HostBindings( + dirIndex: number, elIndex: number) { + let tmp: any; + const instance = loadDirective(dirIndex)[0]; + queryRefresh(tmp = loadDirective(dirIndex)[1]) && + (instance.foos = tmp as QueryList); + }, + }); + } + + let shallowInstance: ShallowQueryDirective; + let deepInstance: DeepQueryDirective; + + const AppComponent = createComponent( + 'app-component', + /** + *
+ * + *
+ */ + function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + elementStart( + 0, 'div', [AttributeMarker.SelectOnly, 'shallow-query', 'deep-query'], + ['shallow', 'shallow-query', 'deep', 'deep-query']); + { element(3, 'span', ['id', 'foo'], ['foo', '']); } + elementEnd(); + } + if (rf & RenderFlags.Update) { + shallowInstance = (load(1))[0] as ShallowQueryDirective; + deepInstance = (load(2))[0] as DeepQueryDirective; + } + }, + [ShallowQueryDirective, DeepQueryDirective]); + + const fixture = new ComponentFixture(AppComponent); + expect(shallowInstance !.foos.length).toBe(1); + expect(deepInstance !.foos.length).toBe(1); + }); }); });