feat(ivy): support OnPush change detection (#22417)

PR Close #22417
This commit is contained in:
Kara Erickson 2018-02-23 13:17:20 -08:00 committed by Alex Eagle
parent e454c5a98e
commit 8c358844dd
11 changed files with 470 additions and 110 deletions

View File

@ -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<T>(
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<T>(
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<T>(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<T>(component: T) {
const rootContext = getRootContext(component);
if (rootContext.clean == CLEAN_PROMISE) {
let res: null|((val: null) => void);
rootContext.clean = new Promise<null>((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<T>(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.
*

View File

@ -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<T>(componentDefinition: ComponentDefArgs<T>): 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<T>).changeDetection ===
ChangeDetectionStrategy.OnPush
};
const feature = componentDefinition.features;
feature && feature.forEach((fn) => fn(def));

View File

@ -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,
};

View File

@ -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<any>| 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<T>(
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<T>(
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<any>) {
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<T>(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<T>(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<T>(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<T>(data[directiveIndex]);
const oldView = enterView(hostView, element);
try {
template(directive, creationMode);
} finally {
refreshDynamicChildren();
leaveView(oldView);
}
}
}
}
@ -1389,6 +1408,97 @@ export function addToViewTree<T extends LView|LContainer>(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<T>(rootContext: RootContext) {
if (rootContext.clean == _CLEAN_PROMISE) {
let res: null|((val: null) => void);
rootContext.clean = new Promise<null>((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<T>(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<T>(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<T>(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;

View File

@ -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<T> extends DirectiveDef<T> {
* 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<T> extends DirectiveDefArgs<T> {
template: ComponentTemplate<T>;
features?: ComponentDefFeature[];
rendererType?: RendererType2;
changeDetection?: ChangeDetectionStrategy;
}
export type DirectiveDefFeature = <T>(directiveDef: DirectiveDef<T>) => void;

View File

@ -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 */

View File

@ -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"
},

View File

@ -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 }}
* <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({
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');
});
});

View File

@ -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(`<div aria-label="some label" hostbindingdir=""></div>`);
});
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: `
<my-comp [name]="name"></my-comp>
`
})
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(`<my-comp>some name</my-comp>`);
});
xit('should support structural directives', () => {
type $MyComponent$ = MyComponent;

View File

@ -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';

View File

@ -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);