diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index b5401934af..ee426baab0 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -8,8 +8,7 @@ import {createComponentRef, detectChanges, getHostElement, markDirty, renderComponent} from './component'; import {NgOnChangesFeature, PublicFeature, defineComponent, defineDirective} from './definition'; -import {ComponentDef, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFlags} from './definition_interfaces'; - +import {ComponentDef, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFlags, DirectiveType} from './definition_interfaces'; // Naming scheme: // - Capital letters are for creating things: T(Text), E(Element), D(Directive), V(View), @@ -78,6 +77,7 @@ export { ComponentType, DirectiveDef, DirectiveDefFlags, + DirectiveType, NgOnChangesFeature, PublicFeature, defineComponent, diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index a406c38be6..a2610ccb8f 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -35,7 +35,7 @@ export {queryRefresh} from './query'; export const enum LifecycleHook {ON_INIT = 1, ON_DESTROY = 2, ON_CHANGES = 4} /** - * directive (D) sets a property on all component instances using this constant as a key and the + * Directive (D) sets a property on all component instances using this constant as a key and the * component's host node (LElement) as the value. This is used in methods like detectChanges to * facilitate jumping from an instance to the host node. */ @@ -645,7 +645,7 @@ function createNodeStatic( return { tagName: tagName, attrs: attrs, - localName: localName, + localNames: localName ? [localName, -1] : null, initialInputs: undefined, inputs: undefined, outputs: undefined, @@ -821,8 +821,10 @@ export function textBinding(index: number, value: T | NO_CHANGE): void { * @param directiveDef DirectiveDef object which contains information about the template. */ export function directive(index: number): T; -export function directive(index: number, directive: T, directiveDef: DirectiveDef): T; -export function directive(index: number, directive?: T, directiveDef?: DirectiveDef): T { +export function directive( + index: number, directive: T, directiveDef: DirectiveDef, localName?: string): T; +export function directive( + index: number, directive?: T, directiveDef?: DirectiveDef, localName?: string): T { let instance; if (directive == null) { // return existing @@ -844,10 +846,17 @@ export function directive(index: number, directive?: T, directiveDef?: Direct ngDevMode && assertDataInRange(index - 1); Object.defineProperty( directive, NG_HOST_SYMBOL, {enumerable: false, value: previousOrParentNode}); + data[index] = instance = directive; if (index >= ngStaticData.length) { ngStaticData[index] = directiveDef !; + if (localName) { + ngDevMode && + assertNotNull(previousOrParentNode.staticData, 'previousOrParentNode.staticData'); + const nodeStaticData = previousOrParentNode !.staticData !; + (nodeStaticData.localNames || (nodeStaticData.localNames = [])).push(localName, index); + } } const diPublic = directiveDef !.diPublic; diff --git a/packages/core/src/render3/l_node_static.ts b/packages/core/src/render3/l_node_static.ts index c47c691da3..d1c1d99c86 100644 --- a/packages/core/src/render3/l_node_static.ts +++ b/packages/core/src/render3/l_node_static.ts @@ -41,9 +41,23 @@ export interface LNodeStatic { attrs: string[]|null; /** - * A local name under which a given element is exported in a view. + * A set of local names under which a given element is exported in a template and + * visible to queries. An entry in this array can be created for different reasons: + * - an element itself is referenced, ex.: `
` + * - a component is referenced, ex.: `` + * - a directive is referenced, ex.: ``. + * + * A given element might have different local names and those names can be associated + * with a directive. We store local names at even indexes while odd indexes are reserved + * for directive index in a view (or `-1` if there is no associated directive). + * + * Some examples: + * - `
` => `["foo", -1]` + * - `` => `["foo", myCmptIdx]` + * - `` => `["foo", myCmptIdx, "bar", directiveIdx]` + * - `
` => `["foo", -1, "bar", directiveIdx]` */ - localName: string|null; + localNames: (string|number)[]|null; /** * This property contains information about input properties that diff --git a/packages/core/src/render3/query.ts b/packages/core/src/render3/query.ts index 28aca44d8e..6a106ae3dc 100644 --- a/packages/core/src/render3/query.ts +++ b/packages/core/src/render3/query.ts @@ -20,10 +20,9 @@ import {assertNotNull} from './assert'; import {DirectiveDef} from './definition_interfaces'; import {getOrCreateContainerRef, getOrCreateElementRef, getOrCreateNodeInjectorForNode, getOrCreateTemplateRef} from './di'; import {LContainer, LElement, LNode, LNodeFlags, LNodeInjector, LView, QueryReadType, QueryState} from './interfaces'; +import {LNodeStatic} from './l_node_static'; import {assertNodeOfPossibleTypes} from './node_assert'; - - /** * A predicate which determines if a given element/directive should be included in the query */ @@ -121,9 +120,7 @@ function readDefaultInjectable(nodeInjector: LNodeInjector, node: LNode): viewEn 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) { + if (read === QueryReadType.ElementRef) { return getOrCreateElementRef(nodeInjector); } else if (read === QueryReadType.ViewContainerRef) { return getOrCreateContainerRef(nodeInjector); @@ -136,6 +133,26 @@ function readFromNodeInjector(nodeInjector: LNodeInjector, node: LNode, read: Qu } } +/** + * Goes over local names for a given node and returns directive index + * (or -1 if a local name points to an element). + * + * @param staticData static data of a node to check + * @param selector selector to match + * @returns directive index, -1 or null if a selector didn't match any of the local names + */ +function getIdxOfMatchingSelector(staticData: LNodeStatic, selector: string): number|null { + const localNames = staticData.localNames; + if (localNames) { + for (let i = 0; i < localNames.length; i += 2) { + if (localNames[i] === selector) { + return localNames[i + 1] as number; + } + } + } + return null; +} + function add(predicate: QueryPredicate| null, node: LNode) { while (predicate) { const type = predicate.type; @@ -151,15 +168,22 @@ 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) { - const injectable = readFromNodeInjector(nodeInjector, node, predicate.read); - assertNotNull(injectable, 'injectable'); - predicate.values.push(injectable); + const selector = predicate.selector !; + for (let i = 0; i < selector.length; i++) { + ngDevMode && assertNotNull(node.staticData, 'node.staticData'); + const directiveIdx = getIdxOfMatchingSelector(node.staticData !, selector[i]); + // is anything on a node matching a selector? + if (directiveIdx !== null) { + if (predicate.read != null) { + predicate.values.push(readFromNodeInjector(nodeInjector, node, predicate.read)); + } else { + // is local name pointing to a directive? + if (directiveIdx > -1) { + predicate.values.push(node.view.data[directiveIdx]); + } else { + predicate.values.push(readDefaultInjectable(nodeInjector, node)); + } } } } diff --git a/packages/core/test/render3/node_selector_matcher_spec.ts b/packages/core/test/render3/node_selector_matcher_spec.ts index c69dfa096c..6a0fb5e357 100644 --- a/packages/core/test/render3/node_selector_matcher_spec.ts +++ b/packages/core/test/render3/node_selector_matcher_spec.ts @@ -14,7 +14,7 @@ function testLStaticData(tagName: string, attrs: string[] | null): LNodeStatic { return { tagName, attrs, - localName: null, + localNames: null, initialInputs: undefined, inputs: undefined, outputs: undefined, diff --git a/packages/core/test/render3/query_spec.ts b/packages/core/test/render3/query_spec.ts index eb1e8eba74..71f302d5cd 100644 --- a/packages/core/test/render3/query_spec.ts +++ b/packages/core/test/render3/query_spec.ts @@ -8,7 +8,7 @@ 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'; +import {createComponent, createDirective, renderComponent} from './render_util'; /** @@ -288,5 +288,147 @@ describe('query', () => { expect(isTemplateRef(query.first)).toBeTruthy(); }); + it('should read component instance if element queried for is a component host', () => { + const Child = createComponent('child', function(ctx: any, cm: boolean) {}); + + let childInstance; + /** + * + * class Cmpt { + * @ViewChildren('foo') query; + * } + */ + const Cmpt = createComponent('cmpt', function(ctx: any, cm: boolean) { + let tmp: any; + if (cm) { + m(0, Q(['foo'])); + E(1, Child.ngComponentDef, []); + { childInstance = D(2, Child.ngComponentDef.n(), Child.ngComponentDef, '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(query.first).toBe(childInstance); + }); + + it('should read directive instance if element queried for has an exported directive with a matching name', + () => { + const Child = createDirective(); + + let childInstance; + /** + *
+ * class Cmpt { + * @ViewChildren('foo') query; + * } + */ + const Cmpt = createComponent('cmpt', function(ctx: any, cm: boolean) { + let tmp: any; + if (cm) { + m(0, Q(['foo'])); + E(1, 'div'); + { childInstance = D(2, Child.ngDirectiveDef.n(), Child.ngDirectiveDef, '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(query.first).toBe(childInstance); + }); + + it('should read all matching directive instances from a given element', () => { + const Child1 = createDirective(); + const Child2 = createDirective(); + + let child1Instance, child2Instance; + /** + *
+ * class Cmpt { + * @ViewChildren('foo, bar') query; + * } + */ + const Cmpt = createComponent('cmpt', function(ctx: any, cm: boolean) { + let tmp: any; + if (cm) { + m(0, Q(['foo', 'bar'])); + E(1, 'div'); + { + child1Instance = D(2, Child1.ngDirectiveDef.n(), Child1.ngDirectiveDef, 'foo'); + child2Instance = D(3, Child2.ngDirectiveDef.n(), Child2.ngDirectiveDef, 'bar'); + } + e(); + } + qR(tmp = m>(0)) && (ctx.query = tmp as QueryList); + }); + + const cmptInstance = renderComponent(Cmpt); + const query = (cmptInstance.query as QueryList); + expect(query.length).toBe(2); + expect(query.first).toBe(child1Instance); + expect(query.last).toBe(child2Instance); + }); + + it('should match match on exported directive name and read a requested token', () => { + const Child = createDirective(); + + let div; + /** + *
+ * 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'], undefined, QueryReadType.ElementRef)); + div = E(1, 'div'); + { D(2, Child.ngDirectiveDef.n(), Child.ngDirectiveDef, '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(query.first.nativeElement).toBe(div); + }); + + it('should support reading a mix of ElementRef and directive instances', () => { + const Child = createDirective(); + + let childInstance, div; + /** + *
+ * class Cmpt { + * @ViewChildren('foo, bar') query; + * } + */ + const Cmpt = createComponent('cmpt', function(ctx: any, cm: boolean) { + let tmp: any; + if (cm) { + m(0, Q(['foo', 'bar'])); + div = E(1, 'div', [], 'foo'); + { childInstance = D(2, Child.ngDirectiveDef.n(), Child.ngDirectiveDef, 'bar'); } + e(); + } + qR(tmp = m>(0)) && (ctx.query = tmp as QueryList); + }); + + const cmptInstance = renderComponent(Cmpt); + const query = (cmptInstance.query as QueryList); + expect(query.length).toBe(2); + expect(query.first.nativeElement).toBe(div); + expect(query.last).toBe(childInstance); + }); + }); }); diff --git a/packages/core/test/render3/render_util.ts b/packages/core/test/render3/render_util.ts index 3dc4a8e652..9a6917ec1b 100644 --- a/packages/core/test/render3/render_util.ts +++ b/packages/core/test/render3/render_util.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ComponentTemplate, ComponentType, PublicFeature, defineComponent, renderComponent as _renderComponent} from '../../src/render3/index'; +import {ComponentTemplate, ComponentType, DirectiveType, PublicFeature, defineComponent, defineDirective, renderComponent as _renderComponent} from '../../src/render3/index'; import {NG_HOST_SYMBOL, createLNode, createViewState, renderTemplate} from '../../src/render3/instructions'; import {LElement, LNodeFlags} from '../../src/render3/interfaces'; import {RElement, RText, Renderer3, RendererFactory3, domRendererFactory3} from '../../src/render3/renderer'; @@ -80,6 +80,14 @@ export function createComponent( }; } +export function createDirective(): DirectiveType { + return class Directive { + static ngDirectiveDef = defineDirective({ + type: Directive, + factory: () => new Directive(), + }); + }; +} // Verify that DOM is a type of render. This is here for error checking only and has no use.