diff --git a/modules/@angular/core/src/view/element.ts b/modules/@angular/core/src/view/element.ts index b9b20812d1..7e3bda6452 100644 --- a/modules/@angular/core/src/view/element.ts +++ b/modules/@angular/core/src/view/element.ts @@ -10,7 +10,7 @@ import {isDevMode} from '../application_ref'; import {SecurityContext} from '../security'; 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( 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) { - return entryAction(EntryAction.HandleEvent, (event: any) => { - setCurrentNode(view, index); - return view.def.handleEvent(view, index, eventName, event); - }); + return entryAction( + EntryAction.HandleEvent, (event: any) => dispatchEvent(view, index, eventName, event)); } function directDomEventHandlerClosure(view: ViewData, index: number, eventName: string) { return entryAction(EntryAction.HandleEvent, (event: any) => { - setCurrentNode(view, index); - const result = view.def.handleEvent(view, index, eventName, event); + const result = dispatchEvent(view, index, eventName, event); if (result === false) { event.preventDefault(); } diff --git a/modules/@angular/core/src/view/errors.ts b/modules/@angular/core/src/view/errors.ts index 55b93ad965..0348821f02 100644 --- a/modules/@angular/core/src/view/errors.ts +++ b/modules/@angular/core/src/view/errors.ts @@ -8,10 +8,10 @@ import {BaseError, WrappedError} from '../facade/errors'; -import {DebugContext} from './types'; +import {DebugContext, EntryAction, ViewState} from './types'; export function expressionChangedAfterItHasBeenCheckedError( - context: DebugContext, oldValue: any, currValue: any, isFirstCheck: boolean): ViewError { + context: DebugContext, oldValue: any, currValue: any, isFirstCheck: boolean): ViewDebugError { let msg = `Expression has changed after it was checked. Previous value: '${oldValue}'. Current value: '${currValue}'.`; 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.` + ` 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& - ViewError { - const err = viewError(originalError.message, context) as WrappedError & ViewError; +export function viewWrappedDebugError(originalError: any, context: DebugContext): WrappedError& + ViewDebugError { + const err = viewDebugError(originalError.message, context) as WrappedError & ViewDebugError; err.originalError = originalError; 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; err.context = context; err.stack = context.source; + context.view.state = ViewState.Errored; return err; } -export function isViewError(err: any): boolean { +export function isViewDebugError(err: any): boolean { return err.context; } + +export function viewDestroyedError(action: EntryAction): Error { + return new Error(`View has been used after destroy for ${EntryAction[action]}`); +} diff --git a/modules/@angular/core/src/view/provider.ts b/modules/@angular/core/src/view/provider.ts index bb03884f96..5dcdc8b1b0 100644 --- a/modules/@angular/core/src/view/provider.ts +++ b/modules/@angular/core/src/view/provider.ts @@ -16,8 +16,8 @@ import {ViewContainerRef} from '../linker/view_container_ref'; import {Renderer} from '../render/api'; 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 {checkAndUpdateBinding, checkAndUpdateBindingWithChange, entryAction, setBindingDebugInfo, setCurrentNode, unwrapValue} from './util'; +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, dispatchEvent, entryAction, setBindingDebugInfo, setCurrentNode, unwrapValue} from './util'; const _tokenKeyCache = new Map(); @@ -122,10 +122,8 @@ export function createProvider( } function eventHandlerClosure(view: ViewData, index: number, eventName: string) { - return entryAction(EntryAction.HandleEvent, (event: any) => { - setCurrentNode(view, index); - view.def.handleEvent(view, index, eventName, event); - }); + return entryAction( + EntryAction.HandleEvent, (event: any) => dispatchEvent(view, index, eventName, event)); } export function checkAndUpdateProviderInline( @@ -159,7 +157,7 @@ export function checkAndUpdateProviderInline( if (changes) { provider.ngOnChanges(changes); } - if (view.firstChange && (def.flags & NodeFlags.OnInit)) { + if (view.state === ViewState.FirstCheck && (def.flags & NodeFlags.OnInit)) { provider.ngOnInit(); } if (def.flags & NodeFlags.DoCheck) { @@ -176,7 +174,7 @@ export function checkAndUpdateProviderDynamic(view: ViewData, def: NodeDef, valu if (changes) { provider.ngOnChanges(changes); } - if (view.firstChange && (def.flags & NodeFlags.OnInit)) { + if (view.state === ViewState.FirstCheck && (def.flags & NodeFlags.OnInit)) { provider.ngOnInit(); } if (def.flags & NodeFlags.DoCheck) { @@ -272,8 +270,10 @@ function checkAndUpdateProp( let change: SimpleChange; let changed: boolean; if (def.flags & NodeFlags.OnChanges) { - change = checkAndUpdateBindingWithChange(view, def, bindingIdx, value); - changed = !!change; + const oldValue = view.oldValues[def.bindingIndex + bindingIdx]; + changed = checkAndUpdateBinding(view, def, bindingIdx, value); + change = + changed ? new SimpleChange(oldValue, value, view.state === ViewState.FirstCheck) : null; } else { changed = checkAndUpdateBinding(view, def, bindingIdx, value); } diff --git a/modules/@angular/core/src/view/pure_expression.ts b/modules/@angular/core/src/view/pure_expression.ts index 8d41caee5d..5cbf7a6e70 100644 --- a/modules/@angular/core/src/view/pure_expression.ts +++ b/modules/@angular/core/src/view/pure_expression.ts @@ -132,7 +132,7 @@ export function checkAndUpdatePureExpressionInline( case 4: value[3] = v3; case 3: - value[3] = v2; + value[2] = v2; case 2: value[1] = v1; case 1: @@ -235,7 +235,7 @@ export function checkAndUpdatePureExpressionDynamic(view: ViewData, def: NodeDef for (let i = 0; i < values.length; i++) { params[i] = unwrapValue(values[i]); } - value = data.pipe.transform(params[0], ...params.slice(1)); + value = (data.pipe.transform)(...params); break; } data.value = value; diff --git a/modules/@angular/core/src/view/services.ts b/modules/@angular/core/src/view/services.ts index 0b266d03a7..946fc9c796 100644 --- a/modules/@angular/core/src/view/services.ts +++ b/modules/@angular/core/src/view/services.ts @@ -18,7 +18,7 @@ import {Sanitizer, SecurityContext} from '../security'; import {createInjector} from './provider'; 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 {checkAndUpdateView, checkNoChangesView, createEmbeddedView, destroyView} from './view'; import {attachEmbeddedView, detachEmbeddedView} from './view_attach'; @@ -112,13 +112,22 @@ class ViewRef_ implements EmbeddedViewRef { get context() { return this._view.context; } - get destroyed(): boolean { return unimplemented(); } + get destroyed(): boolean { return this._view.state === ViewState.Destroyed; } - markForCheck(): void { unimplemented(); } - detach(): void { unimplemented(); } + markForCheck(): void { this.reattach(); } + detach(): void { + if (this._view.state === ViewState.ChecksEnabled) { + this._view.state = ViewState.ChecksDisabled; + } + } detectChanges(): void { checkAndUpdateView(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(); } destroy() { unimplemented(); } diff --git a/modules/@angular/core/src/view/types.ts b/modules/@angular/core/src/view/types.ts index ced4e285c9..008193ead4 100644 --- a/modules/@angular/core/src/view/types.ts +++ b/modules/@angular/core/src/view/types.ts @@ -56,7 +56,8 @@ export type ViewHandleEventFn = */ export enum ViewFlags { 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 // the NodeType. nodes: {[key: number]: NodeData}; - firstChange: boolean; + state: ViewState; oldValues: any[]; disposables: DisposableFn[]; } +export enum ViewState { + FirstCheck, + ChecksEnabled, + ChecksDisabled, + Errored, + Destroyed +} + export type DisposableFn = () => void; /** diff --git a/modules/@angular/core/src/view/util.ts b/modules/@angular/core/src/view/util.ts index 2032eaa043..a9aecf7221 100644 --- a/modules/@angular/core/src/view/util.ts +++ b/modules/@angular/core/src/view/util.ts @@ -12,8 +12,8 @@ import {SimpleChange} from '../change_detection/change_detection_util'; import {looseIdentical} from '../facade/lang'; import {Renderer} from '../render/api'; -import {expressionChangedAfterItHasBeenCheckedError, isViewError, viewWrappedError} from './errors'; -import {ElementData, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, ViewData, ViewDefinition, ViewDefinitionFactory, asElementData, asTextData} from './types'; +import {expressionChangedAfterItHasBeenCheckedError, isViewDebugError, viewDestroyedError, viewWrappedDebugError} from './errors'; +import {ElementData, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewState, asElementData, asProviderData, asTextData} from './types'; export function setBindingDebugInfo( renderer: Renderer, renderNode: any, propName: string, value: any) { @@ -36,31 +36,41 @@ function camelCaseToDashCase(input: string): string { export function checkBindingNoChanges( view: ViewData, def: NodeDef, bindingIdx: number, value: any) { const oldValue = view.oldValues[def.bindingIndex + bindingIdx]; - if (view.firstChange || !devModeEqual(oldValue, value)) { + if (view.state === ViewState.FirstCheck || !devModeEqual(oldValue, value)) { 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( view: ViewData, def: NodeDef, bindingIdx: number, value: any): boolean { 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; + 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 false; } -export function checkAndUpdateBindingWithChange( - view: ViewData, def: NodeDef, bindingIdx: number, value: any): SimpleChange { - const oldValues = view.oldValues; - const oldValue = oldValues[def.bindingIndex + bindingIdx]; - if (view.firstChange || !looseIdentical(oldValue, value)) { - oldValues[def.bindingIndex + bindingIdx] = value; - return new SimpleChange(oldValue, value, view.firstChange); +export function dispatchEvent( + view: ViewData, nodeIndex: number, eventName: string, event: any): boolean { + setCurrentNode(view, nodeIndex); + let currView = view; + while (currView) { + if (currView.state === ViewState.ChecksDisabled && currView.def.flags & ViewFlags.OnPush) { + currView.state = ViewState.ChecksEnabled; + } + currView = currView.parent; } - return null; + return view.def.handleEvent(view, nodeIndex, eventName, event); } 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. */ export function setCurrentNode(view: ViewData, nodeIndex: number) { + if (view.state === ViewState.Destroyed) { + throw viewDestroyedError(_currentAction); + } _currentView = view; _currentNodeIndex = nodeIndex; } @@ -170,15 +183,14 @@ function callWithTryCatch(fn: (a: any) => any, arg: any): any { try { return fn(arg); } catch (e) { - if (isViewError(e) || !_currentView) { + if (isViewDebugError(e) || !_currentView) { throw e; } const debugContext = _currentView.services.createDebugContext(_currentView, _currentNodeIndex); - throw viewWrappedError(e, debugContext); + throw viewWrappedDebugError(e, debugContext); } } - export function rootRenderNodes(view: ViewData): any[] { const renderNodes: any[] = []; visitRootRenderNodes(view, RenderNodeAction.Collect, undefined, undefined, renderNodes); diff --git a/modules/@angular/core/src/view/view.ts b/modules/@angular/core/src/view/view.ts index e0ef3c8a5f..d1363bec0c 100644 --- a/modules/@angular/core/src/view/view.ts +++ b/modules/@angular/core/src/view/view.ts @@ -16,7 +16,7 @@ import {callLifecycleHooksChildrenFirst, checkAndUpdateProviderDynamic, checkAnd import {checkAndUpdatePureExpressionDynamic, checkAndUpdatePureExpressionInline, createPureExpression} from './pure_expression'; import {checkAndUpdateQuery, createQuery, queryDef} from './query'; 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'; const NOOP = (): any => undefined; @@ -260,7 +260,7 @@ function createView( parentDiIndex, context: undefined, component: undefined, nodes, - firstChange: true, renderer, services, + state: ViewState.FirstCheck, renderer, services, oldValues: new Array(def.bindingCount), disposables }; return view; @@ -348,13 +348,22 @@ function _checkAndUpdateView(view: ViewData) { execQueriesAction(view, NodeFlags.HasContentQuery, QueryAction.CheckAndUpdate); callLifecycleHooksChildrenFirst( - view, NodeFlags.AfterContentChecked | (view.firstChange ? NodeFlags.AfterContentInit : 0)); + view, NodeFlags.AfterContentChecked | + (view.state === ViewState.FirstCheck ? NodeFlags.AfterContentInit : 0)); execComponentViewsAction(view, ViewAction.CheckAndUpdate); execQueriesAction(view, NodeFlags.HasViewQuery, QueryAction.CheckAndUpdate); callLifecycleHooksChildrenFirst( - view, NodeFlags.AfterViewChecked | (view.firstChange ? NodeFlags.AfterViewInit : 0)); - view.firstChange = false; + view, NodeFlags.AfterViewChecked | + (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( @@ -465,7 +474,8 @@ function checkNoChangesQuery(view: ViewData, nodeDef: NodeDef) { if (queryList.dirty) { throw expressionChangedAfterItHasBeenCheckedError( 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); execEmbeddedViewsAction(view, ViewAction.Destroy); + view.state = ViewState.Destroyed; } enum ViewAction { @@ -536,10 +547,14 @@ function execEmbeddedViewsAction(view: ViewData, action: ViewAction) { function callViewAction(view: ViewData, action: ViewAction) { switch (action) { case ViewAction.CheckNoChanges: - _checkNoChangesView(view); + if (view.state === ViewState.ChecksEnabled || view.state === ViewState.FirstCheck) { + _checkNoChangesView(view); + } break; case ViewAction.CheckAndUpdate: - _checkAndUpdateView(view); + if (view.state === ViewState.ChecksEnabled || view.state === ViewState.FirstCheck) { + _checkAndUpdateView(view); + } break; case ViewAction.Destroy: _destroyView(view); diff --git a/modules/@angular/core/test/view/component_view_spec.ts b/modules/@angular/core/test/view/component_view_spec.ts index 91ac059ad2..3c2711a7d7 100644 --- a/modules/@angular/core/test/view/component_view_spec.ts +++ b/modules/@angular/core/test/view/component_view_spec.ts @@ -7,7 +7,7 @@ */ 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 {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; @@ -35,8 +35,9 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { })); function compViewDef( - nodes: NodeDef[], update?: ViewUpdateFn, handleEvent?: ViewHandleEventFn): ViewDefinition { - return viewDef(config.viewFlags, nodes, update, handleEvent, renderComponentType); + nodes: NodeDef[], update?: ViewUpdateFn, handleEvent?: ViewHandleEventFn, + flags?: ViewFlags): ViewDefinition { + return viewDef(config.viewFlags | flags, nodes, update, handleEvent, renderComponentType); } 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'); }); - it('should dirty check component views', () => { - let value = 'v1'; - class AComp { - a: any; - } + describe('data binding', () => { + it('should dirty check component views', () => { + let value: any; + class AComp { + a: any; + } - const update = jasmine.createSpy('updater').and.callFake((view: ViewData) => { - setCurrentNode(view, 0); - checkNodeInline(value); + const update = jasmine.createSpy('updater').and.callFake((view: ViewData) => { + setCurrentNode(view, 0); + 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( - compViewDef([ + it('should support detaching and attaching component views for dirty checking', () => { + class AComp { + a: any; + } + + 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', null, [[BindingType.ElementAttribute, 'a', SecurityContext.NONE]]), - ], update - )), - ], jasmine.createSpy('parentUpdater'))); - const compView = asProviderData(view, 1).componentView; + providerDef( + NodeFlags.None, null, 0, AComp, [], null, null, + () => compViewDef( + [ + elementDef(NodeFlags.None, null, null, 0, 'span'), + ], + update)), + ])); - checkAndUpdateView(view); + const compView = asProviderData(view, 1).componentView; - expect(update).toHaveBeenCalledWith(compView); + checkAndUpdateView(view); + update.calls.reset(); - update.calls.reset(); - checkNoChangesView(view); + compView.state = ViewState.ChecksDisabled; + checkAndUpdateView(view); + expect(update).not.toHaveBeenCalled(); - expect(update).toHaveBeenCalledWith(compView); + compView.state = ViewState.ChecksEnabled; + checkAndUpdateView(view); + expect(update).toHaveBeenCalled(); + }); - value = 'v2'; - expect(() => checkNoChangesView(view)) - .toThrowError( - `Expression has changed after it was checked. Previous value: 'v1'. Current value: 'v2'.`); - }); + if (isBrowser()) { + it('should support OnPush components', () => { + let compInputValue: any; + class AComp { + a: any; + } - it('should destroy component views', () => { - const log: string[] = []; + const update = jasmine.createSpy('updater'); - 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 { - ngOnDestroy() { log.push('ngOnDestroy'); }; + const compView = asProviderData(view, 1).componentView; + + 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([ - 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, []) - ])), - ])); + it('should stop dirty checking views that threw errors in change detection', () => { + class AComp { + a: any; + } - 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'); + }); + }); + }); } \ No newline at end of file diff --git a/modules/@angular/core/test/view/element_spec.ts b/modules/@angular/core/test/view/element_spec.ts index 1bdf58090a..ebe04337f1 100644 --- a/modules/@angular/core/test/view/element_spec.ts +++ b/modules/@angular/core/test/view/element_spec.ts @@ -207,7 +207,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { elementDef( NodeFlags.None, null, null, 0, 'input', null, [ - [BindingType.ElementProperty, 'title', SecurityContext.NONE], + [BindingType.ElementProperty, 'someProp', SecurityContext.NONE], ]), ], (view) => { @@ -216,7 +216,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { })); const setterSpy = jasmine.createSpy('set'); - Object.defineProperty(rootNodes[0], 'title', {set: setterSpy}); + Object.defineProperty(rootNodes[0], 'someProp', {set: setterSpy}); bindingValue = 'v1'; checkAndUpdateView(view); @@ -234,7 +234,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { }); }); - if (getDOM().supportsDOMEvents()) { + if (isBrowser()) { describe('listen to DOM events', () => { let removeNodes: Node[]; beforeEach(() => { removeNodes = []; }); diff --git a/modules/@angular/core/test/view/text_spec.ts b/modules/@angular/core/test/view/text_spec.ts index 5d22d50c19..4a766653c9 100644 --- a/modules/@angular/core/test/view/text_spec.ts +++ b/modules/@angular/core/test/view/text_spec.ts @@ -98,34 +98,42 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { expect(getDOM().getText(rootNodes[0])).toBe('0a1b2'); }); - it(`should unwrap values with ${InlineDynamic[inlineDynamic]}`, () => { - let bindingValue: any; + if (isBrowser()) { + it(`should unwrap values with ${InlineDynamic[inlineDynamic]}`, () => { + let bindingValue: any; + const setterSpy = jasmine.createSpy('set'); - const {view, rootNodes} = createAndGetRootNodes(compViewDef( - [ - textDef(null, ['', '']), - ], - (view: ViewData) => { - setCurrentNode(view, 0); - checkNodeInlineOrDynamic(inlineDynamic, [bindingValue]); - })); + class FakeTextNode { + set nodeValue(value: any) { setterSpy(value); } + } - const setterSpy = jasmine.createSpy('set'); - Object.defineProperty(rootNodes[0], 'nodeValue', {set: setterSpy}); + spyOn(document, 'createTextNode').and.returnValue(new FakeTextNode()); - bindingValue = 'v1'; - checkAndUpdateView(view); - expect(setterSpy).toHaveBeenCalledWith('v1'); + const {view, rootNodes} = createAndGetRootNodes(compViewDef( + [ + textDef(null, ['', '']), + ], + (view: ViewData) => { + setCurrentNode(view, 0); + checkNodeInlineOrDynamic(inlineDynamic, [bindingValue]); + })); - setterSpy.calls.reset(); - checkAndUpdateView(view); - expect(setterSpy).not.toHaveBeenCalled(); + Object.defineProperty(rootNodes[0], 'nodeValue', {set: setterSpy}); - setterSpy.calls.reset(); - bindingValue = WrappedValue.wrap('v1'); - checkAndUpdateView(view); - expect(setterSpy).toHaveBeenCalledWith('v1'); - }); + bindingValue = 'v1'; + checkAndUpdateView(view); + 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'); + }); + } }); });