feat(change_detection): provide error context for change detection errors
This commit is contained in:
parent
e744409cb9
commit
c2bbda02a1
|
@ -7,6 +7,12 @@ import {ProtoRecord} from './proto_record';
|
|||
import {Locals} from './parser/locals';
|
||||
import {CHECK_ALWAYS, CHECK_ONCE, CHECKED, DETACHED, ON_PUSH} from './constants';
|
||||
|
||||
class _Context {
|
||||
constructor(public element: any, public componentElement: any, public instance: any,
|
||||
public context: any, public locals: any, public injector: any,
|
||||
public expression: any) {}
|
||||
}
|
||||
|
||||
export class AbstractChangeDetector implements ChangeDetector {
|
||||
lightDomChildren: List<any> = [];
|
||||
shadowDomChildren: List<any> = [];
|
||||
|
@ -14,7 +20,7 @@ export class AbstractChangeDetector implements ChangeDetector {
|
|||
mode: string = null;
|
||||
ref: ChangeDetectorRef;
|
||||
|
||||
constructor(public id: string) { this.ref = new ChangeDetectorRef(this); }
|
||||
constructor(public id: string, public dispatcher: any) { this.ref = new ChangeDetectorRef(this); }
|
||||
|
||||
addChild(cd: ChangeDetector): void {
|
||||
this.lightDomChildren.push(cd);
|
||||
|
@ -83,6 +89,9 @@ export class AbstractChangeDetector implements ChangeDetector {
|
|||
}
|
||||
|
||||
throwError(proto: ProtoRecord, exception: any, stack: any): void {
|
||||
throw new ChangeDetectionError(proto, exception, stack);
|
||||
var c = this.dispatcher.getDebugContext(proto.bindingRecord.elementIndex, proto.directiveIndex);
|
||||
var context = new _Context(c["element"], c["componentElement"], c["directive"], c["context"],
|
||||
c["locals"], c["injector"], proto.expressionAsString);
|
||||
throw new ChangeDetectionError(proto, exception, stack, context);
|
||||
}
|
||||
}
|
|
@ -68,8 +68,7 @@ export class ChangeDetectorJITGenerator {
|
|||
var typeName = _sanitizeName(`ChangeDetector_${this.id}`);
|
||||
var classDefinition = `
|
||||
var ${typeName} = function ${typeName}(dispatcher, protos, directiveRecords) {
|
||||
${ABSTRACT_CHANGE_DETECTOR}.call(this, ${JSON.stringify(this.id)});
|
||||
${DISPATCHER_ACCESSOR} = dispatcher;
|
||||
${ABSTRACT_CHANGE_DETECTOR}.call(this, ${JSON.stringify(this.id)}, dispatcher);
|
||||
${PROTOS_ACCESSOR} = protos;
|
||||
${DIRECTIVES_ACCESSOR} = directiveRecords;
|
||||
${LOCALS_ACCESSOR} = null;
|
||||
|
|
|
@ -129,7 +129,7 @@ export class ChangeDetectionUtil {
|
|||
}
|
||||
|
||||
static throwOnChange(proto: ProtoRecord, change) {
|
||||
throw new ExpressionChangedAfterItHasBeenChecked(proto, change);
|
||||
throw new ExpressionChangedAfterItHasBeenChecked(proto, change, null);
|
||||
}
|
||||
|
||||
static throwDehydrated() { throw new DehydratedException(); }
|
||||
|
|
|
@ -20,9 +20,9 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
|
|||
alreadyChecked: boolean = false;
|
||||
private pipes: Pipes = null;
|
||||
|
||||
constructor(id: string, private changeControlStrategy: string, private dispatcher: any,
|
||||
constructor(id: string, private changeControlStrategy: string, dispatcher: any,
|
||||
private protos: List<ProtoRecord>, private directiveRecords: List<any>) {
|
||||
super(id);
|
||||
super(id, dispatcher);
|
||||
this.values = ListWrapper.createFixedSize(protos.length + 1);
|
||||
this.localPipes = ListWrapper.createFixedSize(protos.length + 1);
|
||||
this.prevContexts = ListWrapper.createFixedSize(protos.length + 1);
|
||||
|
|
|
@ -2,7 +2,7 @@ import {ProtoRecord} from './proto_record';
|
|||
import {BaseException} from "angular2/src/facade/lang";
|
||||
|
||||
export class ExpressionChangedAfterItHasBeenChecked extends BaseException {
|
||||
constructor(proto: ProtoRecord, change: any) {
|
||||
constructor(proto: ProtoRecord, change: any, context: any) {
|
||||
super(`Expression '${proto.expressionAsString}' has changed after it was checked. ` +
|
||||
`Previous value: '${change.previousValue}'. Current value: '${change.currentValue}'`);
|
||||
}
|
||||
|
@ -11,9 +11,9 @@ export class ExpressionChangedAfterItHasBeenChecked extends BaseException {
|
|||
export class ChangeDetectionError extends BaseException {
|
||||
location: string;
|
||||
|
||||
constructor(proto: ProtoRecord, originalException: any, originalStack: any) {
|
||||
super(`${originalException} in [${proto.expressionAsString}]`, originalException,
|
||||
originalStack);
|
||||
constructor(proto: ProtoRecord, originalException: any, originalStack: any, context: any) {
|
||||
super(`${originalException} in [${proto.expressionAsString}]`, originalException, originalStack,
|
||||
context);
|
||||
this.location = proto.expressionAsString;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -128,7 +128,7 @@ function _injectorBindings(appComponentType): List<Type | Binding | List<any>> {
|
|||
DirectiveResolver,
|
||||
Parser,
|
||||
Lexer,
|
||||
bind(ExceptionHandler).toFactory(() => new ExceptionHandler(DOM)),
|
||||
bind(ExceptionHandler).toFactory(() => new ExceptionHandler(DOM), []),
|
||||
bind(XHR).toValue(new XHRImpl()),
|
||||
ComponentUrlMapper,
|
||||
UrlResolver,
|
||||
|
|
|
@ -496,10 +496,9 @@ export class ElementInjector extends TreeNode<ElementInjector> implements Depend
|
|||
|
||||
private _debugContext(): any {
|
||||
var p = this._preBuiltObjects;
|
||||
var element = isPresent(p.elementRef) ? p.elementRef.nativeElement : null;
|
||||
var hostRef = p.view.getHostElement();
|
||||
var componentElement = isPresent(hostRef) ? hostRef.nativeElement : null;
|
||||
return new _Context(element, componentElement, this._injector);
|
||||
var index = p.elementRef.boundElementIndex - p.view.elementOffset;
|
||||
var c = this._preBuiltObjects.view.getDebugContext(index, null);
|
||||
return new _Context(c["element"], c["componentElement"], c["injector"]);
|
||||
}
|
||||
|
||||
private _reattachInjectors(imperativelyCreatedInjector: Injector): void {
|
||||
|
@ -573,6 +572,8 @@ export class ElementInjector extends TreeNode<ElementInjector> implements Depend
|
|||
|
||||
getComponent(): any { return this._strategy.getComponent(); }
|
||||
|
||||
getInjector(): Injector { return this._injector; }
|
||||
|
||||
getElementRef(): ElementRef { return this._preBuiltObjects.elementRef; }
|
||||
|
||||
getViewContainerRef(): ViewContainerRef {
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
import {ListWrapper, MapWrapper, Map, StringMapWrapper, List} from 'angular2/src/facade/collection';
|
||||
import {
|
||||
ListWrapper,
|
||||
MapWrapper,
|
||||
Map,
|
||||
StringMapWrapper,
|
||||
List,
|
||||
StringMap
|
||||
} from 'angular2/src/facade/collection';
|
||||
import {
|
||||
AST,
|
||||
Locals,
|
||||
|
@ -202,7 +209,36 @@ export class AppView implements ChangeDispatcher, RenderEventDispatcher {
|
|||
|
||||
getHostElement(): ElementRef {
|
||||
var boundElementIndex = this.mainMergeMapping.hostElementIndicesByViewIndex[this.viewOffset];
|
||||
return this.elementRefs[boundElementIndex];
|
||||
return isPresent(boundElementIndex) ? this.elementRefs[boundElementIndex] : null;
|
||||
}
|
||||
|
||||
getDebugContext(elementIndex: number, directiveIndex: DirectiveIndex): StringMap<string, any> {
|
||||
try {
|
||||
var offsettedIndex = this.elementOffset + elementIndex;
|
||||
var hasRefForIndex = offsettedIndex < this.elementRefs.length;
|
||||
|
||||
var elementRef = hasRefForIndex ? this.elementRefs[this.elementOffset + elementIndex] : null;
|
||||
var host = this.getHostElement();
|
||||
var ei = hasRefForIndex ? this.elementInjectors[this.elementOffset + elementIndex] : null;
|
||||
|
||||
var element = isPresent(elementRef) ? elementRef.nativeElement : null;
|
||||
var componentElement = isPresent(host) ? host.nativeElement : null;
|
||||
var directive = isPresent(directiveIndex) ? this.getDirectiveFor(directiveIndex) : null;
|
||||
var injector = isPresent(ei) ? ei.getInjector() : null;
|
||||
|
||||
return {
|
||||
element: element,
|
||||
componentElement: componentElement,
|
||||
directive: directive,
|
||||
context: this.context,
|
||||
locals: _localsToStringMap(this.locals),
|
||||
injector: injector
|
||||
};
|
||||
} catch (e) {
|
||||
// TODO: vsavkin log the exception once we have a good way to log errors and warnings
|
||||
// if an error happens during getting the debug context, we return an empty map.
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
getDetectorFor(directive: DirectiveIndex): any {
|
||||
|
@ -252,6 +288,16 @@ export class AppView implements ChangeDispatcher, RenderEventDispatcher {
|
|||
}
|
||||
}
|
||||
|
||||
function _localsToStringMap(locals: Locals): StringMap<string, any> {
|
||||
var res = {};
|
||||
var c = locals;
|
||||
while (isPresent(c)) {
|
||||
res = StringMapWrapper.merge(res, MapWrapper.toStringMap(c.current));
|
||||
c = c.parent;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
|
|
@ -50,7 +50,7 @@ export class ExceptionHandler {
|
|||
|
||||
if (isPresent(stackTrace) && isBlank(originalStack)) {
|
||||
this.logger.log("STACKTRACE:");
|
||||
this.logger.log(this._longStackTrace(stackTrace))
|
||||
this.logger.log(this._longStackTrace(stackTrace));
|
||||
}
|
||||
|
||||
if (isPresent(reason)) {
|
||||
|
|
|
@ -105,9 +105,7 @@ export class InstantiationError extends AbstractBindingError {
|
|||
constructor(injector: Injector, originalException, originalStack, key: Key) {
|
||||
super(injector, key, function(keys: List<any>) {
|
||||
var first = stringify(ListWrapper.first(keys).token);
|
||||
return `Error during instantiation of ${first}!${constructResolvingPath(keys)}.` +
|
||||
`\n\n ORIGINAL ERROR: ${originalException}` +
|
||||
`\n\n ORIGINAL STACK: ${originalStack} \n`;
|
||||
return `Error during instantiation of ${first}!${constructResolvingPath(keys)}.`;
|
||||
}, originalException, originalStack);
|
||||
|
||||
this.causeKey = key;
|
||||
|
|
|
@ -34,7 +34,13 @@ class IterableMap extends IterableBase<List> {
|
|||
|
||||
class MapWrapper {
|
||||
static Map clone(Map m) => new Map.from(m);
|
||||
|
||||
// in opposite to JS, Dart does not create a new map
|
||||
static Map createFromStringMap(Map m) => m;
|
||||
|
||||
// in opposite to JS, Dart does not create a new map
|
||||
static Map toStringMap(Map m) => m;
|
||||
|
||||
static Map createFromPairs(List pairs) => pairs.fold({}, (m, p) {
|
||||
m[p[0]] = p[1];
|
||||
return m;
|
||||
|
|
|
@ -62,6 +62,11 @@ export class MapWrapper {
|
|||
}
|
||||
return result;
|
||||
}
|
||||
static toStringMap<T>(m: Map<string, T>): StringMap<string, T> {
|
||||
var r = {};
|
||||
m.forEach((v, k) => r[k] = v);
|
||||
return r;
|
||||
}
|
||||
static createFromPairs(pairs: List<any>): Map<any, any> { return createMapFromPairs(pairs); }
|
||||
static forEach<K, V>(m: Map<K, V>, fn: /*(V, K) => void*/ Function) { m.forEach(<any>fn); }
|
||||
static get<K, V>(map: Map<K, V>, key: K): V { return map.get(key); }
|
||||
|
|
|
@ -124,7 +124,6 @@ class _CodegenState {
|
|||
void _writeToBuf(StringBuffer buf) {
|
||||
buf.write('''\n
|
||||
class $_changeDetectorTypeName extends $_BASE_CLASS {
|
||||
final dynamic $_DISPATCHER_ACCESSOR;
|
||||
$_GEN_PREFIX.Pipes $_PIPES_ACCESSOR;
|
||||
final $_GEN_PREFIX.List<$_GEN_PREFIX.ProtoRecord> $_PROTOS_ACCESSOR;
|
||||
final $_GEN_PREFIX.List<$_GEN_PREFIX.DirectiveRecord>
|
||||
|
@ -140,9 +139,9 @@ class _CodegenState {
|
|||
}).join('')}
|
||||
|
||||
$_changeDetectorTypeName(
|
||||
this.$_DISPATCHER_ACCESSOR,
|
||||
dynamic $_DISPATCHER_ACCESSOR,
|
||||
this.$_PROTOS_ACCESSOR,
|
||||
this.$_DIRECTIVES_ACCESSOR) : super(${_encodeValue(_changeDetectorDefId)});
|
||||
this.$_DIRECTIVES_ACCESSOR) : super(${_encodeValue(_changeDetectorDefId)}, $_DISPATCHER_ACCESSOR);
|
||||
|
||||
void detectChangesInRecords(throwOnChange) {
|
||||
if (!hydrated()) {
|
||||
|
@ -536,7 +535,7 @@ const _CHANGES_LOCAL = 'changes';
|
|||
const _CONTEXT_ACCESSOR = '_context';
|
||||
const _CURRENT_PROTO = 'currentProto';
|
||||
const _DIRECTIVES_ACCESSOR = '_directiveRecords';
|
||||
const _DISPATCHER_ACCESSOR = '_dispatcher';
|
||||
const _DISPATCHER_ACCESSOR = 'dispatcher';
|
||||
const _GEN_PREFIX = '_gen';
|
||||
const _GEN_RECORDS_METHOD_NAME = '_createRecords';
|
||||
const _IDENTICAL_CHECK_FN = '$_GEN_PREFIX.looseIdentical';
|
||||
|
|
|
@ -123,7 +123,7 @@ function _injectorBindings(appComponentType, bus: WorkerMessageBus,
|
|||
DirectiveResolver,
|
||||
Parser,
|
||||
Lexer,
|
||||
bind(ExceptionHandler).toFactory(() => {new ExceptionHandler(new PrintLogger())}),
|
||||
bind(ExceptionHandler).toFactory(() => new ExceptionHandler(new PrintLogger()), []),
|
||||
bind(XHR).toValue(new XHRImpl()),
|
||||
ComponentUrlMapper,
|
||||
UrlResolver,
|
||||
|
|
|
@ -1043,6 +1043,10 @@ class TestDispatcher implements ChangeDispatcher {
|
|||
|
||||
notifyOnAllChangesDone() { this.onAllChangesDoneCalled = true; }
|
||||
|
||||
getDebugContext(a, b) {
|
||||
return {}
|
||||
}
|
||||
|
||||
_asString(value) { return (isBlank(value) ? 'null' : value.toString()); }
|
||||
}
|
||||
|
||||
|
|
|
@ -1165,7 +1165,7 @@ export function main() {
|
|||
});
|
||||
}));
|
||||
|
||||
it('should provide an error context when an error happens in the DI',
|
||||
it('should provide an error context when an error happens in DI',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
|
||||
tcb = tcb.overrideView(MyComp, new viewAnn.View({
|
||||
|
@ -1174,13 +1174,58 @@ export function main() {
|
|||
}));
|
||||
|
||||
PromiseWrapper.catchError(tcb.createAsync(MyComp), (e) => {
|
||||
expect(DOM.nodeName(e.context.element).toUpperCase())
|
||||
.toEqual("DIRECTIVE-THROWING-ERROR");
|
||||
var c = e.context;
|
||||
expect(DOM.nodeName(c.element).toUpperCase()).toEqual("DIRECTIVE-THROWING-ERROR");
|
||||
expect(DOM.nodeName(c.componentElement).toUpperCase()).toEqual("DIV");
|
||||
expect(c.injector).toBeAnInstanceOf(Injector);
|
||||
async.done();
|
||||
return null;
|
||||
});
|
||||
}));
|
||||
|
||||
it('should provide an error context when an error happens in change detection',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
|
||||
tcb = tcb.overrideView(
|
||||
MyComp, new viewAnn.View({template: `<input [value]="one.two.three" #local>`}));
|
||||
|
||||
tcb.createAsync(MyComp).then(rootTC => {
|
||||
try {
|
||||
rootTC.detectChanges();
|
||||
throw "Should throw";
|
||||
} catch (e) {
|
||||
var c = e.context;
|
||||
expect(DOM.nodeName(c.element).toUpperCase()).toEqual("INPUT");
|
||||
expect(DOM.nodeName(c.componentElement).toUpperCase()).toEqual("DIV");
|
||||
expect(c.injector).toBeAnInstanceOf(Injector);
|
||||
expect(c.expression).toContain("one.two.three");
|
||||
expect(c.context).toBe(rootTC.componentInstance);
|
||||
expect(c.locals["local"]).not.toBeNull();
|
||||
}
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should provide an error context when an error happens in change detection (text node)',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||
|
||||
tcb = tcb.overrideView(MyComp, new viewAnn.View({template: `{{one.two.three}}`}));
|
||||
|
||||
tcb.createAsync(MyComp).then(rootTC => {
|
||||
try {
|
||||
rootTC.detectChanges();
|
||||
throw "Should throw";
|
||||
} catch (e) {
|
||||
var c = e.context;
|
||||
expect(c.element).toBeNull();
|
||||
expect(c.injector).toBeNull();
|
||||
}
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
if (!IS_DARTIUM) {
|
||||
it('should report a meaningful error when a directive is undefined',
|
||||
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder,
|
||||
|
|
|
@ -24,7 +24,6 @@ void initReflector() {
|
|||
_MyComponent_ChangeDetector0.newProtoChangeDetector;
|
||||
}
|
||||
class _MyComponent_ChangeDetector0 extends _gen.AbstractChangeDetector {
|
||||
final dynamic _dispatcher;
|
||||
_gen.Pipes _pipes;
|
||||
final _gen.List<_gen.ProtoRecord> _protos;
|
||||
final _gen.List<_gen.DirectiveRecord> _directiveRecords;
|
||||
|
@ -36,8 +35,8 @@ class _MyComponent_ChangeDetector0 extends _gen.AbstractChangeDetector {
|
|||
dynamic _interpolate1 = _gen.ChangeDetectionUtil.uninitialized();
|
||||
|
||||
_MyComponent_ChangeDetector0(
|
||||
this._dispatcher, this._protos, this._directiveRecords)
|
||||
: super("MyComponent_comp_0");
|
||||
dynamic dispatcher, this._protos, this._directiveRecords)
|
||||
: super("MyComponent_comp_0", dispatcher);
|
||||
|
||||
void detectChangesInRecords(throwOnChange) {
|
||||
if (!hydrated()) {
|
||||
|
@ -80,7 +79,7 @@ class _MyComponent_ChangeDetector0 extends _gen.AbstractChangeDetector {
|
|||
_interpolate1, interpolate1));
|
||||
}
|
||||
|
||||
_dispatcher.notifyOnBinding(currentProto.bindingRecord, interpolate1);
|
||||
dispatcher.notifyOnBinding(currentProto.bindingRecord, interpolate1);
|
||||
|
||||
_interpolate1 = interpolate1;
|
||||
}
|
||||
|
@ -95,7 +94,7 @@ class _MyComponent_ChangeDetector0 extends _gen.AbstractChangeDetector {
|
|||
}
|
||||
|
||||
void callOnAllChangesDone() {
|
||||
_dispatcher.notifyOnAllChangesDone();
|
||||
dispatcher.notifyOnAllChangesDone();
|
||||
}
|
||||
|
||||
void hydrate(MyComponent context, locals, directives, pipes) {
|
||||
|
|
Loading…
Reference in New Issue