Kara Erickson 9b93bd625f fix(ivy): ensure views created in constructors dont break queries (#29983)
Previous to this change, we assumed embedded views could only be created after
their parent template node had completed processing. As a result, we only set
up query logic for containers after directives on the node were created.
However, this assumption didn't take into account the case where a directive
on a template node could create views in its constructor.

This commit fixes query logic to work with views created in constructors.
In that case, we need to create a query container before the new view is
rendered so query results in the view aren't lost. But since the query container
is created before directives have completed processing, we also have to ensure
that query results gathered later on the template node are inserted before that
query container. Otherwise, query results in embedded views will clobber query
results on template nodes.

This splice mode may be slightly slower than the normal matching for queries on
containers, but we should only fall back to this strategy in the edge case where
views are created in constructors. (We should encourage developers to create
views in ngOnInit instead).

PR Close #29983
2019-04-19 10:01:32 -07:00

534 lines
17 KiB
TypeScript

/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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
*/
// We are temporarily importing the existing viewEngine_from core so we can be sure we are
// correctly implementing its interfaces for backwards compatibility.
import {Type} from '../interface/type';
import {ElementRef as ViewEngine_ElementRef} from '../linker/element_ref';
import {QueryList} from '../linker/query_list';
import {TemplateRef as ViewEngine_TemplateRef} from '../linker/template_ref';
import {assertDataInRange, assertDefined, assertEqual} from '../util/assert';
import {assertPreviousIsParent} from './assert';
import {getNodeInjectable, locateDirectiveOrProvider} from './di';
import {NG_ELEMENT_ID} from './fields';
import {store, ɵɵload} from './instructions/all';
import {storeCleanupWithContext} from './instructions/shared';
import {unusedValueExportToPlacateAjd as unused1} from './interfaces/definition';
import {unusedValueExportToPlacateAjd as unused2} from './interfaces/injector';
import {TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeType, unusedValueExportToPlacateAjd as unused3} from './interfaces/node';
import {LQueries, unusedValueExportToPlacateAjd as unused4} from './interfaces/query';
import {CONTENT_QUERIES, HEADER_OFFSET, LView, QUERIES, TVIEW} from './interfaces/view';
import {getCurrentQueryIndex, getIsParent, getLView, isCreationMode, setCurrentQueryIndex} from './state';
import {createElementRef, createTemplateRef} from './view_engine_compatibility';
const unusedValueToPlacateAjd = unused1 + unused2 + unused3 + unused4;
/**
* A predicate which determines if a given element/directive should be included in the query
* results.
*/
export interface QueryPredicate<T> {
/**
* If looking for directives then it contains the directive type.
*/
type: Type<T>|null;
/**
* If selector then contains local names to query for.
*/
selector: string[]|null;
/**
* Indicates which token should be read from DI for this query.
*/
read: Type<T>|null;
}
/**
* An object representing a query, which is a combination of:
* - query predicate to determines if a given element/directive should be included in the query
* - values collected based on a predicate
* - `QueryList` to which collected values should be reported
*/
export interface LQuery<T> {
/**
* Next query. Used when queries are stored as a linked list in `LQueries`.
*/
next: LQuery<any>|null;
/**
* Destination to which the value should be added.
*/
list: QueryList<T>;
/**
* A predicate which determines if a given element/directive should be included in the query
* results.
*/
predicate: QueryPredicate<T>;
/**
* Values which have been located.
*
* This is what builds up the `QueryList._valuesTree`.
*/
values: any[];
/**
* A pointer to an array that stores collected values from views. This is necessary so we know a
* container into which to insert nodes collected from views.
*/
containerValues: any[]|null;
}
export class LQueries_ implements LQueries {
constructor(
public parent: LQueries_|null, private shallow: LQuery<any>|null,
private deep: LQuery<any>|null) {}
track<T>(queryList: QueryList<T>, predicate: Type<T>|string[], descend?: boolean, read?: Type<T>):
void {
if (descend) {
this.deep = createQuery(this.deep, queryList, predicate, read != null ? read : null);
} else {
this.shallow = createQuery(this.shallow, queryList, predicate, read != null ? read : null);
}
}
clone(): LQueries { return new LQueries_(this, null, this.deep); }
container(): LQueries|null {
const shallowResults = copyQueriesToContainer(this.shallow);
const deepResults = copyQueriesToContainer(this.deep);
return shallowResults || deepResults ? new LQueries_(this, shallowResults, deepResults) : null;
}
createView(): LQueries|null {
const shallowResults = copyQueriesToView(this.shallow);
const deepResults = copyQueriesToView(this.deep);
return shallowResults || deepResults ? new LQueries_(this, shallowResults, deepResults) : null;
}
insertView(index: number): void {
insertView(index, this.shallow);
insertView(index, this.deep);
}
addNode(tNode: TElementNode|TContainerNode|TElementContainerNode): void {
add(this.deep, tNode, false);
add(this.shallow, tNode, false);
}
insertNodeBeforeViews(tNode: TElementNode|TContainerNode|TElementContainerNode): void {
add(this.deep, tNode, true);
add(this.shallow, tNode, true);
}
removeView(): void {
removeView(this.shallow);
removeView(this.deep);
}
}
function copyQueriesToContainer(query: LQuery<any>| null): LQuery<any>|null {
let result: LQuery<any>|null = null;
while (query) {
const containerValues: any[] = []; // prepare room for views
query.values.push(containerValues);
const clonedQuery: LQuery<any> = {
next: result,
list: query.list,
predicate: query.predicate,
values: containerValues,
containerValues: null
};
result = clonedQuery;
query = query.next;
}
return result;
}
function copyQueriesToView(query: LQuery<any>| null): LQuery<any>|null {
let result: LQuery<any>|null = null;
while (query) {
const clonedQuery: LQuery<any> = {
next: result,
list: query.list,
predicate: query.predicate,
values: [],
containerValues: query.values
};
result = clonedQuery;
query = query.next;
}
return result;
}
function insertView(index: number, query: LQuery<any>| null) {
while (query) {
ngDevMode && assertViewQueryhasPointerToDeclarationContainer(query);
query.containerValues !.splice(index, 0, query.values);
// mark a query as dirty only when inserted view had matching modes
if (query.values.length) {
query.list.setDirty();
}
query = query.next;
}
}
function removeView(query: LQuery<any>| null) {
while (query) {
ngDevMode && assertViewQueryhasPointerToDeclarationContainer(query);
const containerValues = query.containerValues !;
const viewValuesIdx = containerValues.indexOf(query.values);
const removed = containerValues.splice(viewValuesIdx, 1);
// mark a query as dirty only when removed view had matching modes
ngDevMode && assertEqual(removed.length, 1, 'removed.length');
if (removed[0].length) {
query.list.setDirty();
}
query = query.next;
}
}
function assertViewQueryhasPointerToDeclarationContainer(query: LQuery<any>) {
assertDefined(query.containerValues, 'View queries need to have a pointer to container values.');
}
/**
* Iterates over local names for a given node and returns directive index
* (or -1 if a local name points to an element).
*
* @param tNode 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(tNode: TNode, selector: string): number|null {
const localNames = tNode.localNames;
if (localNames) {
for (let i = 0; i < localNames.length; i += 2) {
if (localNames[i] === selector) {
return localNames[i + 1] as number;
}
}
}
return null;
}
// TODO: "read" should be an AbstractType (FW-486)
function queryByReadToken(read: any, tNode: TNode, currentView: LView): any {
const factoryFn = (read as any)[NG_ELEMENT_ID];
if (typeof factoryFn === 'function') {
return factoryFn();
} else {
const matchingIdx =
locateDirectiveOrProvider(tNode, currentView, read as Type<any>, false, false);
if (matchingIdx !== null) {
return getNodeInjectable(
currentView[TVIEW].data, currentView, matchingIdx, tNode as TElementNode);
}
}
return null;
}
function queryByTNodeType(tNode: TNode, currentView: LView): any {
if (tNode.type === TNodeType.Element || tNode.type === TNodeType.ElementContainer) {
return createElementRef(ViewEngine_ElementRef, tNode, currentView);
}
if (tNode.type === TNodeType.Container) {
return createTemplateRef(ViewEngine_TemplateRef, ViewEngine_ElementRef, tNode, currentView);
}
return null;
}
function queryByTemplateRef(
templateRefToken: ViewEngine_TemplateRef<any>, tNode: TNode, currentView: LView,
read: any): any {
const templateRefResult = (templateRefToken as any)[NG_ELEMENT_ID]();
if (read) {
return templateRefResult ? queryByReadToken(read, tNode, currentView) : null;
}
return templateRefResult;
}
function queryRead(tNode: TNode, currentView: LView, read: any, matchingIdx: number): any {
if (read) {
return queryByReadToken(read, tNode, currentView);
}
if (matchingIdx > -1) {
return getNodeInjectable(
currentView[TVIEW].data, currentView, matchingIdx, tNode as TElementNode);
}
// if read token and / or strategy is not specified,
// detect it using appropriate tNode type
return queryByTNodeType(tNode, currentView);
}
/**
* Add query matches for a given node.
*
* @param query The first query in the linked list
* @param tNode The TNode to match against queries
* @param insertBeforeContainer Whether or not we should add matches before the last
* container array. This mode is necessary if the query container had to be created
* out of order (e.g. a view was created in a constructor)
*/
function add(
query: LQuery<any>| null, tNode: TElementNode | TContainerNode | TElementContainerNode,
insertBeforeContainer: boolean) {
const currentView = getLView();
while (query) {
const predicate = query.predicate;
const type = predicate.type as any;
if (type) {
let result = null;
if (type === ViewEngine_TemplateRef) {
result = queryByTemplateRef(type, tNode, currentView, predicate.read);
} else {
const matchingIdx = locateDirectiveOrProvider(tNode, currentView, type, false, false);
if (matchingIdx !== null) {
result = queryRead(tNode, currentView, predicate.read, matchingIdx);
}
}
if (result !== null) {
addMatch(query, result, insertBeforeContainer);
}
} else {
const selector = predicate.selector !;
for (let i = 0; i < selector.length; i++) {
const matchingIdx = getIdxOfMatchingSelector(tNode, selector[i]);
if (matchingIdx !== null) {
const result = queryRead(tNode, currentView, predicate.read, matchingIdx);
if (result !== null) {
addMatch(query, result, insertBeforeContainer);
}
}
}
}
query = query.next;
}
}
function addMatch(query: LQuery<any>, matchingValue: any, insertBeforeViewMatches: boolean): void {
// Views created in constructors may have their container values created too early. In this case,
// ensure template node results are spliced before container results. Otherwise, results inside
// embedded views will appear before results on parent template nodes when flattened.
insertBeforeViewMatches ? query.values.splice(-1, 0, matchingValue) :
query.values.push(matchingValue);
query.list.setDirty();
}
function createPredicate<T>(predicate: Type<T>| string[], read: Type<T>| null): QueryPredicate<T> {
const isArray = Array.isArray(predicate);
return {
type: isArray ? null : predicate as Type<T>,
selector: isArray ? predicate as string[] : null,
read: read
};
}
function createQuery<T>(
previous: LQuery<any>| null, queryList: QueryList<T>, predicate: Type<T>| string[],
read: Type<T>| null): LQuery<T> {
return {
next: previous,
list: queryList,
predicate: createPredicate(predicate, read),
values: (queryList as any as QueryList_<T>)._valuesTree,
containerValues: null
};
}
type QueryList_<T> = QueryList<T>& {_valuesTree: any[], _static: boolean};
/**
* Creates and returns a QueryList.
*
* @param predicate The type for which the query will search
* @param descend Whether or not to descend into children
* @param read What to save in the query
* @returns QueryList<T>
*/
export function query<T>(
// TODO: "read" should be an AbstractType (FW-486)
predicate: Type<any>| string[], descend: boolean, read: any): QueryList<T> {
ngDevMode && assertPreviousIsParent(getIsParent());
const lView = getLView();
const queryList = new QueryList<T>() as QueryList_<T>;
const queries = lView[QUERIES] || (lView[QUERIES] = new LQueries_(null, null, null));
queryList._valuesTree = [];
queryList._static = false;
queries.track(queryList, predicate, descend, read);
storeCleanupWithContext(lView, queryList, queryList.destroy);
return queryList;
}
/**
* Refreshes a query by combining matches from all active views and removing matches from deleted
* views.
*
* @returns `true` if a query got dirty during change detection or if this is a static query
* resolving in creation mode, `false` otherwise.
*
* @codeGenApi
*/
export function ɵɵqueryRefresh(queryList: QueryList<any>): boolean {
const queryListImpl = (queryList as any as QueryList_<any>);
const creationMode = isCreationMode();
// if creation mode and static or update mode and not static
if (queryList.dirty && creationMode === queryListImpl._static) {
queryList.reset(queryListImpl._valuesTree || []);
queryList.notifyOnChanges();
return true;
}
return false;
}
/**
* Creates new QueryList for a static view query.
*
* @param predicate The type for which the query will search
* @param descend Whether or not to descend into children
* @param read What to save in the query
*
* @codeGenApi
*/
export function ɵɵstaticViewQuery<T>(
// TODO(FW-486): "read" should be an AbstractType
predicate: Type<any>| string[], descend: boolean, read: any): void {
const queryList = ɵɵviewQuery(predicate, descend, read) as QueryList_<T>;
const tView = getLView()[TVIEW];
queryList._static = true;
if (!tView.staticViewQueries) {
tView.staticViewQueries = true;
}
}
/**
* Creates new QueryList, stores the reference in LView and returns QueryList.
*
* @param predicate The type for which the query will search
* @param descend Whether or not to descend into children
* @param read What to save in the query
* @returns QueryList<T>
*
* @codeGenApi
*/
export function ɵɵviewQuery<T>(
// TODO(FW-486): "read" should be an AbstractType
predicate: Type<any>| string[], descend: boolean, read: any): QueryList<T> {
const lView = getLView();
const tView = lView[TVIEW];
if (tView.firstTemplatePass) {
tView.expandoStartIndex++;
}
const index = getCurrentQueryIndex();
const viewQuery: QueryList<T> = query<T>(predicate, descend, read);
store(index - HEADER_OFFSET, viewQuery);
setCurrentQueryIndex(index + 1);
return viewQuery;
}
/**
* Loads current View Query and moves the pointer/index to the next View Query in LView.
*
* @codeGenApi
*/
export function ɵɵloadViewQuery<T>(): T {
const index = getCurrentQueryIndex();
setCurrentQueryIndex(index + 1);
return ɵɵload<T>(index - HEADER_OFFSET);
}
/**
* Registers a QueryList, associated with a content query, for later refresh (part of a view
* refresh).
*
* @param directiveIndex Current directive index
* @param predicate The type for which the query will search
* @param descend Whether or not to descend into children
* @param read What to save in the query
* @returns QueryList<T>
*
* @codeGenApi
*/
export function ɵɵcontentQuery<T>(
directiveIndex: number, predicate: Type<any>| string[], descend: boolean,
// TODO(FW-486): "read" should be an AbstractType
read: any): QueryList<T> {
const lView = getLView();
const tView = lView[TVIEW];
const contentQuery: QueryList<T> = query<T>(predicate, descend, read);
(lView[CONTENT_QUERIES] || (lView[CONTENT_QUERIES] = [])).push(contentQuery);
if (tView.firstTemplatePass) {
const tViewContentQueries = tView.contentQueries || (tView.contentQueries = []);
const lastSavedDirectiveIndex =
tView.contentQueries.length ? tView.contentQueries[tView.contentQueries.length - 1] : -1;
if (directiveIndex !== lastSavedDirectiveIndex) {
tViewContentQueries.push(directiveIndex);
}
}
return contentQuery;
}
/**
* Registers a QueryList, associated with a static content query, for later refresh
* (part of a view refresh).
*
* @param directiveIndex Current directive index
* @param predicate The type for which the query will search
* @param descend Whether or not to descend into children
* @param read What to save in the query
* @returns QueryList<T>
*
* @codeGenApi
*/
export function ɵɵstaticContentQuery<T>(
directiveIndex: number, predicate: Type<any>| string[], descend: boolean,
// TODO(FW-486): "read" should be an AbstractType
read: any): void {
const queryList = ɵɵcontentQuery(directiveIndex, predicate, descend, read) as QueryList_<T>;
const tView = getLView()[TVIEW];
queryList._static = true;
if (!tView.staticContentQueries) {
tView.staticContentQueries = true;
}
}
/**
*
* @codeGenApi
*/
export function ɵɵloadContentQuery<T>(): QueryList<T> {
const lView = getLView();
ngDevMode &&
assertDefined(
lView[CONTENT_QUERIES], 'Content QueryList array should be defined if reading a query.');
const index = getCurrentQueryIndex();
ngDevMode && assertDataInRange(lView[CONTENT_QUERIES] !, index);
setCurrentQueryIndex(index + 1);
return lView[CONTENT_QUERIES] ![index];
}