fix(ivy): support queries for views inserted in lifecycle hooks (#24587)

Closes #23707

PR Close #24587
This commit is contained in:
Pawel Kozlowski 2018-06-19 17:58:42 +02:00 committed by Miško Hevery
parent 1e6a226703
commit 3e1a3b2e32
13 changed files with 1243 additions and 862 deletions

View File

@ -192,7 +192,7 @@ The goal is for the `@Component` (and friends) to be the compiler of template. S
| `@ContentChildren` | ✅ | ✅ | ❌ | | `@ContentChildren` | ✅ | ✅ | ❌ |
| `@ContentChild` | ✅ | ✅ | ✅ | | `@ContentChild` | ✅ | ✅ | ✅ |
| `@ViewChildren` | ✅ | ✅ | ❌ | | `@ViewChildren` | ✅ | ✅ | ❌ |
| `@ViewChild` | ✅ | ✅ | | | `@ViewChild` | ✅ | ✅ | |

View File

@ -107,7 +107,7 @@ export function renderComponent<T>(
const rootView: LViewData = createLViewData( const rootView: LViewData = createLViewData(
rendererFactory.createRenderer(hostNode, componentDef.rendererType), rendererFactory.createRenderer(hostNode, componentDef.rendererType),
createTView(-1, null, null, null), rootContext, createTView(-1, null, null, null, null), rootContext,
componentDef.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways); componentDef.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways);
rootView[INJECTOR] = opts.injector || null; rootView[INJECTOR] = opts.injector || null;

View File

@ -96,7 +96,7 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
// Create the root view. Uses empty TView and ContentTemplate. // Create the root view. Uses empty TView and ContentTemplate.
const rootView: LViewData = createLViewData( const rootView: LViewData = createLViewData(
rendererFactory.createRenderer(hostNode, this.componentDef.rendererType), rendererFactory.createRenderer(hostNode, this.componentDef.rendererType),
createTView(-1, null, null, null), null, createTView(-1, null, null, null, null), null,
this.componentDef.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways); this.componentDef.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways);
rootView[INJECTOR] = ngModule && ngModule.injector || null; rootView[INJECTOR] = ngModule && ngModule.injector || null;

View File

@ -8,7 +8,6 @@
import {SimpleChange} from '../change_detection/change_detection_util'; import {SimpleChange} from '../change_detection/change_detection_util';
import {ChangeDetectionStrategy} from '../change_detection/constants'; import {ChangeDetectionStrategy} from '../change_detection/constants';
import {PipeTransform} from '../change_detection/pipe_transform';
import {Provider} from '../core'; import {Provider} from '../core';
import {OnChanges, SimpleChanges} from '../metadata/lifecycle_hooks'; import {OnChanges, SimpleChanges} from '../metadata/lifecycle_hooks';
import {NgModuleDef, NgModuleDefInternal} from '../metadata/ng_module'; import {NgModuleDef, NgModuleDefInternal} from '../metadata/ng_module';
@ -17,7 +16,7 @@ import {Type} from '../type';
import {resolveRendererType2} from '../view/util'; import {resolveRendererType2} from '../view/util';
import {diPublic} from './di'; import {diPublic} from './di';
import {ComponentDefFeature, ComponentDefInternal, ComponentTemplate, ComponentType, DirectiveDefFeature, DirectiveDefInternal, DirectiveDefListOrFactory, DirectiveType, DirectiveTypesOrFactory, PipeDef, PipeType, PipeTypesOrFactory} from './interfaces/definition'; import {ComponentDefFeature, ComponentDefInternal, ComponentQuery, ComponentTemplate, ComponentType, DirectiveDefFeature, DirectiveDefInternal, DirectiveDefListOrFactory, DirectiveType, DirectiveTypesOrFactory, PipeDef, PipeType, PipeTypesOrFactory} from './interfaces/definition';
import {CssSelectorList, SelectorFlags} from './interfaces/projection'; import {CssSelectorList, SelectorFlags} from './interfaces/projection';
@ -126,6 +125,16 @@ export function defineComponent<T>(componentDefinition: {
*/ */
template: ComponentTemplate<T>; template: ComponentTemplate<T>;
/**
* Additional set of instructions specific to view query processing. This could be seen as a
* set of instruction to be inserted into the template function.
*
* Query-related instructions need to be pulled out to a specific function as a timing of
* execution is different as compared to all other instructions (after change detection hooks but
* before view hooks).
*/
viewQuery?: ComponentQuery<T>| null;
/** /**
* A list of optional features to apply. * A list of optional features to apply.
* *
@ -193,7 +202,8 @@ export function defineComponent<T>(componentDefinition: {
pipeDefs: pipeTypes ? pipeDefs: pipeTypes ?
() => (typeof pipeTypes === 'function' ? pipeTypes() : pipeTypes).map(extractPipeDef) : () => (typeof pipeTypes === 'function' ? pipeTypes() : pipeTypes).map(extractPipeDef) :
null, null,
selectors: componentDefinition.selectors selectors: componentDefinition.selectors,
viewQuery: componentDefinition.viewQuery || null,
}; };
const feature = componentDefinition.features; const feature = componentDefinition.features;
feature && feature.forEach((fn) => fn(def)); feature && feature.forEach((fn) => fn(def));

View File

@ -23,7 +23,7 @@ import {AttributeMarker, TAttributes, LContainerNode, LElementNode, LNode, TNode
import {assertNodeType} from './node_assert'; import {assertNodeType} from './node_assert';
import {appendChild, insertView, appendProjectedNode, removeView, canInsertNativeNode, createTextNode, getNextLNode, getChildLNode, getParentLNode, getLViewChild} from './node_manipulation'; import {appendChild, insertView, appendProjectedNode, removeView, canInsertNativeNode, createTextNode, getNextLNode, getChildLNode, getParentLNode, getLViewChild} from './node_manipulation';
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
import {ComponentDefInternal, ComponentTemplate, DirectiveDefInternal, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags} from './interfaces/definition'; import {ComponentDefInternal, ComponentTemplate, ComponentQuery, DirectiveDefInternal, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags} from './interfaces/definition';
import {RComment, RElement, RText, Renderer3, RendererFactory3, ProceduralRenderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces/renderer'; import {RComment, RElement, RText, Renderer3, RendererFactory3, ProceduralRenderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces/renderer';
import {isDifferent, stringify} from './util'; import {isDifferent, stringify} from './util';
import {ViewRef} from './view_ref'; import {ViewRef} from './view_ref';
@ -446,7 +446,7 @@ export function renderTemplate<T>(
if (host == null) { if (host == null) {
resetApplicationState(); resetApplicationState();
rendererFactory = providedRendererFactory; rendererFactory = providedRendererFactory;
const tView = getOrCreateTView(template, directives || null, pipes || null); const tView = getOrCreateTView(template, directives || null, pipes || null, null);
host = createLNode( host = createLNode(
-1, TNodeType.Element, hostNode, null, null, -1, TNodeType.Element, hostNode, null, null,
createLViewData( createLViewData(
@ -809,7 +809,7 @@ function saveResolvedLocalsInData(): void {
*/ */
function getOrCreateTView( function getOrCreateTView(
template: ComponentTemplate<any>, directives: DirectiveDefListOrFactory | null, template: ComponentTemplate<any>, directives: DirectiveDefListOrFactory | null,
pipes: PipeDefListOrFactory | null): TView { pipes: PipeDefListOrFactory | null, viewQuery: ComponentQuery<any>| null): TView {
// TODO(misko): reading `ngPrivateData` here is problematic for two reasons // TODO(misko): reading `ngPrivateData` here is problematic for two reasons
// 1. It is a megamorphic call on each invocation. // 1. It is a megamorphic call on each invocation.
// 2. For nested embedded views (ngFor inside ngFor) the template instance is per // 2. For nested embedded views (ngFor inside ngFor) the template instance is per
@ -818,7 +818,7 @@ function getOrCreateTView(
// and not on embedded templates. // and not on embedded templates.
return template.ngPrivateData || return template.ngPrivateData ||
(template.ngPrivateData = createTView(-1, template, directives, pipes) as never); (template.ngPrivateData = createTView(-1, template, directives, pipes, viewQuery) as never);
} }
/** /**
@ -830,11 +830,13 @@ function getOrCreateTView(
*/ */
export function createTView( export function createTView(
viewIndex: number, template: ComponentTemplate<any>| null, viewIndex: number, template: ComponentTemplate<any>| null,
directives: DirectiveDefListOrFactory | null, pipes: PipeDefListOrFactory | null): TView { directives: DirectiveDefListOrFactory | null, pipes: PipeDefListOrFactory | null,
viewQuery: ComponentQuery<any>| null): TView {
ngDevMode && ngDevMode.tView++; ngDevMode && ngDevMode.tView++;
return { return {
id: viewIndex, id: viewIndex,
template: template, template: template,
viewQuery: viewQuery,
node: null !, node: null !,
data: HEADER_FILLER.slice(), // Fill in to match HEADER_OFFSET in LViewData data: HEADER_FILLER.slice(), // Fill in to match HEADER_OFFSET in LViewData
childIndex: -1, // Children set in addToViewTree(), if any childIndex: -1, // Children set in addToViewTree(), if any
@ -937,8 +939,8 @@ export function hostElement(
const node = createLNode( const node = createLNode(
0, TNodeType.Element, rNode, null, null, 0, TNodeType.Element, rNode, null, null,
createLViewData( createLViewData(
renderer, getOrCreateTView(def.template, def.directiveDefs, def.pipeDefs), null, renderer, getOrCreateTView(def.template, def.directiveDefs, def.pipeDefs, def.viewQuery),
def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways, sanitizer)); null, def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways, sanitizer));
if (firstTemplatePass) { if (firstTemplatePass) {
node.tNode.flags = TNodeFlags.isComponent; node.tNode.flags = TNodeFlags.isComponent;
@ -1418,7 +1420,7 @@ export function directiveCreate<T>(
function addComponentLogic<T>( function addComponentLogic<T>(
directiveIndex: number, instance: T, def: ComponentDefInternal<T>): void { directiveIndex: number, instance: T, def: ComponentDefInternal<T>): void {
const tView = getOrCreateTView(def.template, def.directiveDefs, def.pipeDefs); const tView = getOrCreateTView(def.template, def.directiveDefs, def.pipeDefs, def.viewQuery);
// Only component views should be added to the view tree directly. Embedded views are // Only component views should be added to the view tree directly. Embedded views are
// accessed through their containers because they may be removed / re-added later. // accessed through their containers because they may be removed / re-added later.
@ -1608,8 +1610,9 @@ export function container(
appendChild(getParentLNode(node), comment, viewData); appendChild(getParentLNode(node), comment, viewData);
if (firstTemplatePass) { if (firstTemplatePass) {
node.tNode.tViews = node.tNode.tViews = template ?
template ? createTView(-1, template, tView.directiveRegistry, tView.pipeRegistry) : []; createTView(-1, template, tView.directiveRegistry, tView.pipeRegistry, null) :
[];
} }
// Containers are added to the current view tree instead of their embedded views // Containers are added to the current view tree instead of their embedded views
@ -1775,7 +1778,7 @@ function getOrCreateEmbeddedTView(viewIndex: number, parent: LContainerNode): TV
ngDevMode && assertEqual(Array.isArray(containerTViews), true, 'TViews should be in an array'); ngDevMode && assertEqual(Array.isArray(containerTViews), true, 'TViews should be in an array');
if (viewIndex >= containerTViews.length || containerTViews[viewIndex] == null) { if (viewIndex >= containerTViews.length || containerTViews[viewIndex] == null) {
containerTViews[viewIndex] = containerTViews[viewIndex] =
createTView(viewIndex, null, tView.directiveRegistry, tView.pipeRegistry); createTView(viewIndex, null, tView.directiveRegistry, tView.pipeRegistry, null);
} }
return containerTViews[viewIndex]; return containerTViews[viewIndex];
} }
@ -2198,17 +2201,34 @@ export function checkNoChanges<T>(component: T): void {
export function detectChangesInternal<T>( export function detectChangesInternal<T>(
hostView: LViewData, hostNode: LElementNode, component: T) { hostView: LViewData, hostNode: LElementNode, component: T) {
const oldView = enterView(hostView, hostNode); const oldView = enterView(hostView, hostNode);
const template = hostView[TVIEW].template !; const hostTView = hostView[TVIEW];
const template = hostTView.template !;
const viewQuery = hostTView.viewQuery;
try { try {
namespaceHTML(); namespaceHTML();
createViewQuery(viewQuery, hostView[FLAGS], component);
template(getRenderFlags(hostView), component); template(getRenderFlags(hostView), component);
refreshView(); refreshView();
updateViewQuery(viewQuery, component);
} finally { } finally {
leaveView(oldView); leaveView(oldView);
} }
} }
function createViewQuery<T>(
viewQuery: ComponentQuery<{}>| null, flags: LViewFlags, component: T): void {
if (viewQuery && (flags & LViewFlags.CreationMode)) {
viewQuery(RenderFlags.Create, component);
}
}
function updateViewQuery<T>(viewQuery: ComponentQuery<{}>| null, component: T): void {
if (viewQuery) {
viewQuery(RenderFlags.Update, component);
}
}
/** /**
* Mark the component as dirty (needing change detection). * Mark the component as dirty (needing change detection).

View File

@ -18,6 +18,11 @@ export type ComponentTemplate<T> = {
(rf: RenderFlags, ctx: T): void; ngPrivateData?: never; (rf: RenderFlags, ctx: T): void; ngPrivateData?: never;
}; };
/**
* Definition of what a query function should look like.
*/
export type ComponentQuery<T> = ComponentTemplate<T>;
/** /**
* Flags passed into template functions to determine which blocks (i.e. creation, update) * Flags passed into template functions to determine which blocks (i.e. creation, update)
* should be executed. * should be executed.
@ -153,15 +158,16 @@ export type ComponentDefInternal<T> = ComponentDef<T, string>;
export interface ComponentDef<T, Selector extends string> extends DirectiveDef<T, Selector> { export interface ComponentDef<T, Selector extends string> extends DirectiveDef<T, Selector> {
/** /**
* The View template of the component. * The View template of the component.
*
* NOTE: only used with component directives.
*/ */
readonly template: ComponentTemplate<T>; readonly template: ComponentTemplate<T>;
/**
* Query-related instructions for a component.
*/
readonly viewQuery: ComponentQuery<T>|null;
/** /**
* Renderer type data of the component. * Renderer type data of the component.
*
* NOTE: only used with component directives.
*/ */
readonly rendererType: RendererType2|null; readonly rendererType: RendererType2|null;

View File

@ -10,7 +10,7 @@ import {Injector} from '../../di/injector';
import {Sanitizer} from '../../sanitization/security'; import {Sanitizer} from '../../sanitization/security';
import {LContainer} from './container'; import {LContainer} from './container';
import {ComponentTemplate, DirectiveDefInternal, DirectiveDefList, PipeDef, PipeDefList} from './definition'; import {ComponentQuery, ComponentTemplate, DirectiveDefInternal, DirectiveDefList, PipeDef, PipeDefList} from './definition';
import {LElementNode, LViewNode, TNode} from './node'; import {LElementNode, LViewNode, TNode} from './node';
import {LQueries} from './query'; import {LQueries} from './query';
import {Renderer3} from './renderer'; import {Renderer3} from './renderer';
@ -200,6 +200,11 @@ export interface TView {
*/ */
template: ComponentTemplate<{}>|null; template: ComponentTemplate<{}>|null;
/**
* A function containing query-related instructions.
*/
viewQuery: ComponentQuery<{}>|null;
/** /**
* Pointer to the `TNode` that represents the root of the view. * Pointer to the `TNode` that represents the root of the view.
* *

View File

@ -119,6 +119,9 @@
{ {
"name": "createTextNode" "name": "createTextNode"
}, },
{
"name": "createViewQuery"
},
{ {
"name": "defineComponent" "name": "defineComponent"
}, },
@ -221,6 +224,9 @@
{ {
"name": "text" "name": "text"
}, },
{
"name": "updateViewQuery"
},
{ {
"name": "viewAttached" "name": "viewAttached"
} }

View File

@ -266,6 +266,9 @@
{ {
"name": "createTextNode" "name": "createTextNode"
}, },
{
"name": "createViewQuery"
},
{ {
"name": "defineComponent" "name": "defineComponent"
}, },
@ -548,6 +551,9 @@
{ {
"name": "tickRootContext" "name": "tickRootContext"
}, },
{
"name": "updateViewQuery"
},
{ {
"name": "viewAttached" "name": "viewAttached"
}, },

View File

@ -54,13 +54,17 @@ describe('queries', () => {
factory: function ViewQueryComponent_Factory() { return new ViewQueryComponent(); }, factory: function ViewQueryComponent_Factory() { return new ViewQueryComponent(); },
template: function ViewQueryComponent_Template( template: function ViewQueryComponent_Template(
rf: $RenderFlags$, ctx: $ViewQueryComponent$) { rf: $RenderFlags$, ctx: $ViewQueryComponent$) {
let $tmp$: any; if (rf & 1) {
$r3$.ɵEe(2, 'div', $e1_attrs$);
}
},
viewQuery: function ViewQueryComponent_Query(rf: $RenderFlags$, ctx: $ViewQueryComponent$) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵQ(0, SomeDirective, false); $r3$.ɵQ(0, SomeDirective, false);
$r3$.ɵQ(1, SomeDirective, false); $r3$.ɵQ(1, SomeDirective, false);
$r3$.ɵEe(2, 'div', $e1_attrs$);
} }
if (rf & 2) { if (rf & 2) {
let $tmp$: any;
$r3$.ɵqR($tmp$ = $r3$.ɵld<QueryList<any>>(0)) && (ctx.someDir = $tmp$.first); $r3$.ɵqR($tmp$ = $r3$.ɵld<QueryList<any>>(0)) && (ctx.someDir = $tmp$.first);
$r3$.ɵqR($tmp$ = $r3$.ɵld<QueryList<any>>(1)) && $r3$.ɵqR($tmp$ = $r3$.ɵld<QueryList<any>>(1)) &&
(ctx.someDirList = $tmp$ as QueryList<any>); (ctx.someDirList = $tmp$ as QueryList<any>);

View File

@ -1403,8 +1403,8 @@ describe('di', () => {
describe('getOrCreateNodeInjector', () => { describe('getOrCreateNodeInjector', () => {
it('should handle initial undefined state', () => { it('should handle initial undefined state', () => {
const contentView = const contentView = createLViewData(
createLViewData(null !, createTView(-1, null, null, null), null, LViewFlags.CheckAlways); null !, createTView(-1, null, null, null, null), null, LViewFlags.CheckAlways);
const oldView = enterView(contentView, null !); const oldView = enterView(contentView, null !);
try { try {
const parent = createLNode(0, TNodeType.Element, null, null, null, null); const parent = createLNode(0, TNodeType.Element, null, null, null, null);

File diff suppressed because it is too large Load Diff

View File

@ -228,7 +228,8 @@ export function toHtml<T>(componentOrElement: T | RElement): string {
export function createComponent( export function createComponent(
name: string, template: ComponentTemplate<any>, directives: DirectiveTypesOrFactory = [], name: string, template: ComponentTemplate<any>, directives: DirectiveTypesOrFactory = [],
pipes: PipeTypesOrFactory = []): ComponentType<any> { pipes: PipeTypesOrFactory = [],
viewQuery: ComponentTemplate<any>| null = null): ComponentType<any> {
return class Component { return class Component {
value: any; value: any;
static ngComponentDef = defineComponent({ static ngComponentDef = defineComponent({
@ -236,6 +237,7 @@ export function createComponent(
selectors: [[name]], selectors: [[name]],
factory: () => new Component, factory: () => new Component,
template: template, template: template,
viewQuery: viewQuery,
features: [PublicFeature], features: [PublicFeature],
directives: directives, directives: directives,
pipes: pipes pipes: pipes