diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index 59b1fc220d..a206cc51ba 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -11,10 +11,12 @@ import * as viewEngine from '../core'; import {LContainer, LElement, LNodeFlags, LNodeInjector} from './l_node'; +import {assertNodeType} from './node_assert'; import {ComponentTemplate, DirectiveDef} from './public_interfaces'; import {notImplemented, stringify} from './util'; + /** * If a directive is diPublic, bloomAdd sets a property on the instance with this constant as * the key and the directive's unique ID as the value. This allows us to map directives to their @@ -316,10 +318,8 @@ class ElementRef implements viewEngine.ElementRef { * @returns The TemplateRef instance to use */ export function getOrCreateTemplateRef(di: LNodeInjector): viewEngine.TemplateRef { + ngDevMode && assertNodeType(di.node, LNodeFlags.Container); const data = (di.node as LContainer).data; - if (data === null || data.template === null) { - throw createInjectionError('Directive does not have a template.', null); - } return di.templateRef || (di.templateRef = new TemplateRef(getOrCreateElementRef(di), data.template)); } @@ -328,7 +328,7 @@ export function getOrCreateTemplateRef(di: LNodeInjector): viewEngine.Templat class TemplateRef implements viewEngine.TemplateRef { readonly elementRef: viewEngine.ElementRef; - constructor(elementRef: viewEngine.ElementRef, template: ComponentTemplate) { + constructor(elementRef: viewEngine.ElementRef, template: ComponentTemplate|null) { this.elementRef = elementRef; } diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 95c7508458..04cca17157 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -11,7 +11,7 @@ import './ng_dev_mode'; import {ElementRef, TemplateRef, Type, ViewContainerRef} from '../core'; import {assertEqual, assertLessThan, assertNotEqual, assertNotNull} from './assert'; -import {ContainerState, CssSelector, ProjectionState, QueryState, ViewState} from './interfaces'; +import {ContainerState, CssSelector, ProjectionState, QueryReadType, QueryState, ViewState} from './interfaces'; import {LContainer, LElement, LNode, LNodeFlags, LNodeInjector, LProjection, LText, LView} from './l_node'; import {NgStaticData, LNodeStatic, LContainerStatic, InitialInputData, InitialInputs, PropertyAliases, PropertyAliasValue,} from './l_node_static'; @@ -966,7 +966,8 @@ export function lifecycle(lifeCycle: LifecycleHook, self?: any, method?: Functio * @param attrs The attrs attached to the container, if applicable */ export function containerStart( - index: number, template?: ComponentTemplate, tagName?: string, attrs?: string[]): void { + index: number, template?: ComponentTemplate, tagName?: string, attrs?: string[], + localName?: string): void { ngDevMode && assertEqual(currentView.bindingStartIndex, null, 'bindingStartIndex'); // If the direct parent of the container is a view, its views (including its comment) @@ -993,7 +994,7 @@ export function containerStart( if (node.staticData == null) { node.staticData = ngStaticData[index] = - createNodeStatic(tagName || null, attrs || null, [], null); + createNodeStatic(tagName || null, attrs || null, [], localName || null); } // Containers are added to the current view tree instead of their embedded views @@ -1009,6 +1010,8 @@ export function containerEnd() { previousOrParentNode = previousOrParentNode.parent !; } ngDevMode && assertNodeType(previousOrParentNode, LNodeFlags.Container); + const query = previousOrParentNode.query; + query && query.addNode(previousOrParentNode); } /** @@ -1716,11 +1719,12 @@ function valueInData(data: any[], index: number, value?: T): T { return value !; } -export function query(predicate: Type| string[], descend?: boolean): QueryList { +export function query( + predicate: Type| string[], descend?: boolean, read?: QueryReadType): QueryList { ngDevMode && assertPreviousIsParent(); const queryList = new QueryList(); const query = currentQuery || (currentQuery = new QueryState_()); - query.track(queryList, predicate, descend); + query.track(queryList, predicate, descend, read); return queryList; } diff --git a/packages/core/src/render3/interfaces.ts b/packages/core/src/render3/interfaces.ts index 16f0b80878..f0db9cefe4 100644 --- a/packages/core/src/render3/interfaces.ts +++ b/packages/core/src/render3/interfaces.ts @@ -210,6 +210,14 @@ export interface ViewOrContainerState { */ export type ProjectionState = Array; +/** + * An enum representing possible values of the "read" option for queries. + */ +export const enum QueryReadType { + ElementRef = 0, + ViewContainerRef = 1, + TemplateRef = 2, +} /** * Used for tracking queries (e.g. ViewChild, ContentChild). @@ -245,8 +253,11 @@ export interface QueryState { * @param queryList `QueryList` to update with changes. * @param predicate Either `Type` or selector array of [key, value] predicates. * @param descend If true the query will recursively apply to the children. + * @param read Indicates which token should be read from DI for this query. */ - track(queryList: QueryList, predicate: Type|any[], descend?: boolean): void; + track( + queryList: QueryList, predicate: Type|string[], descend?: boolean, + read?: QueryReadType): void; } /** diff --git a/packages/core/src/render3/node_assert.ts b/packages/core/src/render3/node_assert.ts index 54c6d79bc0..f9a69abec4 100644 --- a/packages/core/src/render3/node_assert.ts +++ b/packages/core/src/render3/node_assert.ts @@ -14,6 +14,17 @@ export function assertNodeType(node: LNode, type: LNodeFlags) { assertEqual(node.flags & LNodeFlags.TYPE_MASK, type, 'Node.type', typeSerializer); } +export function assertNodeOfPossibleTypes(node: LNode, ...types: LNodeFlags[]) { + assertNotEqual(node, null, 'node'); + const nodeType = (node.flags & LNodeFlags.TYPE_MASK); + for (let i = 0; i < types.length; i++) { + if (nodeType === types[i]) { + return; + } + } + throw new Error( + `Expected node of possible types: ${types.map(typeSerializer).join(', ')} but got ${typeSerializer(nodeType)}`); +} function typeSerializer(type: LNodeFlags): string { if (type == LNodeFlags.Projection) return 'Projection'; diff --git a/packages/core/src/render3/query.ts b/packages/core/src/render3/query.ts index c0a1432860..71d6c8fca0 100644 --- a/packages/core/src/render3/query.ts +++ b/packages/core/src/render3/query.ts @@ -13,9 +13,10 @@ import {Observable} from 'rxjs/Observable'; import * as viewEngine from '../core'; import {assertNotNull} from './assert'; -import {getOrCreateElementRef, getOrCreateNodeInjectorForNode} from './di'; -import {QueryState} from './interfaces'; -import {LContainer, LElement, LNode, LNodeFlags, LView} from './l_node'; +import {getOrCreateContainerRef, getOrCreateElementRef, getOrCreateNodeInjectorForNode, getOrCreateTemplateRef} from './di'; +import {QueryReadType, QueryState} from './interfaces'; +import {LContainer, LElement, LNode, LNodeFlags, LNodeInjector, LView} from './l_node'; +import {assertNodeOfPossibleTypes} from './node_assert'; import {DirectiveDef} from './public_interfaces'; @@ -44,6 +45,11 @@ export interface QueryPredicate { */ selector: string[]|null; + /** + * Indicates which token should be read from DI for this query. + */ + read: QueryReadType|null; + /** * Values which have been located. * @@ -59,14 +65,15 @@ export class QueryState_ implements QueryState { constructor(deep?: QueryPredicate) { this.deep = deep == null ? null : deep; } track( - queryList: viewEngine.QueryList, predicate: viewEngine.Type|string[], - descend?: boolean): void { + queryList: viewEngine.QueryList, predicate: viewEngine.Type|string[], descend?: boolean, + read?: QueryReadType): void { // TODO(misko): This is not right. In case of inherited state, a calling track will incorrectly // mutate parent. if (descend) { - this.deep = createPredicate(this.deep, queryList, predicate); + this.deep = createPredicate(this.deep, queryList, predicate, read != null ? read : null); } else { - this.shallow = createPredicate(this.shallow, queryList, predicate); + this.shallow = + createPredicate(this.shallow, queryList, predicate, read != null ? read : null); } } @@ -99,6 +106,33 @@ export class QueryState_ implements QueryState { } } +function readDefaultInjectable(nodeInjector: LNodeInjector, node: LNode): + viewEngine.ElementRef|viewEngine.TemplateRef|undefined { + ngDevMode && assertNodeOfPossibleTypes(node, LNodeFlags.Container, LNodeFlags.Element); + if ((node.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.Element) { + return getOrCreateElementRef(nodeInjector); + } else if ((node.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.Container) { + return getOrCreateTemplateRef(nodeInjector); + } +} + +function readFromNodeInjector(nodeInjector: LNodeInjector, node: LNode, read: QueryReadType | null): + viewEngine.ElementRef|viewEngine.ViewContainerRef|viewEngine.TemplateRef|undefined { + if (read === null) { + return readDefaultInjectable(nodeInjector, node); + } else if (read === QueryReadType.ElementRef) { + return getOrCreateElementRef(nodeInjector); + } else if (read === QueryReadType.ViewContainerRef) { + return getOrCreateContainerRef(nodeInjector); + } else if (read === QueryReadType.TemplateRef) { + return getOrCreateTemplateRef(nodeInjector); + } + + if (ngDevMode) { + throw new Error(`Unrecognised read type for queries: ${read}`); + } +} + function add(predicate: QueryPredicate| null, node: LNode) { while (predicate) { const type = predicate.type; @@ -115,12 +149,14 @@ function add(predicate: QueryPredicate| null, node: LNode) { } } else { const staticData = node.staticData; + const nodeInjector = getOrCreateNodeInjectorForNode(node as LElement | LContainer); if (staticData && staticData.localName) { const selector = predicate.selector !; for (let i = 0; i < selector.length; i++) { if (selector[i] === staticData.localName) { - predicate.values.push(getOrCreateElementRef( - getOrCreateNodeInjectorForNode(node as LElement | LContainer))); + const injectable = readFromNodeInjector(nodeInjector, node, predicate.read); + assertNotNull(injectable, 'injectable'); + predicate.values.push(injectable); } } } @@ -131,7 +167,7 @@ function add(predicate: QueryPredicate| null, node: LNode) { function createPredicate( previous: QueryPredicate| null, queryList: QueryList, - predicate: viewEngine.Type| string[]): QueryPredicate { + predicate: viewEngine.Type| string[], read: QueryReadType | null): QueryPredicate { const isArray = Array.isArray(predicate); const values = []; if ((queryList as any as QueryList_)._valuesTree === null) { @@ -142,6 +178,7 @@ function createPredicate( list: queryList, type: isArray ? null : predicate as viewEngine.Type, selector: isArray ? predicate as string[] : null, + read: read, values: values }; } diff --git a/packages/core/test/render3/query_spec.ts b/packages/core/test/render3/query_spec.ts index 9828822a52..9e99f34204 100644 --- a/packages/core/test/render3/query_spec.ts +++ b/packages/core/test/render3/query_spec.ts @@ -5,11 +5,39 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -import {D, E, Q, QueryList, e, m, qR} from '../../src/render3/index'; +import {C, D, E, Q, QueryList, c, e, m, qR} from '../../src/render3/index'; +import {QueryReadType} from '../../src/render3/interfaces'; import {createComponent, renderComponent} from './render_util'; + +/** + * Helper function to check if a given candidate object resembles ElementRef + * @param candidate + * @returns {boolean} + */ +function isElementRef(candidate: any): boolean { + return candidate.nativeElement != null; +} + +/** + * Helper function to check if a given candidate object resembles TemplateRef + * @param candidate + * @returns {boolean} + */ +function isTemplateRef(candidate: any): boolean { + return candidate.createEmbeddedView != null && candidate.createComponent == null; +} + +/** + * Helper function to check if a given candidate object resembles ViewContainerRef + * @param candidate + * @returns {boolean} + */ +function isViewContainerRef(candidate: any): boolean { + return candidate.createEmbeddedView != null && candidate.createComponent != null; +} + describe('query', () => { it('should project query children', () => { const Child = createComponent('child', function(ctx: any, cm: boolean) {}); @@ -51,7 +79,7 @@ describe('query', () => { describe('local names', () => { - it('should query for a single element', () => { + it('should query for a single element and read ElementRef', () => { let elToQuery; /** @@ -79,7 +107,7 @@ describe('query', () => { expect(query.first.nativeElement).toEqual(elToQuery); }); - it('should query for multiple elements', () => { + it('should query for multiple elements and read ElementRef', () => { let el1ToQuery; let el2ToQuery; @@ -112,5 +140,153 @@ describe('query', () => { expect(query.last.nativeElement).toEqual(el2ToQuery); }); + it('should read ElementRef from an element when explicitly asked for', () => { + + let elToQuery; + /** + *
+ *
+ * class Cmpt { + * @ViewChildren('foo', {read: ElementRef}) query; + * } + */ + const Cmpt = createComponent('cmpt', function(ctx: any, cm: boolean) { + let tmp: any; + if (cm) { + m(0, Q(['foo'], false, QueryReadType.ElementRef)); + elToQuery = E(1, 'div', [], 'foo'); + e(); + E(2, 'div'); + e(); + } + qR(tmp = m>(0)) && (ctx.query = tmp as QueryList); + }); + + const cmptInstance = renderComponent(Cmpt); + const query = (cmptInstance.query as QueryList); + expect(query.length).toBe(1); + expect(isElementRef(query.first)).toBeTruthy(); + expect(query.first.nativeElement).toEqual(elToQuery); + }); + + it('should read ViewContainerRef from element nodes when explicitly asked for', () => { + /** + *
+ * class Cmpt { + * @ViewChildren('foo', {read: ViewContainerRef}) query; + * } + */ + const Cmpt = createComponent('cmpt', function(ctx: any, cm: boolean) { + let tmp: any; + if (cm) { + m(0, Q(['foo'], false, QueryReadType.ViewContainerRef)); + E(1, 'div', [], 'foo'); + e(); + } + qR(tmp = m>(0)) && (ctx.query = tmp as QueryList); + }); + + const cmptInstance = renderComponent(Cmpt); + const query = (cmptInstance.query as QueryList); + expect(query.length).toBe(1); + expect(isViewContainerRef(query.first)).toBeTruthy(); + }); + + it('should read ViewContainerRef from container nodes when explicitly asked for', () => { + /** + * + * class Cmpt { + * @ViewChildren('foo', {read: ViewContainerRef}) query; + * } + */ + const Cmpt = createComponent('cmpt', function(ctx: any, cm: boolean) { + let tmp: any; + if (cm) { + m(0, Q(['foo'], false, QueryReadType.ViewContainerRef)); + C(1, undefined, undefined, undefined, 'foo'); + c(); + } + qR(tmp = m>(0)) && (ctx.query = tmp as QueryList); + }); + + const cmptInstance = renderComponent(Cmpt); + const query = (cmptInstance.query as QueryList); + expect(query.length).toBe(1); + expect(isViewContainerRef(query.first)).toBeTruthy(); + }); + + it('should read ElementRef with a native element pointing to comment DOM node from containers', + () => { + /** + * + * class Cmpt { + * @ViewChildren('foo', {read: ElementRef}) query; + * } + */ + const Cmpt = createComponent('cmpt', function(ctx: any, cm: boolean) { + let tmp: any; + if (cm) { + m(0, Q(['foo'], false, QueryReadType.ElementRef)); + C(1, undefined, undefined, undefined, 'foo'); + c(); + } + qR(tmp = m>(0)) && (ctx.query = tmp as QueryList); + }); + + const cmptInstance = renderComponent(Cmpt); + const query = (cmptInstance.query as QueryList); + expect(query.length).toBe(1); + expect(isElementRef(query.first)).toBeTruthy(); + expect(query.first.nativeElement.nodeType).toBe(8); // Node.COMMENT_NODE = 8 + }); + + it('should read TemplateRef from container nodes by default', () => { + // http://plnkr.co/edit/BVpORly8wped9I3xUYsX?p=preview + /** + * + * class Cmpt { + * @ViewChildren('foo') query; + * } + */ + const Cmpt = createComponent('cmpt', function(ctx: any, cm: boolean) { + let tmp: any; + if (cm) { + m(0, Q(['foo'])); + C(1, undefined, undefined, undefined, 'foo'); + c(); + } + qR(tmp = m>(0)) && (ctx.query = tmp as QueryList); + }); + + const cmptInstance = renderComponent(Cmpt); + const query = (cmptInstance.query as QueryList); + expect(query.length).toBe(1); + expect(isTemplateRef(query.first)).toBeTruthy(); + }); + + + it('should read TemplateRef from container nodes when explicitly asked for', () => { + /** + * + * class Cmpt { + * @ViewChildren('foo', {read: TemplateRef}) query; + * } + */ + const Cmpt = createComponent('cmpt', function(ctx: any, cm: boolean) { + let tmp: any; + if (cm) { + m(0, Q(['foo'], false, QueryReadType.TemplateRef)); + C(1, undefined, undefined, undefined, 'foo'); + c(); + } + qR(tmp = m>(0)) && (ctx.query = tmp as QueryList); + }); + + const cmptInstance = renderComponent(Cmpt); + const query = (cmptInstance.query as QueryList); + expect(query.length).toBe(1); + expect(isTemplateRef(query.first)).toBeTruthy(); + }); + }); });