fix(ivy): cache local names and support multiple locals with same value (#22807)

PR Close #22807
This commit is contained in:
Kara Erickson 2018-03-15 12:18:31 -07:00 committed by Miško Hevery
parent 9220521149
commit bafdad9083
2 changed files with 102 additions and 27 deletions

View File

@ -491,13 +491,11 @@ export function elementStart(
// accessed through their containers because they may be removed / re-added later. // accessed through their containers because they may be removed / re-added later.
node = createLNode(index, LNodeFlags.Element, native, componentView); node = createLNode(index, LNodeFlags.Element, native, componentView);
// TODO(misko): implement code which caches the local reference resolution
const queryName: string|null = hack_findQueryName(hostComponentDef, localRefs, '');
if (node.tNode == null) { if (node.tNode == null) {
const localNames: (string | number)[]|null =
findMatchingLocalNames(hostComponentDef, localRefs, isHostElement ? index + 1 : -1, '');
ngDevMode && assertDataInRange(index - 1); ngDevMode && assertDataInRange(index - 1);
node.tNode = tData[index] = node.tNode = tData[index] = createTNode(name, attrs || null, null, localNames);
createTNode(name, attrs || null, null, hostComponentDef ? null : queryName);
} }
if (attrs) setUpAttributes(native, attrs); if (attrs) setUpAttributes(native, attrs);
@ -508,7 +506,7 @@ export function elementStart(
// is not guaranteed. Must be refactored to take it into account. // is not guaranteed. Must be refactored to take it into account.
const instance = hostComponentDef.n(); const instance = hostComponentDef.n();
storeComponentIndex(index); storeComponentIndex(index);
directiveCreate(++index, instance, hostComponentDef, queryName); directiveCreate(++index, instance, hostComponentDef, null);
initChangeDetectorIfExisting(node.nodeInjector, instance); initChangeDetectorIfExisting(node.nodeInjector, instance);
} }
hack_declareDirectives(index, directiveTypes, localRefs); hack_declareDirectives(index, directiveTypes, localRefs);
@ -532,8 +530,8 @@ export function initChangeDetectorIfExisting(injector: LInjector | null, instanc
} }
/** /**
* This function instantiates a directive with a correct queryName. It is a hack since we should * This function instantiates the given directives. It is a hack since it assumes the directives
* compute the query value only once and store it with the template (rather than on each invocation) * come in the correct order for DI.
*/ */
function hack_declareDirectives( function hack_declareDirectives(
index: number, directiveTypes: DirectiveType<any>[] | null | undefined, index: number, directiveTypes: DirectiveType<any>[] | null | undefined,
@ -546,30 +544,35 @@ function hack_declareDirectives(
const directiveType = directiveTypes[i]; const directiveType = directiveTypes[i];
const directiveDef = currentView.tView.firstTemplatePass ? directiveType.ngDirectiveDef : const directiveDef = currentView.tView.firstTemplatePass ? directiveType.ngDirectiveDef :
tData[index] as DirectiveDef<any>; tData[index] as DirectiveDef<any>;
directiveCreate( const localNames = currentView.tView.firstTemplatePass ?
index, directiveDef.n(), directiveDef, hack_findQueryName(directiveDef, localRefs)); findMatchingLocalNames(directiveDef, localRefs, index) :
null;
directiveCreate(index, directiveDef.n(), directiveDef, localNames);
} }
} }
} }
/** /**
* This function returns the queryName for a directive. It is a hack since we should * Finds any local names that match the given directive's exportAs and returns them with directive
* compute the query value only once and store it with the template (rather than on each invocation) * index. If the directiveDef is null, it matches against the default '' value instead of
* exportAs.
*/ */
function hack_findQueryName( function findMatchingLocalNames(
directiveDef: DirectiveDef<any>| null, localRefs: string[] | null | undefined, directiveDef: DirectiveDef<any>| null, localRefs: string[] | null | undefined, index: number,
defaultExport?: string, ): string|null { defaultExport?: string): (string | number)[]|null {
const exportAs = directiveDef && directiveDef.exportAs || defaultExport; const exportAs = directiveDef && directiveDef.exportAs || defaultExport;
let matches: (string | number)[]|null = null;
if (exportAs != null && localRefs) { if (exportAs != null && localRefs) {
for (let i = 0; i < localRefs.length; i = i + 2) { for (let i = 0; i < localRefs.length; i = i + 2) {
const local = localRefs[i]; const local = localRefs[i];
const toExportAs = localRefs[i | 1]; const toExportAs = localRefs[i | 1];
if (toExportAs === exportAs || toExportAs === defaultExport) { if (toExportAs === exportAs || toExportAs === defaultExport) {
return local; (matches || (matches = [])).push(local, index);
} }
} }
} }
return null; return matches;
} }
/** /**
@ -801,15 +804,16 @@ export function elementProperty<T>(
* @param tagName * @param tagName
* @param attrs * @param attrs
* @param data * @param data
* @param localNames A list of local names and their matching indices
* @returns the TNode object * @returns the TNode object
*/ */
function createTNode( function createTNode(
tagName: string | null, attrs: string[] | null, data: TContainer | null, tagName: string | null, attrs: string[] | null, data: TContainer | null,
localName: string | null): TNode { localNames: (string | number)[] | null): TNode {
return { return {
tagName: tagName, tagName: tagName,
attrs: attrs, attrs: attrs,
localNames: localName ? [localName, -1] : null, localNames: localNames,
initialInputs: undefined, initialInputs: undefined,
inputs: undefined, inputs: undefined,
outputs: undefined, outputs: undefined,
@ -1043,10 +1047,11 @@ export function textBinding<T>(index: number, value: T | NO_CHANGE): void {
* be created or retrieved out of order. * be created or retrieved out of order.
* @param directive The directive instance. * @param directive The directive instance.
* @param directiveDef DirectiveDef object which contains information about the template. * @param directiveDef DirectiveDef object which contains information about the template.
* @param queryName Name under which the query can retrieve the directive instance. * @param localNames Names under which a query can retrieve the directive instance
*/ */
export function directiveCreate<T>( export function directiveCreate<T>(
index: number, directive: T, directiveDef: DirectiveDef<T>, queryName?: string | null): T { index: number, directive: T, directiveDef: DirectiveDef<T>,
localNames?: (string | number)[] | null): T {
let instance; let instance;
ngDevMode && ngDevMode &&
assertNull(currentView.bindingStartIndex, 'directives should be created before any bindings'); assertNull(currentView.bindingStartIndex, 'directives should be created before any bindings');
@ -1068,10 +1073,10 @@ export function directiveCreate<T>(
if (index >= tData.length) { if (index >= tData.length) {
tData[index] = directiveDef !; tData[index] = directiveDef !;
if (queryName) { if (localNames) {
ngDevMode && assertNotNull(previousOrParentNode.tNode, 'previousOrParentNode.tNode'); ngDevMode && assertNotNull(previousOrParentNode.tNode, 'previousOrParentNode.tNode');
const tNode = previousOrParentNode !.tNode !; const tNode = previousOrParentNode !.tNode !;
(tNode.localNames || (tNode.localNames = [])).push(queryName, index); tNode.localNames = tNode.localNames ? tNode.localNames.concat(localNames) : localNames;
} }
} }
@ -1197,9 +1202,8 @@ export function container(
const node = createLNode(index, LNodeFlags.Container, undefined, lContainer); const node = createLNode(index, LNodeFlags.Container, undefined, lContainer);
if (node.tNode == null) { if (node.tNode == null) {
// TODO(misko): implement queryName caching const localNames: (string | number)[]|null = findMatchingLocalNames(null, localRefs, -1, '');
const queryName: string|null = hack_findQueryName(null, localRefs, ''); node.tNode = tData[index] = createTNode(tagName || null, attrs || null, [], localNames);
node.tNode = tData[index] = createTNode(tagName || null, attrs || null, [], queryName || null);
} }
// Containers are added to the current view tree instead of their embedded views // Containers are added to the current view tree instead of their embedded views

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {QUERY_READ_CONTAINER_REF, QUERY_READ_ELEMENT_REF, QUERY_READ_FROM_NODE, QUERY_READ_TEMPLATE_REF} from '../../src/render3/di'; import {QUERY_READ_CONTAINER_REF, QUERY_READ_ELEMENT_REF, QUERY_READ_FROM_NODE, QUERY_READ_TEMPLATE_REF} from '../../src/render3/di';
import {QueryList, detectChanges, defineComponent} from '../../src/render3/index'; import {QueryList, defineComponent, detectChanges} from '../../src/render3/index';
import {container, containerRefreshEnd, containerRefreshStart, elementEnd, elementStart, embeddedViewEnd, embeddedViewStart, load} from '../../src/render3/instructions'; import {container, containerRefreshEnd, containerRefreshStart, elementEnd, elementStart, embeddedViewEnd, embeddedViewStart, load} from '../../src/render3/instructions';
import {query, queryRefresh} from '../../src/render3/query'; import {query, queryRefresh} from '../../src/render3/query';
@ -191,6 +191,42 @@ describe('query', () => {
expect(qList.first.nativeElement).toEqual(elToQuery); expect(qList.first.nativeElement).toEqual(elToQuery);
}); });
it('should query multiple locals on the same element', () => {
let elToQuery;
/**
* <div #foo #bar></div>
* <div></div>
* class Cmpt {
* @ViewChildren('foo') fooQuery;
* @ViewChildren('bar') barQuery;
* }
*/
const Cmpt = createComponent('cmpt', function(ctx: any, cm: boolean) {
let tmp: any;
if (cm) {
query(0, ['foo'], false, QUERY_READ_FROM_NODE);
query(1, ['bar'], false, QUERY_READ_FROM_NODE);
elToQuery = elementStart(2, 'div', null, null, ['foo', '', 'bar', '']);
elementEnd();
elementStart(3, 'div');
elementEnd();
}
queryRefresh(tmp = load<QueryList<any>>(0)) && (ctx.fooQuery = tmp as QueryList<any>);
queryRefresh(tmp = load<QueryList<any>>(1)) && (ctx.barQuery = tmp as QueryList<any>);
});
const cmptInstance = renderComponent(Cmpt);
const fooList = (cmptInstance.fooQuery as QueryList<any>);
expect(fooList.length).toBe(1);
expect(fooList.first.nativeElement).toEqual(elToQuery);
const barList = (cmptInstance.barQuery as QueryList<any>);
expect(barList.length).toBe(1);
expect(barList.first.nativeElement).toEqual(elToQuery);
});
it('should query for multiple elements and read ElementRef by default', () => { it('should query for multiple elements and read ElementRef by default', () => {
let el1ToQuery; let el1ToQuery;
@ -489,6 +525,41 @@ describe('query', () => {
expect(qList.last).toBe(child2Instance); expect(qList.last).toBe(child2Instance);
}); });
it('should read multiple locals exporting the same directive from a given element', () => {
const Child = createDirective({exportAs: 'child'});
let childInstance;
/**
* <div child #foo="child" #bar="child"></div>
* class Cmpt {
* @ViewChildren('foo') fooQuery;
* @ViewChildren('bar') barQuery;
* }
*/
const Cmpt = createComponent('cmpt', function(ctx: any, cm: boolean) {
let tmp: any;
if (cm) {
query(0, ['foo'], true, QUERY_READ_FROM_NODE);
query(1, ['bar'], true, QUERY_READ_FROM_NODE);
elementStart(2, 'div', null, [Child], ['foo', 'child', 'bar', 'child']);
{ childInstance = load(3); }
elementEnd();
}
queryRefresh(tmp = load<QueryList<any>>(0)) && (ctx.fooQuery = tmp as QueryList<any>);
queryRefresh(tmp = load<QueryList<any>>(1)) && (ctx.barQuery = tmp as QueryList<any>);
});
const cmptInstance = renderComponent(Cmpt);
const fooList = cmptInstance.fooQuery as QueryList<any>;
expect(fooList.length).toBe(1);
expect(fooList.first).toBe(childInstance);
const barList = cmptInstance.barQuery as QueryList<any>;
expect(barList.length).toBe(1);
expect(barList.first).toBe(childInstance);
});
it('should match on exported directive name and read a requested token', () => { it('should match on exported directive name and read a requested token', () => {
const Child = createDirective({exportAs: 'child'}); const Child = createDirective({exportAs: 'child'});