fix(ivy): mark views dirty by default when events fire (#28474)
In Ivy, we support a new manual mode that allows for stricter control over change detection in OnPush components. Specifically, in this mode, events do not automatically mark OnPush views as dirty. Only changed inputs and manual calls to `markDirty()` actually mark a view dirty. However, this mode cannot be the default for OnPush components if we want to be backwards compatible with View Engine. This commit re-adds the legacy logic for OnPush components where events always mark views dirty and makes it the default behavior. Note: It is still TODO to add a public API for manual change detection. PR Close #28474
This commit is contained in:

committed by
Matias Niemelä

parent
8930f60a4b
commit
5c4d95541e
@ -11,11 +11,12 @@ import {withBody} from '@angular/private/testing';
|
||||
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, DoCheck, RendererType2} from '../../src/core';
|
||||
import {whenRendered} from '../../src/render3/component';
|
||||
import {LifecycleHooksFeature, NgOnChangesFeature, defineComponent, defineDirective, getRenderedText, templateRefExtractor} from '../../src/render3/index';
|
||||
import {LifecycleHooksFeature, NgOnChangesFeature, defineComponent, defineDirective, getCurrentView, getRenderedText, templateRefExtractor} from '../../src/render3/index';
|
||||
|
||||
import {bind, container, containerRefreshEnd, containerRefreshStart, detectChanges, directiveInject, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation2, listener, markDirty, reference, text, template, textBinding, tick} from '../../src/render3/instructions';
|
||||
import {RenderFlags} from '../../src/render3/interfaces/definition';
|
||||
import {RElement, Renderer3, RendererFactory3} from '../../src/render3/interfaces/renderer';
|
||||
import {FLAGS, LViewFlags} from '../../src/render3/interfaces/view';
|
||||
|
||||
import {ComponentFixture, containerEl, createComponent, renderComponent, requestAnimationFrame} from './render_util';
|
||||
|
||||
@ -183,41 +184,39 @@ describe('change detection', () => {
|
||||
|
||||
myApp.name = 'Bess';
|
||||
tick(myApp);
|
||||
expect(comp.doCheckCount).toEqual(2);
|
||||
// View should update, as changed input marks view dirty
|
||||
expect(getRenderedText(myApp)).toEqual('2 - Bess');
|
||||
|
||||
myApp.name = 'George';
|
||||
tick(myApp);
|
||||
// View should update, as changed input marks view dirty
|
||||
expect(comp.doCheckCount).toEqual(3);
|
||||
expect(getRenderedText(myApp)).toEqual('3 - George');
|
||||
|
||||
tick(myApp);
|
||||
expect(comp.doCheckCount).toEqual(4);
|
||||
// View should not be updated to "4", as inputs have not changed.
|
||||
expect(getRenderedText(myApp)).toEqual('3 - George');
|
||||
});
|
||||
|
||||
it('should not check OnPush components in update mode when component events occur, unless marked dirty',
|
||||
() => {
|
||||
const myApp = renderComponent(MyApp);
|
||||
expect(comp.doCheckCount).toEqual(1);
|
||||
expect(getRenderedText(myApp)).toEqual('1 - Nancy');
|
||||
it('should check OnPush components in update mode when component events occur', () => {
|
||||
const myApp = renderComponent(MyApp);
|
||||
expect(comp.doCheckCount).toEqual(1);
|
||||
expect(getRenderedText(myApp)).toEqual('1 - Nancy');
|
||||
|
||||
const button = containerEl.querySelector('button') !;
|
||||
button.click();
|
||||
requestAnimationFrame.flush();
|
||||
// No ticks should have been scheduled.
|
||||
expect(comp.doCheckCount).toEqual(1);
|
||||
expect(getRenderedText(myApp)).toEqual('1 - Nancy');
|
||||
const button = containerEl.querySelector('button') !;
|
||||
button.click();
|
||||
requestAnimationFrame.flush();
|
||||
// No ticks should have been scheduled.
|
||||
expect(comp.doCheckCount).toEqual(1);
|
||||
expect(getRenderedText(myApp)).toEqual('1 - Nancy');
|
||||
|
||||
tick(myApp);
|
||||
// The comp should still be clean. So doCheck will run, but the view should display 1.
|
||||
expect(comp.doCheckCount).toEqual(2);
|
||||
expect(getRenderedText(myApp)).toEqual('1 - Nancy');
|
||||
|
||||
markDirty(comp);
|
||||
requestAnimationFrame.flush();
|
||||
// Now that markDirty has been manually called, the view should be dirty and a tick
|
||||
// should be scheduled to check the view.
|
||||
expect(comp.doCheckCount).toEqual(3);
|
||||
expect(getRenderedText(myApp)).toEqual('3 - Nancy');
|
||||
});
|
||||
tick(myApp);
|
||||
// Because the onPush comp should be dirty, it should update once CD runs
|
||||
expect(comp.doCheckCount).toEqual(2);
|
||||
expect(getRenderedText(myApp)).toEqual('2 - Nancy');
|
||||
});
|
||||
|
||||
it('should not check OnPush components in update mode when parent events occur', () => {
|
||||
function noop() {}
|
||||
@ -238,76 +237,230 @@ describe('change detection', () => {
|
||||
(button as HTMLButtonElement).click();
|
||||
tick(buttonParent);
|
||||
// The comp should still be clean. So doCheck will run, but the view should display 1.
|
||||
expect(comp.doCheckCount).toEqual(2);
|
||||
expect(getRenderedText(buttonParent)).toEqual('1 - Nancy');
|
||||
});
|
||||
|
||||
it('should not check parent OnPush components in update mode when child events occur, unless marked dirty',
|
||||
() => {
|
||||
let parent: ButtonParent;
|
||||
it('should check parent OnPush components in update mode when child events occur', () => {
|
||||
let parent: ButtonParent;
|
||||
|
||||
class ButtonParent implements DoCheck {
|
||||
doCheckCount = 0;
|
||||
ngDoCheck(): void { this.doCheckCount++; }
|
||||
class ButtonParent implements DoCheck {
|
||||
doCheckCount = 0;
|
||||
ngDoCheck(): void { this.doCheckCount++; }
|
||||
|
||||
static ngComponentDef = defineComponent({
|
||||
type: ButtonParent,
|
||||
selectors: [['button-parent']],
|
||||
factory: () => parent = new ButtonParent(),
|
||||
consts: 2,
|
||||
vars: 1,
|
||||
/** {{ doCheckCount }} - <my-comp></my-comp> */
|
||||
template: (rf: RenderFlags, ctx: ButtonParent) => {
|
||||
if (rf & RenderFlags.Create) {
|
||||
text(0);
|
||||
element(1, 'my-comp');
|
||||
}
|
||||
if (rf & RenderFlags.Update) {
|
||||
textBinding(0, interpolation1('', ctx.doCheckCount, ' - '));
|
||||
}
|
||||
},
|
||||
directives: () => [MyComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
});
|
||||
}
|
||||
static ngComponentDef = defineComponent({
|
||||
type: ButtonParent,
|
||||
selectors: [['button-parent']],
|
||||
factory: () => parent = new ButtonParent(),
|
||||
consts: 2,
|
||||
vars: 1,
|
||||
/** {{ doCheckCount }} - <my-comp></my-comp> */
|
||||
template: (rf: RenderFlags, ctx: ButtonParent) => {
|
||||
if (rf & RenderFlags.Create) {
|
||||
text(0);
|
||||
element(1, 'my-comp');
|
||||
}
|
||||
if (rf & RenderFlags.Update) {
|
||||
textBinding(0, interpolation1('', ctx.doCheckCount, ' - '));
|
||||
}
|
||||
},
|
||||
directives: () => [MyComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
});
|
||||
}
|
||||
|
||||
const MyButtonApp = createComponent('my-button-app', function(rf: RenderFlags, ctx: any) {
|
||||
if (rf & RenderFlags.Create) {
|
||||
element(0, 'button-parent');
|
||||
const MyButtonApp = createComponent('my-button-app', function(rf: RenderFlags, ctx: any) {
|
||||
if (rf & RenderFlags.Create) {
|
||||
element(0, 'button-parent');
|
||||
}
|
||||
}, 1, 0, [ButtonParent]);
|
||||
|
||||
const myButtonApp = renderComponent(MyButtonApp);
|
||||
expect(parent !.doCheckCount).toEqual(1);
|
||||
expect(comp !.doCheckCount).toEqual(1);
|
||||
expect(getRenderedText(myButtonApp)).toEqual('1 - 1 - Nancy');
|
||||
|
||||
tick(myButtonApp);
|
||||
expect(parent !.doCheckCount).toEqual(2);
|
||||
// parent isn't checked, so child doCheck won't run
|
||||
expect(comp !.doCheckCount).toEqual(1);
|
||||
expect(getRenderedText(myButtonApp)).toEqual('1 - 1 - Nancy');
|
||||
|
||||
const button = containerEl.querySelector('button');
|
||||
button !.click();
|
||||
requestAnimationFrame.flush();
|
||||
// No ticks should have been scheduled.
|
||||
expect(parent !.doCheckCount).toEqual(2);
|
||||
expect(comp !.doCheckCount).toEqual(1);
|
||||
|
||||
tick(myButtonApp);
|
||||
expect(parent !.doCheckCount).toEqual(3);
|
||||
expect(comp !.doCheckCount).toEqual(2);
|
||||
expect(getRenderedText(myButtonApp)).toEqual('3 - 2 - Nancy');
|
||||
});
|
||||
|
||||
describe('Manual mode', () => {
|
||||
class ManualComponent implements DoCheck {
|
||||
/* @Input() */
|
||||
name = 'Nancy';
|
||||
doCheckCount = 0;
|
||||
|
||||
ngDoCheck(): void { this.doCheckCount++; }
|
||||
|
||||
onClick() {}
|
||||
|
||||
static ngComponentDef = defineComponent({
|
||||
type: ManualComponent,
|
||||
selectors: [['manual-comp']],
|
||||
factory: () => comp = new ManualComponent(),
|
||||
consts: 2,
|
||||
vars: 2,
|
||||
/**
|
||||
* {{ doCheckCount }} - {{ name }}
|
||||
* <button (click)="onClick()"></button>
|
||||
*/
|
||||
template: (rf: RenderFlags, ctx: ManualComponent) => {
|
||||
if (rf & RenderFlags.Create) {
|
||||
// This is temporarily the only way to turn on manual change detection
|
||||
// because public API has not yet been added.
|
||||
const view = getCurrentView() as any;
|
||||
view[FLAGS] |= LViewFlags.ManualOnPush;
|
||||
|
||||
text(0);
|
||||
elementStart(1, 'button');
|
||||
{
|
||||
listener('click', () => { ctx.onClick(); });
|
||||
}
|
||||
elementEnd();
|
||||
}
|
||||
if (rf & RenderFlags.Update) {
|
||||
textBinding(0, interpolation2('', ctx.doCheckCount, ' - ', ctx.name, ''));
|
||||
}
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
inputs: {name: 'name'}
|
||||
});
|
||||
}
|
||||
|
||||
class ManualApp {
|
||||
name: string = 'Nancy';
|
||||
|
||||
static ngComponentDef = defineComponent({
|
||||
type: ManualApp,
|
||||
selectors: [['manual-app']],
|
||||
factory: () => new ManualApp(),
|
||||
consts: 1,
|
||||
vars: 1,
|
||||
/** <manual-comp [name]="name"></manual-comp> */
|
||||
template: (rf: RenderFlags, ctx: ManualApp) => {
|
||||
if (rf & RenderFlags.Create) {
|
||||
element(0, 'manual-comp');
|
||||
}
|
||||
if (rf & RenderFlags.Update) {
|
||||
elementProperty(0, 'name', bind(ctx.name));
|
||||
}
|
||||
|
||||
},
|
||||
directives: () => [ManualComponent]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
it('should not check OnPush components in update mode when component events occur, unless marked dirty',
|
||||
() => {
|
||||
const myApp = renderComponent(ManualApp);
|
||||
expect(comp.doCheckCount).toEqual(1);
|
||||
expect(getRenderedText(myApp)).toEqual('1 - Nancy');
|
||||
|
||||
const button = containerEl.querySelector('button') !;
|
||||
button.click();
|
||||
requestAnimationFrame.flush();
|
||||
// No ticks should have been scheduled.
|
||||
expect(comp.doCheckCount).toEqual(1);
|
||||
expect(getRenderedText(myApp)).toEqual('1 - Nancy');
|
||||
|
||||
tick(myApp);
|
||||
// The comp should still be clean. So doCheck will run, but the view should display 1.
|
||||
expect(comp.doCheckCount).toEqual(2);
|
||||
expect(getRenderedText(myApp)).toEqual('1 - Nancy');
|
||||
|
||||
markDirty(comp);
|
||||
requestAnimationFrame.flush();
|
||||
// Now that markDirty has been manually called, the view should be dirty and a tick
|
||||
// should be scheduled to check the view.
|
||||
expect(comp.doCheckCount).toEqual(3);
|
||||
expect(getRenderedText(myApp)).toEqual('3 - Nancy');
|
||||
});
|
||||
|
||||
it('should not check parent OnPush components in update mode when child events occur, unless marked dirty',
|
||||
() => {
|
||||
let parent: ButtonParent;
|
||||
|
||||
class ButtonParent implements DoCheck {
|
||||
doCheckCount = 0;
|
||||
ngDoCheck(): void { this.doCheckCount++; }
|
||||
|
||||
static ngComponentDef = defineComponent({
|
||||
type: ButtonParent,
|
||||
selectors: [['button-parent']],
|
||||
factory: () => parent = new ButtonParent(),
|
||||
consts: 2,
|
||||
vars: 1,
|
||||
/** {{ doCheckCount }} - <manual-comp></manual-comp> */
|
||||
template: (rf: RenderFlags, ctx: ButtonParent) => {
|
||||
if (rf & RenderFlags.Create) {
|
||||
text(0);
|
||||
element(1, 'manual-comp');
|
||||
}
|
||||
if (rf & RenderFlags.Update) {
|
||||
textBinding(0, interpolation1('', ctx.doCheckCount, ' - '));
|
||||
}
|
||||
},
|
||||
directives: () => [ManualComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
});
|
||||
}
|
||||
}, 1, 0, [ButtonParent]);
|
||||
|
||||
const myButtonApp = renderComponent(MyButtonApp);
|
||||
expect(parent !.doCheckCount).toEqual(1);
|
||||
expect(comp !.doCheckCount).toEqual(1);
|
||||
expect(getRenderedText(myButtonApp)).toEqual('1 - 1 - Nancy');
|
||||
const MyButtonApp =
|
||||
createComponent('my-button-app', function(rf: RenderFlags, ctx: any) {
|
||||
if (rf & RenderFlags.Create) {
|
||||
element(0, 'button-parent');
|
||||
}
|
||||
}, 1, 0, [ButtonParent]);
|
||||
|
||||
tick(myButtonApp);
|
||||
expect(parent !.doCheckCount).toEqual(2);
|
||||
// parent isn't checked, so child doCheck won't run
|
||||
expect(comp !.doCheckCount).toEqual(1);
|
||||
expect(getRenderedText(myButtonApp)).toEqual('1 - 1 - Nancy');
|
||||
const myButtonApp = renderComponent(MyButtonApp);
|
||||
expect(parent !.doCheckCount).toEqual(1);
|
||||
expect(comp !.doCheckCount).toEqual(1);
|
||||
expect(getRenderedText(myButtonApp)).toEqual('1 - 1 - Nancy');
|
||||
|
||||
const button = containerEl.querySelector('button');
|
||||
button !.click();
|
||||
requestAnimationFrame.flush();
|
||||
// No ticks should have been scheduled.
|
||||
expect(parent !.doCheckCount).toEqual(2);
|
||||
expect(comp !.doCheckCount).toEqual(1);
|
||||
tick(myButtonApp);
|
||||
expect(parent !.doCheckCount).toEqual(2);
|
||||
// parent isn't checked, so child doCheck won't run
|
||||
expect(comp !.doCheckCount).toEqual(1);
|
||||
expect(getRenderedText(myButtonApp)).toEqual('1 - 1 - Nancy');
|
||||
|
||||
tick(myButtonApp);
|
||||
expect(parent !.doCheckCount).toEqual(3);
|
||||
// parent isn't checked, so child doCheck won't run
|
||||
expect(comp !.doCheckCount).toEqual(1);
|
||||
expect(getRenderedText(myButtonApp)).toEqual('1 - 1 - Nancy');
|
||||
const button = containerEl.querySelector('button');
|
||||
button !.click();
|
||||
requestAnimationFrame.flush();
|
||||
// No ticks should have been scheduled.
|
||||
expect(parent !.doCheckCount).toEqual(2);
|
||||
expect(comp !.doCheckCount).toEqual(1);
|
||||
|
||||
markDirty(comp);
|
||||
requestAnimationFrame.flush();
|
||||
// Now that markDirty has been manually called, both views should be dirty and a tick
|
||||
// should be scheduled to check the view.
|
||||
expect(parent !.doCheckCount).toEqual(4);
|
||||
expect(comp !.doCheckCount).toEqual(2);
|
||||
expect(getRenderedText(myButtonApp)).toEqual('4 - 2 - Nancy');
|
||||
});
|
||||
tick(myButtonApp);
|
||||
expect(parent !.doCheckCount).toEqual(3);
|
||||
// parent isn't checked, so child doCheck won't run
|
||||
expect(comp !.doCheckCount).toEqual(1);
|
||||
expect(getRenderedText(myButtonApp)).toEqual('1 - 1 - Nancy');
|
||||
|
||||
markDirty(comp);
|
||||
requestAnimationFrame.flush();
|
||||
// Now that markDirty has been manually called, both views should be dirty and a tick
|
||||
// should be scheduled to check the view.
|
||||
expect(parent !.doCheckCount).toEqual(4);
|
||||
expect(comp !.doCheckCount).toEqual(2);
|
||||
expect(getRenderedText(myButtonApp)).toEqual('4 - 2 - Nancy');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ChangeDetectorRef', () => {
|
||||
|
Reference in New Issue
Block a user