parent
e454c5a98e
commit
8c358844dd
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,9 +1241,12 @@ 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.`);
|
||||
const hostView = element.data !;
|
||||
|
||||
// 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 hostView = element.data !;
|
||||
const oldView = enterView(hostView, element);
|
||||
try {
|
||||
template(directive, creationMode);
|
||||
|
@ -1238,6 +1256,7 @@ export function directiveRefresh<T>(directiveIndex: number, elementIndex: number
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruction to distribute projectable nodes among <ng-content> occurrences in a given template.
|
||||
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
||||
});
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue