feat(core): view engine - add support for `OnPush` and detached views. (#14216)

Part of #14013

PR Close #14216
This commit is contained in:
Tobias Bosch 2017-01-31 14:52:01 -08:00 committed by Miško Hevery
parent 08ff67ea11
commit 45e1e36477
11 changed files with 333 additions and 131 deletions

View File

@ -10,7 +10,7 @@ import {isDevMode} from '../application_ref';
import {SecurityContext} from '../security'; import {SecurityContext} from '../security';
import {BindingDef, BindingType, DebugContext, DisposableFn, ElementData, ElementOutputDef, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, QueryValueType, ViewData, ViewDefinition, ViewFlags, asElementData} from './types'; import {BindingDef, BindingType, DebugContext, DisposableFn, ElementData, ElementOutputDef, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, QueryValueType, ViewData, ViewDefinition, ViewFlags, asElementData} from './types';
import {checkAndUpdateBinding, entryAction, setBindingDebugInfo, setCurrentNode, sliceErrorStack, unwrapValue} from './util'; import {checkAndUpdateBinding, dispatchEvent, entryAction, setBindingDebugInfo, setCurrentNode, sliceErrorStack, unwrapValue} from './util';
export function anchorDef( export function anchorDef(
flags: NodeFlags, matchedQueries: [string, QueryValueType][], ngContentIndex: number, flags: NodeFlags, matchedQueries: [string, QueryValueType][], ngContentIndex: number,
@ -192,17 +192,14 @@ export function createElement(view: ViewData, renderHost: any, def: NodeDef): El
} }
function renderEventHandlerClosure(view: ViewData, index: number, eventName: string) { function renderEventHandlerClosure(view: ViewData, index: number, eventName: string) {
return entryAction(EntryAction.HandleEvent, (event: any) => { return entryAction(
setCurrentNode(view, index); EntryAction.HandleEvent, (event: any) => dispatchEvent(view, index, eventName, event));
return view.def.handleEvent(view, index, eventName, event);
});
} }
function directDomEventHandlerClosure(view: ViewData, index: number, eventName: string) { function directDomEventHandlerClosure(view: ViewData, index: number, eventName: string) {
return entryAction(EntryAction.HandleEvent, (event: any) => { return entryAction(EntryAction.HandleEvent, (event: any) => {
setCurrentNode(view, index); const result = dispatchEvent(view, index, eventName, event);
const result = view.def.handleEvent(view, index, eventName, event);
if (result === false) { if (result === false) {
event.preventDefault(); event.preventDefault();
} }

View File

@ -8,10 +8,10 @@
import {BaseError, WrappedError} from '../facade/errors'; import {BaseError, WrappedError} from '../facade/errors';
import {DebugContext} from './types'; import {DebugContext, EntryAction, ViewState} from './types';
export function expressionChangedAfterItHasBeenCheckedError( export function expressionChangedAfterItHasBeenCheckedError(
context: DebugContext, oldValue: any, currValue: any, isFirstCheck: boolean): ViewError { context: DebugContext, oldValue: any, currValue: any, isFirstCheck: boolean): ViewDebugError {
let msg = let msg =
`Expression has changed after it was checked. Previous value: '${oldValue}'. Current value: '${currValue}'.`; `Expression has changed after it was checked. Previous value: '${oldValue}'. Current value: '${currValue}'.`;
if (isFirstCheck) { if (isFirstCheck) {
@ -19,25 +19,30 @@ export function expressionChangedAfterItHasBeenCheckedError(
` It seems like the view has been created after its parent and its children have been dirty checked.` + ` It seems like the view has been created after its parent and its children have been dirty checked.` +
` Has it been created in a change detection hook ?`; ` Has it been created in a change detection hook ?`;
} }
return viewError(msg, context); return viewDebugError(msg, context);
} }
export function viewWrappedError(originalError: any, context: DebugContext): WrappedError& export function viewWrappedDebugError(originalError: any, context: DebugContext): WrappedError&
ViewError { ViewDebugError {
const err = viewError(originalError.message, context) as WrappedError & ViewError; const err = viewDebugError(originalError.message, context) as WrappedError & ViewDebugError;
err.originalError = originalError; err.originalError = originalError;
return err; return err;
} }
export interface ViewError { context: DebugContext; } export interface ViewDebugError { context: DebugContext; }
export function viewError(msg: string, context: DebugContext): ViewError { export function viewDebugError(msg: string, context: DebugContext): ViewDebugError {
const err = new Error(msg) as any; const err = new Error(msg) as any;
err.context = context; err.context = context;
err.stack = context.source; err.stack = context.source;
context.view.state = ViewState.Errored;
return err; return err;
} }
export function isViewError(err: any): boolean { export function isViewDebugError(err: any): boolean {
return err.context; return err.context;
} }
export function viewDestroyedError(action: EntryAction): Error {
return new Error(`View has been used after destroy for ${EntryAction[action]}`);
}

View File

@ -16,8 +16,8 @@ import {ViewContainerRef} from '../linker/view_container_ref';
import {Renderer} from '../render/api'; import {Renderer} from '../render/api';
import {queryDef} from './query'; import {queryDef} from './query';
import {BindingDef, BindingType, DepDef, DepFlags, DisposableFn, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, ProviderData, ProviderOutputDef, QueryBindingType, QueryDef, QueryValueType, Services, ViewData, ViewDefinition, ViewFlags, asElementData, asProviderData} from './types'; import {BindingDef, BindingType, DepDef, DepFlags, DisposableFn, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, ProviderData, ProviderOutputDef, QueryBindingType, QueryDef, QueryValueType, Services, ViewData, ViewDefinition, ViewFlags, ViewState, asElementData, asProviderData} from './types';
import {checkAndUpdateBinding, checkAndUpdateBindingWithChange, entryAction, setBindingDebugInfo, setCurrentNode, unwrapValue} from './util'; import {checkAndUpdateBinding, dispatchEvent, entryAction, setBindingDebugInfo, setCurrentNode, unwrapValue} from './util';
const _tokenKeyCache = new Map<any, string>(); const _tokenKeyCache = new Map<any, string>();
@ -122,10 +122,8 @@ export function createProvider(
} }
function eventHandlerClosure(view: ViewData, index: number, eventName: string) { function eventHandlerClosure(view: ViewData, index: number, eventName: string) {
return entryAction(EntryAction.HandleEvent, (event: any) => { return entryAction(
setCurrentNode(view, index); EntryAction.HandleEvent, (event: any) => dispatchEvent(view, index, eventName, event));
view.def.handleEvent(view, index, eventName, event);
});
} }
export function checkAndUpdateProviderInline( export function checkAndUpdateProviderInline(
@ -159,7 +157,7 @@ export function checkAndUpdateProviderInline(
if (changes) { if (changes) {
provider.ngOnChanges(changes); provider.ngOnChanges(changes);
} }
if (view.firstChange && (def.flags & NodeFlags.OnInit)) { if (view.state === ViewState.FirstCheck && (def.flags & NodeFlags.OnInit)) {
provider.ngOnInit(); provider.ngOnInit();
} }
if (def.flags & NodeFlags.DoCheck) { if (def.flags & NodeFlags.DoCheck) {
@ -176,7 +174,7 @@ export function checkAndUpdateProviderDynamic(view: ViewData, def: NodeDef, valu
if (changes) { if (changes) {
provider.ngOnChanges(changes); provider.ngOnChanges(changes);
} }
if (view.firstChange && (def.flags & NodeFlags.OnInit)) { if (view.state === ViewState.FirstCheck && (def.flags & NodeFlags.OnInit)) {
provider.ngOnInit(); provider.ngOnInit();
} }
if (def.flags & NodeFlags.DoCheck) { if (def.flags & NodeFlags.DoCheck) {
@ -272,8 +270,10 @@ function checkAndUpdateProp(
let change: SimpleChange; let change: SimpleChange;
let changed: boolean; let changed: boolean;
if (def.flags & NodeFlags.OnChanges) { if (def.flags & NodeFlags.OnChanges) {
change = checkAndUpdateBindingWithChange(view, def, bindingIdx, value); const oldValue = view.oldValues[def.bindingIndex + bindingIdx];
changed = !!change; changed = checkAndUpdateBinding(view, def, bindingIdx, value);
change =
changed ? new SimpleChange(oldValue, value, view.state === ViewState.FirstCheck) : null;
} else { } else {
changed = checkAndUpdateBinding(view, def, bindingIdx, value); changed = checkAndUpdateBinding(view, def, bindingIdx, value);
} }

View File

@ -132,7 +132,7 @@ export function checkAndUpdatePureExpressionInline(
case 4: case 4:
value[3] = v3; value[3] = v3;
case 3: case 3:
value[3] = v2; value[2] = v2;
case 2: case 2:
value[1] = v1; value[1] = v1;
case 1: case 1:
@ -235,7 +235,7 @@ export function checkAndUpdatePureExpressionDynamic(view: ViewData, def: NodeDef
for (let i = 0; i < values.length; i++) { for (let i = 0; i < values.length; i++) {
params[i] = unwrapValue(values[i]); params[i] = unwrapValue(values[i]);
} }
value = data.pipe.transform(params[0], ...params.slice(1)); value = (<any>data.pipe.transform)(...params);
break; break;
} }
data.value = value; data.value = value;

View File

@ -18,7 +18,7 @@ import {Sanitizer, SecurityContext} from '../security';
import {createInjector} from './provider'; import {createInjector} from './provider';
import {getQueryValue} from './query'; import {getQueryValue} from './query';
import {DebugContext, ElementData, NodeData, NodeDef, NodeType, Services, ViewData, ViewDefinition, asElementData} from './types'; import {DebugContext, ElementData, NodeData, NodeDef, NodeType, Services, ViewData, ViewDefinition, ViewState, asElementData} from './types';
import {isComponentView, renderNode, rootRenderNodes} from './util'; import {isComponentView, renderNode, rootRenderNodes} from './util';
import {checkAndUpdateView, checkNoChangesView, createEmbeddedView, destroyView} from './view'; import {checkAndUpdateView, checkNoChangesView, createEmbeddedView, destroyView} from './view';
import {attachEmbeddedView, detachEmbeddedView} from './view_attach'; import {attachEmbeddedView, detachEmbeddedView} from './view_attach';
@ -112,13 +112,22 @@ class ViewRef_ implements EmbeddedViewRef<any> {
get context() { return this._view.context; } get context() { return this._view.context; }
get destroyed(): boolean { return unimplemented(); } get destroyed(): boolean { return this._view.state === ViewState.Destroyed; }
markForCheck(): void { unimplemented(); } markForCheck(): void { this.reattach(); }
detach(): void { unimplemented(); } detach(): void {
if (this._view.state === ViewState.ChecksEnabled) {
this._view.state = ViewState.ChecksDisabled;
}
}
detectChanges(): void { checkAndUpdateView(this._view); } detectChanges(): void { checkAndUpdateView(this._view); }
checkNoChanges(): void { checkNoChangesView(this._view); } checkNoChanges(): void { checkNoChangesView(this._view); }
reattach(): void { unimplemented(); }
reattach(): void {
if (this._view.state === ViewState.ChecksDisabled) {
this._view.state = ViewState.ChecksEnabled;
}
}
onDestroy(callback: Function) { unimplemented(); } onDestroy(callback: Function) { unimplemented(); }
destroy() { unimplemented(); } destroy() { unimplemented(); }

View File

@ -56,7 +56,8 @@ export type ViewHandleEventFn =
*/ */
export enum ViewFlags { export enum ViewFlags {
None = 0, None = 0,
DirectDom = 1 << 1 DirectDom = 1 << 1,
OnPush = 1 << 2
} }
/** /**
@ -271,11 +272,19 @@ export interface ViewData {
// and call the right accessor (e.g. `elementData`) based on // and call the right accessor (e.g. `elementData`) based on
// the NodeType. // the NodeType.
nodes: {[key: number]: NodeData}; nodes: {[key: number]: NodeData};
firstChange: boolean; state: ViewState;
oldValues: any[]; oldValues: any[];
disposables: DisposableFn[]; disposables: DisposableFn[];
} }
export enum ViewState {
FirstCheck,
ChecksEnabled,
ChecksDisabled,
Errored,
Destroyed
}
export type DisposableFn = () => void; export type DisposableFn = () => void;
/** /**

View File

@ -12,8 +12,8 @@ import {SimpleChange} from '../change_detection/change_detection_util';
import {looseIdentical} from '../facade/lang'; import {looseIdentical} from '../facade/lang';
import {Renderer} from '../render/api'; import {Renderer} from '../render/api';
import {expressionChangedAfterItHasBeenCheckedError, isViewError, viewWrappedError} from './errors'; import {expressionChangedAfterItHasBeenCheckedError, isViewDebugError, viewDestroyedError, viewWrappedDebugError} from './errors';
import {ElementData, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, ViewData, ViewDefinition, ViewDefinitionFactory, asElementData, asTextData} from './types'; import {ElementData, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewState, asElementData, asProviderData, asTextData} from './types';
export function setBindingDebugInfo( export function setBindingDebugInfo(
renderer: Renderer, renderNode: any, propName: string, value: any) { renderer: Renderer, renderNode: any, propName: string, value: any) {
@ -36,31 +36,41 @@ function camelCaseToDashCase(input: string): string {
export function checkBindingNoChanges( export function checkBindingNoChanges(
view: ViewData, def: NodeDef, bindingIdx: number, value: any) { view: ViewData, def: NodeDef, bindingIdx: number, value: any) {
const oldValue = view.oldValues[def.bindingIndex + bindingIdx]; const oldValue = view.oldValues[def.bindingIndex + bindingIdx];
if (view.firstChange || !devModeEqual(oldValue, value)) { if (view.state === ViewState.FirstCheck || !devModeEqual(oldValue, value)) {
throw expressionChangedAfterItHasBeenCheckedError( throw expressionChangedAfterItHasBeenCheckedError(
view.services.createDebugContext(view, def.index), oldValue, value, view.firstChange); view.services.createDebugContext(view, def.index), oldValue, value,
view.state === ViewState.FirstCheck);
} }
} }
export function checkAndUpdateBinding( export function checkAndUpdateBinding(
view: ViewData, def: NodeDef, bindingIdx: number, value: any): boolean { view: ViewData, def: NodeDef, bindingIdx: number, value: any): boolean {
const oldValues = view.oldValues; const oldValues = view.oldValues;
if (view.firstChange || !looseIdentical(oldValues[def.bindingIndex + bindingIdx], value)) { if (view.state === ViewState.FirstCheck ||
!looseIdentical(oldValues[def.bindingIndex + bindingIdx], value)) {
oldValues[def.bindingIndex + bindingIdx] = value; oldValues[def.bindingIndex + bindingIdx] = value;
if (def.flags & NodeFlags.HasComponent) {
const compView = asProviderData(view, def.index).componentView;
if (compView.state === ViewState.ChecksDisabled && compView.def.flags & ViewFlags.OnPush) {
compView.state = ViewState.ChecksEnabled;
}
}
return true; return true;
} }
return false; return false;
} }
export function checkAndUpdateBindingWithChange( export function dispatchEvent(
view: ViewData, def: NodeDef, bindingIdx: number, value: any): SimpleChange { view: ViewData, nodeIndex: number, eventName: string, event: any): boolean {
const oldValues = view.oldValues; setCurrentNode(view, nodeIndex);
const oldValue = oldValues[def.bindingIndex + bindingIdx]; let currView = view;
if (view.firstChange || !looseIdentical(oldValue, value)) { while (currView) {
oldValues[def.bindingIndex + bindingIdx] = value; if (currView.state === ViewState.ChecksDisabled && currView.def.flags & ViewFlags.OnPush) {
return new SimpleChange(oldValue, value, view.firstChange); currView.state = ViewState.ChecksEnabled;
}
currView = currView.parent;
} }
return null; return view.def.handleEvent(view, nodeIndex, eventName, event);
} }
export function unwrapValue(value: any): any { export function unwrapValue(value: any): any {
@ -141,6 +151,9 @@ export function currentAction() {
* or code of the framework that might throw as a valid use case. * or code of the framework that might throw as a valid use case.
*/ */
export function setCurrentNode(view: ViewData, nodeIndex: number) { export function setCurrentNode(view: ViewData, nodeIndex: number) {
if (view.state === ViewState.Destroyed) {
throw viewDestroyedError(_currentAction);
}
_currentView = view; _currentView = view;
_currentNodeIndex = nodeIndex; _currentNodeIndex = nodeIndex;
} }
@ -170,15 +183,14 @@ function callWithTryCatch(fn: (a: any) => any, arg: any): any {
try { try {
return fn(arg); return fn(arg);
} catch (e) { } catch (e) {
if (isViewError(e) || !_currentView) { if (isViewDebugError(e) || !_currentView) {
throw e; throw e;
} }
const debugContext = _currentView.services.createDebugContext(_currentView, _currentNodeIndex); const debugContext = _currentView.services.createDebugContext(_currentView, _currentNodeIndex);
throw viewWrappedError(e, debugContext); throw viewWrappedDebugError(e, debugContext);
} }
} }
export function rootRenderNodes(view: ViewData): any[] { export function rootRenderNodes(view: ViewData): any[] {
const renderNodes: any[] = []; const renderNodes: any[] = [];
visitRootRenderNodes(view, RenderNodeAction.Collect, undefined, undefined, renderNodes); visitRootRenderNodes(view, RenderNodeAction.Collect, undefined, undefined, renderNodes);

View File

@ -16,7 +16,7 @@ import {callLifecycleHooksChildrenFirst, checkAndUpdateProviderDynamic, checkAnd
import {checkAndUpdatePureExpressionDynamic, checkAndUpdatePureExpressionInline, createPureExpression} from './pure_expression'; import {checkAndUpdatePureExpressionDynamic, checkAndUpdatePureExpressionInline, createPureExpression} from './pure_expression';
import {checkAndUpdateQuery, createQuery, queryDef} from './query'; import {checkAndUpdateQuery, createQuery, queryDef} from './query';
import {checkAndUpdateTextDynamic, checkAndUpdateTextInline, createText} from './text'; import {checkAndUpdateTextDynamic, checkAndUpdateTextInline, createText} from './text';
import {ElementDef, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, ProviderData, ProviderDef, Services, TextDef, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewHandleEventFn, ViewUpdateFn, asElementData, asProviderData, asPureExpressionData, asQueryList} from './types'; import {ElementDef, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, ProviderData, ProviderDef, Services, TextDef, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewHandleEventFn, ViewState, ViewUpdateFn, asElementData, asProviderData, asPureExpressionData, asQueryList} from './types';
import {checkBindingNoChanges, currentAction, currentNodeIndex, currentView, entryAction, isComponentView, resolveViewDefinition, setCurrentNode} from './util'; import {checkBindingNoChanges, currentAction, currentNodeIndex, currentView, entryAction, isComponentView, resolveViewDefinition, setCurrentNode} from './util';
const NOOP = (): any => undefined; const NOOP = (): any => undefined;
@ -260,7 +260,7 @@ function createView(
parentDiIndex, parentDiIndex,
context: undefined, context: undefined,
component: undefined, nodes, component: undefined, nodes,
firstChange: true, renderer, services, state: ViewState.FirstCheck, renderer, services,
oldValues: new Array(def.bindingCount), disposables oldValues: new Array(def.bindingCount), disposables
}; };
return view; return view;
@ -348,13 +348,22 @@ function _checkAndUpdateView(view: ViewData) {
execQueriesAction(view, NodeFlags.HasContentQuery, QueryAction.CheckAndUpdate); execQueriesAction(view, NodeFlags.HasContentQuery, QueryAction.CheckAndUpdate);
callLifecycleHooksChildrenFirst( callLifecycleHooksChildrenFirst(
view, NodeFlags.AfterContentChecked | (view.firstChange ? NodeFlags.AfterContentInit : 0)); view, NodeFlags.AfterContentChecked |
(view.state === ViewState.FirstCheck ? NodeFlags.AfterContentInit : 0));
execComponentViewsAction(view, ViewAction.CheckAndUpdate); execComponentViewsAction(view, ViewAction.CheckAndUpdate);
execQueriesAction(view, NodeFlags.HasViewQuery, QueryAction.CheckAndUpdate); execQueriesAction(view, NodeFlags.HasViewQuery, QueryAction.CheckAndUpdate);
callLifecycleHooksChildrenFirst( callLifecycleHooksChildrenFirst(
view, NodeFlags.AfterViewChecked | (view.firstChange ? NodeFlags.AfterViewInit : 0)); view, NodeFlags.AfterViewChecked |
view.firstChange = false; (view.state === ViewState.FirstCheck ? NodeFlags.AfterViewInit : 0));
if (view.state === ViewState.FirstCheck || view.state === ViewState.ChecksEnabled) {
if (view.def.flags & ViewFlags.OnPush) {
view.state = ViewState.ChecksDisabled;
} else {
view.state = ViewState.ChecksEnabled;
}
}
} }
export function checkNodeInline( export function checkNodeInline(
@ -465,7 +474,8 @@ function checkNoChangesQuery(view: ViewData, nodeDef: NodeDef) {
if (queryList.dirty) { if (queryList.dirty) {
throw expressionChangedAfterItHasBeenCheckedError( throw expressionChangedAfterItHasBeenCheckedError(
view.services.createDebugContext(view, nodeDef.index), view.services.createDebugContext(view, nodeDef.index),
`Query ${nodeDef.query.id} not dirty`, `Query ${nodeDef.query.id} dirty`, view.firstChange); `Query ${nodeDef.query.id} not dirty`, `Query ${nodeDef.query.id} dirty`,
view.state === ViewState.FirstCheck);
} }
} }
@ -480,6 +490,7 @@ function _destroyView(view: ViewData) {
} }
execComponentViewsAction(view, ViewAction.Destroy); execComponentViewsAction(view, ViewAction.Destroy);
execEmbeddedViewsAction(view, ViewAction.Destroy); execEmbeddedViewsAction(view, ViewAction.Destroy);
view.state = ViewState.Destroyed;
} }
enum ViewAction { enum ViewAction {
@ -536,10 +547,14 @@ function execEmbeddedViewsAction(view: ViewData, action: ViewAction) {
function callViewAction(view: ViewData, action: ViewAction) { function callViewAction(view: ViewData, action: ViewAction) {
switch (action) { switch (action) {
case ViewAction.CheckNoChanges: case ViewAction.CheckNoChanges:
_checkNoChangesView(view); if (view.state === ViewState.ChecksEnabled || view.state === ViewState.FirstCheck) {
_checkNoChangesView(view);
}
break; break;
case ViewAction.CheckAndUpdate: case ViewAction.CheckAndUpdate:
_checkAndUpdateView(view); if (view.state === ViewState.ChecksEnabled || view.state === ViewState.FirstCheck) {
_checkAndUpdateView(view);
}
break; break;
case ViewAction.Destroy: case ViewAction.Destroy:
_destroyView(view); _destroyView(view);

View File

@ -7,7 +7,7 @@
*/ */
import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core'; import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core';
import {BindingType, DefaultServices, NodeDef, NodeFlags, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asProviderData, checkAndUpdateView, checkNoChangesView, checkNodeDynamic, checkNodeInline, createRootView, destroyView, elementDef, providerDef, rootRenderNodes, setCurrentNode, textDef, viewDef} from '@angular/core/src/view/index'; import {BindingType, DefaultServices, NodeDef, NodeFlags, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewState, ViewUpdateFn, anchorDef, asProviderData, checkAndUpdateView, checkNoChangesView, checkNodeDynamic, checkNodeInline, createRootView, destroyView, elementDef, providerDef, rootRenderNodes, setCurrentNode, textDef, viewDef} from '@angular/core/src/view/index';
import {inject} from '@angular/core/testing'; import {inject} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
@ -35,8 +35,9 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
})); }));
function compViewDef( function compViewDef(
nodes: NodeDef[], update?: ViewUpdateFn, handleEvent?: ViewHandleEventFn): ViewDefinition { nodes: NodeDef[], update?: ViewUpdateFn, handleEvent?: ViewHandleEventFn,
return viewDef(config.viewFlags, nodes, update, handleEvent, renderComponentType); flags?: ViewFlags): ViewDefinition {
return viewDef(config.viewFlags | flags, nodes, update, handleEvent, renderComponentType);
} }
function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} { function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} {
@ -69,65 +70,211 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
expect(getDOM().nodeName(compRootEl).toLowerCase()).toBe('span'); expect(getDOM().nodeName(compRootEl).toLowerCase()).toBe('span');
}); });
it('should dirty check component views', () => { describe('data binding', () => {
let value = 'v1'; it('should dirty check component views', () => {
class AComp { let value: any;
a: any; class AComp {
} a: any;
}
const update = jasmine.createSpy('updater').and.callFake((view: ViewData) => { const update = jasmine.createSpy('updater').and.callFake((view: ViewData) => {
setCurrentNode(view, 0); setCurrentNode(view, 0);
checkNodeInline(value); checkNodeInline(value);
});
const {view, rootNodes} = createAndGetRootNodes(
compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'div'),
providerDef(NodeFlags.None, null, 0, AComp, [], null, null, () => compViewDef(
[
elementDef(NodeFlags.None, null, null, 0, 'span', null, [[BindingType.ElementAttribute, 'a', SecurityContext.NONE]]),
], update
)),
]));
const compView = asProviderData(view, 1).componentView;
value = 'v1';
checkAndUpdateView(view);
expect(update).toHaveBeenCalledWith(compView);
update.calls.reset();
checkNoChangesView(view);
expect(update).toHaveBeenCalledWith(compView);
value = 'v2';
expect(() => checkNoChangesView(view))
.toThrowError(
`Expression has changed after it was checked. Previous value: 'v1'. Current value: 'v2'.`);
}); });
const {view, rootNodes} = createAndGetRootNodes( it('should support detaching and attaching component views for dirty checking', () => {
compViewDef([ class AComp {
a: any;
}
const update = jasmine.createSpy('updater');
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'div'), elementDef(NodeFlags.None, null, null, 1, 'div'),
providerDef(NodeFlags.None, null, 0, AComp, [], null, null, () => compViewDef( providerDef(
[ NodeFlags.None, null, 0, AComp, [], null, null,
elementDef(NodeFlags.None, null, null, 0, 'span', null, [[BindingType.ElementAttribute, 'a', SecurityContext.NONE]]), () => compViewDef(
], update [
)), elementDef(NodeFlags.None, null, null, 0, 'span'),
], jasmine.createSpy('parentUpdater'))); ],
const compView = asProviderData(view, 1).componentView; update)),
]));
checkAndUpdateView(view); const compView = asProviderData(view, 1).componentView;
expect(update).toHaveBeenCalledWith(compView); checkAndUpdateView(view);
update.calls.reset();
update.calls.reset(); compView.state = ViewState.ChecksDisabled;
checkNoChangesView(view); checkAndUpdateView(view);
expect(update).not.toHaveBeenCalled();
expect(update).toHaveBeenCalledWith(compView); compView.state = ViewState.ChecksEnabled;
checkAndUpdateView(view);
expect(update).toHaveBeenCalled();
});
value = 'v2'; if (isBrowser()) {
expect(() => checkNoChangesView(view)) it('should support OnPush components', () => {
.toThrowError( let compInputValue: any;
`Expression has changed after it was checked. Previous value: 'v1'. Current value: 'v2'.`); class AComp {
}); a: any;
}
it('should destroy component views', () => { const update = jasmine.createSpy('updater');
const log: string[] = [];
class AComp {} const addListenerSpy = spyOn(HTMLElement.prototype, 'addEventListener').and.callThrough();
const {view, rootNodes} =
createAndGetRootNodes(
compViewDef(
[
elementDef(NodeFlags.None, null, null, 1, 'div'),
providerDef(
NodeFlags.None, null, 0, AComp, [], {a: [0, 'a']}, null,
() =>
compViewDef(
[
elementDef(NodeFlags.None, null, null, 0, 'span', null, null, ['click']),
],
update, null, ViewFlags.OnPush)),
],
(view) => {
setCurrentNode(view, 1);
checkNodeInline(compInputValue);
}));
class ChildProvider { const compView = asProviderData(view, 1).componentView;
ngOnDestroy() { log.push('ngOnDestroy'); };
checkAndUpdateView(view);
// auto detach
update.calls.reset();
checkAndUpdateView(view);
expect(update).not.toHaveBeenCalled();
// auto attach on input changes
update.calls.reset();
compInputValue = 'v1';
checkAndUpdateView(view);
expect(update).toHaveBeenCalled();
// auto detach
update.calls.reset();
checkAndUpdateView(view);
expect(update).not.toHaveBeenCalled();
// auto attach on events
addListenerSpy.calls.mostRecent().args[1]('SomeEvent');
update.calls.reset();
checkAndUpdateView(view);
expect(update).toHaveBeenCalled();
// auto detach
update.calls.reset();
checkAndUpdateView(view);
expect(update).not.toHaveBeenCalled();
});
} }
const {view, rootNodes} = createAndGetRootNodes(compViewDef([ it('should stop dirty checking views that threw errors in change detection', () => {
elementDef(NodeFlags.None, null, null, 1, 'div'), class AComp {
providerDef( a: any;
NodeFlags.None, null, 0, AComp, [], null, null, }
() => compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'span'),
providerDef(NodeFlags.OnDestroy, null, 0, ChildProvider, [])
])),
]));
destroyView(view); const update = jasmine.createSpy('updater');
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'div'),
providerDef(
NodeFlags.None, null, 0, AComp, [], null, null,
() => compViewDef(
[
elementDef(NodeFlags.None, null, null, 0, 'span'),
],
update)),
]));
const compView = asProviderData(view, 1).componentView;
update.and.callFake((view: ViewData) => {
setCurrentNode(view, 0);
throw new Error('Test');
});
expect(() => checkAndUpdateView(view)).toThrow();
expect(update).toHaveBeenCalled();
update.calls.reset();
checkAndUpdateView(view);
expect(update).not.toHaveBeenCalled();
});
expect(log).toEqual(['ngOnDestroy']);
}); });
describe('destroy', () => {
it('should destroy component views', () => {
const log: string[] = [];
class AComp {}
class ChildProvider {
ngOnDestroy() { log.push('ngOnDestroy'); };
}
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'div'),
providerDef(
NodeFlags.None, null, 0, AComp, [], null, null,
() => compViewDef([
elementDef(NodeFlags.None, null, null, 1, 'span'),
providerDef(NodeFlags.OnDestroy, null, 0, ChildProvider, [])
])),
]));
destroyView(view);
expect(log).toEqual(['ngOnDestroy']);
});
it('should throw on dirty checking destroyed views', () => {
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, null, null, 0, 'div'),
],
(view) => { setCurrentNode(view, 0); }));
destroyView(view);
expect(() => checkAndUpdateView(view))
.toThrowError('View has been used after destroy for CheckAndUpdate');
});
});
}); });
} }

