From 8c358844dd3d3b78c61be62d3a46581002a6b8ea Mon Sep 17 00:00:00 2001 From: Kara Erickson Date: Fri, 23 Feb 2018 13:17:20 -0800 Subject: [PATCH] feat(ivy): support OnPush change detection (#22417) PR Close #22417 --- packages/core/src/render3/component.ts | 64 +---- packages/core/src/render3/definition.ts | 5 +- packages/core/src/render3/index.ts | 6 +- packages/core/src/render3/instructions.ts | 169 +++++++++++-- .../core/src/render3/interfaces/definition.ts | 5 + packages/core/src/render3/interfaces/view.ts | 31 ++- .../hello_world/bundle.golden_symbols.json | 9 +- .../test/render3/change_detection_spec.ts | 222 ++++++++++++++++++ .../compiler_canonical_spec.ts | 61 ++++- packages/core/test/render3/component_spec.ts | 4 +- packages/core/test/render3/di_spec.ts | 4 +- 11 files changed, 470 insertions(+), 110 deletions(-) create mode 100644 packages/core/test/render3/change_detection_spec.ts diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index 6a43cb3917..3ba8945603 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -13,11 +13,11 @@ import {ComponentRef as viewEngine_ComponentRef} from '../linker/component_facto import {EmbeddedViewRef as viewEngine_EmbeddedViewRef} from '../linker/view_ref'; import {assertNotNull} from './assert'; -import {NG_HOST_SYMBOL, createError, createLView, createTView, directiveCreate, enterView, getDirectiveInstance, hostElement, leaveView, locateHostElement, renderComponentOrTemplate} from './instructions'; +import {CLEAN_PROMISE, NG_HOST_SYMBOL, _getComponentHostLElementNode, createError, createLView, createTView, detectChanges, directiveCreate, enterView, getDirectiveInstance, hostElement, leaveView, locateHostElement, scheduleChangeDetection} from './instructions'; import {ComponentDef, ComponentType} from './interfaces/definition'; import {LElementNode} from './interfaces/node'; import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from './interfaces/renderer'; -import {RootContext} from './interfaces/view'; +import {LViewFlags, RootContext} from './interfaces/view'; import {notImplemented, stringify} from './util'; @@ -169,12 +169,6 @@ export const NULL_INJECTOR: Injector = { } }; -/** - * A permanent marker promise which signifies that the current CD tree is - * clean. - */ -const CLEAN_PROMISE = Promise.resolve(null); - /** * Bootstraps a Component into an existing host element and returns an instance * of the component. @@ -204,7 +198,7 @@ export function renderComponent( const oldView = enterView( createLView( -1, rendererFactory.createRenderer(hostNode, componentDef.rendererType), createTView(), - null, rootContext), + null, rootContext, componentDef.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways), null !); try { // Create element node at index 0 in data array @@ -221,51 +215,6 @@ export function renderComponent( return component; } -/** - * Synchronously perform change detection on a component (and possibly its sub-components). - * - * This function triggers change detection in a synchronous way on a component. There should - * be very little reason to call this function directly since a preferred way to do change - * detection is to {@link markDirty} the component and wait for the scheduler to call this method - * at some future point in time. This is because a single user action often results in many - * components being invalidated and calling change detection on each component synchronously - * would be inefficient. It is better to wait until all components are marked as dirty and - * then perform single change detection across all of the components - * - * @param component The component which the change detection should be performed on. - */ -export function detectChanges(component: T): void { - const hostNode = _getComponentHostLElementNode(component); - ngDevMode && assertNotNull(hostNode.data, 'Component host node should be attached to an LView'); - renderComponentOrTemplate(hostNode, hostNode.view, component); -} - -/** - * Mark the component as dirty (needing change detection). - * - * Marking a component dirty will schedule a change detection on this - * component at some point in the future. Marking an already dirty - * component as dirty is a noop. Only one outstanding change detection - * can be scheduled per component tree. (Two components bootstrapped with - * separate `renderComponent` will have separate schedulers) - * - * When the root component is bootstrapped with `renderComponent` a scheduler - * can be provided. - * - * @param component Component to mark as dirty. - */ -export function markDirty(component: T) { - const rootContext = getRootContext(component); - if (rootContext.clean == CLEAN_PROMISE) { - let res: null|((val: null) => void); - rootContext.clean = new Promise((r) => res = r); - rootContext.scheduler(() => { - detectChanges(rootContext.component); - res !(null); - rootContext.clean = CLEAN_PROMISE; - }); - } -} /** * Retrieve the root component of any component by walking the parent `LView` until @@ -285,13 +234,6 @@ function getRootContext(component: any): RootContext { return rootContext; } -function _getComponentHostLElementNode(component: T): LElementNode { - ngDevMode && assertNotNull(component, 'expecting component got null'); - const lElementNode = (component as any)[NG_HOST_SYMBOL] as LElementNode; - ngDevMode && assertNotNull(component, 'object is not a component'); - return lElementNode; -} - /** * Retrieve the host element of the component. * diff --git a/packages/core/src/render3/definition.ts b/packages/core/src/render3/definition.ts index 8fbdaf6f2d..5c47c59d30 100644 --- a/packages/core/src/render3/definition.ts +++ b/packages/core/src/render3/definition.ts @@ -7,6 +7,7 @@ */ import {SimpleChange} from '../change_detection/change_detection_util'; +import {ChangeDetectionStrategy} from '../change_detection/constants'; import {PipeTransform} from '../change_detection/pipe_transform'; import {OnChanges, SimpleChanges} from '../metadata/lifecycle_hooks'; import {RendererType2} from '../render/api'; @@ -55,7 +56,9 @@ export function defineComponent(componentDefinition: ComponentDefArgs): Co afterContentChecked: type.prototype.ngAfterContentChecked || null, afterViewInit: type.prototype.ngAfterViewInit || null, afterViewChecked: type.prototype.ngAfterViewChecked || null, - onDestroy: type.prototype.ngOnDestroy || null + onDestroy: type.prototype.ngOnDestroy || null, + onPush: (componentDefinition as ComponentDefArgs).changeDetection === + ChangeDetectionStrategy.OnPush }; const feature = componentDefinition.features; feature && feature.forEach((fn) => fn(def)); diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 5489b1b10b..855d7f3d16 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {createComponentRef, detectChanges, getHostElement, getRenderedText, markDirty, renderComponent, whenRendered} from './component'; +import {createComponentRef, getHostElement, getRenderedText, renderComponent, whenRendered} from './component'; import {NgOnChangesFeature, PublicFeature, defineComponent, defineDirective, definePipe} from './definition'; import {InjectFlags} from './di'; import {ComponentDef, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFlags, DirectiveType} from './interfaces/definition'; @@ -64,6 +64,8 @@ export { embeddedViewStart as V, embeddedViewEnd as v, + detectChanges, + markDirty, } from './instructions'; export { @@ -109,11 +111,9 @@ export { defineComponent, defineDirective, definePipe, - detectChanges, createComponentRef, getHostElement, getRenderedText, - markDirty, renderComponent, whenRendered, }; diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index b2b977f1f2..c07c05108d 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -12,14 +12,14 @@ import {assertEqual, assertLessThan, assertNotEqual, assertNotNull, assertNull, import {LContainer, TContainer} from './interfaces/container'; import {CssSelector, LProjection} from './interfaces/projection'; import {LQueries} from './interfaces/query'; -import {LView, LViewFlags, LifecycleStage, TData, TView} from './interfaces/view'; +import {LView, LViewFlags, LifecycleStage, RootContext, TData, TView} from './interfaces/view'; import {LContainerNode, LElementNode, LNode, LNodeFlags, LProjectionNode, LTextNode, LViewNode, TNode, TContainerNode, InitialInputData, InitialInputs, PropertyAliases, PropertyAliasValue,} from './interfaces/node'; import {assertNodeType} from './node_assert'; import {appendChild, insertChild, insertView, appendProjectedNode, removeView, canInsertNativeNode} from './node_manipulation'; import {matchingSelectorIndex} from './node_selector_matcher'; import {ComponentDef, ComponentTemplate, ComponentType, DirectiveDef, DirectiveType} from './interfaces/definition'; -import {RElement, RText, Renderer3, RendererFactory3, ProceduralRenderer3, ObjectOrientedRenderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces/renderer'; +import {RElement, RText, Renderer3, RendererFactory3, ProceduralRenderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces/renderer'; import {isDifferent, stringify} from './util'; import {executeHooks, executeContentHooks, queueLifecycleHooks, queueInitHooks, executeInitHooks} from './hooks'; @@ -30,6 +30,13 @@ import {executeHooks, executeContentHooks, queueLifecycleHooks, queueInitHooks, */ export const NG_HOST_SYMBOL = '__ngHostLNode__'; +/** + * A permanent marker promise which signifies that the current CD tree is + * clean. + */ +const _CLEAN_PROMISE = Promise.resolve(null); + + /** * This property gets set before entering a template. * @@ -159,7 +166,7 @@ export function enterView(newView: LView, host: LElementNode | LViewNode | null) data = newView && newView.data; bindingIndex = newView && newView.bindingStartIndex || 0; tData = newView && newView.tView.data; - creationMode = newView && (newView.flags & LViewFlags.CreationMode) === 1; + creationMode = newView && (newView.flags & LViewFlags.CreationMode) === LViewFlags.CreationMode; cleanup = newView && newView.cleanup; renderer = newView && newView.renderer; @@ -183,7 +190,8 @@ export function leaveView(newView: LView): void { executeHooks( currentView.data, currentView.tView.viewHooks, currentView.tView.viewCheckHooks, creationMode); - currentView.flags &= ~LViewFlags.CreationMode; // Clear creationMode bit in view flags + // 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.tView.firstTemplatePass = false; enterView(newView, null); @@ -191,11 +199,11 @@ export function leaveView(newView: LView): void { export function createLView( viewId: number, renderer: Renderer3, tView: TView, template: ComponentTemplate| null, - context: any | null): LView { + context: any | null, flags: LViewFlags): LView { const newView = { parent: currentView, id: viewId, // -1 for component views - flags: LViewFlags.CreationMode, + flags: flags | LViewFlags.CreationMode, node: null !, // until we initialize it in createNode. data: [], tView: tView, @@ -326,7 +334,7 @@ export function renderTemplate( null, LNodeFlags.Element, hostNode, createLView( -1, providedRendererFactory.createRenderer(null, null), getOrCreateTView(template), - null, null)); + null, {}, LViewFlags.CheckAlways)); } const hostView = host.data !; ngDevMode && assertNotNull(hostView, 'Host node should have an LView defined in host.data.'); @@ -344,7 +352,8 @@ export function renderEmbeddedTemplate( previousOrParentNode = null !; let cm: boolean = false; if (viewNode == null) { - const view = createLView(-1, renderer, createTView(), template, context); + const view = + createLView(-1, renderer, createTView(), template, context, LViewFlags.CheckAlways); viewNode = createLNode(null, LNodeFlags.View, null, view); cm = true; } @@ -431,9 +440,10 @@ export function elementStart( let componentView: LView|null = null; if (isHostElement) { const tView = getOrCreateTView(hostComponentDef !.template); - componentView = addToViewTree(createLView( + const hostView = createLView( -1, rendererFactory.createRenderer(native, hostComponentDef !.rendererType), tView, - null, null)); + null, null, hostComponentDef !.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways); + componentView = addToViewTree(hostView); } // Only component views should be added to the view tree directly. Embedded views are @@ -583,8 +593,9 @@ export function locateHostElement( export function hostElement(rNode: RElement | null, def: ComponentDef) { resetApplicationState(); createLNode( - 0, LNodeFlags.Element, rNode, - createLView(-1, renderer, getOrCreateTView(def.template), null, null)); + 0, LNodeFlags.Element, rNode, createLView( + -1, renderer, getOrCreateTView(def.template), null, null, + def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways)); } @@ -602,15 +613,17 @@ export function listener(eventName: string, listener: EventListener, useCapture ngDevMode && assertPreviousIsParent(); const node = previousOrParentNode; const native = node.native as RElement; + const wrappedListener = wrapListenerWithDirtyLogic(currentView, listener); // In order to match current behavior, native DOM event listeners must be added for all // events (including outputs). + const cleanupFns = cleanup || (cleanup = currentView.cleanup = []); if (isProceduralRenderer(renderer)) { - const cleanupFn = renderer.listen(native, eventName, listener); - (cleanup || (cleanup = currentView.cleanup = [])).push(cleanupFn, null); + const cleanupFn = renderer.listen(native, eventName, wrappedListener); + cleanupFns.push(cleanupFn, null); } else { - native.addEventListener(eventName, listener, useCapture); - (cleanup || (cleanup = currentView.cleanup = [])).push(eventName, native, listener, useCapture); + native.addEventListener(eventName, wrappedListener, useCapture); + cleanupFns.push(eventName, native, wrappedListener, useCapture); } let tNode: TNode|null = node.tNode !; @@ -703,6 +716,7 @@ export function elementProperty(index: number, propName: string, value: T | N let dataValue: PropertyAliasValue|undefined; if (inputData && (dataValue = inputData[propName])) { setInputsForProperty(dataValue, value); + markDirtyIfOnPush(node); } else { const native = node.native; isProceduralRenderer(renderer) ? renderer.setProperty(native, propName, value) : @@ -1149,7 +1163,8 @@ export function embeddedViewStart(viewBlockId: number): boolean { } else { // When we create a new LView, we always reset the state of the instructions. const newView = createLView( - viewBlockId, renderer, getOrCreateEmbeddedTView(viewBlockId, container), null, null); + viewBlockId, renderer, getOrCreateEmbeddedTView(viewBlockId, container), null, null, + LViewFlags.CheckAlways); if (lContainer.queries) { newView.queries = lContainer.queries.enterView(lContainer.nextIndex); } @@ -1226,15 +1241,19 @@ export function directiveRefresh(directiveIndex: number, elementIndex: number ngDevMode && assertNodeType(element, LNodeFlags.Element); ngDevMode && assertNotNull(element.data, `Component's host node should have an LView attached.`); - ngDevMode && assertDataInRange(directiveIndex); - const directive = getDirectiveInstance(data[directiveIndex]); const hostView = element.data !; - const oldView = enterView(hostView, element); - try { - template(directive, creationMode); - } finally { - refreshDynamicChildren(); - leaveView(oldView); + + // Only CheckAlways components or dirty OnPush components should be checked + if (hostView.flags & (LViewFlags.CheckAlways | LViewFlags.Dirty)) { + ngDevMode && assertDataInRange(directiveIndex); + const directive = getDirectiveInstance(data[directiveIndex]); + const oldView = enterView(hostView, element); + try { + template(directive, creationMode); + } finally { + refreshDynamicChildren(); + leaveView(oldView); + } } } } @@ -1389,6 +1408,97 @@ export function addToViewTree(state: T): T { return state; } +/////////////////////////////// +//// Change detection +/////////////////////////////// + +/** If node is an OnPush component, marks its LView dirty. */ +export function markDirtyIfOnPush(node: LElementNode): void { + // Because data flows down the component tree, ancestors do not need to be marked dirty + if (node.data && !(node.data.flags & LViewFlags.CheckAlways)) { + node.data.flags |= LViewFlags.Dirty; + } +} + +/** + * Wraps an event listener so its host view and its ancestor views will be marked dirty + * whenever the event fires. Necessary to support OnPush components. + */ +export function wrapListenerWithDirtyLogic(view: LView, listener: EventListener): EventListener { + return function(e: Event) { + markViewDirty(view); + listener(e); + }; +} + +/** Marks current view and all ancestors dirty */ +function markViewDirty(view: LView): void { + let currentView: LView|null = view; + + while (currentView.parent != null) { + currentView.flags |= LViewFlags.Dirty; + currentView = currentView.parent; + } + currentView.flags |= LViewFlags.Dirty; + + ngDevMode && assertNotNull(currentView !.context, 'rootContext'); + scheduleChangeDetection(currentView !.context as RootContext); +} + + +/** Given a root context, schedules change detection at that root. */ +export function scheduleChangeDetection(rootContext: RootContext) { + if (rootContext.clean == _CLEAN_PROMISE) { + let res: null|((val: null) => void); + rootContext.clean = new Promise((r) => res = r); + rootContext.scheduler(() => { + detectChanges(rootContext.component); + res !(null); + rootContext.clean = _CLEAN_PROMISE; + }); + } +} + +/** + * Synchronously perform change detection on a component (and possibly its sub-components). + * + * This function triggers change detection in a synchronous way on a component. There should + * be very little reason to call this function directly since a preferred way to do change + * detection is to {@link markDirty} the component and wait for the scheduler to call this method + * at some future point in time. This is because a single user action often results in many + * components being invalidated and calling change detection on each component synchronously + * would be inefficient. It is better to wait until all components are marked as dirty and + * then perform single change detection across all of the components + * + * @param component The component which the change detection should be performed on. + */ +export function detectChanges(component: T): void { + const hostNode = _getComponentHostLElementNode(component); + ngDevMode && assertNotNull(hostNode.data, 'Component host node should be attached to an LView'); + renderComponentOrTemplate(hostNode, hostNode.view, component); +} + + +/** + * Mark the component as dirty (needing change detection). + * + * Marking a component dirty will schedule a change detection on this + * component at some point in the future. Marking an already dirty + * component as dirty is a noop. Only one outstanding change detection + * can be scheduled per component tree. (Two components bootstrapped with + * separate `renderComponent` will have separate schedulers) + * + * When the root component is bootstrapped with `renderComponent`, a scheduler + * can be provided. + * + * @param component Component to mark as dirty. + */ +export function markDirty(component: T) { + ngDevMode && assertNotNull(component, 'component'); + const lElementNode = _getComponentHostLElementNode(component); + markViewDirty(lElementNode.view); +} + /////////////////////////////// //// Bindings & interpolations /////////////////////////////// @@ -1649,3 +1759,12 @@ function assertDataInRange(index: number, arr?: any[]) { function assertDataNext(index: number) { assertEqual(data.length, index, 'index expected to be at the end of data'); } + +export function _getComponentHostLElementNode(component: T): LElementNode { + ngDevMode && assertNotNull(component, 'expecting component got null'); + const lElementNode = (component as any)[NG_HOST_SYMBOL] as LElementNode; + ngDevMode && assertNotNull(component, 'object is not a component'); + return lElementNode; +} + +export const CLEAN_PROMISE = _CLEAN_PROMISE; diff --git a/packages/core/src/render3/interfaces/definition.ts b/packages/core/src/render3/interfaces/definition.ts index d156135911..b1a9d067d4 100644 --- a/packages/core/src/render3/interfaces/definition.ts +++ b/packages/core/src/render3/interfaces/definition.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {ChangeDetectionStrategy} from '../../change_detection/constants'; import {PipeTransform} from '../../change_detection/pipe_transform'; import {RendererType2} from '../../render/api'; import {Type} from '../../type'; @@ -124,6 +125,9 @@ export interface ComponentDef extends DirectiveDef { * NOTE: only used with component directives. */ readonly rendererType: RendererType2|null; + + /** Whether or not this component's ChangeDetectionStrategy is OnPush */ + readonly onPush: boolean; } /** @@ -169,6 +173,7 @@ export interface ComponentDefArgs extends DirectiveDefArgs { template: ComponentTemplate; features?: ComponentDefFeature[]; rendererType?: RendererType2; + changeDetection?: ChangeDetectionStrategy; } export type DirectiveDefFeature = (directiveDef: DirectiveDef) => void; diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index 252ff9387d..3796ad0d6c 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -24,16 +24,7 @@ import {Renderer3} from './renderer'; * don't have to edit the data array based on which views are present. */ export interface LView { - /** - * Flags for this view. - * - * First bit: Whether or not the view is in creationMode. - * - * This must be stored in the view rather than using `data` as a marker so that - * we can properly support embedded views. Otherwise, when exiting a child view - * back into the parent view, `data` will be defined and `creationMode` will be - * improperly reported as false. - */ + /** Flags for this view (see LViewFlags for definition of each bit). */ flags: LViewFlags; /** @@ -182,9 +173,23 @@ export interface LView { queries: LQueries|null; } -/** Flags associated with an LView (see LView.flags) */ -export enum LViewFlags { - CreationMode = 0b001 +/** Flags associated with an LView (saved in LView.flags) */ +export const enum LViewFlags { + /** + * Whether or not the view is in creationMode. + * + * This must be stored in the view rather than using `data` as a marker so that + * we can properly support embedded views. Otherwise, when exiting a child view + * back into the parent view, `data` will be defined and `creationMode` will be + * improperly reported as false. + */ + CreationMode = 0b001, + + /** Whether this view has default change detection strategy (checks always) or onPush */ + CheckAlways = 0b010, + + /** Whether or not this view is currently dirty (needing check) */ + Dirty = 0b100 } /** Interface necessary to work with view tree traversal */ diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index d5c3e8f52c..538b2c74d5 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -5,6 +5,9 @@ { "name": "EMPTY$1" }, + { + "name": "NG_HOST_SYMBOL" + }, { "name": "NO_CHANGE" }, @@ -38,6 +41,9 @@ { "name": "currentView" }, + { + "name": "detectChanges" + }, { "name": "domRendererFactory3" }, @@ -77,9 +83,6 @@ { "name": "refreshDynamicChildren" }, - { - "name": "renderComponentOrTemplate" - }, { "name": "renderEmbeddedTemplate" }, diff --git a/packages/core/test/render3/change_detection_spec.ts b/packages/core/test/render3/change_detection_spec.ts new file mode 100644 index 0000000000..143c476790 --- /dev/null +++ b/packages/core/test/render3/change_detection_spec.ts @@ -0,0 +1,222 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ChangeDetectionStrategy, DoCheck} from '../../src/core'; +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', () => { + 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(), + /** + * {{ doCheckCount }} - {{ name }} + * + */ + 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(), + /** */ + 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({ + type: ButtonParent, + tag: 'button-parent', + factory: () => new ButtonParent(), + /** + * + * + */ + 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 }} - */ + 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(), + /** */ + 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'); + }); + +}); diff --git a/packages/core/test/render3/compiler_canonical/compiler_canonical_spec.ts b/packages/core/test/render3/compiler_canonical/compiler_canonical_spec.ts index a3c13167d0..e69069ff23 100644 --- a/packages/core/test/render3/compiler_canonical/compiler_canonical_spec.ts +++ b/packages/core/test/render3/compiler_canonical/compiler_canonical_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, ContentChild, ContentChildren, Directive, HostBinding, HostListener, Injectable, Input, NgModule, OnDestroy, Optional, Pipe, PipeTransform, QueryList, SimpleChanges, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '../../../src/core'; +import {ChangeDetectionStrategy, Component, ContentChild, ContentChildren, Directive, HostBinding, HostListener, Injectable, Input, NgModule, OnDestroy, Optional, Pipe, PipeTransform, QueryList, SimpleChanges, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '../../../src/core'; import * as $r3$ from '../../../src/core_render3_private_export'; import {renderComponent, toHtml} from '../render_util'; @@ -316,6 +316,65 @@ describe('compiler specification', () => { expect(renderComp(MyApp)).toEqual(`
`); }); + it('should support onPush components', () => { + type $MyApp$ = MyApp; + type $MyComp$ = MyComp; + + @Component({ + selector: 'my-comp', + template: ` + {{ name }} + `, + changeDetection: ChangeDetectionStrategy.OnPush + }) + class MyComp { + @Input() name: string; + + // NORMATIVE + static ngComponentDef = $r3$.ɵdefineComponent({ + type: MyComp, + tag: 'my-comp', + factory: function MyComp_Factory() { return new MyComp(); }, + template: function MyComp_Template(ctx: $MyComp$, cm: $boolean$) { + if (cm) { + $r3$.ɵT(0); + } + $r3$.ɵt(0, $r3$.ɵb(ctx.name)); + }, + inputs: {name: 'name'}, + changeDetection: ChangeDetectionStrategy.OnPush + }); + // /NORMATIVE + } + + @Component({ + selector: 'my-app', + template: ` + + ` + }) + class MyApp { + name = 'some name'; + + static ngComponentDef = $r3$.ɵdefineComponent({ + type: MyApp, + tag: 'my-app', + factory: function MyApp_Factory() { return new MyApp(); }, + template: function MyApp_Template(ctx: $MyApp$, cm: $boolean$) { + if (cm) { + $r3$.ɵE(0, MyComp); + $r3$.ɵe(); + } + $r3$.ɵp(0, 'name', $r3$.ɵb(ctx.name)); + MyComp.ngComponentDef.h(1, 0); + $r3$.ɵr(1, 0); + } + }); + } + + expect(renderComp(MyApp)).toEqual(`some name`); + }); + xit('should support structural directives', () => { type $MyComponent$ = MyComponent; diff --git a/packages/core/test/render3/component_spec.ts b/packages/core/test/render3/component_spec.ts index ee769c47b2..83989080ea 100644 --- a/packages/core/test/render3/component_spec.ts +++ b/packages/core/test/render3/component_spec.ts @@ -9,9 +9,9 @@ import {withBody} from '@angular/core/testing'; import {DoCheck, ViewEncapsulation} from '../../src/core'; -import {detectChanges, getRenderedText, whenRendered} from '../../src/render3/component'; +import {getRenderedText, whenRendered} from '../../src/render3/component'; import {defineComponent, markDirty} from '../../src/render3/index'; -import {bind, container, containerRefreshEnd, containerRefreshStart, 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 {getRendererFactory2} from './imported_renderer2'; diff --git a/packages/core/test/render3/di_spec.ts b/packages/core/test/render3/di_spec.ts index 5e08c25542..8184d55c31 100644 --- a/packages/core/test/render3/di_spec.ts +++ b/packages/core/test/render3/di_spec.ts @@ -14,6 +14,7 @@ import {PublicFeature, defineDirective, inject, injectElementRef, injectTemplate import {bind, container, containerRefreshEnd, containerRefreshStart, createLNode, createLView, createTView, elementEnd, elementStart, embeddedViewEnd, embeddedViewStart, enterView, interpolation2, leaveView, load, text, textBinding} from '../../src/render3/instructions'; import {LInjector} from '../../src/render3/interfaces/injector'; import {LNodeFlags} from '../../src/render3/interfaces/node'; +import {LViewFlags} from '../../src/render3/interfaces/view'; import {renderComponent, renderToHtml} from './render_util'; @@ -320,7 +321,8 @@ describe('di', () => { describe('getOrCreateNodeInjector', () => { it('should handle initial undefined state', () => { - const contentView = createLView(-1, null !, createTView(), null, null); + const contentView = + createLView(-1, null !, createTView(), null, null, LViewFlags.CheckAlways); const oldView = enterView(contentView, null !); try { const parent = createLNode(0, LNodeFlags.Element, null, null);