feat(ivy): support ChangeDetectorRef.detectChanges (#22614)
PR Close #22614
This commit is contained in:
parent
d485346d3c
commit
4c089c1d93
|
@ -13,7 +13,7 @@ import {ComponentRef as viewEngine_ComponentRef} from '../linker/component_facto
|
||||||
|
|
||||||
import {assertNotNull} from './assert';
|
import {assertNotNull} from './assert';
|
||||||
import {queueLifecycleHooks} from './hooks';
|
import {queueLifecycleHooks} from './hooks';
|
||||||
import {CLEAN_PROMISE, _getComponentHostLElementNode, createLView, createTView, detectChanges, directiveCreate, enterView, getDirectiveInstance, hostElement, initChangeDetectorIfExisting, leaveView, locateHostElement, scheduleChangeDetection} from './instructions';
|
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 {ComponentDef, ComponentType} from './interfaces/definition';
|
||||||
import {RElement, RendererFactory3, domRendererFactory3} from './interfaces/renderer';
|
import {RElement, RendererFactory3, domRendererFactory3} from './interfaces/renderer';
|
||||||
import {LViewFlags, RootContext} from './interfaces/view';
|
import {LViewFlags, RootContext} from './interfaces/view';
|
||||||
|
@ -43,12 +43,12 @@ export interface CreateComponentOptions {
|
||||||
* Typically, the features in this list are features that cannot be added to the
|
* Typically, the features in this list are features that cannot be added to the
|
||||||
* other features list in the component definition because they rely on other factors.
|
* other features list in the component definition because they rely on other factors.
|
||||||
*
|
*
|
||||||
* Example: RootLifecycleHooks is a function that adds lifecycle hook capabilities
|
* Example: `RootLifecycleHooks` is a function that adds lifecycle hook capabilities
|
||||||
* to root components in a tree-shakable way. It cannot be added to the component
|
* to root components in a tree-shakable way. It cannot be added to the component
|
||||||
* features list because there's no way of knowing when the component will be used as
|
* features list because there's no way of knowing when the component will be used as
|
||||||
* a root component.
|
* a root component.
|
||||||
*/
|
*/
|
||||||
features?: (<T>(component: T, componentDef: ComponentDef<T>) => void)[];
|
hostFeatures?: (<T>(component: T, componentDef: ComponentDef<T>) => void)[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A function which is used to schedule change detection work in the future.
|
* A function which is used to schedule change detection work in the future.
|
||||||
|
@ -141,12 +141,11 @@ export function renderComponent<T>(
|
||||||
enterView(oldView, null);
|
enterView(oldView, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
opts.features && opts.features.forEach((feature) => feature(component, componentDef));
|
opts.hostFeatures && opts.hostFeatures.forEach((feature) => feature(component, componentDef));
|
||||||
detectChanges(component);
|
tick(component);
|
||||||
return component;
|
return component;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to enable lifecycle hooks on the root component.
|
* Used to enable lifecycle hooks on the root component.
|
||||||
*
|
*
|
||||||
|
@ -156,9 +155,11 @@ export function renderComponent<T>(
|
||||||
*
|
*
|
||||||
* Example:
|
* Example:
|
||||||
*
|
*
|
||||||
|
* ```
|
||||||
* renderComponent(AppComponent, {features: [RootLifecycleHooks]});
|
* renderComponent(AppComponent, {features: [RootLifecycleHooks]});
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
export function RootLifecycleHooks(component: any, def: ComponentDef<any>): void {
|
export function LifecycleHooksFeature(component: any, def: ComponentDef<any>): void {
|
||||||
const elementNode = _getComponentHostLElementNode(component);
|
const elementNode = _getComponentHostLElementNode(component);
|
||||||
queueLifecycleHooks(elementNode.flags, elementNode.view);
|
queueLifecycleHooks(elementNode.flags, elementNode.view);
|
||||||
}
|
}
|
||||||
|
@ -170,13 +171,7 @@ export function RootLifecycleHooks(component: any, def: ComponentDef<any>): void
|
||||||
* @param component any component
|
* @param component any component
|
||||||
*/
|
*/
|
||||||
function getRootContext(component: any): RootContext {
|
function getRootContext(component: any): RootContext {
|
||||||
ngDevMode && assertNotNull(component, 'component');
|
const rootContext = getRootView(component).context as RootContext;
|
||||||
const lElementNode = _getComponentHostLElementNode(component);
|
|
||||||
let lView = lElementNode.view;
|
|
||||||
while (lView.parent) {
|
|
||||||
lView = lView.parent;
|
|
||||||
}
|
|
||||||
const rootContext = lView.context as RootContext;
|
|
||||||
ngDevMode && assertNotNull(rootContext, 'rootContext');
|
ngDevMode && assertNotNull(rootContext, 'rootContext');
|
||||||
return rootContext;
|
return rootContext;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {RootLifecycleHooks, createComponentRef, getHostElement, getRenderedText, renderComponent, whenRendered} from './component';
|
import {LifecycleHooksFeature, createComponentRef, getHostElement, getRenderedText, renderComponent, whenRendered} from './component';
|
||||||
import {NgOnChangesFeature, PublicFeature, defineComponent, defineDirective, definePipe} from './definition';
|
import {NgOnChangesFeature, PublicFeature, defineComponent, defineDirective, definePipe} from './definition';
|
||||||
import {InjectFlags} from './di';
|
import {InjectFlags} from './di';
|
||||||
import {ComponentDef, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFlags, DirectiveType} from './interfaces/definition';
|
import {ComponentDef, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFlags, DirectiveType} from './interfaces/definition';
|
||||||
|
@ -109,7 +109,7 @@ export {
|
||||||
DirectiveType,
|
DirectiveType,
|
||||||
NgOnChangesFeature,
|
NgOnChangesFeature,
|
||||||
PublicFeature,
|
PublicFeature,
|
||||||
RootLifecycleHooks,
|
LifecycleHooksFeature,
|
||||||
defineComponent,
|
defineComponent,
|
||||||
defineDirective,
|
defineDirective,
|
||||||
definePipe,
|
definePipe,
|
||||||
|
|
|
@ -1284,14 +1284,7 @@ export function directiveRefresh<T>(directiveIndex: number, elementIndex: number
|
||||||
// Only CheckAlways components or dirty OnPush components should be checked
|
// Only CheckAlways components or dirty OnPush components should be checked
|
||||||
if (hostView.flags & (LViewFlags.CheckAlways | LViewFlags.Dirty)) {
|
if (hostView.flags & (LViewFlags.CheckAlways | LViewFlags.Dirty)) {
|
||||||
ngDevMode && assertDataInRange(directiveIndex);
|
ngDevMode && assertDataInRange(directiveIndex);
|
||||||
const directive = getDirectiveInstance<T>(data[directiveIndex]);
|
detectChangesInternal(hostView, element, getDirectiveInstance<T>(data[directiveIndex]));
|
||||||
const oldView = enterView(hostView, element);
|
|
||||||
try {
|
|
||||||
template(directive, creationMode);
|
|
||||||
} finally {
|
|
||||||
refreshDynamicChildren();
|
|
||||||
leaveView(oldView);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1511,23 +1504,71 @@ function markViewDirty(view: LView): void {
|
||||||
currentView.flags |= LViewFlags.Dirty;
|
currentView.flags |= LViewFlags.Dirty;
|
||||||
|
|
||||||
ngDevMode && assertNotNull(currentView !.context, 'rootContext');
|
ngDevMode && assertNotNull(currentView !.context, 'rootContext');
|
||||||
scheduleChangeDetection(currentView !.context as RootContext);
|
scheduleTick(currentView !.context as RootContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Given a root context, schedules change detection at that root. */
|
/**
|
||||||
export function scheduleChangeDetection<T>(rootContext: RootContext) {
|
* Used to schedule change detection on the whole application.
|
||||||
|
*
|
||||||
|
* Unlike `tick`, `scheduleTick` coalesces multiple calls into one change detection run.
|
||||||
|
* It is usually called indirectly by calling `markDirty` when the view needs to be
|
||||||
|
* re-rendered.
|
||||||
|
*
|
||||||
|
* Typically `scheduleTick` uses `requestAnimationFrame` to coalesce multiple
|
||||||
|
* `scheduleTick` requests. The scheduling function can be overridden in
|
||||||
|
* `renderComponent`'s `scheduler` option.
|
||||||
|
*/
|
||||||
|
export function scheduleTick<T>(rootContext: RootContext) {
|
||||||
if (rootContext.clean == _CLEAN_PROMISE) {
|
if (rootContext.clean == _CLEAN_PROMISE) {
|
||||||
let res: null|((val: null) => void);
|
let res: null|((val: null) => void);
|
||||||
rootContext.clean = new Promise<null>((r) => res = r);
|
rootContext.clean = new Promise<null>((r) => res = r);
|
||||||
rootContext.scheduler(() => {
|
rootContext.scheduler(() => {
|
||||||
detectChanges(rootContext.component);
|
tick(rootContext.component);
|
||||||
res !(null);
|
res !(null);
|
||||||
rootContext.clean = _CLEAN_PROMISE;
|
rootContext.clean = _CLEAN_PROMISE;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to perform change detection on the whole application.
|
||||||
|
*
|
||||||
|
* This is equivalent to `detectChanges`, but invoked on root component. Additionally, `tick`
|
||||||
|
* executes lifecycle hooks and conditionally checks components based on their
|
||||||
|
* `ChangeDetectionStrategy` and dirtiness.
|
||||||
|
*
|
||||||
|
* The preferred way to trigger change detection is to call `markDirty`. `markDirty` internally
|
||||||
|
* schedules `tick` using a scheduler in order to coalesce multiple `markDirty` calls into a
|
||||||
|
* single change detection run. By default, the scheduler is `requestAnimationFrame`, but can
|
||||||
|
* be changed when calling `renderComponent` and providing the `scheduler` option.
|
||||||
|
*/
|
||||||
|
export function tick<T>(component: T): void {
|
||||||
|
const rootView = getRootView(component);
|
||||||
|
const rootComponent = (rootView.context as RootContext).component;
|
||||||
|
const hostNode = _getComponentHostLElementNode(rootComponent);
|
||||||
|
|
||||||
|
ngDevMode && assertNotNull(hostNode.data, 'Component host node should be attached to an LView');
|
||||||
|
renderComponentOrTemplate(hostNode, rootView, rootComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the root view from any component by walking the parent `LView` until
|
||||||
|
* reaching the root `LView`.
|
||||||
|
*
|
||||||
|
* @param component any component
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function getRootView(component: any): LView {
|
||||||
|
ngDevMode && assertNotNull(component, 'component');
|
||||||
|
const lElementNode = _getComponentHostLElementNode(component);
|
||||||
|
let lView = lElementNode.view;
|
||||||
|
while (lView.parent) {
|
||||||
|
lView = lView.parent;
|
||||||
|
}
|
||||||
|
return lView;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronously perform change detection on a component (and possibly its sub-components).
|
* Synchronously perform change detection on a component (and possibly its sub-components).
|
||||||
*
|
*
|
||||||
|
@ -1544,7 +1585,24 @@ export function scheduleChangeDetection<T>(rootContext: RootContext) {
|
||||||
export function detectChanges<T>(component: T): void {
|
export function detectChanges<T>(component: T): void {
|
||||||
const hostNode = _getComponentHostLElementNode(component);
|
const hostNode = _getComponentHostLElementNode(component);
|
||||||
ngDevMode && assertNotNull(hostNode.data, 'Component host node should be attached to an LView');
|
ngDevMode && assertNotNull(hostNode.data, 'Component host node should be attached to an LView');
|
||||||
renderComponentOrTemplate(hostNode, hostNode.view, component);
|
detectChangesInternal(hostNode.data as LView, hostNode, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Checks the view of the component provided. Does not gate on dirty checks or execute doCheck. */
|
||||||
|
function detectChangesInternal<T>(hostView: LView, hostNode: LElementNode, component: T) {
|
||||||
|
const componentIndex = hostNode.flags >> LNodeFlags.INDX_SHIFT;
|
||||||
|
const template = (hostNode.view.tView.data[componentIndex] as ComponentDef<T>).template;
|
||||||
|
const oldView = enterView(hostView, hostNode);
|
||||||
|
|
||||||
|
if (template != null) {
|
||||||
|
try {
|
||||||
|
template(component, creationMode);
|
||||||
|
} finally {
|
||||||
|
refreshDynamicChildren();
|
||||||
|
leaveView(oldView);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, ViewRef as viewEngine_ViewRef} from '../linker/view_ref';
|
import {EmbeddedViewRef as viewEngine_EmbeddedViewRef} from '../linker/view_ref';
|
||||||
|
import {detectChanges} from './instructions';
|
||||||
import {ComponentTemplate} from './interfaces/definition';
|
import {ComponentTemplate} from './interfaces/definition';
|
||||||
import {LViewNode} from './interfaces/node';
|
import {LViewNode} from './interfaces/node';
|
||||||
import {notImplemented} from './util';
|
import {notImplemented} from './util';
|
||||||
|
@ -26,7 +26,9 @@ export class ViewRef<T> implements viewEngine_EmbeddedViewRef<T> {
|
||||||
onDestroy(callback: Function) { notImplemented(); }
|
onDestroy(callback: Function) { notImplemented(); }
|
||||||
markForCheck(): void { notImplemented(); }
|
markForCheck(): void { notImplemented(); }
|
||||||
detach(): void { notImplemented(); }
|
detach(): void { notImplemented(); }
|
||||||
detectChanges(): void { notImplemented(); }
|
|
||||||
|
detectChanges(): void { detectChanges(this.context); }
|
||||||
|
|
||||||
checkNoChanges(): void { notImplemented(); }
|
checkNoChanges(): void { notImplemented(); }
|
||||||
reattach(): void { notImplemented(); }
|
reattach(): void { notImplemented(); }
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,9 @@
|
||||||
{
|
{
|
||||||
"name": "__window$1"
|
"name": "__window$1"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "_getComponentHostLElementNode"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "_renderCompCount"
|
"name": "_renderCompCount"
|
||||||
},
|
},
|
||||||
|
@ -41,9 +44,6 @@
|
||||||
{
|
{
|
||||||
"name": "currentView"
|
"name": "currentView"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "detectChanges"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "domRendererFactory3"
|
"name": "domRendererFactory3"
|
||||||
},
|
},
|
||||||
|
@ -80,9 +80,21 @@
|
||||||
{
|
{
|
||||||
"name": "noop$2"
|
"name": "noop$2"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "queueContentHooks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "queueDestroyHooks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "queueViewHooks"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "refreshDynamicChildren"
|
"name": "refreshDynamicChildren"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "renderComponentOrTemplate"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "renderEmbeddedTemplate"
|
"name": "renderEmbeddedTemplate"
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,217 +6,577 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ChangeDetectionStrategy, DoCheck} from '../../src/core';
|
import {withBody} from '@angular/core/testing';
|
||||||
import {getRenderedText} from '../../src/render3/component';
|
|
||||||
import {defineComponent} from '../../src/render3/index';
|
|
||||||
import {bind, detectChanges, directiveRefresh, elementEnd, elementProperty, elementStart, interpolation1, interpolation2, listener, text, textBinding} from '../../src/render3/instructions';
|
|
||||||
import {containerEl, renderComponent, requestAnimationFrame} from './render_util';
|
|
||||||
|
|
||||||
describe('OnPush change detection', () => {
|
import {ChangeDetectionStrategy, ChangeDetectorRef, DoCheck, EmbeddedViewRef, TemplateRef, ViewContainerRef} from '../../src/core';
|
||||||
let comp: MyComponent;
|
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';
|
||||||
|
|
||||||
class MyComponent implements DoCheck {
|
import {containerEl, renderComponent, requestAnimationFrame, toHtml} from './render_util';
|
||||||
/* @Input() */
|
|
||||||
name = 'Nancy';
|
|
||||||
doCheckCount = 0;
|
|
||||||
|
|
||||||
ngDoCheck(): void { this.doCheckCount++; }
|
describe('change detection', () => {
|
||||||
|
|
||||||
onClick() {}
|
describe('markDirty, detectChanges, whenRendered, getRenderedText', () => {
|
||||||
|
class MyComponent implements DoCheck {
|
||||||
static ngComponentDef = defineComponent({
|
value: string = 'works';
|
||||||
type: MyComponent,
|
doCheckCount = 0;
|
||||||
tag: 'my-comp',
|
ngDoCheck(): void { this.doCheckCount++; }
|
||||||
factory: () => comp = new MyComponent(),
|
|
||||||
/**
|
|
||||||
* {{ doCheckCount }} - {{ name }}
|
|
||||||
* <button (click)="onClick()"></button>
|
|
||||||
*/
|
|
||||||
template: (ctx: MyComponent, cm: boolean) => {
|
|
||||||
if (cm) {
|
|
||||||
text(0);
|
|
||||||
elementStart(1, 'button');
|
|
||||||
{
|
|
||||||
listener('click', () => { ctx.onClick(); });
|
|
||||||
}
|
|
||||||
elementEnd();
|
|
||||||
}
|
|
||||||
textBinding(0, interpolation2('', ctx.doCheckCount, ' - ', ctx.name, ''));
|
|
||||||
},
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
inputs: {name: 'name'}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class MyApp {
|
|
||||||
name: string = 'Nancy';
|
|
||||||
|
|
||||||
static ngComponentDef = defineComponent({
|
|
||||||
type: MyApp,
|
|
||||||
tag: 'my-app',
|
|
||||||
factory: () => new MyApp(),
|
|
||||||
/** <my-comp [name]="name"></my-comp> */
|
|
||||||
template: (ctx: MyApp, cm: boolean) => {
|
|
||||||
if (cm) {
|
|
||||||
elementStart(0, MyComponent);
|
|
||||||
elementEnd();
|
|
||||||
}
|
|
||||||
elementProperty(0, 'name', bind(ctx.name));
|
|
||||||
MyComponent.ngComponentDef.h(1, 0);
|
|
||||||
directiveRefresh(1, 0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
it('should check OnPush components on initialization', () => {
|
|
||||||
const myApp = renderComponent(MyApp);
|
|
||||||
expect(getRenderedText(myApp)).toEqual('1 - Nancy');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call doCheck even when OnPush components are not dirty', () => {
|
|
||||||
const myApp = renderComponent(MyApp);
|
|
||||||
|
|
||||||
detectChanges(myApp);
|
|
||||||
expect(comp.doCheckCount).toEqual(2);
|
|
||||||
|
|
||||||
detectChanges(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);
|
|
||||||
// doCheckCount is 2, but 1 should be rendered since it has not been marked dirty.
|
|
||||||
expect(getRenderedText(myApp)).toEqual('1 - Nancy');
|
|
||||||
|
|
||||||
detectChanges(myApp);
|
|
||||||
// doCheckCount is 3, but 1 should be rendered since it has not been marked dirty.
|
|
||||||
expect(getRenderedText(myApp)).toEqual('1 - Nancy');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should check OnPush components in update mode when inputs change', () => {
|
|
||||||
const myApp = renderComponent(MyApp);
|
|
||||||
|
|
||||||
myApp.name = 'Bess';
|
|
||||||
detectChanges(myApp);
|
|
||||||
expect(getRenderedText(myApp)).toEqual('2 - Bess');
|
|
||||||
|
|
||||||
myApp.name = 'George';
|
|
||||||
detectChanges(myApp);
|
|
||||||
expect(getRenderedText(myApp)).toEqual('3 - George');
|
|
||||||
|
|
||||||
detectChanges(myApp);
|
|
||||||
expect(getRenderedText(myApp)).toEqual('3 - George');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should check OnPush components in update mode when component events occur', () => {
|
|
||||||
const myApp = renderComponent(MyApp);
|
|
||||||
expect(getRenderedText(myApp)).toEqual('1 - Nancy');
|
|
||||||
|
|
||||||
const button = containerEl.querySelector('button') !;
|
|
||||||
button.click();
|
|
||||||
requestAnimationFrame.flush();
|
|
||||||
expect(getRenderedText(myApp)).toEqual('2 - Nancy');
|
|
||||||
|
|
||||||
detectChanges(myApp);
|
|
||||||
expect(getRenderedText(myApp)).toEqual('2 - Nancy');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not check OnPush components in update mode when parent events occur', () => {
|
|
||||||
class ButtonParent {
|
|
||||||
noop() {}
|
|
||||||
|
|
||||||
static ngComponentDef = defineComponent({
|
static ngComponentDef = defineComponent({
|
||||||
type: ButtonParent,
|
type: MyComponent,
|
||||||
tag: 'button-parent',
|
tag: 'my-comp',
|
||||||
factory: () => new ButtonParent(),
|
factory: () => new MyComponent(),
|
||||||
|
template: (ctx: MyComponent, cm: boolean) => {
|
||||||
|
if (cm) {
|
||||||
|
elementStart(0, 'span');
|
||||||
|
text(1);
|
||||||
|
elementEnd();
|
||||||
|
}
|
||||||
|
textBinding(1, bind(ctx.value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should mark a component dirty and schedule change detection', withBody('my-comp', () => {
|
||||||
|
const myComp = renderComponent(MyComponent);
|
||||||
|
expect(getRenderedText(myComp)).toEqual('works');
|
||||||
|
myComp.value = 'updated';
|
||||||
|
markDirty(myComp);
|
||||||
|
expect(getRenderedText(myComp)).toEqual('works');
|
||||||
|
requestAnimationFrame.flush();
|
||||||
|
expect(getRenderedText(myComp)).toEqual('updated');
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should detectChanges on a component', withBody('my-comp', () => {
|
||||||
|
const myComp = renderComponent(MyComponent);
|
||||||
|
expect(getRenderedText(myComp)).toEqual('works');
|
||||||
|
myComp.value = 'updated';
|
||||||
|
detectChanges(myComp);
|
||||||
|
expect(getRenderedText(myComp)).toEqual('updated');
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should detectChanges only once if markDirty is called multiple times',
|
||||||
|
withBody('my-comp', () => {
|
||||||
|
const myComp = renderComponent(MyComponent);
|
||||||
|
expect(getRenderedText(myComp)).toEqual('works');
|
||||||
|
expect(myComp.doCheckCount).toBe(1);
|
||||||
|
myComp.value = 'ignore';
|
||||||
|
markDirty(myComp);
|
||||||
|
myComp.value = 'updated';
|
||||||
|
markDirty(myComp);
|
||||||
|
expect(getRenderedText(myComp)).toEqual('works');
|
||||||
|
requestAnimationFrame.flush();
|
||||||
|
expect(getRenderedText(myComp)).toEqual('updated');
|
||||||
|
expect(myComp.doCheckCount).toBe(2);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should notify whenRendered', withBody('my-comp', async() => {
|
||||||
|
const myComp = renderComponent(MyComponent);
|
||||||
|
await whenRendered(myComp);
|
||||||
|
myComp.value = 'updated';
|
||||||
|
markDirty(myComp);
|
||||||
|
setTimeout(requestAnimationFrame.flush, 0);
|
||||||
|
await whenRendered(myComp);
|
||||||
|
expect(getRenderedText(myComp)).toEqual('updated');
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onPush', () => {
|
||||||
|
let comp: MyComponent;
|
||||||
|
|
||||||
|
class MyComponent implements DoCheck {
|
||||||
|
/* @Input() */
|
||||||
|
name = 'Nancy';
|
||||||
|
doCheckCount = 0;
|
||||||
|
|
||||||
|
ngDoCheck(): void { this.doCheckCount++; }
|
||||||
|
|
||||||
|
onClick() {}
|
||||||
|
|
||||||
|
static ngComponentDef = defineComponent({
|
||||||
|
type: MyComponent,
|
||||||
|
tag: 'my-comp',
|
||||||
|
factory: () => comp = new MyComponent(),
|
||||||
/**
|
/**
|
||||||
* <my-comp></my-comp>
|
* {{ doCheckCount }} - {{ name }}
|
||||||
* <button id="parent" (click)="noop()"></button>
|
* <button (click)="onClick()"></button>
|
||||||
*/
|
*/
|
||||||
template: (ctx: ButtonParent, cm: boolean) => {
|
template: (ctx: MyComponent, cm: boolean) => {
|
||||||
|
if (cm) {
|
||||||
|
text(0);
|
||||||
|
elementStart(1, 'button');
|
||||||
|
{
|
||||||
|
listener('click', () => { ctx.onClick(); });
|
||||||
|
}
|
||||||
|
elementEnd();
|
||||||
|
}
|
||||||
|
textBinding(0, interpolation2('', ctx.doCheckCount, ' - ', ctx.name, ''));
|
||||||
|
},
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
inputs: {name: 'name'}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyApp {
|
||||||
|
name: string = 'Nancy';
|
||||||
|
|
||||||
|
static ngComponentDef = defineComponent({
|
||||||
|
type: MyApp,
|
||||||
|
tag: 'my-app',
|
||||||
|
factory: () => new MyApp(),
|
||||||
|
/** <my-comp [name]="name"></my-comp> */
|
||||||
|
template: (ctx: MyApp, cm: boolean) => {
|
||||||
if (cm) {
|
if (cm) {
|
||||||
elementStart(0, MyComponent);
|
elementStart(0, MyComponent);
|
||||||
elementEnd();
|
elementEnd();
|
||||||
elementStart(2, 'button', ['id', 'parent']);
|
|
||||||
{ listener('click', () => ctx.noop()); }
|
|
||||||
elementEnd();
|
|
||||||
}
|
}
|
||||||
|
elementProperty(0, 'name', bind(ctx.name));
|
||||||
MyComponent.ngComponentDef.h(1, 0);
|
MyComponent.ngComponentDef.h(1, 0);
|
||||||
directiveRefresh(1, 0);
|
directiveRefresh(1, 0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const buttonParent = renderComponent(ButtonParent);
|
|
||||||
expect(getRenderedText(buttonParent)).toEqual('1 - Nancy');
|
|
||||||
|
|
||||||
const button = containerEl.querySelector('button#parent') !;
|
it('should check OnPush components on initialization', () => {
|
||||||
(button as HTMLButtonElement).click();
|
const myApp = renderComponent(MyApp);
|
||||||
requestAnimationFrame.flush();
|
expect(getRenderedText(myApp)).toEqual('1 - Nancy');
|
||||||
expect(getRenderedText(buttonParent)).toEqual('1 - Nancy');
|
});
|
||||||
|
|
||||||
|
it('should call doCheck even when OnPush components are not dirty', () => {
|
||||||
|
const myApp = renderComponent(MyApp);
|
||||||
|
|
||||||
|
detectChanges(myApp);
|
||||||
|
expect(comp.doCheckCount).toEqual(2);
|
||||||
|
|
||||||
|
detectChanges(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);
|
||||||
|
// doCheckCount is 2, but 1 should be rendered since it has not been marked dirty.
|
||||||
|
expect(getRenderedText(myApp)).toEqual('1 - Nancy');
|
||||||
|
|
||||||
|
detectChanges(myApp);
|
||||||
|
// doCheckCount is 3, but 1 should be rendered since it has not been marked dirty.
|
||||||
|
expect(getRenderedText(myApp)).toEqual('1 - Nancy');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check OnPush components in update mode when inputs change', () => {
|
||||||
|
const myApp = renderComponent(MyApp);
|
||||||
|
|
||||||
|
myApp.name = 'Bess';
|
||||||
|
detectChanges(myApp);
|
||||||
|
expect(getRenderedText(myApp)).toEqual('2 - Bess');
|
||||||
|
|
||||||
|
myApp.name = 'George';
|
||||||
|
detectChanges(myApp);
|
||||||
|
expect(getRenderedText(myApp)).toEqual('3 - George');
|
||||||
|
|
||||||
|
detectChanges(myApp);
|
||||||
|
expect(getRenderedText(myApp)).toEqual('3 - George');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check OnPush components in update mode when component events occur', () => {
|
||||||
|
const myApp = renderComponent(MyApp);
|
||||||
|
expect(getRenderedText(myApp)).toEqual('1 - Nancy');
|
||||||
|
|
||||||
|
const button = containerEl.querySelector('button') !;
|
||||||
|
button.click();
|
||||||
|
requestAnimationFrame.flush();
|
||||||
|
expect(getRenderedText(myApp)).toEqual('2 - Nancy');
|
||||||
|
|
||||||
|
detectChanges(myApp);
|
||||||
|
expect(getRenderedText(myApp)).toEqual('2 - Nancy');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not check OnPush components in update mode when parent events occur', () => {
|
||||||
|
class ButtonParent {
|
||||||
|
noop() {}
|
||||||
|
|
||||||
|
static ngComponentDef = defineComponent({
|
||||||
|
type: ButtonParent,
|
||||||
|
tag: 'button-parent',
|
||||||
|
factory: () => new ButtonParent(),
|
||||||
|
/**
|
||||||
|
* <my-comp></my-comp>
|
||||||
|
* <button id="parent" (click)="noop()"></button>
|
||||||
|
*/
|
||||||
|
template: (ctx: ButtonParent, cm: boolean) => {
|
||||||
|
if (cm) {
|
||||||
|
elementStart(0, MyComponent);
|
||||||
|
elementEnd();
|
||||||
|
elementStart(2, 'button', ['id', 'parent']);
|
||||||
|
{ listener('click', () => ctx.noop()); }
|
||||||
|
elementEnd();
|
||||||
|
}
|
||||||
|
MyComponent.ngComponentDef.h(1, 0);
|
||||||
|
directiveRefresh(1, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const buttonParent = renderComponent(ButtonParent);
|
||||||
|
expect(getRenderedText(buttonParent)).toEqual('1 - Nancy');
|
||||||
|
|
||||||
|
const button = containerEl.querySelector('button#parent') !;
|
||||||
|
(button as HTMLButtonElement).click();
|
||||||
|
requestAnimationFrame.flush();
|
||||||
|
expect(getRenderedText(buttonParent)).toEqual('1 - Nancy');
|
||||||
|
});
|
||||||
|
|
||||||
|
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++; }
|
||||||
|
|
||||||
|
static ngComponentDef = defineComponent({
|
||||||
|
type: ButtonParent,
|
||||||
|
tag: 'button-parent',
|
||||||
|
factory: () => parent = new ButtonParent(),
|
||||||
|
/** {{ doCheckCount }} - <my-comp></my-comp> */
|
||||||
|
template: (ctx: ButtonParent, cm: boolean) => {
|
||||||
|
if (cm) {
|
||||||
|
text(0);
|
||||||
|
elementStart(1, MyComponent);
|
||||||
|
elementEnd();
|
||||||
|
}
|
||||||
|
textBinding(0, interpolation1('', ctx.doCheckCount, ' - '));
|
||||||
|
MyComponent.ngComponentDef.h(2, 1);
|
||||||
|
directiveRefresh(2, 1);
|
||||||
|
},
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyButtonApp {
|
||||||
|
static ngComponentDef = defineComponent({
|
||||||
|
type: MyButtonApp,
|
||||||
|
tag: 'my-button-app',
|
||||||
|
factory: () => new MyButtonApp(),
|
||||||
|
/** <button-parent></button-parent> */
|
||||||
|
template: (ctx: MyButtonApp, cm: boolean) => {
|
||||||
|
if (cm) {
|
||||||
|
elementStart(0, ButtonParent);
|
||||||
|
elementEnd();
|
||||||
|
}
|
||||||
|
ButtonParent.ngComponentDef.h(1, 0);
|
||||||
|
directiveRefresh(1, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const myButtonApp = renderComponent(MyButtonApp);
|
||||||
|
expect(parent !.doCheckCount).toEqual(1);
|
||||||
|
expect(comp !.doCheckCount).toEqual(1);
|
||||||
|
expect(getRenderedText(myButtonApp)).toEqual('1 - 1 - Nancy');
|
||||||
|
|
||||||
|
detectChanges(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();
|
||||||
|
expect(parent !.doCheckCount).toEqual(3);
|
||||||
|
expect(comp !.doCheckCount).toEqual(2);
|
||||||
|
expect(getRenderedText(myButtonApp)).toEqual('3 - 2 - Nancy');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should check parent OnPush components in update mode when child events occur', () => {
|
describe('ChangeDetectorRef', () => {
|
||||||
let parent: ButtonParent;
|
|
||||||
|
|
||||||
class ButtonParent implements DoCheck {
|
describe('detectChanges()', () => {
|
||||||
doCheckCount = 0;
|
let myComp: MyComp;
|
||||||
ngDoCheck(): void { this.doCheckCount++; }
|
let dir: Dir;
|
||||||
|
|
||||||
static ngComponentDef = defineComponent({
|
class MyComp {
|
||||||
type: ButtonParent,
|
doCheckCount = 0;
|
||||||
tag: 'button-parent',
|
name = 'Nancy';
|
||||||
factory: () => parent = new ButtonParent(),
|
|
||||||
/** {{ doCheckCount }} - <my-comp></my-comp> */
|
constructor(public cdr: ChangeDetectorRef) {}
|
||||||
template: (ctx: ButtonParent, cm: boolean) => {
|
|
||||||
if (cm) {
|
ngDoCheck() { this.doCheckCount++; }
|
||||||
text(0);
|
|
||||||
elementStart(1, MyComponent);
|
static ngComponentDef = defineComponent({
|
||||||
elementEnd();
|
type: MyComp,
|
||||||
|
tag: 'my-comp',
|
||||||
|
factory: () => myComp = new MyComp(injectChangeDetectorRef()),
|
||||||
|
/** {{ name }} */
|
||||||
|
template: (ctx: MyComp, cm: boolean) => {
|
||||||
|
if (cm) {
|
||||||
|
text(0);
|
||||||
|
}
|
||||||
|
textBinding(0, bind(ctx.name));
|
||||||
|
},
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ParentComp {
|
||||||
|
doCheckCount = 0;
|
||||||
|
|
||||||
|
constructor(public cdr: ChangeDetectorRef) {}
|
||||||
|
|
||||||
|
ngDoCheck() { this.doCheckCount++; }
|
||||||
|
|
||||||
|
static ngComponentDef = defineComponent({
|
||||||
|
type: ParentComp,
|
||||||
|
tag: 'parent-comp',
|
||||||
|
factory: () => new ParentComp(injectChangeDetectorRef()),
|
||||||
|
/**
|
||||||
|
* {{ doCheckCount}} -
|
||||||
|
* <my-comp></my-comp>
|
||||||
|
*/
|
||||||
|
template: (ctx: ParentComp, cm: boolean) => {
|
||||||
|
if (cm) {
|
||||||
|
text(0);
|
||||||
|
elementStart(1, MyComp);
|
||||||
|
elementEnd();
|
||||||
|
}
|
||||||
|
textBinding(0, interpolation1('', ctx.doCheckCount, ' - '));
|
||||||
|
MyComp.ngComponentDef.h(2, 1);
|
||||||
|
directiveRefresh(2, 1);
|
||||||
}
|
}
|
||||||
textBinding(0, interpolation1('', ctx.doCheckCount, ' - '));
|
});
|
||||||
MyComponent.ngComponentDef.h(2, 1);
|
}
|
||||||
directiveRefresh(2, 1);
|
|
||||||
},
|
class Dir {
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
constructor(public cdr: ChangeDetectorRef) {}
|
||||||
|
|
||||||
|
static ngDirectiveDef =
|
||||||
|
defineDirective({type: Dir, factory: () => dir = new Dir(injectChangeDetectorRef())});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
it('should check the component view when called by component (even when OnPush && clean)',
|
||||||
|
() => {
|
||||||
|
const comp = renderComponent(MyComp);
|
||||||
|
expect(getRenderedText(comp)).toEqual('Nancy');
|
||||||
|
|
||||||
|
comp.name = 'Bess'; // as this is not an Input, the component stays clean
|
||||||
|
comp.cdr.detectChanges();
|
||||||
|
expect(getRenderedText(comp)).toEqual('Bess');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT call component doCheck when called by a component', () => {
|
||||||
|
const comp = renderComponent(MyComp);
|
||||||
|
expect(comp.doCheckCount).toEqual(1);
|
||||||
|
|
||||||
|
// NOTE: in current Angular, detectChanges does not itself trigger doCheck, but you
|
||||||
|
// may see doCheck called in some cases bc of the extra CD run triggered by zone.js.
|
||||||
|
// It's important not to call doCheck to allow calls to detectChanges in that hook.
|
||||||
|
comp.cdr.detectChanges();
|
||||||
|
expect(comp.doCheckCount).toEqual(1);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
class MyButtonApp {
|
it('should NOT check the component parent when called by a child component', () => {
|
||||||
static ngComponentDef = defineComponent({
|
const parentComp = renderComponent(ParentComp);
|
||||||
type: MyButtonApp,
|
expect(getRenderedText(parentComp)).toEqual('1 - Nancy');
|
||||||
tag: 'my-button-app',
|
|
||||||
factory: () => new MyButtonApp(),
|
parentComp.doCheckCount = 100;
|
||||||
/** <button-parent></button-parent> */
|
myComp.cdr.detectChanges();
|
||||||
template: (ctx: MyButtonApp, cm: boolean) => {
|
expect(parentComp.doCheckCount).toEqual(100);
|
||||||
if (cm) {
|
expect(getRenderedText(parentComp)).toEqual('1 - Nancy');
|
||||||
elementStart(0, ButtonParent);
|
});
|
||||||
elementEnd();
|
|
||||||
}
|
it('should check component children when called by component if dirty or check-always',
|
||||||
ButtonParent.ngComponentDef.h(1, 0);
|
() => {
|
||||||
directiveRefresh(1, 0);
|
const parentComp = renderComponent(ParentComp);
|
||||||
|
expect(parentComp.doCheckCount).toEqual(1);
|
||||||
|
|
||||||
|
myComp.name = 'Bess';
|
||||||
|
parentComp.cdr.detectChanges();
|
||||||
|
expect(parentComp.doCheckCount).toEqual(1);
|
||||||
|
expect(myComp.doCheckCount).toEqual(2);
|
||||||
|
// OnPush child is not dirty, so its change isn't rendered.
|
||||||
|
expect(getRenderedText(parentComp)).toEqual('1 - Nancy');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not group detectChanges calls (call every time)', () => {
|
||||||
|
const parentComp = renderComponent(ParentComp);
|
||||||
|
expect(myComp.doCheckCount).toEqual(1);
|
||||||
|
|
||||||
|
parentComp.cdr.detectChanges();
|
||||||
|
parentComp.cdr.detectChanges();
|
||||||
|
expect(myComp.doCheckCount).toEqual(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check component view when called by directive on component node', () => {
|
||||||
|
class MyApp {
|
||||||
|
static ngComponentDef = defineComponent({
|
||||||
|
type: MyApp,
|
||||||
|
tag: 'my-app',
|
||||||
|
factory: () => new MyApp(),
|
||||||
|
/** <my-comp dir></my-comp> */
|
||||||
|
template: (ctx: MyApp, cm: boolean) => {
|
||||||
|
if (cm) {
|
||||||
|
elementStart(0, MyComp, ['dir', ''], [Dir]);
|
||||||
|
elementEnd();
|
||||||
|
}
|
||||||
|
MyComp.ngComponentDef.h(1, 0);
|
||||||
|
Dir.ngDirectiveDef.h(2, 0);
|
||||||
|
directiveRefresh(1, 0);
|
||||||
|
directiveRefresh(2, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const app = renderComponent(MyApp);
|
||||||
|
expect(getRenderedText(app)).toEqual('Nancy');
|
||||||
|
|
||||||
|
myComp.name = 'George';
|
||||||
|
dir !.cdr.detectChanges();
|
||||||
|
expect(getRenderedText(app)).toEqual('George');
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const myButtonApp = renderComponent(MyButtonApp);
|
it('should check host component when called by directive on element node', () => {
|
||||||
expect(parent !.doCheckCount).toEqual(1);
|
class MyApp {
|
||||||
expect(comp !.doCheckCount).toEqual(1);
|
name = 'Frank';
|
||||||
expect(getRenderedText(myButtonApp)).toEqual('1 - 1 - Nancy');
|
|
||||||
|
|
||||||
detectChanges(myButtonApp);
|
static ngComponentDef = defineComponent({
|
||||||
expect(parent !.doCheckCount).toEqual(2);
|
type: MyApp,
|
||||||
// parent isn't checked, so child doCheck won't run
|
tag: 'my-app',
|
||||||
expect(comp !.doCheckCount).toEqual(1);
|
factory: () => new MyApp(),
|
||||||
expect(getRenderedText(myButtonApp)).toEqual('1 - 1 - Nancy');
|
/**
|
||||||
|
* {{ name }}
|
||||||
|
* <div dir></div>
|
||||||
|
*/
|
||||||
|
template: (ctx: MyApp, cm: boolean) => {
|
||||||
|
if (cm) {
|
||||||
|
text(0);
|
||||||
|
elementStart(1, 'div', ['dir', ''], [Dir]);
|
||||||
|
elementEnd();
|
||||||
|
}
|
||||||
|
textBinding(1, bind(ctx.name));
|
||||||
|
Dir.ngDirectiveDef.h(2, 1);
|
||||||
|
directiveRefresh(2, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = renderComponent(MyApp);
|
||||||
|
expect(getRenderedText(app)).toEqual('Frank');
|
||||||
|
|
||||||
|
app.name = 'Joe';
|
||||||
|
dir !.cdr.detectChanges();
|
||||||
|
expect(getRenderedText(app)).toEqual('Joe');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check the host component when called from EmbeddedViewRef', () => {
|
||||||
|
class MyApp {
|
||||||
|
showing = true;
|
||||||
|
name = 'Amelia';
|
||||||
|
|
||||||
|
constructor(public cdr: ChangeDetectorRef) {}
|
||||||
|
|
||||||
|
static ngComponentDef = defineComponent({
|
||||||
|
type: MyApp,
|
||||||
|
tag: 'my-app',
|
||||||
|
factory: () => new MyApp(injectChangeDetectorRef()),
|
||||||
|
/**
|
||||||
|
* {{ name}}
|
||||||
|
* % if (showing) {
|
||||||
|
* <div dir></div>
|
||||||
|
* % }
|
||||||
|
*/
|
||||||
|
template: function(ctx: MyApp, cm: boolean) {
|
||||||
|
if (cm) {
|
||||||
|
text(0);
|
||||||
|
container(1);
|
||||||
|
}
|
||||||
|
textBinding(0, bind(ctx.name));
|
||||||
|
containerRefreshStart(1);
|
||||||
|
{
|
||||||
|
if (ctx.showing) {
|
||||||
|
if (embeddedViewStart(0)) {
|
||||||
|
elementStart(0, 'div', ['dir', ''], [Dir]);
|
||||||
|
elementEnd();
|
||||||
|
}
|
||||||
|
Dir.ngDirectiveDef.h(1, 0);
|
||||||
|
directiveRefresh(1, 0);
|
||||||
|
}
|
||||||
|
embeddedViewEnd();
|
||||||
|
}
|
||||||
|
containerRefreshEnd();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = renderComponent(MyApp);
|
||||||
|
expect(getRenderedText(app)).toEqual('Amelia');
|
||||||
|
|
||||||
|
app.name = 'Emerson';
|
||||||
|
dir !.cdr.detectChanges();
|
||||||
|
expect(getRenderedText(app)).toEqual('Emerson');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support call in ngOnInit', () => {
|
||||||
|
class DetectChangesComp {
|
||||||
|
value = 0;
|
||||||
|
|
||||||
|
constructor(public cdr: ChangeDetectorRef) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.value++;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
static ngComponentDef = defineComponent({
|
||||||
|
type: DetectChangesComp,
|
||||||
|
tag: 'detect-changes-comp',
|
||||||
|
factory: () => new DetectChangesComp(injectChangeDetectorRef()),
|
||||||
|
/** {{ value }} */
|
||||||
|
template: (ctx: DetectChangesComp, cm: boolean) => {
|
||||||
|
if (cm) {
|
||||||
|
text(0);
|
||||||
|
}
|
||||||
|
textBinding(0, bind(ctx.value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const comp = renderComponent(DetectChangesComp);
|
||||||
|
expect(getRenderedText(comp)).toEqual('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support call in ngDoCheck', () => {
|
||||||
|
class DetectChangesComp {
|
||||||
|
doCheckCount = 0;
|
||||||
|
|
||||||
|
constructor(public cdr: ChangeDetectorRef) {}
|
||||||
|
|
||||||
|
ngDoCheck() {
|
||||||
|
this.doCheckCount++;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
static ngComponentDef = defineComponent({
|
||||||
|
type: DetectChangesComp,
|
||||||
|
tag: 'detect-changes-comp',
|
||||||
|
factory: () => new DetectChangesComp(injectChangeDetectorRef()),
|
||||||
|
/** {{ doCheckCount }} */
|
||||||
|
template: (ctx: DetectChangesComp, cm: boolean) => {
|
||||||
|
if (cm) {
|
||||||
|
text(0);
|
||||||
|
}
|
||||||
|
textBinding(0, bind(ctx.doCheckCount));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const comp = renderComponent(DetectChangesComp);
|
||||||
|
expect(getRenderedText(comp)).toEqual('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
const button = containerEl.querySelector('button');
|
|
||||||
button !.click();
|
|
||||||
requestAnimationFrame.flush();
|
|
||||||
expect(parent !.doCheckCount).toEqual(3);
|
|
||||||
expect(comp !.doCheckCount).toEqual(2);
|
|
||||||
expect(getRenderedText(myButtonApp)).toEqual('3 - 2 - Nancy');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,10 +6,8 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {withBody} from '@angular/core/testing';
|
|
||||||
|
|
||||||
import {DoCheck, ViewEncapsulation} from '../../src/core';
|
import {DoCheck, ViewEncapsulation} from '../../src/core';
|
||||||
import {getRenderedText, whenRendered} from '../../src/render3/component';
|
|
||||||
import {defineComponent, markDirty} from '../../src/render3/index';
|
import {defineComponent, markDirty} from '../../src/render3/index';
|
||||||
import {bind, container, containerRefreshEnd, containerRefreshStart, detectChanges, directiveRefresh, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, text, textBinding} from '../../src/render3/instructions';
|
import {bind, container, containerRefreshEnd, containerRefreshStart, detectChanges, directiveRefresh, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, text, textBinding} from '../../src/render3/instructions';
|
||||||
import {createRendererType2} from '../../src/view/index';
|
import {createRendererType2} from '../../src/view/index';
|
||||||
|
@ -236,68 +234,4 @@ describe('encapsulation', () => {
|
||||||
/<div host="" _nghost-c(\d+)=""><leaf _ngcontent-c\1="" _nghost-c(\d+)=""><span _ngcontent-c\2="">bar<\/span><\/leaf><\/div>/);
|
/<div host="" _nghost-c(\d+)=""><leaf _ngcontent-c\1="" _nghost-c(\d+)=""><span _ngcontent-c\2="">bar<\/span><\/leaf><\/div>/);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('markDirty, detectChanges, whenRendered, getRenderedText', () => {
|
|
||||||
class MyComponent implements DoCheck {
|
|
||||||
value: string = 'works';
|
|
||||||
doCheckCount = 0;
|
|
||||||
ngDoCheck(): void { this.doCheckCount++; }
|
|
||||||
|
|
||||||
static ngComponentDef = defineComponent({
|
|
||||||
type: MyComponent,
|
|
||||||
tag: 'my-comp',
|
|
||||||
factory: () => new MyComponent(),
|
|
||||||
template: (ctx: MyComponent, cm: boolean) => {
|
|
||||||
if (cm) {
|
|
||||||
elementStart(0, 'span');
|
|
||||||
text(1);
|
|
||||||
elementEnd();
|
|
||||||
}
|
|
||||||
textBinding(1, bind(ctx.value));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
it('should mark a component dirty and schedule change detection', withBody('my-comp', () => {
|
|
||||||
const myComp = renderComponent(MyComponent);
|
|
||||||
expect(getRenderedText(myComp)).toEqual('works');
|
|
||||||
myComp.value = 'updated';
|
|
||||||
markDirty(myComp);
|
|
||||||
expect(getRenderedText(myComp)).toEqual('works');
|
|
||||||
requestAnimationFrame.flush();
|
|
||||||
expect(getRenderedText(myComp)).toEqual('updated');
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should detectChanges on a component', withBody('my-comp', () => {
|
|
||||||
const myComp = renderComponent(MyComponent);
|
|
||||||
expect(getRenderedText(myComp)).toEqual('works');
|
|
||||||
myComp.value = 'updated';
|
|
||||||
detectChanges(myComp);
|
|
||||||
expect(getRenderedText(myComp)).toEqual('updated');
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should detectChanges only once if markDirty is called multiple times',
|
|
||||||
withBody('my-comp', () => {
|
|
||||||
const myComp = renderComponent(MyComponent);
|
|
||||||
expect(getRenderedText(myComp)).toEqual('works');
|
|
||||||
expect(myComp.doCheckCount).toBe(1);
|
|
||||||
myComp.value = 'ignore';
|
|
||||||
markDirty(myComp);
|
|
||||||
myComp.value = 'updated';
|
|
||||||
markDirty(myComp);
|
|
||||||
expect(getRenderedText(myComp)).toEqual('works');
|
|
||||||
requestAnimationFrame.flush();
|
|
||||||
expect(getRenderedText(myComp)).toEqual('updated');
|
|
||||||
expect(myComp.doCheckCount).toBe(2);
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should notify whenRendered', withBody('my-comp', async() => {
|
|
||||||
const myComp = renderComponent(MyComponent);
|
|
||||||
await whenRendered(myComp);
|
|
||||||
myComp.value = 'updated';
|
|
||||||
markDirty(myComp);
|
|
||||||
setTimeout(requestAnimationFrame.flush, 0);
|
|
||||||
await whenRendered(myComp);
|
|
||||||
expect(getRenderedText(myComp)).toEqual('updated');
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {SimpleChanges} from '../../src/core';
|
import {SimpleChanges} from '../../src/core';
|
||||||
import {ComponentTemplate, NgOnChangesFeature, RootLifecycleHooks, defineComponent, defineDirective} from '../../src/render3/index';
|
import {ComponentTemplate, LifecycleHooksFeature, NgOnChangesFeature, defineComponent, defineDirective} from '../../src/render3/index';
|
||||||
import {bind, container, containerRefreshEnd, containerRefreshStart, directiveRefresh, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, listener, markDirty, projection, projectionDef, store, text} from '../../src/render3/instructions';
|
import {bind, container, containerRefreshEnd, containerRefreshStart, directiveRefresh, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, listener, markDirty, projection, projectionDef, store, text} from '../../src/render3/instructions';
|
||||||
|
|
||||||
import {containerEl, renderComponent, renderToHtml, requestAnimationFrame} from './render_util';
|
import {containerEl, renderComponent, renderToHtml, requestAnimationFrame} from './render_util';
|
||||||
|
@ -87,7 +87,7 @@ describe('lifecycles', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be called on root component in creation mode', () => {
|
it('should be called on root component in creation mode', () => {
|
||||||
const comp = renderComponent(Comp, {features: [RootLifecycleHooks]});
|
const comp = renderComponent(Comp, {hostFeatures: [LifecycleHooksFeature]});
|
||||||
expect(events).toEqual(['comp']);
|
expect(events).toEqual(['comp']);
|
||||||
|
|
||||||
markDirty(comp);
|
markDirty(comp);
|
||||||
|
@ -423,7 +423,7 @@ describe('lifecycles', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be called on root component', () => {
|
it('should be called on root component', () => {
|
||||||
const comp = renderComponent(Comp, {features: [RootLifecycleHooks]});
|
const comp = renderComponent(Comp, {hostFeatures: [LifecycleHooksFeature]});
|
||||||
expect(events).toEqual(['comp']);
|
expect(events).toEqual(['comp']);
|
||||||
|
|
||||||
markDirty(comp);
|
markDirty(comp);
|
||||||
|
@ -583,7 +583,7 @@ describe('lifecycles', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be called on root component in creation mode', () => {
|
it('should be called on root component in creation mode', () => {
|
||||||
const comp = renderComponent(Comp, {features: [RootLifecycleHooks]});
|
const comp = renderComponent(Comp, {hostFeatures: [LifecycleHooksFeature]});
|
||||||
expect(events).toEqual(['comp']);
|
expect(events).toEqual(['comp']);
|
||||||
|
|
||||||
markDirty(comp);
|
markDirty(comp);
|
||||||
|
@ -870,7 +870,7 @@ describe('lifecycles', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be called on root component', () => {
|
it('should be called on root component', () => {
|
||||||
const comp = renderComponent(Comp, {features: [RootLifecycleHooks]});
|
const comp = renderComponent(Comp, {hostFeatures: [LifecycleHooksFeature]});
|
||||||
expect(allEvents).toEqual(['comp init', 'comp check']);
|
expect(allEvents).toEqual(['comp init', 'comp check']);
|
||||||
|
|
||||||
markDirty(comp);
|
markDirty(comp);
|
||||||
|
@ -985,7 +985,7 @@ describe('lifecycles', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be called on root component in creation mode', () => {
|
it('should be called on root component in creation mode', () => {
|
||||||
const comp = renderComponent(Comp, {features: [RootLifecycleHooks]});
|
const comp = renderComponent(Comp, {hostFeatures: [LifecycleHooksFeature]});
|
||||||
expect(events).toEqual(['comp']);
|
expect(events).toEqual(['comp']);
|
||||||
|
|
||||||
markDirty(comp);
|
markDirty(comp);
|
||||||
|
@ -1296,7 +1296,7 @@ describe('lifecycles', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be called on root component', () => {
|
it('should be called on root component', () => {
|
||||||
const comp = renderComponent(Comp, {features: [RootLifecycleHooks]});
|
const comp = renderComponent(Comp, {hostFeatures: [LifecycleHooksFeature]});
|
||||||
expect(allEvents).toEqual(['comp init', 'comp check']);
|
expect(allEvents).toEqual(['comp init', 'comp check']);
|
||||||
|
|
||||||
markDirty(comp);
|
markDirty(comp);
|
||||||
|
|
|
@ -8,9 +8,9 @@
|
||||||
|
|
||||||
import {stringifyElement} from '@angular/platform-browser/testing/src/browser_util';
|
import {stringifyElement} from '@angular/platform-browser/testing/src/browser_util';
|
||||||
|
|
||||||
|
import {CreateComponentOptions} from '../../src/render3/component';
|
||||||
import {ComponentTemplate, ComponentType, DirectiveType, PublicFeature, defineComponent, defineDirective, renderComponent as _renderComponent} from '../../src/render3/index';
|
import {ComponentTemplate, ComponentType, DirectiveType, PublicFeature, defineComponent, defineDirective, renderComponent as _renderComponent} from '../../src/render3/index';
|
||||||
import {NG_HOST_SYMBOL, createLNode, createLView, renderTemplate} from '../../src/render3/instructions';
|
import {NG_HOST_SYMBOL, createLNode, createLView, renderTemplate} from '../../src/render3/instructions';
|
||||||
import {CreateComponentOptions} from '../../src/render3/component';
|
|
||||||
import {DirectiveDefArgs} from '../../src/render3/interfaces/definition';
|
import {DirectiveDefArgs} from '../../src/render3/interfaces/definition';
|
||||||
import {LElementNode, LNodeFlags} from '../../src/render3/interfaces/node';
|
import {LElementNode, LNodeFlags} from '../../src/render3/interfaces/node';
|
||||||
import {RElement, RText, Renderer3, RendererFactory3, domRendererFactory3} from '../../src/render3/interfaces/renderer';
|
import {RElement, RText, Renderer3, RendererFactory3, domRendererFactory3} from '../../src/render3/interfaces/renderer';
|
||||||
|
@ -118,7 +118,7 @@ export function renderComponent<T>(type: ComponentType<T>, opts?: CreateComponen
|
||||||
rendererFactory: opts && opts.rendererFactory || testRendererFactory,
|
rendererFactory: opts && opts.rendererFactory || testRendererFactory,
|
||||||
host: containerEl,
|
host: containerEl,
|
||||||
scheduler: requestAnimationFrame,
|
scheduler: requestAnimationFrame,
|
||||||
features: opts && opts.features
|
hostFeatures: opts && opts.hostFeatures
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/brow
|
||||||
|
|
||||||
import {RendererType2, ViewEncapsulation} from '../../src/core';
|
import {RendererType2, ViewEncapsulation} from '../../src/core';
|
||||||
import {defineComponent, detectChanges} from '../../src/render3/index';
|
import {defineComponent, detectChanges} from '../../src/render3/index';
|
||||||
import {bind, directiveRefresh, elementEnd, elementProperty, elementStart, listener, text} from '../../src/render3/instructions';
|
import {bind, directiveRefresh, elementEnd, elementProperty, elementStart, listener, text, tick} from '../../src/render3/instructions';
|
||||||
import {createRendererType2} from '../../src/view/index';
|
import {createRendererType2} from '../../src/view/index';
|
||||||
|
|
||||||
import {getAnimationRendererFactory2, getRendererFactory2} from './imported_renderer2';
|
import {getAnimationRendererFactory2, getRendererFactory2} from './imported_renderer2';
|
||||||
|
@ -78,7 +78,7 @@ describe('renderer factory lifecycle', () => {
|
||||||
expect(logs).toEqual(['create', 'create', 'begin', 'component', 'end']);
|
expect(logs).toEqual(['create', 'create', 'begin', 'component', 'end']);
|
||||||
|
|
||||||
logs = [];
|
logs = [];
|
||||||
detectChanges(component);
|
tick(component);
|
||||||
expect(logs).toEqual(['begin', 'component', 'end']);
|
expect(logs).toEqual(['begin', 'component', 'end']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -188,7 +188,7 @@ describe('animation renderer factory', () => {
|
||||||
.toMatch(/<div class="ng-tns-c\d+-0 ng-trigger ng-trigger-myAnimation">foo<\/div>/);
|
.toMatch(/<div class="ng-tns-c\d+-0 ng-trigger ng-trigger-myAnimation">foo<\/div>/);
|
||||||
|
|
||||||
component.exp = 'on';
|
component.exp = 'on';
|
||||||
detectChanges(component);
|
tick(component);
|
||||||
|
|
||||||
const [player] = getLog();
|
const [player] = getLog();
|
||||||
expect(player.keyframes).toEqual([
|
expect(player.keyframes).toEqual([
|
||||||
|
|
Loading…
Reference in New Issue