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
|
||||
* 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 {
|
||||
if (!checkNoChangesMode) {
|
||||
executeHooks(
|
||||
directives !, currentView.tView.viewHooks, currentView.tView.viewCheckHooks, creationMode);
|
||||
export function leaveView(newView: LView, creationOnly?: boolean): void {
|
||||
if (!creationOnly) {
|
||||
if (!checkNoChangesMode) {
|
||||
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.bindingIndex = -1;
|
||||
enterView(newView, null);
|
||||
}
|
||||
|
||||
/** Refreshes directives in this view and triggers any init/content hooks. */
|
||||
function refreshDirectives() {
|
||||
executeInitAndContentHooks();
|
||||
|
||||
/**
|
||||
* Refreshes the view, executing the following steps in that order:
|
||||
* 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;
|
||||
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
|
||||
tView.firstTemplatePass = firstTemplatePass = false;
|
||||
|
||||
|
@ -456,10 +474,11 @@ export function renderEmbeddedTemplate<T>(
|
|||
const _isParent = isParent;
|
||||
const _previousOrParentNode = previousOrParentNode;
|
||||
let oldView: LView;
|
||||
let rf: RenderFlags = RenderFlags.Update;
|
||||
try {
|
||||
isParent = true;
|
||||
previousOrParentNode = null !;
|
||||
let rf: RenderFlags = RenderFlags.Update;
|
||||
|
||||
if (viewNode == null) {
|
||||
const tView = getOrCreateTView(template, directives || null, pipes || null);
|
||||
const lView = createLView(-1, renderer, tView, template, context, LViewFlags.CheckAlways);
|
||||
|
@ -468,13 +487,17 @@ export function renderEmbeddedTemplate<T>(
|
|||
rf = RenderFlags.Create;
|
||||
}
|
||||
oldView = enterView(viewNode.data, viewNode);
|
||||
|
||||
template(rf, context);
|
||||
refreshDirectives();
|
||||
refreshDynamicChildren();
|
||||
|
||||
if (rf & RenderFlags.Update) {
|
||||
refreshView();
|
||||
} else {
|
||||
viewNode.data.tView.firstTemplatePass = firstTemplatePass = false;
|
||||
}
|
||||
} 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;
|
||||
previousOrParentNode = _previousOrParentNode;
|
||||
}
|
||||
|
@ -490,8 +513,7 @@ export function renderComponentOrTemplate<T>(
|
|||
}
|
||||
if (template) {
|
||||
template(getRenderFlags(hostView), componentOrContext !);
|
||||
refreshDynamicChildren();
|
||||
refreshDirectives();
|
||||
refreshView();
|
||||
} else {
|
||||
executeInitAndContentHooks();
|
||||
|
||||
|
@ -1557,7 +1579,7 @@ function getOrCreateEmbeddedTView(viewIndex: number, parent: LContainerNode): TV
|
|||
|
||||
/** Marks the end of an embedded view. */
|
||||
export function embeddedViewEnd(): void {
|
||||
refreshDirectives();
|
||||
refreshView();
|
||||
isParent = false;
|
||||
const viewNode = previousOrParentNode = currentView.node as LViewNode;
|
||||
const containerNode = previousOrParentNode.parent as LContainerNode;
|
||||
|
@ -1968,8 +1990,7 @@ export function detectChangesInternal<T>(
|
|||
|
||||
try {
|
||||
template(getRenderFlags(hostView), component);
|
||||
refreshDirectives();
|
||||
refreshDynamicChildren();
|
||||
refreshView();
|
||||
} finally {
|
||||
leaveView(oldView);
|
||||
}
|
||||
|
|
|
@ -132,10 +132,10 @@
|
|||
"name": "refreshChildComponents"
|
||||
},
|
||||
{
|
||||
"name": "refreshDirectives"
|
||||
"name": "refreshDynamicChildren"
|
||||
},
|
||||
{
|
||||
"name": "refreshDynamicChildren"
|
||||
"name": "refreshView"
|
||||
},
|
||||
{
|
||||
"name": "renderComponent"
|
||||
|
|
|
@ -555,10 +555,10 @@
|
|||
"name": "refreshChildComponents"
|
||||
},
|
||||
{
|
||||
"name": "refreshDirectives"
|
||||
"name": "refreshDynamicChildren"
|
||||
},
|
||||
{
|
||||
"name": "refreshDynamicChildren"
|
||||
"name": "refreshView"
|
||||
},
|
||||
{
|
||||
"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 {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 {RenderFlags} from '../../src/render3/interfaces/definition';
|
||||
import {pipe, pipeBind1} from '../../src/render3/pipe';
|
||||
|
@ -414,9 +414,11 @@ describe('ViewContainerRef', () => {
|
|||
|
||||
const fixture = new ComponentFixture(SomeComponent);
|
||||
directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, fixture.component);
|
||||
directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, fixture.component);
|
||||
fixture.update();
|
||||
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…
Reference in New Issue