angular2
Version:
Angular 2 - a web framework for modern web apps
297 lines (245 loc) • 10.7 kB
text/typescript
import {assertionsEnabled, isPresent, isBlank, StringWrapper} from 'angular2/src/facade/lang';
import {ListWrapper} from 'angular2/src/facade/collection';
import {ChangeDetectionUtil} from './change_detection_util';
import {ChangeDetectorRef, ChangeDetectorRef_} from './change_detector_ref';
import {DirectiveIndex} from './directive_record';
import {ChangeDetector, ChangeDispatcher} from './interfaces';
import {Pipes} from './pipes';
import {
ChangeDetectionError,
ExpressionChangedAfterItHasBeenCheckedException,
DehydratedException,
EventEvaluationErrorContext,
EventEvaluationError
} from './exceptions';
import {BindingTarget} from './binding_record';
import {Locals} from './parser/locals';
import {ChangeDetectionStrategy, ChangeDetectorState} from './constants';
import {wtfCreateScope, wtfLeave, WtfScopeFn} from '../profile/profile';
import {ObservableWrapper} from 'angular2/src/facade/async';
var _scope_check: WtfScopeFn = wtfCreateScope(`ChangeDetector#check(ascii id, bool throwOnChange)`);
class _Context {
constructor(public element: any, public componentElement: any, public context: any,
public locals: any, public injector: any, public expression: any) {}
}
export class AbstractChangeDetector<T> implements ChangeDetector {
contentChildren: any[] = [];
viewChildren: any[] = [];
parent: ChangeDetector;
ref: ChangeDetectorRef;
// The names of the below fields must be kept in sync with codegen_name_util.ts or
// change detection will fail.
state: ChangeDetectorState = ChangeDetectorState.NeverChecked;
context: T;
locals: Locals = null;
mode: ChangeDetectionStrategy = null;
pipes: Pipes = null;
propertyBindingIndex: number;
outputSubscriptions: any[];
dispatcher: ChangeDispatcher;
constructor(public id: string, public numberOfPropertyProtoRecords: number,
public bindingTargets: BindingTarget[], public directiveIndices: DirectiveIndex[],
public strategy: ChangeDetectionStrategy) {
this.ref = new ChangeDetectorRef_(this);
}
addContentChild(cd: ChangeDetector): void {
this.contentChildren.push(cd);
cd.parent = this;
}
removeContentChild(cd: ChangeDetector): void { ListWrapper.remove(this.contentChildren, cd); }
addViewChild(cd: ChangeDetector): void {
this.viewChildren.push(cd);
cd.parent = this;
}
removeViewChild(cd: ChangeDetector): void { ListWrapper.remove(this.viewChildren, cd); }
remove(): void { this.parent.removeContentChild(this); }
handleEvent(eventName: string, elIndex: number, event: any): boolean {
if (!this.hydrated()) {
this.throwDehydratedError(`${this.id} -> ${eventName}`);
}
try {
var locals = new Map<string, any>();
locals.set('$event', event);
var res = !this.handleEventInternal(eventName, elIndex, new Locals(this.locals, locals));
this.markPathToRootAsCheckOnce();
return res;
} catch (e) {
var c = this.dispatcher.getDebugContext(null, elIndex, null);
var context = isPresent(c) ?
new EventEvaluationErrorContext(c.element, c.componentElement, c.context,
c.locals, c.injector) :
null;
throw new EventEvaluationError(eventName, e, e.stack, context);
}
}
handleEventInternal(eventName: string, elIndex: number, locals: Locals): boolean { return false; }
detectChanges(): void { this.runDetectChanges(false); }
checkNoChanges(): void {
if (assertionsEnabled()) {
this.runDetectChanges(true);
}
}
runDetectChanges(throwOnChange: boolean): void {
if (this.mode === ChangeDetectionStrategy.Detached ||
this.mode === ChangeDetectionStrategy.Checked || this.state === ChangeDetectorState.Errored)
return;
var s = _scope_check(this.id, throwOnChange);
this.detectChangesInRecords(throwOnChange);
this._detectChangesContentChildren(throwOnChange);
if (!throwOnChange) this.afterContentLifecycleCallbacks();
this._detectChangesInViewChildren(throwOnChange);
if (!throwOnChange) this.afterViewLifecycleCallbacks();
if (this.mode === ChangeDetectionStrategy.CheckOnce)
this.mode = ChangeDetectionStrategy.Checked;
this.state = ChangeDetectorState.CheckedBefore;
wtfLeave(s);
}
// This method is not intended to be overridden. Subclasses should instead provide an
// implementation of `detectChangesInRecordsInternal` which does the work of detecting changes
// and which this method will call.
// This method expects that `detectChangesInRecordsInternal` will set the property
// `this.propertyBindingIndex` to the propertyBindingIndex of the first proto record. This is to
// facilitate error reporting.
detectChangesInRecords(throwOnChange: boolean): void {
if (!this.hydrated()) {
this.throwDehydratedError(this.id);
}
try {
this.detectChangesInRecordsInternal(throwOnChange);
} catch (e) {
// throwOnChange errors aren't counted as fatal errors.
if (!(e instanceof ExpressionChangedAfterItHasBeenCheckedException)) {
this.state = ChangeDetectorState.Errored;
}
this._throwError(e, e.stack);
}
}
// Subclasses should override this method to perform any work necessary to detect and report
// changes. For example, changes should be reported via `ChangeDetectionUtil.addChange`, lifecycle
// methods should be called, etc.
// This implementation should also set `this.propertyBindingIndex` to the propertyBindingIndex of
// the
// first proto record to facilitate error reporting. See {@link #detectChangesInRecords}.
detectChangesInRecordsInternal(throwOnChange: boolean): void {}
// This method is not intended to be overridden. Subclasses should instead provide an
// implementation of `hydrateDirectives`.
hydrate(context: T, locals: Locals, dispatcher: ChangeDispatcher, pipes: Pipes): void {
this.dispatcher = dispatcher;
this.mode = ChangeDetectionUtil.changeDetectionMode(this.strategy);
this.context = context;
this.locals = locals;
this.pipes = pipes;
this.hydrateDirectives(dispatcher);
this.state = ChangeDetectorState.NeverChecked;
}
// Subclasses should override this method to hydrate any directives.
hydrateDirectives(dispatcher: ChangeDispatcher): void {}
// This method is not intended to be overridden. Subclasses should instead provide an
// implementation of `dehydrateDirectives`.
dehydrate(): void {
this.dehydrateDirectives(true);
this._unsubscribeFromOutputs();
this.dispatcher = null;
this.context = null;
this.locals = null;
this.pipes = null;
}
// Subclasses should override this method to dehydrate any directives. This method should reverse
// any work done in `hydrateDirectives`.
dehydrateDirectives(destroyPipes: boolean): void {}
hydrated(): boolean { return isPresent(this.context); }
destroyRecursive(): void {
this.dispatcher.notifyOnDestroy();
this.dehydrate();
var children = this.contentChildren;
for (var i = 0; i < children.length; i++) {
children[i].destroyRecursive();
}
children = this.viewChildren;
for (var i = 0; i < children.length; i++) {
children[i].destroyRecursive();
}
}
afterContentLifecycleCallbacks(): void {
this.dispatcher.notifyAfterContentChecked();
this.afterContentLifecycleCallbacksInternal();
}
afterContentLifecycleCallbacksInternal(): void {}
afterViewLifecycleCallbacks(): void {
this.dispatcher.notifyAfterViewChecked();
this.afterViewLifecycleCallbacksInternal();
}
afterViewLifecycleCallbacksInternal(): void {}
/** @internal */
_detectChangesContentChildren(throwOnChange: boolean): void {
var c = this.contentChildren;
for (var i = 0; i < c.length; ++i) {
c[i].runDetectChanges(throwOnChange);
}
}
/** @internal */
_detectChangesInViewChildren(throwOnChange: boolean): void {
var c = this.viewChildren;
for (var i = 0; i < c.length; ++i) {
c[i].runDetectChanges(throwOnChange);
}
}
markAsCheckOnce(): void { this.mode = ChangeDetectionStrategy.CheckOnce; }
markPathToRootAsCheckOnce(): void {
var c: ChangeDetector = this;
while (isPresent(c) && c.mode !== ChangeDetectionStrategy.Detached) {
if (c.mode === ChangeDetectionStrategy.Checked) c.mode = ChangeDetectionStrategy.CheckOnce;
c = c.parent;
}
}
private _unsubscribeFromOutputs(): void {
if (isPresent(this.outputSubscriptions)) {
for (var i = 0; i < this.outputSubscriptions.length; ++i) {
ObservableWrapper.dispose(this.outputSubscriptions[i]);
this.outputSubscriptions[i] = null;
}
}
}
getDirectiveFor(directives: any, index: number): any {
return directives.getDirectiveFor(this.directiveIndices[index]);
}
getDetectorFor(directives: any, index: number): ChangeDetector {
return directives.getDetectorFor(this.directiveIndices[index]);
}
notifyDispatcher(value: any): void {
this.dispatcher.notifyOnBinding(this._currentBinding(), value);
}
logBindingUpdate(value: any): void {
this.dispatcher.logBindingUpdate(this._currentBinding(), value);
}
addChange(changes: {[key: string]: any}, oldValue: any, newValue: any): {[key: string]: any} {
if (isBlank(changes)) {
changes = {};
}
changes[this._currentBinding().name] = ChangeDetectionUtil.simpleChange(oldValue, newValue);
return changes;
}
private _throwError(exception: any, stack: any): void {
var error;
try {
var c = this.dispatcher.getDebugContext(null, this._currentBinding().elementIndex, null);
var context = isPresent(c) ? new _Context(c.element, c.componentElement, c.context, c.locals,
c.injector, this._currentBinding().debug) :
null;
error = new ChangeDetectionError(this._currentBinding().debug, exception, stack, context);
} catch (e) {
// if an error happens during getting the debug context, we throw a ChangeDetectionError
// without the extra information.
error = new ChangeDetectionError(null, exception, stack, null);
}
throw error;
}
throwOnChangeError(oldValue: any, newValue: any): void {
throw new ExpressionChangedAfterItHasBeenCheckedException(this._currentBinding().debug,
oldValue, newValue, null);
}
throwDehydratedError(detail: string): void { throw new DehydratedException(detail); }
private _currentBinding(): BindingTarget {
return this.bindingTargets[this.propertyBindingIndex];
}
}