feat(core): more read options for ngIvy queries (#21187)
PR Close #21187
This commit is contained in:
		
							parent
							
								
									c516bc3b35
								
							
						
					
					
						commit
						a62371c0eb
					
				| @ -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, | ||||
|  | ||||
| @ -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<T>(index: number, value: T | NO_CHANGE): void { | ||||
|  * @param directiveDef DirectiveDef object which contains information about the template. | ||||
|  */ | ||||
| export function directive<T>(index: number): T; | ||||
| export function directive<T>(index: number, directive: T, directiveDef: DirectiveDef<T>): T; | ||||
| export function directive<T>(index: number, directive?: T, directiveDef?: DirectiveDef<T>): T { | ||||
| export function directive<T>( | ||||
|     index: number, directive: T, directiveDef: DirectiveDef<T>, localName?: string): T; | ||||
| export function directive<T>( | ||||
|     index: number, directive?: T, directiveDef?: DirectiveDef<T>, localName?: string): T { | ||||
|   let instance; | ||||
|   if (directive == null) { | ||||
|     // return existing
 | ||||
| @ -844,10 +846,17 @@ export function directive<T>(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; | ||||
|  | ||||
| @ -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.: `<div #foo>` | ||||
|    * - a component is referenced, ex.: `<my-cmpt #foo>` | ||||
|    * - a directive is referenced, ex.: `<my-cmpt #foo="directiveExportAs">`. | ||||
|    * | ||||
|    * 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: | ||||
|    * - `<div #foo>` => `["foo", -1]` | ||||
|    * - `<my-cmpt #foo>` => `["foo", myCmptIdx]` | ||||
|    * - `<my-cmpt #foo #bar="directiveExportAs">` => `["foo", myCmptIdx, "bar", directiveIdx]` | ||||
|    * - `<div #foo #bar="directiveExportAs">` => `["foo", -1, "bar", directiveIdx]` | ||||
|    */ | ||||
|   localName: string|null; | ||||
|   localNames: (string|number)[]|null; | ||||
| 
 | ||||
|   /** | ||||
|    * This property contains information about input properties that | ||||
|  | ||||
| @ -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<any>|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<any>| null, node: LNode) { | ||||
|   while (predicate) { | ||||
|     const type = predicate.type; | ||||
| @ -151,15 +168,22 @@ function add(predicate: QueryPredicate<any>| 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); | ||||
|         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)); | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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; | ||||
|       /** | ||||
|        * <cmpt #foo></cmpt> | ||||
|        * 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<QueryList<any>>(0)) && (ctx.query = tmp as QueryList<any>); | ||||
|       }); | ||||
| 
 | ||||
|       const cmptInstance = renderComponent(Cmpt); | ||||
|       const query = (cmptInstance.query as QueryList<any>); | ||||
|       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; | ||||
|          /** | ||||
|           * <div #foo="child" child></div> | ||||
|           * 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<QueryList<any>>(0)) && (ctx.query = tmp as QueryList<any>); | ||||
|          }); | ||||
| 
 | ||||
|          const cmptInstance = renderComponent(Cmpt); | ||||
|          const query = (cmptInstance.query as QueryList<any>); | ||||
|          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; | ||||
|       /** | ||||
|        * <div #foo="child1" child1 #bar="child2" child2></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'])); | ||||
|           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<QueryList<any>>(0)) && (ctx.query = tmp as QueryList<any>); | ||||
|       }); | ||||
| 
 | ||||
|       const cmptInstance = renderComponent(Cmpt); | ||||
|       const query = (cmptInstance.query as QueryList<any>); | ||||
|       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; | ||||
|       /** | ||||
|        * <div #foo="child" child></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<QueryList<any>>(0)) && (ctx.query = tmp as QueryList<any>); | ||||
|       }); | ||||
| 
 | ||||
|       const cmptInstance = renderComponent(Cmpt); | ||||
|       const query = (cmptInstance.query as QueryList<any>); | ||||
|       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; | ||||
|       /** | ||||
|        * <div #foo #bar="child" child></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<QueryList<any>>(0)) && (ctx.query = tmp as QueryList<any>); | ||||
|       }); | ||||
| 
 | ||||
|       const cmptInstance = renderComponent(Cmpt); | ||||
|       const query = (cmptInstance.query as QueryList<any>); | ||||
|       expect(query.length).toBe(2); | ||||
|       expect(query.first.nativeElement).toBe(div); | ||||
|       expect(query.last).toBe(childInstance); | ||||
|     }); | ||||
| 
 | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| @ -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<any> { | ||||
|   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.
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user