feat(ivy): support lifecycle hooks of ViewContainerRef (#23396)
PR Close #23396
This commit is contained in:
parent
b1f040f5a2
commit
1a44a0b4a8
@ -224,24 +224,42 @@ export function enterView(newView: LView, host: LElementNode | LViewNode | null)
|
|||||||
/**
|
/**
|
||||||
* Used in lieu of enterView to make it clear when we are exiting a child view. This makes
|
* Used in lieu of enterView to make it clear when we are exiting a child view. This makes
|
||||||
* the direction of traversal (up or down the view tree) a bit clearer.
|
* the direction of traversal (up or down the view tree) a bit clearer.
|
||||||
|
*
|
||||||
|
* @param newView New state to become active
|
||||||
|
* @param creationOnly An optional boolean to indicate that the view was processed in creation mode
|
||||||
|
* only, i.e. the first update will be done later. Only possible for dynamically created views.
|
||||||
*/
|
*/
|
||||||
export function leaveView(newView: LView): void {
|
export function leaveView(newView: LView, creationOnly?: boolean): void {
|
||||||
if (!checkNoChangesMode) {
|
if (!creationOnly) {
|
||||||
executeHooks(
|
if (!checkNoChangesMode) {
|
||||||
directives !, currentView.tView.viewHooks, currentView.tView.viewCheckHooks, creationMode);
|
executeHooks(
|
||||||
|
directives !, currentView.tView.viewHooks, currentView.tView.viewCheckHooks,
|
||||||
|
creationMode);
|
||||||
|
}
|
||||||
|
// Views are clean and in update mode after being checked, so these bits are cleared
|
||||||
|
currentView.flags &= ~(LViewFlags.CreationMode | LViewFlags.Dirty);
|
||||||
}
|
}
|
||||||
// Views should be clean and in update mode after being checked, so these bits are cleared
|
|
||||||
currentView.flags &= ~(LViewFlags.CreationMode | LViewFlags.Dirty);
|
|
||||||
currentView.lifecycleStage = LifecycleStage.Init;
|
currentView.lifecycleStage = LifecycleStage.Init;
|
||||||
currentView.bindingIndex = -1;
|
currentView.bindingIndex = -1;
|
||||||
enterView(newView, null);
|
enterView(newView, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Refreshes directives in this view and triggers any init/content hooks. */
|
/**
|
||||||
function refreshDirectives() {
|
* Refreshes the view, executing the following steps in that order:
|
||||||
executeInitAndContentHooks();
|
* triggers init hooks, refreshes dynamic children, triggers content hooks, sets host bindings,
|
||||||
|
* refreshes child components.
|
||||||
|
* Note: view hooks are triggered later when leaving the view.
|
||||||
|
* */
|
||||||
|
function refreshView() {
|
||||||
const tView = currentView.tView;
|
const tView = currentView.tView;
|
||||||
|
if (!checkNoChangesMode) {
|
||||||
|
executeInitHooks(currentView, tView, creationMode);
|
||||||
|
}
|
||||||
|
refreshDynamicChildren();
|
||||||
|
if (!checkNoChangesMode) {
|
||||||
|
executeHooks(directives !, tView.contentHooks, tView.contentCheckHooks, creationMode);
|
||||||
|
}
|
||||||
|
|
||||||
// This needs to be set before children are processed to support recursive components
|
// This needs to be set before children are processed to support recursive components
|
||||||
tView.firstTemplatePass = firstTemplatePass = false;
|
tView.firstTemplatePass = firstTemplatePass = false;
|
||||||
|
|
||||||
@ -456,10 +474,11 @@ export function renderEmbeddedTemplate<T>(
|
|||||||
const _isParent = isParent;
|
const _isParent = isParent;
|
||||||
const _previousOrParentNode = previousOrParentNode;
|
const _previousOrParentNode = previousOrParentNode;
|
||||||
let oldView: LView;
|
let oldView: LView;
|
||||||
|
let rf: RenderFlags = RenderFlags.Update;
|
||||||
try {
|
try {
|
||||||
isParent = true;
|
isParent = true;
|
||||||
previousOrParentNode = null !;
|
previousOrParentNode = null !;
|
||||||
let rf: RenderFlags = RenderFlags.Update;
|
|
||||||
if (viewNode == null) {
|
if (viewNode == null) {
|
||||||
const tView = getOrCreateTView(template, directives || null, pipes || null);
|
const tView = getOrCreateTView(template, directives || null, pipes || null);
|
||||||
const lView = createLView(-1, renderer, tView, template, context, LViewFlags.CheckAlways);
|
const lView = createLView(-1, renderer, tView, template, context, LViewFlags.CheckAlways);
|
||||||
@ -468,13 +487,17 @@ export function renderEmbeddedTemplate<T>(
|
|||||||
rf = RenderFlags.Create;
|
rf = RenderFlags.Create;
|
||||||
}
|
}
|
||||||
oldView = enterView(viewNode.data, viewNode);
|
oldView = enterView(viewNode.data, viewNode);
|
||||||
|
|
||||||
template(rf, context);
|
template(rf, context);
|
||||||
refreshDirectives();
|
if (rf & RenderFlags.Update) {
|
||||||
refreshDynamicChildren();
|
refreshView();
|
||||||
|
} else {
|
||||||
|
viewNode.data.tView.firstTemplatePass = firstTemplatePass = false;
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
leaveView(oldView !);
|
// renderEmbeddedTemplate() is called twice in fact, once for creation only and then once for
|
||||||
|
// update. When for creation only, leaveView() must not trigger view hooks, nor clean flags.
|
||||||
|
const isCreationOnly = (rf & RenderFlags.Create) === RenderFlags.Create;
|
||||||
|
leaveView(oldView !, isCreationOnly);
|
||||||
isParent = _isParent;
|
isParent = _isParent;
|
||||||
previousOrParentNode = _previousOrParentNode;
|
previousOrParentNode = _previousOrParentNode;
|
||||||
}
|
}
|
||||||
@ -490,8 +513,7 @@ export function renderComponentOrTemplate<T>(
|
|||||||
}
|
}
|
||||||
if (template) {
|
if (template) {
|
||||||
template(getRenderFlags(hostView), componentOrContext !);
|
template(getRenderFlags(hostView), componentOrContext !);
|
||||||
refreshDynamicChildren();
|
refreshView();
|
||||||
refreshDirectives();
|
|
||||||
} else {
|
} else {
|
||||||
executeInitAndContentHooks();
|
executeInitAndContentHooks();
|
||||||
|
|
||||||
@ -1557,7 +1579,7 @@ function getOrCreateEmbeddedTView(viewIndex: number, parent: LContainerNode): TV
|
|||||||
|
|
||||||
/** Marks the end of an embedded view. */
|
/** Marks the end of an embedded view. */
|
||||||
export function embeddedViewEnd(): void {
|
export function embeddedViewEnd(): void {
|
||||||
refreshDirectives();
|
refreshView();
|
||||||
isParent = false;
|
isParent = false;
|
||||||
const viewNode = previousOrParentNode = currentView.node as LViewNode;
|
const viewNode = previousOrParentNode = currentView.node as LViewNode;
|
||||||
const containerNode = previousOrParentNode.parent as LContainerNode;
|
const containerNode = previousOrParentNode.parent as LContainerNode;
|
||||||
@ -1968,8 +1990,7 @@ export function detectChangesInternal<T>(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
template(getRenderFlags(hostView), component);
|
template(getRenderFlags(hostView), component);
|
||||||
refreshDirectives();
|
refreshView();
|
||||||
refreshDynamicChildren();
|
|
||||||
} finally {
|
} finally {
|
||||||
leaveView(oldView);
|
leaveView(oldView);
|
||||||
}
|
}
|
||||||
|
@ -132,10 +132,10 @@
|
|||||||
"name": "refreshChildComponents"
|
"name": "refreshChildComponents"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "refreshDirectives"
|
"name": "refreshDynamicChildren"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "refreshDynamicChildren"
|
"name": "refreshView"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "renderComponent"
|
"name": "renderComponent"
|
||||||
|
@ -555,10 +555,10 @@
|
|||||||
"name": "refreshChildComponents"
|
"name": "refreshChildComponents"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "refreshDirectives"
|
"name": "refreshDynamicChildren"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "refreshDynamicChildren"
|
"name": "refreshView"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "removeListeners"
|
"name": "removeListeners"
|
||||||
|
@ -2436,6 +2436,94 @@ describe('lifecycles', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Angular 5 reference: https://stackblitz.com/edit/lifecycle-hooks-ng
|
||||||
|
it('should call all hooks in correct order with view and content', () => {
|
||||||
|
const Content = createAllHooksComponent('content', (rf: RenderFlags, ctx: any) => {});
|
||||||
|
|
||||||
|
const View = createAllHooksComponent('view', (rf: RenderFlags, ctx: any) => {});
|
||||||
|
|
||||||
|
/** <ng-content></ng-content><view [val]="val"></view> */
|
||||||
|
const Parent = createAllHooksComponent('parent', (rf: RenderFlags, ctx: any) => {
|
||||||
|
if (rf & RenderFlags.Create) {
|
||||||
|
projectionDef(0);
|
||||||
|
projection(1, 0);
|
||||||
|
elementStart(2, 'view');
|
||||||
|
elementEnd();
|
||||||
|
}
|
||||||
|
if (rf & RenderFlags.Update) {
|
||||||
|
elementProperty(2, 'val', bind(ctx.val));
|
||||||
|
}
|
||||||
|
}, [View]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <parent [val]="1">
|
||||||
|
* <content [val]="1"></content>
|
||||||
|
* </parent>
|
||||||
|
* <parent [val]="2">
|
||||||
|
* <content [val]="2"></content>
|
||||||
|
* </parent>
|
||||||
|
*/
|
||||||
|
function Template(rf: RenderFlags, ctx: any) {
|
||||||
|
if (rf & RenderFlags.Create) {
|
||||||
|
elementStart(0, 'parent');
|
||||||
|
{
|
||||||
|
elementStart(1, 'content');
|
||||||
|
elementEnd();
|
||||||
|
}
|
||||||
|
elementEnd();
|
||||||
|
elementStart(2, 'parent');
|
||||||
|
{
|
||||||
|
elementStart(3, 'content');
|
||||||
|
elementEnd();
|
||||||
|
}
|
||||||
|
elementEnd();
|
||||||
|
}
|
||||||
|
if (rf & RenderFlags.Update) {
|
||||||
|
elementProperty(0, 'val', bind(1));
|
||||||
|
elementProperty(1, 'val', bind(1));
|
||||||
|
elementProperty(2, 'val', bind(2));
|
||||||
|
elementProperty(3, 'val', bind(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defs = [Parent, Content];
|
||||||
|
renderToHtml(Template, {}, defs);
|
||||||
|
expect(events).toEqual([
|
||||||
|
'changes parent1', 'init parent1',
|
||||||
|
'check parent1', 'changes content1',
|
||||||
|
'init content1', 'check content1',
|
||||||
|
'changes parent2', 'init parent2',
|
||||||
|
'check parent2', 'changes content2',
|
||||||
|
'init content2', 'check content2',
|
||||||
|
'contentInit content1', 'contentCheck content1',
|
||||||
|
'contentInit parent1', 'contentCheck parent1',
|
||||||
|
'contentInit content2', 'contentCheck content2',
|
||||||
|
'contentInit parent2', 'contentCheck parent2',
|
||||||
|
'changes view1', 'init view1',
|
||||||
|
'check view1', 'contentInit view1',
|
||||||
|
'contentCheck view1', 'viewInit view1',
|
||||||
|
'viewCheck view1', 'changes view2',
|
||||||
|
'init view2', 'check view2',
|
||||||
|
'contentInit view2', 'contentCheck view2',
|
||||||
|
'viewInit view2', 'viewCheck view2',
|
||||||
|
'viewInit content1', 'viewCheck content1',
|
||||||
|
'viewInit parent1', 'viewCheck parent1',
|
||||||
|
'viewInit content2', 'viewCheck content2',
|
||||||
|
'viewInit parent2', 'viewCheck parent2'
|
||||||
|
]);
|
||||||
|
|
||||||
|
events = [];
|
||||||
|
renderToHtml(Template, {}, defs);
|
||||||
|
expect(events).toEqual([
|
||||||
|
'check parent1', 'check content1', 'check parent2', 'check content2',
|
||||||
|
'contentCheck content1', 'contentCheck parent1', 'contentCheck content2',
|
||||||
|
'contentCheck parent2', 'check view1', 'contentCheck view1', 'viewCheck view1',
|
||||||
|
'check view2', 'contentCheck view2', 'viewCheck view2', 'viewCheck content1',
|
||||||
|
'viewCheck parent1', 'viewCheck content2', 'viewCheck parent2'
|
||||||
|
]);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import {Component, Directive, Pipe, PipeTransform, TemplateRef, ViewContainerRef} from '../../src/core';
|
import {Component, Directive, Pipe, PipeTransform, TemplateRef, ViewContainerRef} from '../../src/core';
|
||||||
import {getOrCreateNodeInjectorForNode, getOrCreateTemplateRef} from '../../src/render3/di';
|
import {getOrCreateNodeInjectorForNode, getOrCreateTemplateRef} from '../../src/render3/di';
|
||||||
import {defineComponent, defineDirective, definePipe, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index';
|
import {NgOnChangesFeature, defineComponent, defineDirective, definePipe, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index';
|
||||||
import {bind, container, containerRefreshEnd, containerRefreshStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, load, loadDirective, projection, projectionDef, text, textBinding} from '../../src/render3/instructions';
|
import {bind, container, containerRefreshEnd, containerRefreshStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, load, loadDirective, projection, projectionDef, text, textBinding} from '../../src/render3/instructions';
|
||||||
import {RenderFlags} from '../../src/render3/interfaces/definition';
|
import {RenderFlags} from '../../src/render3/interfaces/definition';
|
||||||
import {pipe, pipeBind1} from '../../src/render3/pipe';
|
import {pipe, pipeBind1} from '../../src/render3/pipe';
|
||||||
@ -414,9 +414,11 @@ describe('ViewContainerRef', () => {
|
|||||||
|
|
||||||
const fixture = new ComponentFixture(SomeComponent);
|
const fixture = new ComponentFixture(SomeComponent);
|
||||||
directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, fixture.component);
|
directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, fixture.component);
|
||||||
|
directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, fixture.component);
|
||||||
fixture.update();
|
fixture.update();
|
||||||
expect(fixture.html)
|
expect(fixture.html)
|
||||||
.toEqual('<child vcref="">**A**</child><child>**C**</child><child>**B**</child>');
|
.toEqual(
|
||||||
|
'<child vcref="">**A**</child><child>**C**</child><child>**C**</child><child>**B**</child>');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -822,4 +824,121 @@ describe('ViewContainerRef', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('life cycle hooks', () => {
|
||||||
|
|
||||||
|
// Angular 5 reference: https://stackblitz.com/edit/lifecycle-hooks-vcref
|
||||||
|
const log: string[] = [];
|
||||||
|
it('should call all hooks in correct order', () => {
|
||||||
|
@Component({selector: 'hooks', template: `{{name}}`})
|
||||||
|
class ComponentWithHooks {
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
private log(msg: string) { log.push(msg); }
|
||||||
|
|
||||||
|
ngOnChanges() { this.log('onChanges-' + this.name); }
|
||||||
|
ngOnInit() { this.log('onInit-' + this.name); }
|
||||||
|
ngDoCheck() { this.log('doCheck-' + this.name); }
|
||||||
|
|
||||||
|
ngAfterContentInit() { this.log('afterContentInit-' + this.name); }
|
||||||
|
ngAfterContentChecked() { this.log('afterContentChecked-' + this.name); }
|
||||||
|
|
||||||
|
ngAfterViewInit() { this.log('afterViewInit-' + this.name); }
|
||||||
|
ngAfterViewChecked() { this.log('afterViewChecked-' + this.name); }
|
||||||
|
|
||||||
|
static ngComponentDef = defineComponent({
|
||||||
|
type: ComponentWithHooks,
|
||||||
|
selectors: [['hooks']],
|
||||||
|
factory: () => new ComponentWithHooks(),
|
||||||
|
template: (rf: RenderFlags, cmp: ComponentWithHooks) => {
|
||||||
|
if (rf & RenderFlags.Create) {
|
||||||
|
text(0);
|
||||||
|
}
|
||||||
|
if (rf & RenderFlags.Update) {
|
||||||
|
textBinding(0, interpolation1('', cmp.name, ''));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
features: [NgOnChangesFeature()],
|
||||||
|
inputs: {name: 'name'}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<ng-template #foo>
|
||||||
|
<hooks [name]="'C'"></hooks>
|
||||||
|
</ng-template>
|
||||||
|
<hooks vcref [tplRef]="foo" [name]="'A'"></hooks>
|
||||||
|
<hooks [name]="'B'"></hooks>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
class SomeComponent {
|
||||||
|
static ngComponentDef = defineComponent({
|
||||||
|
type: SomeComponent,
|
||||||
|
selectors: [['some-comp']],
|
||||||
|
factory: () => new SomeComponent(),
|
||||||
|
template: (rf: RenderFlags, cmp: SomeComponent) => {
|
||||||
|
if (rf & RenderFlags.Create) {
|
||||||
|
container(0, (rf: RenderFlags, ctx: any) => {
|
||||||
|
if (rf & RenderFlags.Create) {
|
||||||
|
elementStart(0, 'hooks');
|
||||||
|
elementEnd();
|
||||||
|
}
|
||||||
|
if (rf & RenderFlags.Update) {
|
||||||
|
elementProperty(0, 'name', bind('C'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
elementStart(1, 'hooks', ['vcref', '']);
|
||||||
|
elementEnd();
|
||||||
|
elementStart(2, 'hooks');
|
||||||
|
elementEnd();
|
||||||
|
}
|
||||||
|
if (rf & RenderFlags.Update) {
|
||||||
|
const tplRef = getOrCreateTemplateRef(getOrCreateNodeInjectorForNode(load(0)));
|
||||||
|
elementProperty(1, 'tplRef', bind(tplRef));
|
||||||
|
elementProperty(1, 'name', bind('A'));
|
||||||
|
elementProperty(2, 'name', bind('B'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
directives: [ComponentWithHooks, DirectiveWithVCRef]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fixture = new ComponentFixture(SomeComponent);
|
||||||
|
expect(log).toEqual([
|
||||||
|
'onChanges-A', 'onInit-A', 'doCheck-A', 'onChanges-B', 'onInit-B', 'doCheck-B',
|
||||||
|
'afterContentInit-A', 'afterContentChecked-A', 'afterContentInit-B',
|
||||||
|
'afterContentChecked-B', 'afterViewInit-A', 'afterViewChecked-A', 'afterViewInit-B',
|
||||||
|
'afterViewChecked-B'
|
||||||
|
]);
|
||||||
|
|
||||||
|
log.length = 0;
|
||||||
|
fixture.update();
|
||||||
|
expect(log).toEqual([
|
||||||
|
'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B',
|
||||||
|
'afterViewChecked-A', 'afterViewChecked-B'
|
||||||
|
]);
|
||||||
|
|
||||||
|
log.length = 0;
|
||||||
|
directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, fixture.component);
|
||||||
|
expect(fixture.html).toEqual('<hooks vcref="">A</hooks><hooks></hooks><hooks>B</hooks>');
|
||||||
|
expect(log).toEqual([]);
|
||||||
|
|
||||||
|
log.length = 0;
|
||||||
|
fixture.update();
|
||||||
|
expect(fixture.html).toEqual('<hooks vcref="">A</hooks><hooks>C</hooks><hooks>B</hooks>');
|
||||||
|
expect(log).toEqual([
|
||||||
|
'doCheck-A', 'doCheck-B', 'onChanges-C', 'onInit-C', 'doCheck-C', 'afterContentInit-C',
|
||||||
|
'afterContentChecked-C', 'afterViewInit-C', 'afterViewChecked-C', 'afterContentChecked-A',
|
||||||
|
'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B'
|
||||||
|
]);
|
||||||
|
|
||||||
|
log.length = 0;
|
||||||
|
fixture.update();
|
||||||
|
expect(log).toEqual([
|
||||||
|
'doCheck-A', 'doCheck-B', 'doCheck-C', 'afterContentChecked-C', 'afterViewChecked-C',
|
||||||
|
'afterContentChecked-A', 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user