fix(ivy): dynamically created components should run init hooks (#26864)

PR Close #26864
This commit is contained in:
Kara Erickson 2018-10-30 22:10:23 -07:00
parent 911bfef04c
commit a2929dfd57
10 changed files with 212 additions and 85 deletions

View File

@ -17,7 +17,7 @@ import {getComponentViewByInstance} from './context_discovery';
import {getComponentDef} from './definition';
import {diPublicInInjector, getOrCreateNodeInjectorForNode} from './di';
import {queueInitHooks, queueLifecycleHooks} from './hooks';
import {CLEAN_PROMISE, createLViewData, createNodeAtIndex, createTView, detectChangesInternal, executeInitAndContentHooks, getOrCreateTView, initNodeFlags, instantiateRootComponent, locateHostElement, prefillHostVars, setHostBindings} from './instructions';
import {CLEAN_PROMISE, createLViewData, createNodeAtIndex, createTView, getOrCreateTView, initNodeFlags, instantiateRootComponent, locateHostElement, prefillHostVars, queueComponentIndexForCheck, refreshDescendantViews} from './instructions';
import {ComponentDef, ComponentType} from './interfaces/definition';
import {TElementNode, TNodeFlags, TNodeType} from './interfaces/node';
import {PlayerHandler} from './interfaces/player';
@ -134,8 +134,7 @@ export function renderComponent<T>(
component = createRootComponent(
hostRNode, componentView, componentDef, rootView, rootContext, opts.hostFeatures || null);
executeInitAndContentHooks(rootView);
detectChangesInternal(componentView, component);
refreshDescendantViews(rootView, null);
} finally {
leaveView(oldView);
if (rendererFactory.end) rendererFactory.end();
@ -171,6 +170,7 @@ export function createRootComponentView(
diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, rootView), rootView, def.type);
tNode.flags = TNodeFlags.isComponent;
initNodeFlags(tNode, rootView.length, 1);
queueComponentIndexForCheck(tNode);
}
// Store component view at node index, with node as the HOST
@ -196,7 +196,6 @@ export function createRootComponent<T>(
hostFeatures && hostFeatures.forEach((feature) => feature(component, componentDef));
if (tView.firstTemplatePass) prefillHostVars(tView, rootView, componentDef.hostVars);
setHostBindings(tView, rootView);
return component;
}

View File

@ -20,12 +20,12 @@ import {Type} from '../type';
import {assertComponentType, assertDefined} from './assert';
import {LifecycleHooksFeature, createRootComponent, createRootComponentView, createRootContext} from './component';
import {getComponentDef} from './definition';
import {createLViewData, createNodeAtIndex, createTView, createViewNode, elementCreate, locateHostElement, renderEmbeddedTemplate} from './instructions';
import {createLViewData, createNodeAtIndex, createTView, createViewNode, elementCreate, locateHostElement, refreshDescendantViews} from './instructions';
import {ComponentDef, RenderFlags} from './interfaces/definition';
import {TElementNode, TNode, TNodeType, TViewNode} from './interfaces/node';
import {RElement, RendererFactory3, domRendererFactory3} from './interfaces/renderer';
import {FLAGS, HEADER_OFFSET, INJECTOR, LViewData, LViewFlags, RootContext, TVIEW} from './interfaces/view';
import {enterView} from './state';
import {enterView, leaveView} from './state';
import {getTNode} from './util';
import {createElementRef} from './view_engine_compatibility';
import {RootViewRef, ViewRef} from './view_ref';
@ -177,11 +177,9 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
hostRNode, componentView, this.componentDef, rootView, rootContext,
[LifecycleHooksFeature]);
// Execute the template in creation mode only, and then turn off the CreationMode flag
renderEmbeddedTemplate(componentView, componentView[TVIEW], component, RenderFlags.Create);
componentView[FLAGS] &= ~LViewFlags.CreationMode;
refreshDescendantViews(rootView, RenderFlags.Create);
} finally {
enterView(oldView, null);
leaveView(oldView, true);
if (rendererFactory.end) rendererFactory.end();
}

View File

@ -64,17 +64,22 @@ type SanitizerFn = (value: any) => string;
* bindings, refreshes child components.
* Note: view hooks are triggered later when leaving the view.
*/
function refreshDescendantViews(viewData: LViewData) {
export function refreshDescendantViews(viewData: LViewData, rf: RenderFlags | null) {
const tView = getTView();
const creationMode = getCreationMode();
const checkNoChangesMode = getCheckNoChangesMode();
setHostBindings(tView, viewData);
const parentFirstTemplatePass = getFirstTemplatePass();
// This needs to be set before children are processed to support recursive components
tView.firstTemplatePass = false;
setFirstTemplatePass(false);
// Dynamically created views must run first only in creation mode. If this is a
// creation-only pass, we should not call lifecycle hooks or evaluate bindings.
// This will be done in the update-only pass.
if (rf !== RenderFlags.Create) {
const creationMode = getCreationMode();
const checkNoChangesMode = getCheckNoChangesMode();
setHostBindings(tView, viewData);
if (!checkNoChangesMode) {
executeInitHooks(viewData, tView, creationMode);
}
@ -86,8 +91,9 @@ function refreshDescendantViews(viewData: LViewData) {
if (!checkNoChangesMode) {
executeHooks(viewData, tView.contentHooks, tView.contentCheckHooks, creationMode);
}
}
refreshChildComponents(tView.components, parentFirstTemplatePass);
refreshChildComponents(tView.components, parentFirstTemplatePass, rf);
}
@ -144,23 +150,14 @@ function refreshContentQueries(tView: TView): void {
/** Refreshes child components in the current view. */
function refreshChildComponents(
components: number[] | null, parentFirstTemplatePass: boolean): void {
components: number[] | null, parentFirstTemplatePass: boolean, rf: RenderFlags | null): void {
if (components != null) {
for (let i = 0; i < components.length; i++) {
componentRefresh(components[i], parentFirstTemplatePass);
componentRefresh(components[i], parentFirstTemplatePass, rf);
}
}
}
export function executeInitAndContentHooks(viewData: LViewData): void {
if (!getCheckNoChangesMode()) {
const tView = getTView();
const creationMode = getCreationMode();
executeInitHooks(viewData, tView, creationMode);
executeHooks(viewData, tView.contentHooks, tView.contentCheckHooks, creationMode);
}
}
export function createLViewData<T>(
renderer: Renderer3, tView: TView, context: T | null, flags: LViewFlags,
sanitizer?: Sanitizer | null): LViewData {
@ -304,7 +301,7 @@ export function renderTemplate<T>(
createLViewData(renderer, componentTView, context, LViewFlags.CheckAlways, sanitizer);
hostView[HOST_NODE] = createNodeAtIndex(0, TNodeType.Element, hostNode, null, null);
}
renderComponentOrTemplate(hostView, context, templateFn);
renderComponentOrTemplate(hostView, context, null, templateFn);
return hostView;
}
@ -369,7 +366,7 @@ export function renderEmbeddedTemplate<T>(
namespaceHTML();
tView.template !(rf, context);
if (rf & RenderFlags.Update) {
refreshDescendantViews(viewToRender);
refreshDescendantViews(viewToRender, null);
} else {
// This must be set to false immediately after the first creation run because in an
// ngFor loop, all the views will be created together before update mode runs and turns
@ -404,7 +401,8 @@ export function nextContext<T = any>(level: number = 1): T {
}
function renderComponentOrTemplate<T>(
hostView: LViewData, componentOrContext: T, templateFn?: ComponentTemplate<T>) {
hostView: LViewData, componentOrContext: T, rf: RenderFlags | null,
templateFn?: ComponentTemplate<T>) {
const rendererFactory = getRendererFactory();
const oldView = enterView(hostView, hostView[HOST_NODE]);
try {
@ -413,17 +411,9 @@ function renderComponentOrTemplate<T>(
}
if (templateFn) {
namespaceHTML();
templateFn(getRenderFlags(hostView), componentOrContext !);
refreshDescendantViews(hostView);
} else {
executeInitAndContentHooks(hostView);
// Element was stored at 0 in data and directive was stored at 0 in directives
// in renderComponent()
setHostBindings(getTView(), hostView);
refreshDynamicEmbeddedViews(hostView);
componentRefresh(HEADER_OFFSET, false);
templateFn(rf || getRenderFlags(hostView), componentOrContext !);
}
refreshDescendantViews(hostView, rf);
} finally {
if (rendererFactory.end) {
rendererFactory.end();
@ -1488,7 +1478,7 @@ function findDirectiveMatches(tView: TView, viewData: LViewData, tNode: TNode):
}
/** Stores index of component's host element so it will be queued for view refresh during CD. */
function queueComponentIndexForCheck(previousOrParentTNode: TNode): void {
export function queueComponentIndexForCheck(previousOrParentTNode: TNode): void {
ngDevMode &&
assertEqual(getFirstTemplatePass(), true, 'Should only be called in first template pass.');
const tView = getTView();
@ -1829,7 +1819,7 @@ export function containerRefreshEnd(): void {
* Goes over dynamic embedded views (ones created through ViewContainerRef APIs) and refreshes them
* by executing an associated template function.
*/
export function refreshDynamicEmbeddedViews(lViewData: LViewData) {
function refreshDynamicEmbeddedViews(lViewData: LViewData) {
for (let current = getLViewChild(lViewData); current !== null; current = current[NEXT]) {
// Note: current can be an LViewData or an LContainer instance, but here we are only interested
// in LContainer. We can tell it's an LContainer because its length is less than the LViewData
@ -1957,7 +1947,7 @@ function getOrCreateEmbeddedTView(
export function embeddedViewEnd(): void {
const viewData = getViewData();
const viewHost = viewData[HOST_NODE];
refreshDescendantViews(viewData);
refreshDescendantViews(viewData, null);
leaveView(viewData[PARENT] !);
setPreviousOrParentTNode(viewHost !);
setIsParent(false);
@ -1971,7 +1961,7 @@ export function embeddedViewEnd(): void {
* @param adjustedElementIndex Element index in LViewData[] (adjusted for HEADER_OFFSET)
*/
export function componentRefresh<T>(
adjustedElementIndex: number, parentFirstTemplatePass: boolean): void {
adjustedElementIndex: number, parentFirstTemplatePass: boolean, rf: RenderFlags | null): void {
ngDevMode && assertDataInRange(adjustedElementIndex);
const hostView = getComponentViewByIndex(adjustedElementIndex, getViewData());
ngDevMode && assertNodeType(getTView().data[adjustedElementIndex] as TNode, TNodeType.Element);
@ -1979,7 +1969,7 @@ export function componentRefresh<T>(
// Only attached CheckAlways components or attached, dirty OnPush components should be checked
if (viewAttached(hostView) && hostView[FLAGS] & (LViewFlags.CheckAlways | LViewFlags.Dirty)) {
parentFirstTemplatePass && syncViewWithBlueprint(hostView);
detectChangesInternal(hostView, hostView[CONTEXT]);
detectChangesInternal(hostView, hostView[CONTEXT], rf);
}
}
@ -2261,7 +2251,8 @@ export function tick<T>(component: T): void {
function tickRootContext(rootContext: RootContext) {
for (let i = 0; i < rootContext.components.length; i++) {
const rootComponent = rootContext.components[i];
renderComponentOrTemplate(readPatchedLViewData(rootComponent) !, rootComponent);
renderComponentOrTemplate(
readPatchedLViewData(rootComponent) !, rootComponent, RenderFlags.Update);
}
}
@ -2279,7 +2270,7 @@ function tickRootContext(rootContext: RootContext) {
* @param component The component which the change detection should be performed on.
*/
export function detectChanges<T>(component: T): void {
detectChangesInternal(getComponentViewByInstance(component) !, component);
detectChangesInternal(getComponentViewByInstance(component) !, component, null);
}
/**
@ -2326,7 +2317,7 @@ export function checkNoChangesInRootView(lViewData: LViewData): void {
}
/** Checks the view of the component provided. Does not gate on dirty checks or execute doCheck. */
export function detectChangesInternal<T>(hostView: LViewData, component: T) {
function detectChangesInternal<T>(hostView: LViewData, component: T, rf: RenderFlags | null) {
const hostTView = hostView[TVIEW];
const oldView = enterView(hostView, hostView[HOST_NODE]);
const templateFn = hostTView.template !;
@ -2334,24 +2325,27 @@ export function detectChangesInternal<T>(hostView: LViewData, component: T) {
try {
namespaceHTML();
createViewQuery(viewQuery, hostView[FLAGS], component);
templateFn(getRenderFlags(hostView), component);
refreshDescendantViews(hostView);
updateViewQuery(viewQuery, component);
createViewQuery(viewQuery, rf, hostView[FLAGS], component);
templateFn(rf || getRenderFlags(hostView), component);
refreshDescendantViews(hostView, rf);
updateViewQuery(viewQuery, hostView[FLAGS], component);
} finally {
leaveView(oldView);
leaveView(oldView, rf === RenderFlags.Create);
}
}
function createViewQuery<T>(
viewQuery: ComponentQuery<{}>| null, flags: LViewFlags, component: T): void {
if (viewQuery && (flags & LViewFlags.CreationMode)) {
viewQuery: ComponentQuery<{}>| null, renderFlags: RenderFlags | null, viewFlags: LViewFlags,
component: T): void {
if (viewQuery && (renderFlags === RenderFlags.Create ||
(renderFlags === null && (viewFlags & LViewFlags.CreationMode)))) {
viewQuery(RenderFlags.Create, component);
}
}
function updateViewQuery<T>(viewQuery: ComponentQuery<{}>| null, component: T): void {
if (viewQuery) {
function updateViewQuery<T>(
viewQuery: ComponentQuery<{}>| null, flags: LViewFlags, component: T): void {
if (viewQuery && flags & RenderFlags.Update) {
viewQuery(RenderFlags.Update, component);
}
}

View File

@ -500,9 +500,6 @@
{
"name": "executeHooks"
},
{
"name": "executeInitAndContentHooks"
},
{
"name": "executeInitHooks"
},

View File

@ -200,9 +200,6 @@
{
"name": "executeHooks"
},
{
"name": "executeInitAndContentHooks"
},
{
"name": "executeInitHooks"
},
@ -371,6 +368,9 @@
{
"name": "prefillHostVars"
},
{
"name": "queueComponentIndexForCheck"
},
{
"name": "queueHostBindingForCheck"
},

View File

@ -3194,9 +3194,6 @@
{
"name": "executeHooks"
},
{
"name": "executeInitAndContentHooks"
},
{
"name": "executeInitHooks"
},
@ -4145,6 +4142,9 @@
{
"name": "queryDef"
},
{
"name": "queueComponentIndexForCheck"
},
{
"name": "queueContentHooks"
},

View File

@ -554,9 +554,6 @@
{
"name": "executeHooks"
},
{
"name": "executeInitAndContentHooks"
},
{
"name": "executeInitHooks"
},

View File

@ -1505,9 +1505,6 @@
{
"name": "executeHooks"
},
{
"name": "executeInitAndContentHooks"
},
{
"name": "executeInitHooks"
},

View File

@ -6,10 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/
import {OnDestroy, SimpleChanges} from '../../src/core';
import {AttributeMarker, ComponentTemplate, LifecycleHooksFeature, NO_CHANGE, NgOnChangesFeature, defineComponent, defineDirective} from '../../src/render3/index';
import {ComponentFactoryResolver, OnDestroy, SimpleChanges, ViewContainerRef} from '../../src/core';
import {AttributeMarker, ComponentTemplate, LifecycleHooksFeature, NO_CHANGE, NgOnChangesFeature, defineComponent, defineDirective, injectComponentFactoryResolver} from '../../src/render3/index';
import {bind, container, containerRefreshEnd, containerRefreshStart, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, listener, markDirty, projection, projectionDef, store, template, text} from '../../src/render3/instructions';
import {bind, container, containerRefreshEnd, containerRefreshStart, directiveInject, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, listener, markDirty, projection, projectionDef, store, template, text} from '../../src/render3/instructions';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {NgIf} from './common_with_def';
@ -77,7 +77,7 @@ describe('lifecycles', () => {
{type: Directive, selectors: [['', 'dir', '']], factory: () => new Directive()});
}
const directives = [Comp, Parent, ProjectedComp, Directive];
const directives = [Comp, Parent, ProjectedComp, Directive, NgIf];
it('should call onInit method after inputs are set in creation mode (and not in update mode)',
() => {
@ -184,6 +184,72 @@ describe('lifecycles', () => {
expect(events).toEqual(['comp', 'comp']);
});
it('should call onInit every time a new view is created (ngIf)', () => {
function IfTemplate(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
element(0, 'comp');
}
}
/** <comp *ngIf="showing"></comp> */
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
template(0, IfTemplate, 1, 0, '', ['ngIf', '']);
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'ngIf', bind(ctx.showing));
}
}, 1, 0, directives);
const fixture = new ComponentFixture(App);
fixture.component.showing = true;
fixture.update();
expect(events).toEqual(['comp']);
fixture.component.showing = false;
fixture.update();
expect(events).toEqual(['comp']);
fixture.component.showing = true;
fixture.update();
expect(events).toEqual(['comp', 'comp']);
});
it('should call onInit for children of dynamically created components', () => {
let viewContainerComp !: ViewContainerComp;
class ViewContainerComp {
constructor(public vcr: ViewContainerRef, public cfr: ComponentFactoryResolver) {}
static ngComponentDef = defineComponent({
type: ViewContainerComp,
selectors: [['view-container-comp']],
factory: () => viewContainerComp = new ViewContainerComp(
directiveInject(ViewContainerRef as any), injectComponentFactoryResolver()),
consts: 0,
vars: 0,
template: (rf: RenderFlags, ctx: ViewContainerComp) => {}
});
}
const DynamicComp = createComponent('dynamic-comp', (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
element(0, 'comp');
}
}, 1, 0, [Comp]);
const fixture = new ComponentFixture(ViewContainerComp);
expect(events).toEqual([]);
viewContainerComp.vcr.createComponent(
viewContainerComp.cfr.resolveComponentFactory(DynamicComp));
fixture.update();
expect(events).toEqual(['comp']);
});
it('should call onInit in hosts before their content children', () => {
/**
* <comp>

View File

@ -6,15 +6,18 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Component, ComponentFactoryResolver, ElementRef, EmbeddedViewRef, NgModuleRef, Pipe, PipeTransform, RendererFactory2, TemplateRef, ViewContainerRef, createInjector, defineInjector, ɵAPP_ROOT as APP_ROOT, ɵNgModuleDef as NgModuleDef} from '../../src/core';
import {Component, ComponentFactoryResolver, ElementRef, EmbeddedViewRef, NgModuleRef, Pipe, PipeTransform, QueryList, RendererFactory2, TemplateRef, ViewContainerRef, createInjector, defineInjector, ɵAPP_ROOT as APP_ROOT, ɵNgModuleDef as NgModuleDef} from '../../src/core';
import {ViewEncapsulation} from '../../src/metadata';
import {AttributeMarker, NgOnChangesFeature, defineComponent, defineDirective, definePipe, injectComponentFactoryResolver, load} from '../../src/render3/index';
import {AttributeMarker, NO_CHANGE, NgOnChangesFeature, defineComponent, defineDirective, definePipe, injectComponentFactoryResolver, load, query, queryRefresh} from '../../src/render3/index';
import {bind, container, containerRefreshEnd, containerRefreshStart, directiveInject, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation3, nextContext, projection, projectionDef, reference, template, text, textBinding} from '../../src/render3/instructions';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {RElement} from '../../src/render3/interfaces/renderer';
import {templateRefExtractor} from '../../src/render3/view_engine_compatibility_prebound';
import {NgModuleFactory} from '../../src/render3/ng_module_ref';
import {pipe, pipeBind1} from '../../src/render3/pipe';
import {getViewData} from '../../src/render3/state';
import {getNativeByIndex} from '../../src/render3/util';
import {NgForOf} from '../../test/render3/common_with_def';
import {getRendererFactory2} from './imported_renderer2';
@ -1759,6 +1762,7 @@ describe('ViewContainerRef', () => {
const componentRef = directiveInstance !.vcref.createComponent(
directiveInstance !.cfr.resolveComponentFactory(HostBindingCmpt));
fixture.update();
expect(fixture.html).toBe('<host-bindings id="attribute" title="initial"></host-bindings>');
@ -1856,5 +1860,80 @@ describe('ViewContainerRef', () => {
'<div host="mark"></div><dynamic-cmpt-with-bindings>check count: 2</dynamic-cmpt-with-bindings>');
});
it('should create deep DOM tree immediately for dynamically created components', () => {
let name = 'text';
const Child = createComponent('child', (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
elementStart(0, 'div');
{ text(1); }
elementEnd();
}
if (rf & RenderFlags.Update) {
textBinding(1, bind(name));
}
}, 2, 1);
const DynamicCompWithChildren =
createComponent('dynamic-cmpt-with-children', (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
element(0, 'child');
}
}, 1, 0, [Child]);
const fixture = new ComponentFixture(AppCmpt);
expect(fixture.outerHtml).toBe('<div host="mark"></div>');
fixture.component.insert(DynamicCompWithChildren);
expect(fixture.outerHtml)
.toBe(
'<div host="mark"></div><dynamic-cmpt-with-children><child><div></div></child></dynamic-cmpt-with-children>');
fixture.update();
expect(fixture.outerHtml)
.toBe(
'<div host="mark"></div><dynamic-cmpt-with-children><child><div>text</div></child></dynamic-cmpt-with-children>');
});
it('should support view queries for dynamically created components', () => {
let dynamicComp !: DynamicCompWithViewQueries;
let fooEl !: RElement;
class DynamicCompWithViewQueries {
// @ViewChildren('foo')
foo !: QueryList<any>;
static ngComponentDef = defineComponent({
type: DynamicCompWithViewQueries,
selectors: [['dynamic-cmpt-with-view-queries']],
factory: () => dynamicComp = new DynamicCompWithViewQueries(),
consts: 2,
vars: 0,
template: (rf: RenderFlags, ctx: DynamicCompWithViewQueries) => {
if (rf & RenderFlags.Create) {
element(1, 'div', ['bar', ''], ['foo', '']);
}
// testing only
fooEl = getNativeByIndex(1, getViewData());
},
viewQuery: function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
query(0, ['foo'], true);
}
if (rf & RenderFlags.Update) {
let tmp: any;
queryRefresh(tmp = load<QueryList<any>>(0)) && (ctx.foo = tmp as QueryList<any>);
}
}
});
}
const fixture = new ComponentFixture(AppCmpt);
fixture.component.insert(DynamicCompWithViewQueries);
fixture.update();
expect(dynamicComp.foo.first.nativeElement).toEqual(fooEl as any);
});
});
});