feat(ivy): support checkNoChanges (#22710)

PR Close #22710
This commit is contained in:
Kara Erickson
2018-03-09 20:22:18 -08:00
parent 0bf6fa5b32
commit aa7dba244b
4 changed files with 227 additions and 14 deletions

View File

@ -151,6 +151,13 @@ let bindingIndex: number;
*/
let cleanup: any[]|null;
/**
* In this mode, any changes in bindings will throw an ExpressionChangedAfterChecked error.
*
* Necessary to support ChangeDetectorRef.checkNoChanges().
*/
let checkNoChangesMode = false;
const enum BindingDirection {
Input,
Output,
@ -194,9 +201,11 @@ export function enterView(newView: LView, host: LElementNode | LViewNode | null)
* the direction of traversal (up or down the view tree) a bit clearer.
*/
export function leaveView(newView: LView): void {
executeHooks(
currentView.data, currentView.tView.viewHooks, currentView.tView.viewCheckHooks,
creationMode);
if (!checkNoChangesMode) {
executeHooks(
currentView.data, currentView.tView.viewHooks, currentView.tView.viewCheckHooks,
creationMode);
}
// 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;
@ -1135,9 +1144,11 @@ export function containerRefreshStart(index: number): void {
(previousOrParentNode as LContainerNode).native, undefined,
`the container's native element should not have been set yet.`);
// We need to execute init hooks here so ngOnInit hooks are called in top level views
// before they are called in embedded views (for backwards compatibility).
executeInitHooks(currentView, currentView.tView, creationMode);
if (!checkNoChangesMode) {
// We need to execute init hooks here so ngOnInit hooks are called in top level views
// before they are called in embedded views (for backwards compatibility).
executeInitHooks(currentView, currentView.tView, creationMode);
}
}
/**
@ -1270,8 +1281,10 @@ export function embeddedViewEnd(): void {
* @param elementIndex
*/
export function directiveRefresh<T>(directiveIndex: number, elementIndex: number): void {
executeInitHooks(currentView, currentView.tView, creationMode);
executeContentHooks(currentView, currentView.tView, creationMode);
if (!checkNoChangesMode) {
executeInitHooks(currentView, currentView.tView, creationMode);
executeContentHooks(currentView, currentView.tView, creationMode);
}
const template = (tData[directiveIndex] as ComponentDef<T>).template;
if (template != null) {
ngDevMode && assertDataInRange(elementIndex);
@ -1594,6 +1607,37 @@ export function detectChanges<T>(component: T): void {
}
/**
* Checks the change detector and its children, and throws if any changes are detected.
*
* This is used in development mode to verify that running change detection doesn't
* introduce other changes.
*/
export function checkNoChanges<T>(component: T): void {
checkNoChangesMode = true;
try {
detectChanges(component);
} finally {
checkNoChangesMode = false;
}
}
/** Throws an ExpressionChangedAfterChecked error if checkNoChanges mode is on. */
function throwErrorIfNoChangesMode(oldValue: any, currValue: any): never|void {
if (checkNoChangesMode) {
let msg =
`ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: '${oldValue}'. Current value: '${currValue}'.`;
if (creationMode) {
msg +=
` 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 ?`;
}
// TODO: include debug context
throw new Error(msg);
}
}
/** Checks the view of the component provided. Does not gate on dirty checks or execute doCheck. */
function detectChangesInternal<T>(hostView: LView, hostNode: LElementNode, component: T) {
const componentIndex = hostNode.flags >> LNodeFlags.INDX_SHIFT;
@ -1672,6 +1716,7 @@ export function bind<T>(value: T | NO_CHANGE): T|NO_CHANGE {
const changed: boolean = value !== NO_CHANGE && isDifferent(data[bindingIndex], value);
if (changed) {
throwErrorIfNoChangesMode(data[bindingIndex], value);
data[bindingIndex] = value;
}
bindingIndex++;
@ -1841,14 +1886,17 @@ export function consumeBinding(): any {
export function bindingUpdated(value: any): boolean {
ngDevMode && assertNotEqual(value, NO_CHANGE, 'Incoming value should never be NO_CHANGE.');
if (creationMode || isDifferent(data[bindingIndex], value)) {
creationMode && initBindings();
data[bindingIndex++] = value;
return true;
if (creationMode) {
initBindings();
} else if (isDifferent(data[bindingIndex], value)) {
throwErrorIfNoChangesMode(data[bindingIndex], value);
} else {
bindingIndex++;
return false;
}
data[bindingIndex++] = value;
return true;
}
/** Updates binding if changed, then returns the latest value. */

View File

@ -8,7 +8,7 @@
import {EmbeddedViewRef as viewEngine_EmbeddedViewRef} from '../linker/view_ref';
import {detectChanges, markViewDirty} from './instructions';
import {checkNoChanges, detectChanges, markViewDirty} from './instructions';
import {ComponentTemplate} from './interfaces/definition';
import {LViewNode} from './interfaces/node';
import {LView, LViewFlags} from './interfaces/view';
@ -195,7 +195,13 @@ export class ViewRef<T> implements viewEngine_EmbeddedViewRef<T> {
*/
detectChanges(): void { detectChanges(this.context); }
checkNoChanges(): void { notImplemented(); }
/**
* Checks the change detector and its children, and throws if any changes are detected.
*
* This is used in development mode to verify that running change detection doesn't
* introduce other changes.
*/
checkNoChanges(): void { checkNoChanges(this.context); }
}