View File

@ -207,7 +207,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
elementDef( elementDef(
NodeFlags.None, null, null, 0, 'input', null, NodeFlags.None, null, null, 0, 'input', null,
[ [
[BindingType.ElementProperty, 'title', SecurityContext.NONE], [BindingType.ElementProperty, 'someProp', SecurityContext.NONE],
]), ]),
], ],
(view) => { (view) => {
@ -216,7 +216,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
})); }));
const setterSpy = jasmine.createSpy('set'); const setterSpy = jasmine.createSpy('set');
Object.defineProperty(rootNodes[0], 'title', {set: setterSpy}); Object.defineProperty(rootNodes[0], 'someProp', {set: setterSpy});
bindingValue = 'v1'; bindingValue = 'v1';
checkAndUpdateView(view); checkAndUpdateView(view);
@ -234,7 +234,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
}); });
}); });
if (getDOM().supportsDOMEvents()) { if (isBrowser()) {
describe('listen to DOM events', () => { describe('listen to DOM events', () => {
let removeNodes: Node[]; let removeNodes: Node[];
beforeEach(() => { removeNodes = []; }); beforeEach(() => { removeNodes = []; });

View File

@ -98,34 +98,42 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
expect(getDOM().getText(rootNodes[0])).toBe('0a1b2'); expect(getDOM().getText(rootNodes[0])).toBe('0a1b2');
}); });
it(`should unwrap values with ${InlineDynamic[inlineDynamic]}`, () => { if (isBrowser()) {
let bindingValue: any; it(`should unwrap values with ${InlineDynamic[inlineDynamic]}`, () => {
let bindingValue: any;
const setterSpy = jasmine.createSpy('set');
const {view, rootNodes} = createAndGetRootNodes(compViewDef( class FakeTextNode {
[ set nodeValue(value: any) { setterSpy(value); }
textDef(null, ['', '']), }
],
(view: ViewData) => {
setCurrentNode(view, 0);
checkNodeInlineOrDynamic(inlineDynamic, [bindingValue]);
}));
const setterSpy = jasmine.createSpy('set'); spyOn(document, 'createTextNode').and.returnValue(new FakeTextNode());
Object.defineProperty(rootNodes[0], 'nodeValue', {set: setterSpy});
bindingValue = 'v1'; const {view, rootNodes} = createAndGetRootNodes(compViewDef(
checkAndUpdateView(view); [
expect(setterSpy).toHaveBeenCalledWith('v1'); textDef(null, ['', '']),
],
(view: ViewData) => {
setCurrentNode(view, 0);
checkNodeInlineOrDynamic(inlineDynamic, [bindingValue]);
}));
setterSpy.calls.reset(); Object.defineProperty(rootNodes[0], 'nodeValue', {set: setterSpy});
checkAndUpdateView(view);
expect(setterSpy).not.toHaveBeenCalled();
setterSpy.calls.reset(); bindingValue = 'v1';
bindingValue = WrappedValue.wrap('v1'); checkAndUpdateView(view);
checkAndUpdateView(view); expect(setterSpy).toHaveBeenCalledWith('v1');
expect(setterSpy).toHaveBeenCalledWith('v1');
}); setterSpy.calls.reset();
checkAndUpdateView(view);
expect(setterSpy).not.toHaveBeenCalled();
setterSpy.calls.reset();
bindingValue = WrappedValue.wrap('v1');
checkAndUpdateView(view);
expect(setterSpy).toHaveBeenCalledWith('v1');
});
}
}); });
}); });