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` | ✅ | ✅ | ❌ |
| `@ContentChild` | ✅ | ✅ | ✅ |
| `@ViewChildren` | ✅ | ✅ | ❌ |
| `@ViewChild` | ✅ | ✅ | |
| `@ViewChild` | ✅ | ✅ | |

View File

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

View File

@ -8,7 +8,6 @@
import {SimpleChange} from '../change_detection/change_detection_util';
import {ChangeDetectionStrategy} from '../change_detection/constants';
import {PipeTransform} from '../change_detection/pipe_transform';
import {Provider} from '../core';
import {OnChanges, SimpleChanges} from '../metadata/lifecycle_hooks';
import {NgModuleDef, NgModuleDefInternal} from '../metadata/ng_module';
@ -17,7 +16,7 @@ import {Type} from '../type';
import {resolveRendererType2} from '../view/util';
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';
@ -126,6 +125,16 @@ export function defineComponent<T>(componentDefinition: {
*/
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.
*
@ -193,7 +202,8 @@ export function defineComponent<T>(componentDefinition: {
pipeDefs: pipeTypes ?
() => (typeof pipeTypes === 'function' ? pipeTypes() : pipeTypes).map(extractPipeDef) :
null,
selectors: componentDefinition.selectors
selectors: componentDefinition.selectors,
viewQuery: componentDefinition.viewQuery || null,
};
const feature = componentDefinition.features;
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 {appendChild, insertView, appendProjectedNode, removeView, canInsertNativeNode, createTextNode, getNextLNode, getChildLNode, getParentLNode, getLViewChild} from './node_manipulation';
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 {isDifferent, stringify} from './util';
import {ViewRef} from './view_ref';
@ -446,7 +446,7 @@ export function renderTemplate<T>(
if (host == null) {
resetApplicationState();
rendererFactory = providedRendererFactory;
const tView = getOrCreateTView(template, directives || null, pipes || null);
const tView = getOrCreateTView(template, directives || null, pipes || null, null);
host = createLNode(
-1, TNodeType.Element, hostNode, null, null,
createLViewData(
@ -809,7 +809,7 @@ function saveResolvedLocalsInData(): void {
*/
function getOrCreateTView(
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
// 1. It is a megamorphic call on each invocation.
// 2. For nested embedded views (ngFor inside ngFor) the template instance is per
@ -818,7 +818,7 @@ function getOrCreateTView(
// and not on embedded templates.
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(
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++;
return {
id: viewIndex,
template: template,
viewQuery: viewQuery,
node: null !,
data: HEADER_FILLER.slice(), // Fill in to match HEADER_OFFSET in LViewData
childIndex: -1, // Children set in addToViewTree(), if any
@ -937,8 +939,8 @@ export function hostElement(
const node = createLNode(
0, TNodeType.Element, rNode, null, null,
createLViewData(
renderer, getOrCreateTView(def.template, def.directiveDefs, def.pipeDefs), null,
def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways, sanitizer));
renderer, getOrCreateTView(def.template, def.directiveDefs, def.pipeDefs, def.viewQuery),
null, def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways, sanitizer));
if (firstTemplatePass) {
node.tNode.flags = TNodeFlags.isComponent;
@ -1418,7 +1420,7 @@ export function directiveCreate<T>(
function addComponentLogic<T>(
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
// accessed through their containers because they may be removed / re-added later.
@ -1608,8 +1610,9 @@ export function container(
appendChild(getParentLNode(node), comment, viewData);
if (firstTemplatePass) {
node.tNode.tViews =
template ? createTView(-1, template, tView.directiveRegistry, tView.pipeRegistry) : [];
node.tNode.tViews = template ?
createTView(-1, template, tView.directiveRegistry, tView.pipeRegistry, null) :
[];
}
// 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');
if (viewIndex >= containerTViews.length || containerTViews[viewIndex] == null) {
containerTViews[viewIndex] =
createTView(viewIndex, null, tView.directiveRegistry, tView.pipeRegistry);
createTView(viewIndex, null, tView.directiveRegistry, tView.pipeRegistry, null);
}
return containerTViews[viewIndex];
}
@ -2198,17 +2201,34 @@ export function checkNoChanges<T>(component: T): void {
export function detectChangesInternal<T>(
hostView: LViewData, hostNode: LElementNode, component: T) {
const oldView = enterView(hostView, hostNode);
const template = hostView[TVIEW].template !;
const hostTView = hostView[TVIEW];
const template = hostTView.template !;
const viewQuery = hostTView.viewQuery;
try {
namespaceHTML();
createViewQuery(viewQuery, hostView[FLAGS], component);
template(getRenderFlags(hostView), component);
refreshView();
updateViewQuery(viewQuery, component);
} finally {
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).

View File

@ -18,6 +18,11 @@ export type ComponentTemplate<T> = {
(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)
* 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> {
/**
* The View template of the component.
*
* NOTE: only used with component directives.
*/
readonly template: ComponentTemplate<T>;
/**
* Query-related instructions for a component.
*/
readonly viewQuery: ComponentQuery<T>|null;
/**
* Renderer type data of the component.
*
* NOTE: only used with component directives.
*/
readonly rendererType: RendererType2|null;

View File

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

View File

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

View File

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

View File

@ -54,13 +54,17 @@ describe('queries', () => {
factory: function ViewQueryComponent_Factory() { return new ViewQueryComponent(); },
template: function ViewQueryComponent_Template(
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) {
$r3$.ɵQ(0, SomeDirective, false);
$r3$.ɵQ(1, SomeDirective, false);
$r3$.ɵEe(2, 'div', $e1_attrs$);
}
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>>(1)) &&
(ctx.someDirList = $tmp$ as QueryList<any>);

View File

@ -1403,8 +1403,8 @@ describe('di', () => {
describe('getOrCreateNodeInjector', () => {
it('should handle initial undefined state', () => {
const contentView =
createLViewData(null !, createTView(-1, null, null, null), null, LViewFlags.CheckAlways);
const contentView = createLViewData(
null !, createTView(-1, null, null, null, null), null, LViewFlags.CheckAlways);
const oldView = enterView(contentView, null !);
try {
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(
name: string, template: ComponentTemplate<any>, directives: DirectiveTypesOrFactory = [],
pipes: PipeTypesOrFactory = []): ComponentType<any> {
pipes: PipeTypesOrFactory = [],
viewQuery: ComponentTemplate<any>| null = null): ComponentType<any> {
return class Component {
value: any;
static ngComponentDef = defineComponent({
@ -236,6 +237,7 @@ export function createComponent(
selectors: [[name]],
factory: () => new Component,
template: template,
viewQuery: viewQuery,
features: [PublicFeature],
directives: directives,
pipes: pipes