feat(ivy): support attaching and detaching views from change detection (#22670)
PR Close #22670
This commit is contained in:
parent
b0b9ca3386
commit
b26a90567c
|
@ -16,7 +16,7 @@ import {queueLifecycleHooks} from './hooks';
|
|||
import {CLEAN_PROMISE, _getComponentHostLElementNode, createLView, createTView, directiveCreate, enterView, getDirectiveInstance, getRootView, hostElement, initChangeDetectorIfExisting, leaveView, locateHostElement, scheduleTick, tick} from './instructions';
|
||||
import {ComponentDef, ComponentType} from './interfaces/definition';
|
||||
import {RElement, RendererFactory3, domRendererFactory3} from './interfaces/renderer';
|
||||
import {LViewFlags, RootContext} from './interfaces/view';
|
||||
import {LView, LViewFlags, RootContext} from './interfaces/view';
|
||||
import {stringify} from './util';
|
||||
import {createViewRef} from './view_ref';
|
||||
|
||||
|
@ -74,13 +74,14 @@ export interface CreateComponentOptions {
|
|||
export function createComponentRef<T>(
|
||||
componentType: ComponentType<T>, opts: CreateComponentOptions): viewEngine_ComponentRef<T> {
|
||||
const component = renderComponent(componentType, opts);
|
||||
const hostView = createViewRef(component);
|
||||
const hostView = _getComponentHostLElementNode(component).data as LView;
|
||||
const hostViewRef = createViewRef(hostView, component);
|
||||
return {
|
||||
location: {nativeElement: getHostElement(component)},
|
||||
injector: opts.injector || NULL_INJECTOR,
|
||||
instance: component,
|
||||
hostView: hostView,
|
||||
changeDetectorRef: hostView,
|
||||
hostView: hostViewRef,
|
||||
changeDetectorRef: hostViewRef,
|
||||
componentType: componentType,
|
||||
// TODO: implement destroy and onDestroy
|
||||
destroy: () => {},
|
||||
|
|
|
@ -25,6 +25,7 @@ import {LInjector} from './interfaces/injector';
|
|||
import {LContainerNode, LElementNode, LNode, LNodeFlags, LViewNode} from './interfaces/node';
|
||||
import {QueryReadType} from './interfaces/query';
|
||||
import {Renderer3} from './interfaces/renderer';
|
||||
import {LView} from './interfaces/view';
|
||||
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
|
||||
import {insertView} from './node_manipulation';
|
||||
import {notImplemented, stringify} from './util';
|
||||
|
@ -301,7 +302,7 @@ export function getOrCreateChangeDetectorRef(
|
|||
return di.changeDetectorRef = getOrCreateHostChangeDetector(currentNode.view.node);
|
||||
} else if ((currentNode.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.Element) {
|
||||
// if it's an element node with data, it's a component and context will be set later
|
||||
return di.changeDetectorRef = createViewRef(context);
|
||||
return di.changeDetectorRef = createViewRef(currentNode.data as LView, context);
|
||||
}
|
||||
return null !;
|
||||
}
|
||||
|
@ -313,8 +314,10 @@ function getOrCreateHostChangeDetector(currentNode: LViewNode | LElementNode):
|
|||
const hostInjector = hostNode.nodeInjector;
|
||||
const existingRef = hostInjector && hostInjector.changeDetectorRef;
|
||||
|
||||
return existingRef ? existingRef :
|
||||
createViewRef(hostNode.view.data[hostNode.flags >> LNodeFlags.INDX_SHIFT]);
|
||||
return existingRef ?
|
||||
existingRef :
|
||||
createViewRef(
|
||||
hostNode.data as LView, hostNode.view.data[hostNode.flags >> LNodeFlags.INDX_SHIFT]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -210,7 +210,7 @@ export function createLView(
|
|||
const newView = {
|
||||
parent: currentView,
|
||||
id: viewId, // -1 for component views
|
||||
flags: flags | LViewFlags.CreationMode,
|
||||
flags: flags | LViewFlags.CreationMode | LViewFlags.Attached,
|
||||
node: null !, // until we initialize it in createNode.
|
||||
data: [],
|
||||
tView: tView,
|
||||
|
@ -1281,14 +1281,19 @@ export function directiveRefresh<T>(directiveIndex: number, elementIndex: number
|
|||
assertNotNull(element.data, `Component's host node should have an LView attached.`);
|
||||
const hostView = element.data !;
|
||||
|
||||
// Only CheckAlways components or dirty OnPush components should be checked
|
||||
if (hostView.flags & (LViewFlags.CheckAlways | LViewFlags.Dirty)) {
|
||||
// Only attached CheckAlways components or attached, dirty OnPush components should be checked
|
||||
if (viewAttached(hostView) && hostView.flags & (LViewFlags.CheckAlways | LViewFlags.Dirty)) {
|
||||
ngDevMode && assertDataInRange(directiveIndex);
|
||||
detectChangesInternal(hostView, element, getDirectiveInstance<T>(data[directiveIndex]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a boolean for whether the view is attached */
|
||||
function viewAttached(view: LView): boolean {
|
||||
return (view.flags & LViewFlags.Attached) === LViewFlags.Attached;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruction to distribute projectable nodes among <ng-content> occurrences in a given template.
|
||||
* It takes all the selectors from the entire component's template and decides where
|
||||
|
|
|
@ -183,13 +183,16 @@ export const enum LViewFlags {
|
|||
* back into the parent view, `data` will be defined and `creationMode` will be
|
||||
* improperly reported as false.
|
||||
*/
|
||||
CreationMode = 0b001,
|
||||
CreationMode = 0b0001,
|
||||
|
||||
/** Whether this view has default change detection strategy (checks always) or onPush */
|
||||
CheckAlways = 0b010,
|
||||
CheckAlways = 0b0010,
|
||||
|
||||
/** Whether or not this view is currently dirty (needing check) */
|
||||
Dirty = 0b100
|
||||
Dirty = 0b0100,
|
||||
|
||||
/** Whether or not this view is currently attached to change detection tree. */
|
||||
Attached = 0b1000,
|
||||
}
|
||||
|
||||
/** Interface necessary to work with view tree traversal */
|
||||
|
|
|
@ -7,16 +7,18 @@
|
|||
*/
|
||||
|
||||
import {EmbeddedViewRef as viewEngine_EmbeddedViewRef} from '../linker/view_ref';
|
||||
|
||||
import {detectChanges} from './instructions';
|
||||
import {ComponentTemplate} from './interfaces/definition';
|
||||
import {LViewNode} from './interfaces/node';
|
||||
import {LView, LViewFlags} from './interfaces/view';
|
||||
import {notImplemented} from './util';
|
||||
|
||||
export class ViewRef<T> implements viewEngine_EmbeddedViewRef<T> {
|
||||
context: T;
|
||||
rootNodes: any[];
|
||||
|
||||
constructor(context: T|null) { this.context = context !; }
|
||||
constructor(private _view: LView, context: T|null, ) { this.context = context !; }
|
||||
|
||||
/** @internal */
|
||||
_setComponentContext(context: T) { this.context = context; }
|
||||
|
@ -25,12 +27,27 @@ export class ViewRef<T> implements viewEngine_EmbeddedViewRef<T> {
|
|||
destroyed: boolean;
|
||||
onDestroy(callback: Function) { notImplemented(); }
|
||||
markForCheck(): void { notImplemented(); }
|
||||
detach(): void { notImplemented(); }
|
||||
|
||||
/**
|
||||
* Detaches a view from the change detection tree.
|
||||
*
|
||||
* Detached views will not be checked during change detection runs, even if the view
|
||||
* is dirty. This can be used in combination with detectChanges to implement local
|
||||
* change detection checks.
|
||||
*/
|
||||
detach(): void { this._view.flags &= ~LViewFlags.Attached; }
|
||||
|
||||
/**
|
||||
* Re-attaches a view to the change detection tree.
|
||||
*
|
||||
* This can be used to re-attach views that were previously detached from the tree
|
||||
* using detach(). Views are attached to the tree by default.
|
||||
*/
|
||||
reattach(): void { this._view.flags |= LViewFlags.Attached; }
|
||||
|
||||
detectChanges(): void { detectChanges(this.context); }
|
||||
|
||||
checkNoChanges(): void { notImplemented(); }
|
||||
reattach(): void { notImplemented(); }
|
||||
}
|
||||
|
||||
|
||||
|
@ -41,7 +58,7 @@ export class EmbeddedViewRef<T> extends ViewRef<T> {
|
|||
_lViewNode: LViewNode;
|
||||
|
||||
constructor(viewNode: LViewNode, template: ComponentTemplate<T>, context: T) {
|
||||
super(context);
|
||||
super(viewNode.data, context);
|
||||
this._lViewNode = viewNode;
|
||||
}
|
||||
}
|
||||
|
@ -52,9 +69,9 @@ export class EmbeddedViewRef<T> extends ViewRef<T> {
|
|||
* @param context The context for this view
|
||||
* @returns The ViewRef
|
||||
*/
|
||||
export function createViewRef<T>(context: T): ViewRef<T> {
|
||||
export function createViewRef<T>(view: LView, context: T): ViewRef<T> {
|
||||
// TODO: add detectChanges back in when implementing ChangeDetectorRef.detectChanges
|
||||
return addDestroyable(new ViewRef(context));
|
||||
return addDestroyable(new ViewRef(view, context));
|
||||
}
|
||||
|
||||
/** Interface for destroy logic. Implemented by addDestroyable. */
|
||||
|
|
|
@ -11,7 +11,7 @@ import {withBody} from '@angular/core/testing';
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, DoCheck, EmbeddedViewRef, TemplateRef, ViewContainerRef} from '../../src/core';
|
||||
import {getRenderedText, whenRendered} from '../../src/render3/component';
|
||||
import {NgOnChangesFeature, PublicFeature, defineComponent, defineDirective, injectChangeDetectorRef, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index';
|
||||
import {bind, container, containerRefreshEnd, containerRefreshStart, detectChanges, directiveRefresh, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation2, listener, markDirty, text, textBinding} from '../../src/render3/instructions';
|
||||
import {bind, container, containerRefreshEnd, containerRefreshStart, detectChanges, directiveRefresh, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation2, listener, markDirty, text, textBinding, tick} from '../../src/render3/instructions';
|
||||
|
||||
import {containerEl, renderComponent, requestAnimationFrame, toHtml} from './render_util';
|
||||
|
||||
|
@ -146,21 +146,21 @@ describe('change detection', () => {
|
|||
it('should call doCheck even when OnPush components are not dirty', () => {
|
||||
const myApp = renderComponent(MyApp);
|
||||
|
||||
detectChanges(myApp);
|
||||
tick(myApp);
|
||||
expect(comp.doCheckCount).toEqual(2);
|
||||
|
||||
detectChanges(myApp);
|
||||
tick(myApp);
|
||||
expect(comp.doCheckCount).toEqual(3);
|
||||
});
|
||||
|
||||
it('should skip OnPush components in update mode when they are not dirty', () => {
|
||||
const myApp = renderComponent(MyApp);
|
||||
|
||||
detectChanges(myApp);
|
||||
tick(myApp);
|
||||
// doCheckCount is 2, but 1 should be rendered since it has not been marked dirty.
|
||||
expect(getRenderedText(myApp)).toEqual('1 - Nancy');
|
||||
|
||||
detectChanges(myApp);
|
||||
tick(myApp);
|
||||
// doCheckCount is 3, but 1 should be rendered since it has not been marked dirty.
|
||||
expect(getRenderedText(myApp)).toEqual('1 - Nancy');
|
||||
});
|
||||
|
@ -169,14 +169,14 @@ describe('change detection', () => {
|
|||
const myApp = renderComponent(MyApp);
|
||||
|
||||
myApp.name = 'Bess';
|
||||
detectChanges(myApp);
|
||||
tick(myApp);
|
||||
expect(getRenderedText(myApp)).toEqual('2 - Bess');
|
||||
|
||||
myApp.name = 'George';
|
||||
detectChanges(myApp);
|
||||
tick(myApp);
|
||||
expect(getRenderedText(myApp)).toEqual('3 - George');
|
||||
|
||||
detectChanges(myApp);
|
||||
tick(myApp);
|
||||
expect(getRenderedText(myApp)).toEqual('3 - George');
|
||||
});
|
||||
|
||||
|
@ -189,7 +189,7 @@ describe('change detection', () => {
|
|||
requestAnimationFrame.flush();
|
||||
expect(getRenderedText(myApp)).toEqual('2 - Nancy');
|
||||
|
||||
detectChanges(myApp);
|
||||
tick(myApp);
|
||||
expect(getRenderedText(myApp)).toEqual('2 - Nancy');
|
||||
});
|
||||
|
||||
|
@ -275,7 +275,7 @@ describe('change detection', () => {
|
|||
expect(comp !.doCheckCount).toEqual(1);
|
||||
expect(getRenderedText(myButtonApp)).toEqual('1 - 1 - Nancy');
|
||||
|
||||
detectChanges(myButtonApp);
|
||||
tick(myButtonApp);
|
||||
expect(parent !.doCheckCount).toEqual(2);
|
||||
// parent isn't checked, so child doCheck won't run
|
||||
expect(comp !.doCheckCount).toEqual(1);
|
||||
|
@ -577,6 +577,185 @@ describe('change detection', () => {
|
|||
|
||||
});
|
||||
|
||||
describe('attach/detach', () => {
|
||||
let comp: DetachedComp;
|
||||
|
||||
class MyApp {
|
||||
constructor(public cdr: ChangeDetectorRef) {}
|
||||
|
||||
static ngComponentDef = defineComponent({
|
||||
type: MyApp,
|
||||
tag: 'my-app',
|
||||
factory: () => new MyApp(injectChangeDetectorRef()),
|
||||
/** <detached-comp></detached-comp> */
|
||||
template: (ctx: MyApp, cm: boolean) => {
|
||||
if (cm) {
|
||||
elementStart(0, DetachedComp);
|
||||
elementEnd();
|
||||
}
|
||||
DetachedComp.ngComponentDef.h(1, 0);
|
||||
directiveRefresh(1, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
class DetachedComp {
|
||||
value = 'one';
|
||||
doCheckCount = 0;
|
||||
|
||||
constructor(public cdr: ChangeDetectorRef) {}
|
||||
|
||||
ngDoCheck() { this.doCheckCount++; }
|
||||
|
||||
static ngComponentDef = defineComponent({
|
||||
type: DetachedComp,
|
||||
tag: 'detached-comp',
|
||||
factory: () => comp = new DetachedComp(injectChangeDetectorRef()),
|
||||
/** {{ value }} */
|
||||
template: (ctx: DetachedComp, cm: boolean) => {
|
||||
if (cm) {
|
||||
text(0);
|
||||
}
|
||||
textBinding(0, bind(ctx.value));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
it('should not check detached components', () => {
|
||||
const app = renderComponent(MyApp);
|
||||
expect(getRenderedText(app)).toEqual('one');
|
||||
|
||||
comp.cdr.detach();
|
||||
|
||||
comp.value = 'two';
|
||||
tick(app);
|
||||
expect(getRenderedText(app)).toEqual('one');
|
||||
});
|
||||
|
||||
it('should check re-attached components', () => {
|
||||
const app = renderComponent(MyApp);
|
||||
expect(getRenderedText(app)).toEqual('one');
|
||||
|
||||
comp.cdr.detach();
|
||||
comp.value = 'two';
|
||||
|
||||
comp.cdr.reattach();
|
||||
tick(app);
|
||||
expect(getRenderedText(app)).toEqual('two');
|
||||
});
|
||||
|
||||
it('should call lifecycle hooks on detached components', () => {
|
||||
const app = renderComponent(MyApp);
|
||||
expect(comp.doCheckCount).toEqual(1);
|
||||
|
||||
comp.cdr.detach();
|
||||
|
||||
tick(app);
|
||||
expect(comp.doCheckCount).toEqual(2);
|
||||
});
|
||||
|
||||
it('should check detached component when detectChanges is called', () => {
|
||||
const app = renderComponent(MyApp);
|
||||
expect(getRenderedText(app)).toEqual('one');
|
||||
|
||||
comp.cdr.detach();
|
||||
|
||||
comp.value = 'two';
|
||||
detectChanges(comp);
|
||||
expect(getRenderedText(app)).toEqual('two');
|
||||
});
|
||||
|
||||
it('should not check detached component when markDirty is called', () => {
|
||||
const app = renderComponent(MyApp);
|
||||
expect(getRenderedText(app)).toEqual('one');
|
||||
|
||||
comp.cdr.detach();
|
||||
|
||||
comp.value = 'two';
|
||||
markDirty(comp);
|
||||
requestAnimationFrame.flush();
|
||||
|
||||
expect(getRenderedText(app)).toEqual('one');
|
||||
});
|
||||
|
||||
it('should detach any child components when parent is detached', () => {
|
||||
const app = renderComponent(MyApp);
|
||||
expect(getRenderedText(app)).toEqual('one');
|
||||
|
||||
app.cdr.detach();
|
||||
|
||||
comp.value = 'two';
|
||||
tick(app);
|
||||
expect(getRenderedText(app)).toEqual('one');
|
||||
|
||||
app.cdr.reattach();
|
||||
|
||||
tick(app);
|
||||
expect(getRenderedText(app)).toEqual('two');
|
||||
});
|
||||
|
||||
it('should detach OnPush components properly', () => {
|
||||
let onPushComp: OnPushComp;
|
||||
|
||||
class OnPushComp {
|
||||
/** @Input() */
|
||||
value: string;
|
||||
|
||||
constructor(public cdr: ChangeDetectorRef) {}
|
||||
|
||||
static ngComponentDef = defineComponent({
|
||||
type: OnPushComp,
|
||||
tag: 'on-push-comp',
|
||||
factory: () => onPushComp = new OnPushComp(injectChangeDetectorRef()),
|
||||
/** {{ value }} */
|
||||
template: (ctx: OnPushComp, cm: boolean) => {
|
||||
if (cm) {
|
||||
text(0);
|
||||
}
|
||||
textBinding(0, bind(ctx.value));
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
inputs: {value: 'value'}
|
||||
});
|
||||
}
|
||||
|
||||
class OnPushApp {
|
||||
value = 'one';
|
||||
|
||||
static ngComponentDef = defineComponent({
|
||||
type: OnPushApp,
|
||||
tag: 'on-push-app',
|
||||
factory: () => new OnPushApp(),
|
||||
/** <on-push-comp [value]="value"></on-push-comp> */
|
||||
template: (ctx: OnPushApp, cm: boolean) => {
|
||||
if (cm) {
|
||||
elementStart(0, OnPushComp);
|
||||
elementEnd();
|
||||
}
|
||||
elementProperty(0, 'value', bind(ctx.value));
|
||||
OnPushComp.ngComponentDef.h(1, 0);
|
||||
directiveRefresh(1, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const app = renderComponent(OnPushApp);
|
||||
expect(getRenderedText(app)).toEqual('one');
|
||||
|
||||
onPushComp !.cdr.detach();
|
||||
|
||||
app.value = 'two';
|
||||
tick(app);
|
||||
expect(getRenderedText(app)).toEqual('one');
|
||||
|
||||
onPushComp !.cdr.reattach();
|
||||
|
||||
tick(app);
|
||||
expect(getRenderedText(app)).toEqual('two');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue