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);
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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…
Reference in New Issue