diff --git a/packages/core/src/linker/query_list.ts b/packages/core/src/linker/query_list.ts index 1642e50184..f8af2b7e1a 100644 --- a/packages/core/src/linker/query_list.ts +++ b/packages/core/src/linker/query_list.ts @@ -119,7 +119,7 @@ export class QueryList/* implements Iterable */ { * on change detection, it will not notify of changes to the queries, unless a new change * occurs. * - * @param resultsTree The results tree to store + * @param resultsTree The query results to store */ reset(resultsTree: Array): void { this._results = flatten(resultsTree); diff --git a/packages/core/src/render3/assert.ts b/packages/core/src/render3/assert.ts index 18a44d0e29..e7e0bcfe89 100644 --- a/packages/core/src/render3/assert.ts +++ b/packages/core/src/render3/assert.ts @@ -69,3 +69,7 @@ export function assertLView(value: any) { assertDefined(value, 'LView must be defined'); assertEqual(isLView(value), true, 'Expecting LView'); } + +export function assertFirstTemplatePass(tView: TView, errMessage: string) { + assertEqual(tView.firstTemplatePass, true, errMessage); +} diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index 9cd49a3045..6c0ca41625 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -11,7 +11,7 @@ import {Type} from '../core'; import {Injector} from '../di/injector'; import {Sanitizer} from '../sanitization/security'; -import {assertDataInRange, assertEqual} from '../util/assert'; +import {assertDataInRange} from '../util/assert'; import {assertComponentType} from './assert'; import {getComponentDef} from './definition'; diff --git a/packages/core/src/render3/instructions/container.ts b/packages/core/src/render3/instructions/container.ts index d444d1ba5a..7c3797f0b0 100644 --- a/packages/core/src/render3/instructions/container.ts +++ b/packages/core/src/render3/instructions/container.ts @@ -12,13 +12,14 @@ import {executePreOrderHooks, registerPostOrderHooks} from '../hooks'; import {ACTIVE_INDEX, CONTAINER_HEADER_OFFSET, LContainer} from '../interfaces/container'; import {ComponentTemplate} from '../interfaces/definition'; import {LocalRefExtractor, TAttributes, TContainerNode, TNode, TNodeType} from '../interfaces/node'; -import {BINDING_INDEX, HEADER_OFFSET, LView, QUERIES, RENDERER, TVIEW, T_HOST} from '../interfaces/view'; +import {BINDING_INDEX, HEADER_OFFSET, LView, RENDERER, TVIEW, T_HOST} from '../interfaces/view'; import {assertNodeType} from '../node_assert'; import {appendChild, removeView} from '../node_manipulation'; import {getCheckNoChangesMode, getIsParent, getLView, getPreviousOrParentTNode, setIsNotParent, setPreviousOrParentTNode} from '../state'; import {getNativeByTNode, loadInternal} from '../util/view_utils'; -import {addToViewTree, createDirectivesAndLocals, createLContainer, createTView, getOrCreateTNode} from './shared'; +import {addToViewTree, createDirectivesAndLocals, createLContainer, createTView, getOrCreateTNode, resolveDirectives} from './shared'; + /** @@ -39,7 +40,6 @@ export function ɵɵcontainer(index: number): void { if (lView[TVIEW].firstTemplatePass) { tNode.tViews = []; } - addTContainerToQueries(lView, tNode); setIsNotParent(); } @@ -72,12 +72,18 @@ export function ɵɵtemplate( // TODO: consider a separate node type for templates const tContainerNode = containerInternal(lView, index, tagName || null, attrs || null); if (tView.firstTemplatePass) { - tContainerNode.tViews = createTView( + ngDevMode && ngDevMode.firstTemplatePass++; + resolveDirectives(tView, lView, tContainerNode, localRefs || null); + + const embeddedTView = tContainerNode.tViews = createTView( -1, templateFn, consts, vars, tView.directiveRegistry, tView.pipeRegistry, null, null); + if (tView.queries !== null) { + tView.queries.template(tView, tContainerNode); + embeddedTView.queries = tView.queries.embeddedTView(tContainerNode); + } } - createDirectivesAndLocals(tView, lView, tContainerNode, localRefs, localRefExtractor); - addTContainerToQueries(lView, tContainerNode); + createDirectivesAndLocals(tView, lView, tContainerNode, localRefExtractor); attachPatchData(getNativeByTNode(tContainerNode, lView), lView); registerPostOrderHooks(tView, tContainerNode); setIsNotParent(); @@ -133,32 +139,6 @@ export function ɵɵcontainerRefreshEnd(): void { } } -/** -* Reporting a TContainer node queries is a 2-step process as we need to: -* - check if the container node itself is matching (query might match a node); -* - prepare room for nodes from views that might be created based on the TemplateRef linked to this -* container. -* -* Those 2 operations need to happen in the specific order (match the container node itself, then -* prepare space for nodes from views). -*/ -function addTContainerToQueries(lView: LView, tContainerNode: TContainerNode): void { - const queries = lView[QUERIES]; - if (queries) { - const lContainer = lView[tContainerNode.index]; - if (lContainer[QUERIES]) { - // Query container should only exist if it was created through a dynamic view - // in a directive constructor. In this case, we must splice the template - // matches in before the view matches to ensure query results in embedded views - // don't clobber query results on the template node itself. - queries.insertNodeBeforeViews(tContainerNode); - } else { - queries.addNode(tContainerNode); - lContainer[QUERIES] = queries.container(); - } - } -} - function containerInternal( lView: LView, nodeIndex: number, tagName: string | null, attrs: TAttributes | null): TContainerNode { diff --git a/packages/core/src/render3/instructions/element.ts b/packages/core/src/render3/instructions/element.ts index e6d4c6c6c1..8e149fc297 100644 --- a/packages/core/src/render3/instructions/element.ts +++ b/packages/core/src/render3/instructions/element.ts @@ -5,13 +5,15 @@ * 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 */ + import {assertDataInRange, assertDefined, assertEqual} from '../../util/assert'; import {assertHasParent} from '../assert'; import {attachPatchData} from '../context_discovery'; import {registerPostOrderHooks} from '../hooks'; -import {PropertyAliases, TAttributes, TNode, TNodeFlags, TNodeType} from '../interfaces/node'; +import {TAttributes, TNodeFlags, TNodeType} from '../interfaces/node'; import {RElement} from '../interfaces/renderer'; -import {BINDING_INDEX, HEADER_OFFSET, LView, QUERIES, RENDERER, TVIEW, T_HOST} from '../interfaces/view'; +import {isContentQueryHost} from '../interfaces/type_checks'; +import {BINDING_INDEX, HEADER_OFFSET, LView, RENDERER, TVIEW, T_HOST} from '../interfaces/view'; import {assertNodeType} from '../node_assert'; import {appendChild} from '../node_manipulation'; import {applyOnCreateInstructions} from '../node_util'; @@ -22,7 +24,7 @@ import {getInitialStylingValue, hasClassInput, hasStyleInput} from '../styling_n import {setUpAttributes} from '../util/attrs_utils'; import {getNativeByTNode, getTNode} from '../util/view_utils'; -import {createDirectivesAndLocals, elementCreate, executeContentQueries, getOrCreateTNode, initializeTNodeInputs, renderInitialStyling, setInputsForProperty} from './shared'; +import {createDirectivesAndLocals, elementCreate, executeContentQueries, getOrCreateTNode, initializeTNodeInputs, renderInitialStyling, resolveDirectives, setInputsForProperty} from './shared'; @@ -66,7 +68,6 @@ export function ɵɵelementStart( renderInitialStyling(renderer, native, tNode); appendChild(native, tNode, lView); - createDirectivesAndLocals(tView, lView, tNode, localRefs); // any immediate children of a component or template container must be pre-emptively // monkey-patched with the component view data so that the element can be inspected @@ -81,6 +82,9 @@ export function ɵɵelementStart( // static class values as well. (Note that this will be fixed once map-based `[style]` // and `[class]` bindings work for multiple directives.) if (tView.firstTemplatePass) { + ngDevMode && ngDevMode.firstTemplatePass++; + resolveDirectives(tView, lView, tNode, localRefs || null); + const inputData = initializeTNodeInputs(tNode); if (inputData && inputData.hasOwnProperty('class')) { tNode.flags |= TNodeFlags.hasClassInput; @@ -89,13 +93,13 @@ export function ɵɵelementStart( if (inputData && inputData.hasOwnProperty('style')) { tNode.flags |= TNodeFlags.hasStyleInput; } + + if (tView.queries !== null) { + tView.queries.elementStart(tView, tNode); + } } - const currentQueries = lView[QUERIES]; - if (currentQueries) { - currentQueries.addNode(tNode); - lView[QUERIES] = currentQueries.clone(tNode); - } + createDirectivesAndLocals(tView, lView, tNode); executeContentQueries(tView, tNode, lView); } @@ -123,15 +127,16 @@ export function ɵɵelementEnd(): void { ngDevMode && assertNodeType(tNode, TNodeType.Element); const lView = getLView(); - const currentQueries = lView[QUERIES]; - // Go back up to parent queries only if queries have been cloned on this element. - if (currentQueries && tNode.index === currentQueries.nodeIndex) { - lView[QUERIES] = currentQueries.parent; - } + const tView = lView[TVIEW]; - registerPostOrderHooks(lView[TVIEW], tNode); + registerPostOrderHooks(tView, previousOrParentTNode); decreaseElementDepthCount(); + if (tView.firstTemplatePass && tView.queries !== null && + isContentQueryHost(previousOrParentTNode)) { + tView.queries !.elementEnd(previousOrParentTNode); + } + if (hasClassInput(tNode) && tNode.classes) { setDirectiveStylingInput(tNode.classes, lView, tNode.inputs !['class']); } diff --git a/packages/core/src/render3/instructions/element_container.ts b/packages/core/src/render3/instructions/element_container.ts index 3476cffd7b..bcbe03db85 100644 --- a/packages/core/src/render3/instructions/element_container.ts +++ b/packages/core/src/render3/instructions/element_container.ts @@ -10,14 +10,15 @@ import {assertHasParent} from '../assert'; import {attachPatchData} from '../context_discovery'; import {registerPostOrderHooks} from '../hooks'; import {TAttributes, TNodeType} from '../interfaces/node'; -import {BINDING_INDEX, HEADER_OFFSET, QUERIES, RENDERER, TVIEW, T_HOST} from '../interfaces/view'; +import {isContentQueryHost} from '../interfaces/type_checks'; +import {BINDING_INDEX, HEADER_OFFSET, RENDERER, TVIEW, T_HOST} from '../interfaces/view'; import {assertNodeType} from '../node_assert'; import {appendChild} from '../node_manipulation'; import {applyOnCreateInstructions} from '../node_util'; import {getIsParent, getLView, getPreviousOrParentTNode, setIsNotParent, setPreviousOrParentTNode} from '../state'; import {registerInitialStylingOnTNode} from '../styling_next/instructions'; -import {createDirectivesAndLocals, executeContentQueries, getOrCreateTNode} from './shared'; +import {createDirectivesAndLocals, executeContentQueries, getOrCreateTNode, resolveDirectives} from './shared'; @@ -61,14 +62,17 @@ export function ɵɵelementContainerStart( } appendChild(native, tNode, lView); - createDirectivesAndLocals(tView, lView, tNode, localRefs); - attachPatchData(native, lView); - const currentQueries = lView[QUERIES]; - if (currentQueries) { - currentQueries.addNode(tNode); - lView[QUERIES] = currentQueries.clone(tNode); + if (tView.firstTemplatePass) { + ngDevMode && ngDevMode.firstTemplatePass++; + resolveDirectives(tView, lView, tNode, localRefs || null); + if (tView.queries) { + tView.queries.elementStart(tView, tNode); + } } + + createDirectivesAndLocals(tView, lView, tNode); + attachPatchData(native, lView); executeContentQueries(tView, tNode, lView); } @@ -90,17 +94,17 @@ export function ɵɵelementContainerEnd(): void { } ngDevMode && assertNodeType(previousOrParentTNode, TNodeType.ElementContainer); - const currentQueries = lView[QUERIES]; - // Go back up to parent queries only if queries have been cloned on this element. - if (currentQueries && previousOrParentTNode.index === currentQueries.nodeIndex) { - lView[QUERIES] = currentQueries.parent; - } // this is required for all host-level styling-related instructions to run // in the correct order previousOrParentTNode.onElementCreationFns && applyOnCreateInstructions(previousOrParentTNode); registerPostOrderHooks(tView, previousOrParentTNode); + + if (tView.firstTemplatePass && tView.queries !== null && + isContentQueryHost(previousOrParentTNode)) { + tView.queries.elementEnd(previousOrParentTNode); + } } /** diff --git a/packages/core/src/render3/instructions/embedded_view.ts b/packages/core/src/render3/instructions/embedded_view.ts index 94d6d2f1b3..91d3ef53e9 100644 --- a/packages/core/src/render3/instructions/embedded_view.ts +++ b/packages/core/src/render3/instructions/embedded_view.ts @@ -49,10 +49,6 @@ export function ɵɵembeddedViewStart( getOrCreateEmbeddedTView(viewBlockId, consts, vars, containerTNode as TContainerNode), null, LViewFlags.CheckAlways, null, null); - if (lContainer[QUERIES]) { - viewToRender[QUERIES] = lContainer[QUERIES] !.createView(); - } - const tParentNode = getIsParent() ? previousOrParentTNode : previousOrParentTNode && previousOrParentTNode.parent; assignTViewNodeToLView(viewToRender[TVIEW], tParentNode, viewBlockId, viewToRender); diff --git a/packages/core/src/render3/instructions/lview_debug.ts b/packages/core/src/render3/instructions/lview_debug.ts index cd98fcf226..f34c93ac6e 100644 --- a/packages/core/src/render3/instructions/lview_debug.ts +++ b/packages/core/src/render3/instructions/lview_debug.ts @@ -10,15 +10,16 @@ import {AttributeMarker, ComponentTemplate} from '..'; import {SchemaMetadata} from '../../core'; import {assertDefined} from '../../util/assert'; import {createNamedArrayType} from '../../util/named_array_type'; -import {ACTIVE_INDEX, CONTAINER_HEADER_OFFSET, LContainer, NATIVE} from '../interfaces/container'; +import {ACTIVE_INDEX, CONTAINER_HEADER_OFFSET, LContainer, MOVED_VIEWS, NATIVE} from '../interfaces/container'; import {DirectiveDefList, PipeDefList, ViewQueriesFunction} from '../interfaces/definition'; import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, TIcu} from '../interfaces/i18n'; import {PropertyAliases, TContainerNode, TElementNode, TNode as ITNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TViewNode} from '../interfaces/node'; import {SelectorFlags} from '../interfaces/projection'; -import {LQueries} from '../interfaces/query'; +import {TQueries} from '../interfaces/query'; import {RComment, RElement, RNode} from '../interfaces/renderer'; import {StylingContext} from '../interfaces/styling'; -import {BINDING_INDEX, CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTENT_QUERIES, CONTEXT, DECLARATION_VIEW, ExpandoInstructions, FLAGS, HEADER_OFFSET, HOST, HookData, INJECTOR, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, TData, TVIEW, TView as ITView, TView, T_HOST} from '../interfaces/view'; + +import {BINDING_INDEX, CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_VIEW, ExpandoInstructions, FLAGS, HEADER_OFFSET, HOST, HookData, INJECTOR, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, TData, TVIEW, TView as ITView, TView, T_HOST} from '../interfaces/view'; import {TStylingContext} from '../styling_next/interfaces'; import {DebugStyling as DebugNewStyling, NodeStylingDebug} from '../styling_next/styling_debug'; import {isStylingContext} from '../styling_next/util'; @@ -78,11 +79,11 @@ export const TViewConstructor = class TView implements ITView { public id: number, // public blueprint: LView, // public template: ComponentTemplate<{}>|null, // + public queries: TQueries|null, // public viewQuery: ViewQueriesFunction<{}>|null, // public node: TViewNode|TElementNode|null, // public data: TData, // public bindingStartIndex: number, // - public viewQueryStartIndex: number, // public expandoStartIndex: number, // public expandoInstructions: ExpandoInstructions|null, // public firstTemplatePass: boolean, // @@ -287,8 +288,7 @@ export class LViewDebug { next: toDebug(this._raw_lView[NEXT]), childTail: toDebug(this._raw_lView[CHILD_TAIL]), declarationView: toDebug(this._raw_lView[DECLARATION_VIEW]), - contentQueries: this._raw_lView[CONTENT_QUERIES], - queries: this._raw_lView[QUERIES], + queries: null, tHost: this._raw_lView[T_HOST], bindingIndex: this._raw_lView[BINDING_INDEX], }; @@ -361,7 +361,7 @@ export class LContainerDebug { .map(toDebug as(l: LView) => LViewDebug); } get parent(): LViewDebug|LContainerDebug|null { return toDebug(this._raw_lContainer[PARENT]); } - get queries(): LQueries|null { return this._raw_lContainer[QUERIES]; } + get movedViews(): LView[]|null { return this._raw_lContainer[MOVED_VIEWS]; } get host(): RElement|RComment|StylingContext|LView { return this._raw_lContainer[HOST]; } get native(): RComment { return this._raw_lContainer[NATIVE]; } get __other__() { diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index b130ce64ae..778ea76cc0 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -23,7 +23,6 @@ import {ACTIVE_INDEX, CONTAINER_HEADER_OFFSET, LContainer} from '../interfaces/c import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, FactoryFn, PipeDefListOrFactory, RenderFlags, ViewQueriesFunction} from '../interfaces/definition'; import {INJECTOR_BLOOM_PARENT_SIZE, NodeInjectorFactory} from '../interfaces/injector'; import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliasValue, PropertyAliases, TAttributes, TContainerNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TProjectionNode, TViewNode} from '../interfaces/node'; -import {LQueries} from '../interfaces/query'; import {RComment, RElement, RText, Renderer3, RendererFactory3, isProceduralRenderer} from '../interfaces/renderer'; import {SanitizerFn} from '../interfaces/sanitization'; import {StylingContext} from '../interfaces/styling'; @@ -157,16 +156,20 @@ export function setHostBindings(tView: TView, viewData: LView): void { } } -/** Refreshes content queries for all directives in the given view. */ +/** Refreshes all content queries declared by directives in a given view */ function refreshContentQueries(tView: TView, lView: LView): void { - if (tView.contentQueries != null) { - setCurrentQueryIndex(0); - for (let i = 0; i < tView.contentQueries.length; i++) { - const directiveDefIdx = tView.contentQueries[i]; - const directiveDef = tView.data[directiveDefIdx] as DirectiveDef; - ngDevMode && - assertDefined(directiveDef.contentQueries, 'contentQueries function should be defined'); - directiveDef.contentQueries !(RenderFlags.Update, lView[directiveDefIdx], directiveDefIdx); + const contentQueries = tView.contentQueries; + if (contentQueries !== null) { + for (let i = 0; i < contentQueries.length; i += 2) { + const queryStartIdx = contentQueries[i]; + const directiveDefIdx = contentQueries[i + 1]; + if (directiveDefIdx !== -1) { + const directiveDef = tView.data[directiveDefIdx] as DirectiveDef; + ngDevMode && + assertDefined(directiveDef.contentQueries, 'contentQueries function should be defined'); + setCurrentQueryIndex(queryStartIdx); + directiveDef.contentQueries !(RenderFlags.Update, lView[directiveDefIdx], directiveDefIdx); + } } } } @@ -361,8 +364,7 @@ export function allocExpando(view: LView, numSlotsToAlloc: number) { * Such lViewNode will then be renderer with renderEmbeddedTemplate() (see below). */ export function createEmbeddedViewAndNode( - tView: TView, context: T, declarationView: LView, queries: LQueries | null, - injectorIndex: number): LView { + tView: TView, context: T, declarationView: LView, injectorIndex: number): LView { const _isParent = getIsParent(); const _previousOrParentTNode = getPreviousOrParentTNode(); setPreviousOrParentTNode(null !, true); @@ -370,9 +372,6 @@ export function createEmbeddedViewAndNode( const lView = createLView(declarationView, tView, context, LViewFlags.CheckAlways, null, null); lView[DECLARATION_VIEW] = declarationView; - if (queries) { - lView[QUERIES] = queries.createView(); - } assignTViewNodeToLView(tView, null, -1, lView); if (tView.firstTemplatePass) { @@ -513,14 +512,8 @@ export function executeContentQueries(tView: TView, tNode: TNode, lView: LView) */ export function createDirectivesAndLocals( tView: TView, lView: LView, tNode: TElementNode | TContainerNode | TElementContainerNode, - localRefs: string[] | null | undefined, localRefExtractor: LocalRefExtractor = getNativeByTNode) { if (!getBindingsEnabled()) return; - if (tView.firstTemplatePass) { - ngDevMode && ngDevMode.firstTemplatePass++; - resolveDirectives( - tView, lView, findDirectiveMatches(tView, lView, tNode), tNode, localRefs || null); - } instantiateAllDirectives(tView, lView, tNode); invokeDirectivesHostBindings(tView, lView, tNode); saveResolvedLocalsInData(lView, tNode, localRefExtractor); @@ -588,11 +581,11 @@ export function createTView( viewIndex, // id: number, blueprint, // blueprint: LView, templateFn, // template: ComponentTemplate<{}>|null, + null, // queries: TQueries|null viewQuery, // viewQuery: ViewQueriesFunction<{}>|null, null !, // node: TViewNode|TElementNode|null, cloneToTViewData(blueprint).fill(null, bindingStartIndex), // data: TData, bindingStartIndex, // bindingStartIndex: number, - initialViewLength, // viewQueryStartIndex: number, initialViewLength, // expandoStartIndex: number, null, // expandoInstructions: ExpandoInstructions|null, true, // firstTemplatePass: boolean, @@ -619,11 +612,11 @@ export function createTView( id: viewIndex, blueprint: blueprint, template: templateFn, + queries: null, viewQuery: viewQuery, node: null !, data: blueprint.slice().fill(null, bindingStartIndex), bindingStartIndex: bindingStartIndex, - viewQueryStartIndex: initialViewLength, expandoStartIndex: initialViewLength, expandoInstructions: null, firstTemplatePass: true, @@ -1031,13 +1024,18 @@ export function instantiateRootComponent( /** * Resolve the matched directives on a node. */ -function resolveDirectives( - tView: TView, viewData: LView, directives: DirectiveDef[] | null, tNode: TNode, +export function resolveDirectives( + tView: TView, lView: LView, tNode: TElementNode | TContainerNode | TElementContainerNode, localRefs: string[] | null): void { // Please make sure to have explicit type for `exportsMap`. Inferred type triggers bug in // tsickle. ngDevMode && assertEqual(tView.firstTemplatePass, true, 'should run on first template pass only'); + + if (!getBindingsEnabled()) return; + + const directives: DirectiveDef[]|null = findDirectiveMatches(tView, lView, tNode); const exportsMap: ({[key: string]: number} | null) = localRefs ? {'': -1} : null; + if (directives) { initNodeFlags(tNode, tView.data.length, directives.length); // When the same token is provided by several directives on the same node, some rules apply in @@ -1059,7 +1057,7 @@ function resolveDirectives( const def = directives[i] as DirectiveDef; const directiveDefIdx = tView.data.length; - baseResolveDirective(tView, viewData, def, def.factory); + baseResolveDirective(tView, lView, def, def.factory); saveNameToExportMap(tView.data !.length - 1, def, exportsMap); @@ -1766,8 +1764,8 @@ export function checkView(hostView: LView, component: T) { function executeViewQueryFn(flags: RenderFlags, tView: TView, component: T): void { const viewQuery = tView.viewQuery; - if (viewQuery) { - setCurrentQueryIndex(tView.viewQueryStartIndex); + if (viewQuery !== null) { + setCurrentQueryIndex(0); viewQuery(flags, component); } } diff --git a/packages/core/src/render3/interfaces/container.ts b/packages/core/src/render3/interfaces/container.ts index a385de073c..eb33ae6b27 100644 --- a/packages/core/src/render3/interfaces/container.ts +++ b/packages/core/src/render3/interfaces/container.ts @@ -9,9 +9,9 @@ import {ViewRef} from '../../linker/view_ref'; import {TNode} from './node'; -import {LQueries} from './query'; import {RComment, RElement} from './renderer'; -import {HOST, LView, NEXT, PARENT, QUERIES, T_HOST} from './view'; + +import {HOST, LView, NEXT, PARENT, T_HOST} from './view'; /** @@ -26,8 +26,15 @@ export const TYPE = 1; * Uglify will inline these when minifying so there shouldn't be a cost. */ export const ACTIVE_INDEX = 2; -// PARENT, NEXT, QUERIES and T_HOST are indices 3, 4, 5 and 6. + +// PARENT and NEXT are indices 3 and 4 // As we already have these constants in LView, we don't need to re-create them. + +export const MOVED_VIEWS = 5; + +// T_HOST is index 6 +// We already have this constants in LView, we don't need to re-create it. + export const NATIVE = 7; export const VIEW_REFS = 8; @@ -84,11 +91,11 @@ export interface LContainer extends Array { [NEXT]: LView|LContainer|null; /** - * Queries active for this container - all the views inserted to / removed from - * this container are reported to queries referenced here. + * A collection of views created based on the underlying `` element but inserted into + * a different `LContainer`. We need to track views created from a given declaration point since + * queries collect matches from the embedded view declaration point and _not_ the insertion point. */ - [QUERIES]: LQueries|null; // TODO(misko): This is abuse of `LContainer` since we are storing - // `[QUERIES]` in it which are not needed for `LContainer` (only needed for Template) + [MOVED_VIEWS]: LView[]|null; /** * Pointer to the `TNode` which represents the host of the container. diff --git a/packages/core/src/render3/interfaces/query.ts b/packages/core/src/render3/interfaces/query.ts index 0615ec7946..bb19958d30 100644 --- a/packages/core/src/render3/interfaces/query.ts +++ b/packages/core/src/render3/interfaces/query.ts @@ -9,99 +9,211 @@ import {Type} from '../../interface/type'; import {QueryList} from '../../linker'; -import {TContainerNode, TElementContainerNode, TElementNode, TNode} from './node'; +import {TNode} from './node'; +import {TView} from './view'; +/** + * An object representing query metadata extracted from query annotations. + */ +export interface TQueryMetadata { + predicate: Type|string[]; + descendants: boolean; + read: any; + isStatic: boolean; +} -/** Used for tracking queries (e.g. ViewChild, ContentChild). */ +/** + * TQuery objects represent all the query-related data that remain the same from one view instance + * to another and can be determined on the very first template pass. Most notably TQuery holds all + * the matches for a given view. + */ +export interface TQuery { + /** + * Query metadata extracted from query annotations. + */ + metadata: TQueryMetadata; + + /** + * Index of a query in a declaration view in case of queries propagated to en embedded view, -1 + * for queries declared in a given view. We are storing this index so we can find a parent query + * to clone for an embedded view (when an embedded view is created). + */ + indexInDeclarationView: number; + + /** + * Matches collected on the first template pass. Each match is a pair of: + * - TNode index; + * - match index; + * + * A TNode index can be either: + * - a positive number (the most common case) to indicate a matching TNode; + * - a negative number to indicate that a given query is crossing a element and + * results from views created based on TemplateRef should be inserted at this place. + * + * A match index is a number used to find an actual value (for a given node) when query results + * are materialized. This index can have one of the following values: + * - -2 - indicates that we need to read a special token (TemplateRef, ViewContainerRef etc.); + * - -1 - indicates that we need to read a default value based on the node type (TemplateRef for + * ng-template and ElementRef for other elements); + * - a positive number - index of an injectable to be read from the element injector. + */ + matches: number[]|null; + + /** + * A flag indicating if a given query crosses an element. This flag exists for + * performance reasons: we can notice that queries not crossing any elements will + * have matches from a given view only (and adapt processing accordingly). + */ + crossesNgTemplate: boolean; + + /** + * A method call when a given query is crossing an element (or element container). This is where a + * given TNode is matched against a query predicate. + * @param tView + * @param tNode + */ + elementStart(tView: TView, tNode: TNode): void; + + /** + * A method called when processing the elementEnd instruction - this is mostly useful to determine + * if a given content query should match any nodes past this point. + * @param tNode + */ + elementEnd(tNode: TNode): void; + + /** + * A method called when processing the template instruction. This is where a + * given TContainerNode is matched against a query predicate. + * @param tView + * @param tNode + */ + template(tView: TView, tNode: TNode): void; + + /** + * A query-related method called when an embedded TView is created based on the content of a + * element. We call this method to determine if a given query should be propagated + * to the embedded view and if so - return a cloned TQuery for this embedded view. + * @param tNode + * @param childQueryIndex + */ + embeddedTView(tNode: TNode, childQueryIndex: number): TQuery|null; +} + +/** + * TQueries represent a collection of individual TQuery objects tracked in a given view. Most of the + * methods on this interface are simple proxy methods to the corresponding functionality on TQuery. + */ +export interface TQueries { + /** + * Adds a new TQuery to a collection of queries tracked in a given view. + * @param tQuery + */ + track(tQuery: TQuery): void; + + /** + * Returns a TQuery instance for at the given index in the queries array. + * @param index + */ + getByIndex(index: number): TQuery; + + /** + * Returns the number of queries tracked in a given view. + */ + length: number; + + /** + * A proxy method that iterates over all the TQueries in a given TView and calls the corresponding + * `elementStart` on each and every TQuery. + * @param tView + * @param tNode + */ + elementStart(tView: TView, tNode: TNode): void; + + /** + * A proxy method that iterates over all the TQueries in a given TView and calls the corresponding + * `elementEnd` on each and every TQuery. + * @param tNode + */ + elementEnd(tNode: TNode): void; + + /** + * A proxy method that iterates over all the TQueries in a given TView and calls the corresponding + * `template` on each and every TQuery. + * @param tView + * @param tNode + */ + template(tView: TView, tNode: TNode): void; + + /** + * A proxy method that iterates over all the TQueries in a given TView and calls the corresponding + * `embeddedTView` on each and every TQuery. + * @param tNode + */ + embeddedTView(tNode: TNode): TQueries|null; +} + +/** + * An interface that represents query-related information specific to a view instance. Most notably + * it contains: + * - materialized query matches; + * - a pointer to a QueryList where materialized query results should be reported. + */ +export interface LQuery { + /** + * Materialized query matches for a given view only (!). Results are initialized lazily so the + * array of matches is set to `null` initially. + */ + matches: (T|null)[]|null; + + /** + * A QueryList where materialized query results should be reported. + */ + queryList: QueryList; + + /** + * Clones an LQuery for an embedded view. A cloned query shares the same `QueryList` but has a + * separate collection of materialized matches. + */ + clone(): LQuery; + + /** + * Called when an embedded view, impacting results of this query, is inserted or removed. + */ + setDirty(): void; +} + +/** + * lQueries represent a collection of individual LQuery objects tracked in a given view. + */ export interface LQueries { /** - * The parent LQueries instance. - * - * When there is a content query, a new LQueries instance is created to avoid mutating any - * existing LQueries. After we are done searching content children, the parent property allows - * us to traverse back up to the original LQueries instance to continue to search for matches - * in the main view. + * A collection of queries tracked in a given view. */ - parent: LQueries|null; + queries: LQuery[]; /** - * The index of the node on which this LQueries instance was created / cloned in a given LView. - * - * This index is stored to minimize LQueries cloning: we can observe that LQueries can be mutated - * only under 2 conditions: - * - we are crossing an element that has directives with content queries (new queries are added); - * - we are descending into element hierarchy (creating a child element of an existing element) - * and the current LQueries object is tracking shallow queries (shallow queries are removed). - * - * Since LQueries are not cloned systematically we need to know exactly where (on each element) - * cloning occurred, so we can properly restore the set of tracked queries when going up the - * elements hierarchy. - * - * Always set to -1 for view queries as view queries are created before we process any node in a - * given view. + * A method called when a new embedded view is created. As a result a set of LQueries applicable + * for a new embedded view is instantiated (cloned) from the declaration view. + * @param tView */ - nodeIndex: number; + createEmbeddedView(tView: TView): LQueries|null; /** - * Ask queries to prepare a copy of itself. This ensures that: - * - tracking new queries on content nodes doesn't mutate list of queries tracked on a parent - * node; - * - we don't track shallow queries when descending into elements hierarchy. - * - * We will clone LQueries before constructing content queries + * A method called when an embedded view is inserted into a container. As a result all impacted + * `LQuery` objects (and associated `QueryList`) are marked as dirty. + * @param tView */ - clone(tNode: TNode): LQueries; + insertView(tView: TView): void; /** - * Notify `LQueries` that a new `TNode` has been created and needs to be added to query results - * if matching query predicate. + * A method called when an embedded view is detached from a container. As a result all impacted + * `LQuery` objects (and associated `QueryList`) are marked as dirty. + * @param tView */ - addNode(tNode: TElementNode|TContainerNode|TElementContainerNode): void; - - /** - * Notify `LQueries` that a new `TNode` has been created and needs to be added to query results - * if matching query predicate. This is a special mode invoked if the query container has to - * be created out of order (e.g. view created in the constructor of a directive). - */ - insertNodeBeforeViews(tNode: TElementNode|TContainerNode|TElementContainerNode): void; - - /** - * Notify `LQueries` that a new LContainer was added to ivy data structures. As a result we need - * to prepare room for views that might be inserted into this container. - */ - container(): LQueries|null; - - /** - * Notify `LQueries` that a new `LView` has been created. As a result we need to prepare room - * and collect nodes that match query predicate. - */ - createView(): LQueries|null; - - /** - * Notify `LQueries` that a new `LView` has been added to `LContainer`. As a result all - * the matching nodes from this view should be added to container's queries. - */ - insertView(newViewIndex: number): void; - - /** - * Notify `LQueries` that an `LView` has been removed from `LContainer`. As a result all - * the matching nodes from this view should be removed from container's queries. - */ - removeView(): void; - - /** - * Add additional `QueryList` to track. - * - * @param queryList `QueryList` to update with changes. - * @param predicate Either `Type` or selector array of [key, value] predicates. - * @param descend If true the query will recursively apply to the children. - * @param read Indicates which token should be read from DI for this query. - */ - track( - queryList: QueryList, predicate: Type|string[], descend?: boolean, - read?: Type): void; + detachView(tView: TView): void; } + // Note: This hack is necessary so we don't erroneously get a circular dependency // failure based on types. export const unusedValueExportToPlacateAjd = 1; diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index 6ae7fb7851..10043563c2 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -9,7 +9,6 @@ import {InjectionToken} from '../../di/injection_token'; import {Injector} from '../../di/injector'; import {Type} from '../../interface/type'; -import {QueryList} from '../../linker'; import {SchemaMetadata} from '../../metadata'; import {Sanitizer} from '../../sanitization/security'; @@ -18,7 +17,7 @@ import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefList, HostBin import {I18nUpdateOpCodes, TI18n} from './i18n'; import {TElementNode, TNode, TViewNode} from './node'; import {PlayerHandler} from './player'; -import {LQueries} from './query'; +import {LQueries, TQueries} from './query'; import {RElement, Renderer3, RendererFactory3} from './renderer'; @@ -42,11 +41,11 @@ export const RENDERER = 12; export const SANITIZER = 13; export const CHILD_HEAD = 14; export const CHILD_TAIL = 15; -export const CONTENT_QUERIES = 16; -export const DECLARATION_VIEW = 17; +export const DECLARATION_VIEW = 16; +export const DECLARATION_LCONTAINER = 17; export const PREORDER_HOOK_FLAGS = 18; /** Size of LView's header. Necessary to adjust for it when setting slots. */ -export const HEADER_OFFSET = 20; +export const HEADER_OFFSET = 19; // This interface replaces the real LView interface if it is an arg or a @@ -179,13 +178,6 @@ export interface LView extends Array { */ [CHILD_TAIL]: LView|LContainer|null; - /** - * Stores QueryLists associated with content queries of a directive. This data structure is - * filled-in as part of a directive creation process and is later used to retrieve a QueryList to - * be refreshed. - */ - [CONTENT_QUERIES]: QueryList[]|null; - /** * View where this view's template was declared. * @@ -212,6 +204,16 @@ export interface LView extends Array { */ [DECLARATION_VIEW]: LView|null; + /** + * A declaration point of embedded views (ones instantiated based on the content of a + * ), null for other types of views. + * + * We need to track all embedded views created from a given declaration point so we can prepare + * query matches in a proper order (query matches are ordered based on their declaration point and + * _not_ the insertion point). + */ + [DECLARATION_LCONTAINER]: LContainer|null; + /** * More flags for this view. See PreOrderHookFlags for more info. */ @@ -410,17 +412,6 @@ export interface TView { */ staticContentQueries: boolean; - /** - * The index where the viewQueries section of `LView` begins. This section contains - * view queries defined for a component/directive. - * - * We store this start index so we know where the list of view queries starts. - * This is required when we invoke view queries at runtime. We invoke queries one by one and - * increment query index after each iteration. This information helps us to reset index back to - * the beginning of view query list before we invoke view queries again. - */ - viewQueryStartIndex: number; - /** * A reference to the first child node located in the view. */ @@ -550,7 +541,19 @@ export interface TView { components: number[]|null; /** - * A list of indices for child directives that have content queries. + * A collection of queries tracked in a given view. + */ + queries: TQueries|null; + + /** + * An array of indices pointing to directives with content queries alongside with the + * corresponding + * query index. Each entry in this array is a tuple of: + * - index of the first content query index declared by a given directive; + * - index of a directive. + * + * We are storing those indexes so we can refresh content queries as part of a view refresh + * process. */ contentQueries: number[]|null; diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index c239229513..98fb9efe8c 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -11,18 +11,18 @@ import {assertDefined, assertDomNode} from '../util/assert'; import {assertLContainer, assertLView} from './assert'; import {attachPatchData} from './context_discovery'; -import {CONTAINER_HEADER_OFFSET, LContainer, NATIVE, unusedValueExportToPlacateAjd as unused1} from './interfaces/container'; +import {CONTAINER_HEADER_OFFSET, LContainer, MOVED_VIEWS, NATIVE, unusedValueExportToPlacateAjd as unused1} from './interfaces/container'; import {ComponentDef} from './interfaces/definition'; import {NodeInjectorFactory} from './interfaces/injector'; import {TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeType, TProjectionNode, TViewNode, unusedValueExportToPlacateAjd as unused2} from './interfaces/node'; import {unusedValueExportToPlacateAjd as unused3} from './interfaces/projection'; import {ProceduralRenderer3, RElement, RNode, RText, Renderer3, isProceduralRenderer, unusedValueExportToPlacateAjd as unused4} from './interfaces/renderer'; import {isLContainer, isLView, isRootView} from './interfaces/type_checks'; -import {CHILD_HEAD, CLEANUP, FLAGS, HOST, HookData, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, TVIEW, T_HOST, unusedValueExportToPlacateAjd as unused5} from './interfaces/view'; +import {CHILD_HEAD, CLEANUP, DECLARATION_LCONTAINER, FLAGS, HOST, HookData, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, TVIEW, T_HOST, unusedValueExportToPlacateAjd as unused5} from './interfaces/view'; import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {renderStringify} from './util/misc_utils'; import {findComponentView, getLViewParent} from './util/view_traversal_utils'; -import {getNativeByTNode, getNativeByTNodeOrNull, unwrapRNode, viewAttachedToContainer} from './util/view_utils'; +import {getNativeByTNode, getNativeByTNodeOrNull, unwrapRNode} from './util/view_utils'; const unusedValueToPlacateAjd = unused1 + unused2 + unused3 + unused4 + unused5; @@ -217,15 +217,46 @@ export function insertView(lView: LView, lContainer: LContainer, index: number) lView[PARENT] = lContainer; - // Notify query that a new view has been added - if (lView[QUERIES]) { - lView[QUERIES] !.insertView(index); + // track views where declaration and insertion points are different + const declarationLContainer = lView[DECLARATION_LCONTAINER]; + if (declarationLContainer !== null && lContainer !== declarationLContainer) { + trackMovedView(declarationLContainer, lView); + } + + // notify query that a new view has been added + const lQueries = lView[QUERIES]; + if (lQueries !== null) { + lQueries.insertView(lView[TVIEW]); } // Sets the attached flag lView[FLAGS] |= LViewFlags.Attached; } +/** + * Track views created from the declaration container (TemplateRef) and inserted into a + * different LContainer. + */ +function trackMovedView(declarationContainer: LContainer, lView: LView) { + ngDevMode && assertLContainer(declarationContainer); + const declaredViews = declarationContainer[MOVED_VIEWS]; + if (declaredViews === null) { + declarationContainer[MOVED_VIEWS] = [lView]; + } else { + declaredViews.push(lView); + } +} + +function detachMovedView(declarationContainer: LContainer, lView: LView) { + ngDevMode && assertLContainer(declarationContainer); + ngDevMode && assertDefined( + declarationContainer[MOVED_VIEWS], + 'A projected view should belong to a non-empty projected views collection'); + const projectedViews = declarationContainer[MOVED_VIEWS] !; + const declaredViewIndex = projectedViews.indexOf(lView); + projectedViews.splice(declaredViewIndex, 1); +} + /** * Detaches a view from a container. * @@ -241,17 +272,26 @@ export function detachView(lContainer: LContainer, removeIndex: number): LView|u const indexInContainer = CONTAINER_HEADER_OFFSET + removeIndex; const viewToDetach = lContainer[indexInContainer]; + if (viewToDetach) { + const declarationLContainer = viewToDetach[DECLARATION_LCONTAINER]; + if (declarationLContainer !== null && declarationLContainer !== lContainer) { + detachMovedView(declarationLContainer, viewToDetach); + } + + if (removeIndex > 0) { lContainer[indexInContainer - 1][NEXT] = viewToDetach[NEXT] as LView; } - lContainer.splice(CONTAINER_HEADER_OFFSET + removeIndex, 1); + const removedLView = lContainer.splice(CONTAINER_HEADER_OFFSET + removeIndex, 1)[0]; addRemoveViewFromContainer(viewToDetach, false); - if ((viewToDetach[FLAGS] & LViewFlags.Attached) && - !(viewToDetach[FLAGS] & LViewFlags.Destroyed) && viewToDetach[QUERIES]) { - viewToDetach[QUERIES] !.removeView(); + // notify query that a view has been removed + const lQueries = removedLView[QUERIES]; + if (lQueries !== null) { + lQueries.detachView(removedLView[TVIEW]); } + viewToDetach[PARENT] = null; viewToDetach[NEXT] = null; // Unsets the attached flag @@ -342,9 +382,20 @@ function cleanUpView(view: LView | LContainer): void { ngDevMode && ngDevMode.rendererDestroy++; (view[RENDERER] as ProceduralRenderer3).destroy(); } - // For embedded views still attached to a container: remove query result from this view. - if (viewAttachedToContainer(view) && view[QUERIES]) { - view[QUERIES] !.removeView(); + + const declarationContainer = view[DECLARATION_LCONTAINER]; + // we are dealing with an embedded view that is still inserted into a container + if (declarationContainer !== null && isLContainer(view[PARENT])) { + // and this is a projected view + if (declarationContainer !== view[PARENT]) { + detachMovedView(declarationContainer, view); + } + + // For embedded views still attached to a container: remove query result from this view. + const lQueries = view[QUERIES]; + if (lQueries !== null) { + lQueries.detachView(view[TVIEW]); + } } } } @@ -878,16 +929,18 @@ function executeActionOnNode( renderer: Renderer3, action: WalkTNodeTreeAction, lView: LView, tNode: TNode, renderParent: RElement | null, beforeNode: RNode | null | undefined): void { const nodeType = tNode.type; - if (nodeType === TNodeType.ElementContainer || nodeType === TNodeType.IcuContainer) { - executeActionOnElementContainerOrIcuContainer( - renderer, action, lView, tNode as TElementContainerNode | TIcuContainerNode, renderParent, - beforeNode); - } else if (nodeType === TNodeType.Projection) { - executeActionOnProjection( - renderer, action, lView, tNode as TProjectionNode, renderParent, beforeNode); - } else { - ngDevMode && assertNodeOfPossibleTypes(tNode, TNodeType.Element, TNodeType.Container); - executeActionOnElementOrContainer( - action, renderer, renderParent, lView[tNode.index], beforeNode); + if (!(tNode.flags & TNodeFlags.isDetached)) { + if (nodeType === TNodeType.ElementContainer || nodeType === TNodeType.IcuContainer) { + executeActionOnElementContainerOrIcuContainer( + renderer, action, lView, tNode as TElementContainerNode | TIcuContainerNode, renderParent, + beforeNode); + } else if (nodeType === TNodeType.Projection) { + executeActionOnProjection( + renderer, action, lView, tNode as TProjectionNode, renderParent, beforeNode); + } else { + ngDevMode && assertNodeOfPossibleTypes(tNode, TNodeType.Element, TNodeType.Container); + executeActionOnElementOrContainer( + action, renderer, renderParent, lView[tNode.index], beforeNode); + } } } diff --git a/packages/core/src/render3/query.ts b/packages/core/src/render3/query.ts index dc31625f1b..434875daa4 100644 --- a/packages/core/src/render3/query.ts +++ b/packages/core/src/render3/query.ts @@ -13,195 +13,240 @@ 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 {ViewContainerRef} from '../linker/view_container_ref'; +import {assertDataInRange, assertDefined, throwError} from '../util/assert'; +import {stringify} from '../util/stringify'; -import {assertPreviousIsParent} from './assert'; +import {assertFirstTemplatePass, assertLContainer} from './assert'; import {getNodeInjectable, locateDirectiveOrProvider} from './di'; -import {NG_ELEMENT_ID} from './fields'; -import {store} from './instructions/all'; import {storeCleanupWithContext} from './instructions/shared'; +import {CONTAINER_HEADER_OFFSET, LContainer, MOVED_VIEWS} from './interfaces/container'; 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 {isContentQueryHost} from './interfaces/type_checks'; -import {CONTENT_QUERIES, HEADER_OFFSET, LView, QUERIES, TVIEW, TView} from './interfaces/view'; -import {getCurrentQueryIndex, getIsParent, getLView, getPreviousOrParentTNode, isCreationMode, setCurrentQueryIndex} from './state'; -import {loadInternal} from './util/view_utils'; -import {createElementRef, createTemplateRef} from './view_engine_compatibility'; +import {LQueries, LQuery, TQueries, TQuery, TQueryMetadata, unusedValueExportToPlacateAjd as unused4} from './interfaces/query'; +import {DECLARATION_LCONTAINER, LView, PARENT, QUERIES, TVIEW, TView} from './interfaces/view'; +import {assertNodeOfPossibleTypes} from './node_assert'; +import {getCurrentQueryIndex, getLView, getPreviousOrParentTNode, isCreationMode, setCurrentQueryIndex} from './state'; +import {createContainerRef, 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 { - /** - * If looking for directives then it contains the directive type. - */ - type: Type|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|null; +class LQuery_ implements LQuery { + matches: (T|null)[]|null = null; + constructor(public queryList: QueryList) {} + clone(): LQuery { return new LQuery_(this.queryList); } + setDirty(): void { this.queryList.setDirty(); } } -/** - * 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 - */ -class LQuery { - constructor( - /** - * Next query. Used when queries are stored as a linked list in `LQueries`. - */ - public next: LQuery|null, +class LQueries_ implements LQueries { + constructor(public queries: LQuery[] = []) {} - /** - * Destination to which the value should be added. - */ - public list: QueryList, + createEmbeddedView(tView: TView): LQueries|null { + const tQueries = tView.queries; + if (tQueries !== null) { + const noOfInheritedQueries = + tView.contentQueries !== null ? tView.contentQueries[0] : tQueries.length; + const viewLQueries: LQuery[] = new Array(noOfInheritedQueries); - /** - * A predicate which determines if a given element/directive should be included in the query - * results. - */ - public predicate: QueryPredicate, + // An embedded view has queries propagated from a declaration view at the beginning of the + // TQueries collection and up until a first content query declared in the embedded view. Only + // propagated LQueries are created at this point (LQuery corresponding to declared content + // queries will be instantiated from the content query instructions for each directive). + for (let i = 0; i < noOfInheritedQueries; i++) { + const tQuery = tQueries.getByIndex(i); + const parentLQuery = this.queries[tQuery.indexInDeclarationView]; + viewLQueries[i] = parentLQuery.clone(); + } - /** - * Values which have been located. - * This is what builds up the `QueryList._valuesTree`. - */ - public values: any[], + return new LQueries_(viewLQueries); + } - /** - * 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. - */ - public containerValues: any[]|null) {} + return null; + } + + insertView(tView: TView): void { this.dirtyQueriesWithMatches(tView); } + + detachView(tView: TView): void { this.dirtyQueriesWithMatches(tView); } + + private dirtyQueriesWithMatches(tView: TView) { + for (let i = 0; i < this.queries.length; i++) { + if (getTQuery(tView, i).matches !== null) { + this.queries[i].setDirty(); + } + } + } } -export class LQueries_ implements LQueries { +class TQueryMetadata_ implements TQueryMetadata { constructor( - public parent: LQueries_|null, private shallow: LQuery|null, - private deep: LQuery|null, public nodeIndex: number = -1) {} + public predicate: Type|string[], public descendants: boolean, public read: any, + public isStatic: boolean) {} +} - track(queryList: QueryList, predicate: Type|string[], descend?: boolean, read?: Type): - void { - if (descend) { - this.deep = createLQuery(this.deep, queryList, predicate, read != null ? read : null); +class TQueries_ implements TQueries { + constructor(private queries: TQuery[] = []) {} + + elementStart(tView: TView, tNode: TNode): void { + ngDevMode && assertFirstTemplatePass( + tView, 'Queries should collect results on the first template pass only'); + for (let query of this.queries) { + query.elementStart(tView, tNode); + } + } + elementEnd(tNode: TNode): void { + for (let query of this.queries) { + query.elementEnd(tNode); + } + } + embeddedTView(tNode: TNode): TQueries|null { + let queriesForTemplateRef: TQuery[]|null = null; + + for (let i = 0; i < this.length; i++) { + const childQueryIndex = queriesForTemplateRef !== null ? queriesForTemplateRef.length : 0; + const tqueryClone = this.getByIndex(i).embeddedTView(tNode, childQueryIndex); + + if (tqueryClone) { + tqueryClone.indexInDeclarationView = i; + if (queriesForTemplateRef !== null) { + queriesForTemplateRef.push(tqueryClone); + } else { + queriesForTemplateRef = [tqueryClone]; + } + } + } + + return queriesForTemplateRef !== null ? new TQueries_(queriesForTemplateRef) : null; + } + + template(tView: TView, tNode: TNode): void { + ngDevMode && assertFirstTemplatePass( + tView, 'Queries should collect results on the first template pass only'); + for (let query of this.queries) { + query.template(tView, tNode); + } + } + + getByIndex(index: number): TQuery { + ngDevMode && assertDataInRange(this.queries, index); + return this.queries[index]; + } + + get length(): number { return this.queries.length; } + + track(tquery: TQuery): void { this.queries.push(tquery); } +} + +class TQuery_ implements TQuery { + matches: number[]|null = null; + indexInDeclarationView = -1; + crossesNgTemplate = false; + + /** + * A node index on which a query was declared (-1 for view queries and ones inherited from the + * declaration template). We use this index (alongside with _appliesToNextNode flag) to know + * when to apply content queries to elements in a template. + */ + private _declarationNodeIndex: number; + + /** + * A flag indicating if a given query still applies to nodes it is crossing. We use this flag + * (alongside with _declarationNodeIndex) to know when to stop applying content queries to + * elements in a template. + */ + private _appliesToNextNode = true; + + constructor(public metadata: TQueryMetadata, nodeIndex: number = -1) { + this._declarationNodeIndex = nodeIndex; + } + + elementStart(tView: TView, tNode: TNode): void { + if (this.isApplyingToNode(tNode)) { + this.matchTNode(tView, tNode); + } + } + + elementEnd(tNode: TNode): void { + if (this._declarationNodeIndex === tNode.index) { + this._appliesToNextNode = false; + } + } + + template(tView: TView, tNode: TNode): void { this.elementStart(tView, tNode); } + + embeddedTView(tNode: TNode, childQueryIndex: number): TQuery|null { + if (this.isApplyingToNode(tNode)) { + this.crossesNgTemplate = true; + // A marker indicating a `` element (a placeholder for query results from + // embedded views created based on this ``). + this.addMatch(-tNode.index, childQueryIndex); + return new TQuery_(this.metadata); + } + return null; + } + + private isApplyingToNode(tNode: TNode): boolean { + if (this._appliesToNextNode && this.metadata.descendants === false) { + return this._declarationNodeIndex === (tNode.parent ? tNode.parent.index : -1); + } + return this._appliesToNextNode; + } + + private matchTNode(tView: TView, tNode: TNode): void { + if (Array.isArray(this.metadata.predicate)) { + this.matchTNodeByLocalNames(tView, tNode, this.metadata.predicate as string[]); } else { - this.shallow = createLQuery(this.shallow, queryList, predicate, read != null ? read : null); + const typePredicate = this.metadata.predicate as any; + if (typePredicate === ViewEngine_TemplateRef) { + this.matchTNodeByTemplateRef(tView, tNode); + } else { + this.matchTNodeByType(tView, tNode, typePredicate); + } } } - clone(tNode: TNode): LQueries { - return this.shallow !== null || isContentQueryHost(tNode) ? - new LQueries_(this, null, this.deep, tNode.index) : - this; - } - - 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| null): LQuery|null { - let result: LQuery|null = null; - - while (query) { - const containerValues: any[] = []; // prepare room for views - query.values.push(containerValues); - result = new LQuery(result, query.list, query.predicate, containerValues, null); - query = query.next; - } - - return result; -} - -function copyQueriesToView(query: LQuery| null): LQuery|null { - let result: LQuery|null = null; - - while (query) { - result = new LQuery(result, query.list, query.predicate, [], query.values); - query = query.next; - } - - return result; -} - -function insertView(index: number, query: LQuery| 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(); + private matchTNodeByLocalNames(tView: TView, tNode: TNode, localNames: string[]): void { + for (let i = 0; i < localNames.length; i++) { + this.matchTNodeWithReadOption(tView, tNode, getIdxOfMatchingSelector(tNode, localNames[i])); } - - query = query.next; } -} -function removeView(query: LQuery| null) { - while (query) { - ngDevMode && assertViewQueryhasPointerToDeclarationContainer(query); + private matchTNodeByTemplateRef(tView: TView, tNode: TNode): void { + this.matchTNodeWithReadOption(tView, tNode, tNode.type === TNodeType.Container ? -1 : null); + } - const containerValues = query.containerValues !; - const viewValuesIdx = containerValues.indexOf(query.values); - const removed = containerValues.splice(viewValuesIdx, 1); + private matchTNodeByType(tView: TView, tNode: TNode, typePredicate: Type): void { + this.matchTNodeWithReadOption( + tView, tNode, locateDirectiveOrProvider(tNode, tView, typePredicate, false, false)); + } - // 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(); + private matchTNodeWithReadOption(tView: TView, tNode: TNode, nodeMatchIdx: number|null): void { + if (nodeMatchIdx !== null) { + const read = this.metadata.read; + if (read !== null) { + if (read === ViewEngine_ElementRef || read === ViewContainerRef || + read === ViewEngine_TemplateRef && tNode.type === TNodeType.Container) { + this.addMatch(tNode.index, -2); + } else { + const directiveOrProviderIdx = + locateDirectiveOrProvider(tNode, tView, read, false, false); + if (directiveOrProviderIdx !== null) { + this.addMatch(tNode.index, directiveOrProviderIdx); + } + } + } else { + this.addMatch(tNode.index, nodeMatchIdx); + } } - - query = query.next; } -} -function assertViewQueryhasPointerToDeclarationContainer(query: LQuery) { - assertDefined(query.containerValues, 'View queries need to have a pointer to container values.'); + private addMatch(tNodeIdx: number, matchIdx: number) { + if (this.matches === null) { + this.matches = [tNodeIdx, matchIdx]; + } else { + this.matches.push(tNodeIdx, matchIdx); + } + } } /** @@ -214,7 +259,7 @@ function assertViewQueryhasPointerToDeclarationContainer(query: LQuery) { */ function getIdxOfMatchingSelector(tNode: TNode, selector: string): number|null { const localNames = tNode.localNames; - if (localNames) { + if (localNames !== null) { for (let i = 0; i < localNames.length; i += 2) { if (localNames[i] === selector) { return localNames[i + 1] as number; @@ -225,148 +270,127 @@ function getIdxOfMatchingSelector(tNode: TNode, selector: string): number|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 tView = currentView[TVIEW]; - const matchingIdx = locateDirectiveOrProvider(tNode, tView, read as Type, false, false); - if (matchingIdx !== null) { - return getNodeInjectable(tView.data, currentView, matchingIdx, tNode as TElementNode); - } - } - return null; -} - -function queryByTNodeType(tNode: TNode, currentView: LView): any { +function createResultByTNodeType(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) { + } else if (tNode.type === TNodeType.Container) { return createTemplateRef(ViewEngine_TemplateRef, ViewEngine_ElementRef, tNode, currentView); } return null; } -function queryByTemplateRef( - templateRefToken: ViewEngine_TemplateRef, tNode: TNode, currentView: LView, - read: any): any { - const templateRefResult = (templateRefToken as any)[NG_ELEMENT_ID](); - if (read) { - return templateRefResult ? queryByReadToken(read, tNode, currentView) : null; + +function createResultForNode(lView: LView, tNode: TNode, matchingIdx: number, read: any): any { + if (matchingIdx === -1) { + // if read token and / or strategy is not specified, detect it using appropriate tNode type + return createResultByTNodeType(tNode, lView); + } else if (matchingIdx === -2) { + // read a special token from a node injector + return createSpecialToken(lView, tNode, read); + } else { + // read a token + return getNodeInjectable(lView[TVIEW].data, lView, matchingIdx, tNode as TElementNode); } - return templateRefResult; } -function queryRead(tNode: TNode, currentView: LView, read: any, matchingIdx: number): any { - if (read) { - return queryByReadToken(read, tNode, currentView); +function createSpecialToken(lView: LView, tNode: TNode, read: any): any { + if (read === ViewEngine_ElementRef) { + return createElementRef(ViewEngine_ElementRef, tNode, lView); + } else if (read === ViewEngine_TemplateRef) { + return createTemplateRef(ViewEngine_TemplateRef, ViewEngine_ElementRef, tNode, lView); + } else if (read === ViewContainerRef) { + ngDevMode && assertNodeOfPossibleTypes( + tNode, TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer); + return createContainerRef( + ViewContainerRef, ViewEngine_ElementRef, + tNode as TElementNode | TContainerNode | TElementContainerNode, lView); + } else { + ngDevMode && + throwError( + `Special token to read should be one of ElementRef, TemplateRef or ViewContainerRef but got ${stringify(read)}.`); } - 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) + * A helper function that creates query results for a given view. This function is meant to do the + * processing once and only once for a given view instance (a set of results for a given view + * doesn't change). */ -function add( - query: LQuery| null, tNode: TElementNode | TContainerNode | TElementContainerNode, - insertBeforeContainer: boolean) { - const lView = getLView(); - const tView = lView[TVIEW]; - - 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, lView, predicate.read); +function materializeViewResults(lView: LView, tQuery: TQuery, queryIndex: number): (T | null)[] { + const lQuery = lView[QUERIES] !.queries ![queryIndex]; + if (lQuery.matches === null) { + const tViewData = lView[TVIEW].data; + const tQueryMatches = tQuery.matches !; + const result: T|null[] = new Array(tQueryMatches.length / 2); + for (let i = 0; i < tQueryMatches.length; i += 2) { + const matchedNodeIdx = tQueryMatches[i]; + if (matchedNodeIdx < 0) { + // we at the marker which might have results in views created based on this + // - those results will be in separate views though, so here we just leave + // null as a placeholder + result[i / 2] = null; } else { - const matchingIdx = locateDirectiveOrProvider(tNode, tView, type, false, false); - if (matchingIdx !== null) { - result = queryRead(tNode, lView, predicate.read, matchingIdx); + ngDevMode && assertDataInRange(tViewData, matchedNodeIdx); + const tNode = tViewData[matchedNodeIdx] as TNode; + result[i / 2] = + createResultForNode(lView, tNode, tQueryMatches[i + 1], tQuery.metadata.read); + } + } + lQuery.matches = result; + } + + return lQuery.matches; +} + +/** + * A helper function that collects (already materialized) query results from a tree of views, + * starting with a provided LView. + */ +function collectQueryResults( + lView: LView, tQuery: TQuery, queryIndex: number, result: T[]): T[] { + ngDevMode && + assertDefined( + tQuery.matches, 'Query results can only be collected for queries with existing matches.'); + + const tQueryMatches = tQuery.matches !; + const lViewResults = materializeViewResults(lView, tQuery, queryIndex); + + for (let i = 0; i < tQueryMatches.length; i += 2) { + const tNodeIdx = tQueryMatches[i]; + if (tNodeIdx > 0) { + const viewResult = lViewResults[i / 2]; + ngDevMode && assertDefined(viewResult, 'materialized query result should be defined'); + result.push(viewResult as T); + } else { + const childQueryIndex = tQueryMatches[i + 1]; + + const declarationLContainer = lView[-tNodeIdx] as LContainer; + ngDevMode && assertLContainer(declarationLContainer); + + // collect matches for views inserted in this container + for (let i = CONTAINER_HEADER_OFFSET; i < declarationLContainer.length; i++) { + const embeddedLView = declarationLContainer[i]; + if (embeddedLView[DECLARATION_LCONTAINER] === embeddedLView[PARENT]) { + const tquery = getTQuery(embeddedLView[TVIEW], childQueryIndex); + if (tquery.matches !== null) { + collectQueryResults(embeddedLView, tquery, childQueryIndex, result); + } } } - 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, lView, predicate.read, matchingIdx); - if (result !== null) { - addMatch(query, result, insertBeforeContainer); + + // collect matches for views created from this declaration container and inserted into + // different containers + if (declarationLContainer[MOVED_VIEWS] !== null) { + for (let embeddedLView of declarationLContainer[MOVED_VIEWS] !) { + const tquery = getTQuery(embeddedLView[TVIEW], childQueryIndex); + if (tquery.matches !== null) { + collectQueryResults(embeddedLView, tquery, childQueryIndex, result); } } } } - query = query.next; } -} - -function addMatch(query: LQuery, 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 unshifted before container results. Otherwise, results inside - // embedded views will appear before results on parent template nodes when flattened. - insertBeforeViewMatches ? query.values.unshift(matchingValue) : query.values.push(matchingValue); - query.list.setDirty(); -} - -function createPredicate(predicate: Type| string[], read: Type| null): QueryPredicate { - const isArray = Array.isArray(predicate); - return { - type: isArray ? null : predicate as Type, - selector: isArray ? predicate as string[] : null, - read: read - }; -} - -function createLQuery( - previous: LQuery| null, queryList: QueryList, predicate: Type| string[], - read: Type| null): LQuery { - return new LQuery( - previous, queryList, createPredicate(predicate, read), - (queryList as any as QueryList_)._valuesTree, null); -} - -type QueryList_ = QueryList& {_valuesTree: any[], _static: boolean}; - -/** - * Creates a QueryList and stores it in LView's collection of active queries (LQueries). - * - * @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 - */ -function createQueryListInLView( - // TODO: "read" should be an AbstractType (FW-486) - lView: LView, predicate: Type| string[], descend: boolean, read: any, isStatic: boolean, - nodeIndex: number): QueryList { - ngDevMode && assertPreviousIsParent(getIsParent()); - const queryList = new QueryList() as QueryList_; - const queries = lView[QUERIES] || (lView[QUERIES] = new LQueries_(null, null, null, nodeIndex)); - queryList._valuesTree = []; - queryList._static = isStatic; - queries.track(queryList, predicate, descend, read); - storeCleanupWithContext(lView, queryList, queryList.destroy); - return queryList; + return result; } /** @@ -379,15 +403,24 @@ function createQueryListInLView( * @codeGenApi */ export function ɵɵqueryRefresh(queryList: QueryList): boolean { - const queryListImpl = (queryList as any as QueryList_); - const creationMode = isCreationMode(); + const lView = getLView(); + const queryIndex = getCurrentQueryIndex(); - // if creation mode and static or update mode and not static - if (queryList.dirty && creationMode === queryListImpl._static) { - queryList.reset(queryListImpl._valuesTree || []); - queryList.notifyOnChanges(); + setCurrentQueryIndex(queryIndex + 1); + + const tQuery = getTQuery(lView[TVIEW], queryIndex); + if (queryList.dirty && (isCreationMode() === tQuery.metadata.isStatic)) { + if (tQuery.matches === null) { + queryList.reset([]); + } else { + const result = tQuery.crossesNgTemplate ? collectQueryResults(lView, tQuery, queryIndex, []) : + materializeViewResults(lView, tQuery, queryIndex); + queryList.reset(result); + queryList.notifyOnChanges(); + } return true; } + return false; } @@ -401,12 +434,8 @@ export function ɵɵqueryRefresh(queryList: QueryList): boolean { * @codeGenApi */ export function ɵɵstaticViewQuery( - // TODO(FW-486): "read" should be an AbstractType predicate: Type| string[], descend: boolean, read: any): void { - const lView = getLView(); - const tView = lView[TVIEW]; - viewQueryInternal(lView, tView, predicate, descend, read, true); - tView.staticViewQueries = true; + viewQueryInternal(getLView(), predicate, descend, read, true); } /** @@ -415,41 +444,33 @@ export function ɵɵstaticViewQuery( * @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 * * @codeGenApi */ -export function ɵɵviewQuery( - // TODO(FW-486): "read" should be an AbstractType - predicate: Type| string[], descend: boolean, read: any): QueryList { - const lView = getLView(); - const tView = lView[TVIEW]; - return viewQueryInternal(lView, tView, predicate, descend, read, false); +export function ɵɵviewQuery(predicate: Type| string[], descend: boolean, read: any): void { + viewQueryInternal(getLView(), predicate, descend, read, false); } function viewQueryInternal( - lView: LView, tView: TView, predicate: Type| string[], descend: boolean, read: any, - isStatic: boolean): QueryList { + lView: LView, predicate: Type| string[], descend: boolean, read: any, + isStatic: boolean): void { + const tView = lView[TVIEW]; if (tView.firstTemplatePass) { - tView.expandoStartIndex++; + createTQuery(tView, new TQueryMetadata_(predicate, descend, read, isStatic), -1); + if (isStatic) { + tView.staticViewQueries = true; + } } - const index = getCurrentQueryIndex(); - const queryList: QueryList = - createQueryListInLView(lView, predicate, descend, read, isStatic, -1); - store(index - HEADER_OFFSET, queryList); - setCurrentQueryIndex(index + 1); - return queryList; + createLQuery(lView); } /** - * Loads current View Query and moves the pointer/index to the next View Query in LView. + * Loads a QueryList corresponding to the current view query. * * @codeGenApi */ -export function ɵɵloadViewQuery(): T { - const index = getCurrentQueryIndex(); - setCurrentQueryIndex(index + 1); - return loadInternal(getLView(), index - HEADER_OFFSET); +export function ɵɵloadViewQuery(): QueryList { + return loadQueryInternal(getLView(), getCurrentQueryIndex()); } /** @@ -465,33 +486,9 @@ export function ɵɵloadViewQuery(): T { * @codeGenApi */ export function ɵɵcontentQuery( - directiveIndex: number, predicate: Type| string[], descend: boolean, - // TODO(FW-486): "read" should be an AbstractType - read: any): QueryList { - const lView = getLView(); - const tView = lView[TVIEW]; - const tNode = getPreviousOrParentTNode(); - return contentQueryInternal( - lView, tView, directiveIndex, predicate, descend, read, false, tNode.index); -} - -function contentQueryInternal( - lView: LView, tView: TView, directiveIndex: number, predicate: Type| string[], - descend: boolean, - // TODO(FW-486): "read" should be an AbstractType - read: any, isStatic: boolean, nodeIndex: number): QueryList { - const contentQuery: QueryList = - createQueryListInLView(lView, predicate, descend, read, isStatic, nodeIndex); - (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; + directiveIndex: number, predicate: Type| string[], descend: boolean, read: any): void { + contentQueryInternal( + getLView(), predicate, descend, read, false, getPreviousOrParentTNode(), directiveIndex); } /** @@ -507,29 +504,65 @@ function contentQueryInternal( * @codeGenApi */ export function ɵɵstaticContentQuery( - directiveIndex: number, predicate: Type| string[], descend: boolean, - // TODO(FW-486): "read" should be an AbstractType - read: any): void { - const lView = getLView(); + directiveIndex: number, predicate: Type| string[], descend: boolean, read: any): void { + contentQueryInternal( + getLView(), predicate, descend, read, true, getPreviousOrParentTNode(), directiveIndex); +} + +function contentQueryInternal( + lView: LView, predicate: Type| string[], descend: boolean, read: any, isStatic: boolean, + tNode: TNode, directiveIndex: number): void { const tView = lView[TVIEW]; - const tNode = getPreviousOrParentTNode(); - contentQueryInternal(lView, tView, directiveIndex, predicate, descend, read, true, tNode.index); - tView.staticContentQueries = true; + if (tView.firstTemplatePass) { + createTQuery(tView, new TQueryMetadata_(predicate, descend, read, isStatic), tNode.index); + saveContentQueryAndDirectiveIndex(tView, directiveIndex); + if (isStatic) { + tView.staticContentQueries = true; + } + } + + createLQuery(lView); } /** + * Loads a QueryList corresponding to the current content query. * * @codeGenApi */ export function ɵɵloadContentQuery(): QueryList { - 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]; + return loadQueryInternal(getLView(), getCurrentQueryIndex()); +} + +function loadQueryInternal(lView: LView, queryIndex: number): QueryList { + ngDevMode && + assertDefined(lView[QUERIES], 'LQueries should be defined when trying to load a query'); + ngDevMode && assertDataInRange(lView[QUERIES] !.queries, queryIndex); + return lView[QUERIES] !.queries[queryIndex].queryList; +} + +function createLQuery(lView: LView) { + const queryList = new QueryList(); + storeCleanupWithContext(lView, queryList, queryList.destroy); + + if (lView[QUERIES] === null) lView[QUERIES] = new LQueries_(); + lView[QUERIES] !.queries.push(new LQuery_(queryList)); +} + +function createTQuery(tView: TView, metadata: TQueryMetadata, nodeIndex: number): void { + if (tView.queries === null) tView.queries = new TQueries_(); + tView.queries.track(new TQuery_(metadata, nodeIndex)); +} + +function saveContentQueryAndDirectiveIndex(tView: TView, directiveIndex: number) { + const tViewContentQueries = tView.contentQueries || (tView.contentQueries = []); + const lastSavedDirectiveIndex = + tView.contentQueries.length ? tViewContentQueries[tViewContentQueries.length - 1] : -1; + if (directiveIndex !== lastSavedDirectiveIndex) { + tViewContentQueries.push(tView.queries !.length - 1, directiveIndex); + } +} + +function getTQuery(tView: TView, index: number): TQuery { + ngDevMode && assertDefined(tView.queries, 'TQueries must be defined to retrieve a TQuery'); + return tView.queries !.getByIndex(index); } diff --git a/packages/core/src/render3/view_engine_compatibility.ts b/packages/core/src/render3/view_engine_compatibility.ts index 3355d8741c..6d1837dc1d 100644 --- a/packages/core/src/render3/view_engine_compatibility.ts +++ b/packages/core/src/render3/view_engine_compatibility.ts @@ -17,13 +17,16 @@ import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, ViewRef as viewEngine_Vie import {Renderer2} from '../render/api'; import {assertDefined, assertGreaterThan, assertLessThan} from '../util/assert'; +import {assertLContainer} from './assert'; import {NodeInjector, getParentInjectorLocation} from './di'; import {addToViewTree, createEmbeddedViewAndNode, createLContainer, renderEmbeddedTemplate} from './instructions/shared'; import {ACTIVE_INDEX, CONTAINER_HEADER_OFFSET, LContainer, VIEW_REFS} from './interfaces/container'; import {TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeType, TViewNode} from './interfaces/node'; import {RComment, RElement, isProceduralRenderer} from './interfaces/renderer'; + import {isComponent, isLContainer, isLView, isRootView} from './interfaces/type_checks'; -import {CONTEXT, LView, QUERIES, RENDERER, TView, T_HOST} from './interfaces/view'; +import {CONTEXT, DECLARATION_LCONTAINER, LView, QUERIES, RENDERER, TView, T_HOST} from './interfaces/view'; + import {assertNodeOfPossibleTypes} from './node_assert'; import {addRemoveViewFromContainer, appendChild, detachView, getBeforeNodeForView, insertView, nativeInsertBefore, nativeNextSibling, nativeParentNode, removeView} from './node_manipulation'; import {getParentInjectorTNode} from './node_util'; @@ -66,9 +69,8 @@ export function createElementRef( } let R3TemplateRef: { - new ( - _declarationParentView: LView, elementRef: ViewEngine_ElementRef, _tView: TView, - _hostLContainer: LContainer, _injectorIndex: number): ViewEngine_TemplateRef + new (_declarationParentView: LView, hostTNode: TContainerNode, elementRef: ViewEngine_ElementRef): + ViewEngine_TemplateRef }; /** @@ -88,9 +90,9 @@ export function injectTemplateRef( * * @param TemplateRefToken The TemplateRef type * @param ElementRefToken The ElementRef type - * @param hostTNode The node that is requesting a TemplateRef + * @param hostTNode The node on which a TemplateRef is requested * @param hostView The view to which the node belongs - * @returns The TemplateRef instance to use + * @returns The TemplateRef instance or null if we can't create a TemplateRef on a given node type */ export function createTemplateRef( TemplateRefToken: typeof ViewEngine_TemplateRef, ElementRefToken: typeof ViewEngine_ElementRef, @@ -99,27 +101,27 @@ export function createTemplateRef( // TODO: Fix class name, should be TemplateRef, but there appears to be a rollup bug R3TemplateRef = class TemplateRef_ extends TemplateRefToken { constructor( - private _declarationParentView: LView, readonly elementRef: ViewEngine_ElementRef, - private _tView: TView, private _hostLContainer: LContainer, - private _injectorIndex: number) { + private _declarationView: LView, private _declarationTContainer: TContainerNode, + readonly elementRef: ViewEngine_ElementRef) { super(); } - createEmbeddedView(context: T, container?: LContainer, index?: number): - viewEngine_EmbeddedViewRef { - const currentQueries = this._declarationParentView[QUERIES]; - // Query container may be missing if this view was created in a directive - // constructor. Create it now to avoid losing results in embedded views. - if (currentQueries && this._hostLContainer[QUERIES] == null) { - this._hostLContainer[QUERIES] = currentQueries !.container(); - } + createEmbeddedView(context: T): viewEngine_EmbeddedViewRef { + const embeddedTView = this._declarationTContainer.tViews as TView; const lView = createEmbeddedViewAndNode( - this._tView, context, this._declarationParentView, this._hostLContainer[QUERIES], - this._injectorIndex); - if (container) { - insertView(lView, container, index !); + embeddedTView, context, this._declarationView, + this._declarationTContainer.injectorIndex); + + const declarationLContainer = this._declarationView[this._declarationTContainer.index]; + ngDevMode && assertLContainer(declarationLContainer); + lView[DECLARATION_LCONTAINER] = declarationLContainer; + + const declarationViewLQueries = this._declarationView[QUERIES]; + if (declarationViewLQueries !== null) { + lView[QUERIES] = declarationViewLQueries.createEmbeddedView(embeddedTView); } - renderEmbeddedTemplate(lView, this._tView, context); + + renderEmbeddedTemplate(lView, embeddedTView, context); const viewRef = new ViewRef(lView, context, -1); viewRef._tViewNode = lView[T_HOST] as TViewNode; return viewRef; @@ -128,11 +130,10 @@ export function createTemplateRef( } if (hostTNode.type === TNodeType.Container) { - const hostContainer: LContainer = hostView[hostTNode.index]; ngDevMode && assertDefined(hostTNode.tViews, 'TView must be allocated'); return new R3TemplateRef( - hostView, createElementRef(ElementRefToken, hostTNode, hostView), hostTNode.tViews as TView, - hostContainer, hostTNode.injectorIndex); + hostView, hostTNode as TContainerNode, + createElementRef(ElementRefToken, hostTNode, hostView)); } else { return null; } @@ -218,12 +219,8 @@ export function createContainerRef( createEmbeddedView(templateRef: ViewEngine_TemplateRef, context?: C, index?: number): viewEngine_EmbeddedViewRef { - this.allocateContainerIfNeeded(); - const adjustedIdx = this._adjustIndex(index); - const viewRef = (templateRef as any) - .createEmbeddedView(context || {}, this._lContainer, adjustedIdx); - (viewRef as ViewRef).attachToViewContainerRef(this); - this._lContainer[VIEW_REFS] !.splice(adjustedIdx, 0, viewRef); + const viewRef = templateRef.createEmbeddedView(context || {}); + this.insert(viewRef, index); return viewRef; } diff --git a/packages/core/test/acceptance/query_spec.ts b/packages/core/test/acceptance/query_spec.ts index 12a75203a1..f067c42f51 100644 --- a/packages/core/test/acceptance/query_spec.ts +++ b/packages/core/test/acceptance/query_spec.ts @@ -7,7 +7,7 @@ */ import {CommonModule} from '@angular/common'; -import {Component, ContentChild, ContentChildren, Directive, ElementRef, Input, QueryList, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core'; +import {Component, ContentChild, ContentChildren, Directive, ElementRef, Input, QueryList, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef, forwardRef} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -786,6 +786,56 @@ describe('query logic', () => { expect(queryList.last.nativeElement.id).toBe('c'); }); + it('should support a mix of content queries from the declaration and embedded view', () => { + @Directive({selector: '[query-for-lots-of-content]'}) + class QueryForLotsOfContent { + @ContentChildren('foo', {descendants: true}) foos1 !: QueryList; + @ContentChildren('foo', {descendants: true}) foos2 !: QueryList; + } + + @Directive({selector: '[query-for-content]'}) + class QueryForContent { + @ContentChildren('foo') foos !: QueryList; + } + + @Component({ + selector: 'test-comp', + template: ` +
+ +
+ +
+
+
+ ` + }) + class TestComponent { + items = [1, 2]; + } + + TestBed.configureTestingModule( + {declarations: [TestComponent, QueryForContent, QueryForLotsOfContent]}); + + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + const lotsOfContentEl = fixture.debugElement.query(By.directive(QueryForLotsOfContent)); + const lotsOfContentInstance = lotsOfContentEl.injector.get(QueryForLotsOfContent); + + const contentEl = fixture.debugElement.query(By.directive(QueryForContent)); + const contentInstance = contentEl.injector.get(QueryForContent); + + expect(lotsOfContentInstance.foos1.length).toBe(2); + expect(lotsOfContentInstance.foos2.length).toBe(2); + expect(contentInstance.foos.length).toBe(1); + + fixture.componentInstance.items = []; + fixture.detectChanges(); + expect(lotsOfContentInstance.foos1.length).toBe(0); + expect(lotsOfContentInstance.foos2.length).toBe(0); + }); + // https://stackblitz.com/edit/angular-rrmmuf?file=src/app/app.component.ts it('should report results when different instances of TemplateRef are inserted into one ViewContainerRefs', () => { @@ -864,12 +914,11 @@ describe('query logic', () => { // https://stackblitz.com/edit/angular-7vvo9j?file=src%2Fapp%2Fapp.component.ts // https://stackblitz.com/edit/angular-xzwp6n - onlyInIvy('FW-1318: QueryList entries are ordered differently in Ivy.') - .it('should report results when the same TemplateRef is inserted into different ViewContainerRefs', - () => { - @Component({ - selector: 'test-comp', - template: ` + it('should report results when the same TemplateRef is inserted into different ViewContainerRefs', + () => { + @Component({ + selector: 'test-comp', + template: `
@@ -877,44 +926,44 @@ describe('query logic', () => { `, - }) - class TestComponent { - @ViewChild('tpl', {static: false}) tpl !: TemplateRef; - @ViewChild('vi0', {static: false}) vi0 !: ViewContainerManipulatorDirective; - @ViewChild('vi1', {static: false}) vi1 !: ViewContainerManipulatorDirective; - @ViewChildren('foo') query !: QueryList; - } + }) + class TestComponent { + @ViewChild('tpl', {static: false}) tpl !: TemplateRef; + @ViewChild('vi0', {static: false}) vi0 !: ViewContainerManipulatorDirective; + @ViewChild('vi1', {static: false}) vi1 !: ViewContainerManipulatorDirective; + @ViewChildren('foo') query !: QueryList; + } - TestBed.configureTestingModule( - {declarations: [ViewContainerManipulatorDirective, TestComponent]}); - const fixture = TestBed.createComponent(TestComponent); - fixture.detectChanges(); + TestBed.configureTestingModule( + {declarations: [ViewContainerManipulatorDirective, TestComponent]}); + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); - const queryList = fixture.componentInstance.query; - const {tpl, vi0, vi1} = fixture.componentInstance; + const queryList = fixture.componentInstance.query; + const {tpl, vi0, vi1} = fixture.componentInstance; - expect(queryList.length).toBe(0); + expect(queryList.length).toBe(0); - vi0.insertTpl(tpl !, {idx: 0, container_idx: 0}, 0); - vi1.insertTpl(tpl !, {idx: 0, container_idx: 1}, 0); - fixture.detectChanges(); + vi0.insertTpl(tpl !, {idx: 0, container_idx: 0}, 0); + vi1.insertTpl(tpl !, {idx: 0, container_idx: 1}, 0); + fixture.detectChanges(); - expect(queryList.length).toBe(2); - let qListArr = queryList.toArray(); - expect(qListArr[0].nativeElement.getAttribute('id')).toBe('foo_1_0'); - expect(qListArr[1].nativeElement.getAttribute('id')).toBe('foo_0_0'); + expect(queryList.length).toBe(2); + let qListArr = queryList.toArray(); + expect(qListArr[0].nativeElement.getAttribute('id')).toBe('foo_0_0'); + expect(qListArr[1].nativeElement.getAttribute('id')).toBe('foo_1_0'); - vi0.remove(); - fixture.detectChanges(); + vi0.remove(); + fixture.detectChanges(); - expect(queryList.length).toBe(1); - qListArr = queryList.toArray(); - expect(qListArr[0].nativeElement.getAttribute('id')).toBe('foo_1_0'); + expect(queryList.length).toBe(1); + qListArr = queryList.toArray(); + expect(qListArr[0].nativeElement.getAttribute('id')).toBe('foo_1_0'); - vi1.remove(); - fixture.detectChanges(); - expect(queryList.length).toBe(0); - }); + vi1.remove(); + fixture.detectChanges(); + expect(queryList.length).toBe(0); + }); // https://stackblitz.com/edit/angular-wpd6gv?file=src%2Fapp%2Fapp.component.ts it('should report results from views inserted in a lifecycle hook', () => { @@ -953,6 +1002,96 @@ describe('query logic', () => { }); }); + describe('non-regression', () => { + + it('should query by provider super-type in an embedded view', () => { + + @Directive({selector: '[child]'}) + class Child { + } + + @Directive({selector: '[parent]', providers: [{provide: Child, useExisting: Parent}]}) + class Parent extends Child { + } + + @Component({ + selector: 'test-cmpt', + template: + `
` + }) + class TestCmpt { + @ViewChildren(Child) instances !: QueryList; + } + + TestBed.configureTestingModule({declarations: [TestCmpt, Parent, Child]}); + const fixture = TestBed.createComponent(TestCmpt); + fixture.detectChanges(); + + expect(fixture.componentInstance.instances.length).toBe(1); + }); + + it('should flatten multi-provider results', () => { + + class MyClass {} + + @Component({ + selector: 'with-multi-provider', + template: '', + providers: + [{provide: MyClass, useExisting: forwardRef(() => WithMultiProvider), multi: true}] + }) + class WithMultiProvider { + } + + @Component({selector: 'test-cmpt', template: ``}) + class TestCmpt { + @ViewChildren(MyClass) queryResults !: QueryList; + } + + TestBed.configureTestingModule({declarations: [TestCmpt, WithMultiProvider]}); + const fixture = TestBed.createComponent(TestCmpt); + fixture.detectChanges(); + + expect(fixture.componentInstance.queryResults.length).toBe(1); + expect(fixture.componentInstance.queryResults.first).toBeAnInstanceOf(WithMultiProvider); + }); + + it('should flatten multi-provider results when crossing ng-template', () => { + + class MyClass {} + + @Component({ + selector: 'with-multi-provider', + template: '', + providers: + [{provide: MyClass, useExisting: forwardRef(() => WithMultiProvider), multi: true}] + }) + class WithMultiProvider { + } + + @Component({ + selector: 'test-cmpt', + template: ` + + + ` + }) + class TestCmpt { + @ViewChildren(MyClass) queryResults !: QueryList; + } + + TestBed.configureTestingModule({declarations: [TestCmpt, WithMultiProvider]}); + const fixture = TestBed.createComponent(TestCmpt); + fixture.detectChanges(); + + expect(fixture.componentInstance.queryResults.length).toBe(2); + expect(fixture.componentInstance.queryResults.first).toBeAnInstanceOf(WithMultiProvider); + expect(fixture.componentInstance.queryResults.last).toBeAnInstanceOf(WithMultiProvider); + }); + + + }); + }); function initWithTemplate(compType: Type, template: string) { diff --git a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json index 955ef5aa0a..b216614fd0 100644 --- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json +++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json @@ -110,9 +110,6 @@ { "name": "PREORDER_HOOK_FLAGS" }, - { - "name": "QUERIES" - }, { "name": "RENDERER" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 37c9e928de..9fe5ee1f26 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -38,6 +38,9 @@ { "name": "ChangeDetectionStrategy" }, + { + "name": "DECLARATION_LCONTAINER" + }, { "name": "DECLARATION_VIEW" }, @@ -116,6 +119,9 @@ { "name": "MONKEY_PATCH_KEY_NAME" }, + { + "name": "MOVED_VIEWS" + }, { "name": "NATIVE" }, @@ -437,9 +443,6 @@ { "name": "addRemoveViewFromContainer" }, - { - "name": "addTContainerToQueries" - }, { "name": "addToViewTree" }, @@ -602,6 +605,9 @@ { "name": "destroyViewTree" }, + { + "name": "detachMovedView" + }, { "name": "detachView" }, @@ -1367,6 +1373,9 @@ { "name": "trackByIdentity" }, + { + "name": "trackMovedView" + }, { "name": "unwrapRNode" }, diff --git a/packages/core/test/render3/query_spec.ts b/packages/core/test/render3/query_spec.ts index 935bca6a50..2d819d1316 100644 --- a/packages/core/test/render3/query_spec.ts +++ b/packages/core/test/render3/query_spec.ts @@ -1344,378 +1344,6 @@ describe('query', () => { }); }); - describe('view boundaries', () => { - - describe('JS blocks', () => { - - it('should report results in embedded views', () => { - let firstEl; - /** - * % if (exp) { - *
- * % } - * class Cmpt { - * @ViewChildren('foo') query; - * } - */ - const Cmpt = createComponent( - 'cmpt', - function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - if (ctx.exp) { - let rf1 = ɵɵembeddedViewStart(1, 2, 0); - { - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'div', null, ['foo', '']); - firstEl = getNativeByIndex(0, getLView()); - } - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, - 1, 0, [], [], - function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵviewQuery(['foo'], true, null); - } - if (rf & RenderFlags.Update) { - let tmp: any; - ɵɵqueryRefresh(tmp = ɵɵloadViewQuery>()) && - (ctx.query = tmp as QueryList); - } - }); - - const cmptInstance = renderComponent(Cmpt); - const qList = (cmptInstance.query as any); - expect(qList.length).toBe(0); - - cmptInstance.exp = true; - detectChanges(cmptInstance); - expect(qList.length).toBe(1); - expect(qList.first.nativeElement).toBe(firstEl); - - cmptInstance.exp = false; - detectChanges(cmptInstance); - expect(qList.length).toBe(0); - }); - - it('should add results from embedded views in the correct order - views and elements mix', - () => { - let firstEl, lastEl, viewEl; - /** - * - * % if (exp) { - *
- * % } - * - * class Cmpt { - * @ViewChildren('foo') query; - * } - */ - const Cmpt = createComponent( - 'cmpt', - function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'span', null, ['foo', '']); - firstEl = getNativeByIndex(0, getLView()); - ɵɵcontainer(2); - ɵɵelement(3, 'span', null, ['foo', '']); - lastEl = getNativeByIndex(3, getLView()); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(2); - { - if (ctx.exp) { - let rf1 = ɵɵembeddedViewStart(1, 2, 0); - { - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'div', null, ['foo', '']); - viewEl = getNativeByIndex(0, getLView()); - } - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, - 5, 0, [], [], - function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵviewQuery(['foo'], true, null); - } - if (rf & RenderFlags.Update) { - let tmp: any; - ɵɵqueryRefresh(tmp = ɵɵloadViewQuery>()) && - (ctx.query = tmp as QueryList); - } - }); - - const cmptInstance = renderComponent(Cmpt); - const qList = (cmptInstance.query as any); - expect(qList.length).toBe(2); - expect(qList.first.nativeElement).toBe(firstEl); - expect(qList.last.nativeElement).toBe(lastEl); - - cmptInstance.exp = true; - detectChanges(cmptInstance); - expect(qList.length).toBe(3); - expect(qList.toArray()[0].nativeElement).toBe(firstEl); - expect(qList.toArray()[1].nativeElement).toBe(viewEl); - expect(qList.toArray()[2].nativeElement).toBe(lastEl); - - cmptInstance.exp = false; - detectChanges(cmptInstance); - expect(qList.length).toBe(2); - expect(qList.first.nativeElement).toBe(firstEl); - expect(qList.last.nativeElement).toBe(lastEl); - }); - - it('should add results from embedded views in the correct order - views side by side', () => { - let firstEl, lastEl; - /** - * % if (exp1) { - *
- * % } if (exp2) { - * - * % } - * class Cmpt { - * @ViewChildren('foo') query; - * } - */ - const Cmpt = createComponent( - 'cmpt', - function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - if (ctx.exp1) { - let rf0 = ɵɵembeddedViewStart(0, 2, 0); - { - if (rf0 & RenderFlags.Create) { - ɵɵelement(0, 'div', null, ['foo', '']); - firstEl = getNativeByIndex(0, getLView()); - } - } - ɵɵembeddedViewEnd(); - } - if (ctx.exp2) { - let rf1 = ɵɵembeddedViewStart(1, 2, 0); - { - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'span', null, ['foo', '']); - lastEl = getNativeByIndex(0, getLView()); - } - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, - 1, 0, [], [], - function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵviewQuery(['foo'], true, null); - } - if (rf & RenderFlags.Update) { - let tmp: any; - ɵɵqueryRefresh(tmp = ɵɵloadViewQuery>()) && - (ctx.query = tmp as QueryList); - } - }); - - const cmptInstance = renderComponent(Cmpt); - const qList = (cmptInstance.query as any); - expect(qList.length).toBe(0); - - cmptInstance.exp2 = true; - detectChanges(cmptInstance); - expect(qList.length).toBe(1); - expect(qList.last.nativeElement).toBe(lastEl); - - cmptInstance.exp1 = true; - detectChanges(cmptInstance); - expect(qList.length).toBe(2); - expect(qList.first.nativeElement).toBe(firstEl); - expect(qList.last.nativeElement).toBe(lastEl); - }); - - it('should add results from embedded views in the correct order - nested views', () => { - let firstEl, lastEl; - /** - * % if (exp1) { - *
- * % if (exp2) { - * - * } - * % } - * class Cmpt { - * @ViewChildren('foo') query; - * } - */ - const Cmpt = createComponent( - 'cmpt', - function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - if (ctx.exp1) { - let rf0 = ɵɵembeddedViewStart(0, 3, 0); - { - if (rf0 & RenderFlags.Create) { - ɵɵelement(0, 'div', null, ['foo', '']); - firstEl = getNativeByIndex(0, getLView()); - ɵɵcontainer(2); - } - if (rf0 & RenderFlags.Update) { - ɵɵcontainerRefreshStart(2); - { - if (ctx.exp2) { - let rf2 = ɵɵembeddedViewStart(0, 2, 0); - { - if (rf2) { - ɵɵelement(0, 'span', null, ['foo', '']); - lastEl = getNativeByIndex(0, getLView()); - } - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, - 1, 0, [], [], - function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵviewQuery(['foo'], true, null); - } - if (rf & RenderFlags.Update) { - let tmp: any; - ɵɵqueryRefresh(tmp = ɵɵloadViewQuery>()) && - (ctx.query = tmp as QueryList); - } - }); - - const cmptInstance = renderComponent(Cmpt); - const qList = (cmptInstance.query as any); - expect(qList.length).toBe(0); - - cmptInstance.exp1 = true; - detectChanges(cmptInstance); - expect(qList.length).toBe(1); - expect(qList.first.nativeElement).toBe(firstEl); - - cmptInstance.exp2 = true; - detectChanges(cmptInstance); - expect(qList.length).toBe(2); - expect(qList.first.nativeElement).toBe(firstEl); - expect(qList.last.nativeElement).toBe(lastEl); - }); - - /** - * What is tested here can't be achieved in the Renderer2 as all view queries are deep by - * default and can't be marked as shallow by a user. - */ - it('should support combination of deep and shallow queries', () => { - /** - * % if (exp) { "> - *
- *
- *
- * % } - * - * class Cmpt { - * @ViewChildren('foo') deep; - * @ViewChildren('foo') shallow; - * } - */ - const Cmpt = createComponent( - 'cmpt', - function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - ɵɵelement(1, 'span', null, ['foo', '']); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - if (ctx.exp) { - let rf0 = ɵɵembeddedViewStart(0, 4, 0); - { - if (rf0 & RenderFlags.Create) { - ɵɵelementStart(0, 'div', null, ['foo', '']); - { ɵɵelement(2, 'div', null, ['foo', '']); } - ɵɵelementEnd(); - } - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, - 3, 0, [], [], - function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵviewQuery(['foo'], true, null); - ɵɵviewQuery(['foo'], false, null); - } - if (rf & RenderFlags.Update) { - let tmp: any; - ɵɵqueryRefresh(tmp = ɵɵloadViewQuery>()) && - (ctx.deep = tmp as QueryList); - ɵɵqueryRefresh(tmp = ɵɵloadViewQuery>()) && - (ctx.shallow = tmp as QueryList); - } - }); - - const cmptInstance = renderComponent(Cmpt); - const deep = (cmptInstance.deep as any); - const shallow = (cmptInstance.shallow as any); - expect(deep.length).toBe(1); - expect(shallow.length).toBe(1); - - - cmptInstance.exp = true; - detectChanges(cmptInstance); - expect(deep.length).toBe(3); - - // embedded % if blocks should behave the same way as *ngIf, namely they - // should match shallow queries on the first level of elements underneath - // the embedded view boundary. - expect(shallow.length).toBe(2); - - cmptInstance.exp = false; - detectChanges(cmptInstance); - expect(deep.length).toBe(1); - expect(shallow.length).toBe(1); - }); - - }); - - }); - describe('queryList', () => { it('should be destroyed when the containing view is destroyed', () => { let queryInstance: QueryList; diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index 70fc2e1784..d3813ae88f 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -752,7 +752,7 @@ export declare function ɵɵcontainerRefreshEnd(): void; export declare function ɵɵcontainerRefreshStart(index: number): void; -export declare function ɵɵcontentQuery(directiveIndex: number, predicate: Type | string[], descend: boolean, read: any): QueryList; +export declare function ɵɵcontentQuery(directiveIndex: number, predicate: Type | string[], descend: boolean, read: any): void; export declare const ɵɵdefaultStyleSanitizer: StyleSanitizeFn; @@ -926,7 +926,7 @@ export declare function ɵɵload(index: number): T; export declare function ɵɵloadContentQuery(): QueryList; -export declare function ɵɵloadViewQuery(): T; +export declare function ɵɵloadViewQuery(): QueryList; export declare function ɵɵnamespaceHTML(): void; @@ -1115,7 +1115,7 @@ export declare function ɵɵtextInterpolateV(values: any[]): TsickleIssue1009; export declare function ɵɵupdateSyntheticHostBinding(propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null): TsickleIssue1009; -export declare function ɵɵviewQuery(predicate: Type | string[], descend: boolean, read: any): QueryList; +export declare function ɵɵviewQuery(predicate: Type | string[], descend: boolean, read: any): void; export declare const PACKAGE_ROOT_URL: InjectionToken;