diff --git a/modules/angular2/src/change_detection/abstract_change_detector.ts b/modules/angular2/src/change_detection/abstract_change_detector.ts index e1216adea9..77ae15ce47 100644 --- a/modules/angular2/src/change_detection/abstract_change_detector.ts +++ b/modules/angular2/src/change_detection/abstract_change_detector.ts @@ -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 = []; shadowDomChildren: List = []; @@ -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); } -} +} \ No newline at end of file diff --git a/modules/angular2/src/change_detection/change_detection_jit_generator.ts b/modules/angular2/src/change_detection/change_detection_jit_generator.ts index 1a75a0ee48..2339b4a90d 100644 --- a/modules/angular2/src/change_detection/change_detection_jit_generator.ts +++ b/modules/angular2/src/change_detection/change_detection_jit_generator.ts @@ -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; diff --git a/modules/angular2/src/change_detection/change_detection_util.ts b/modules/angular2/src/change_detection/change_detection_util.ts index 9155246035..4077f1c55a 100644 --- a/modules/angular2/src/change_detection/change_detection_util.ts +++ b/modules/angular2/src/change_detection/change_detection_util.ts @@ -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(); } diff --git a/modules/angular2/src/change_detection/dynamic_change_detector.ts b/modules/angular2/src/change_detection/dynamic_change_detector.ts index 5821ce86de..1d5475cab3 100644 --- a/modules/angular2/src/change_detection/dynamic_change_detector.ts +++ b/modules/angular2/src/change_detection/dynamic_change_detector.ts @@ -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, private directiveRecords: List) { - 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); diff --git a/modules/angular2/src/change_detection/exceptions.ts b/modules/angular2/src/change_detection/exceptions.ts index f5863f0573..4bee36c0ce 100644 --- a/modules/angular2/src/change_detection/exceptions.ts +++ b/modules/angular2/src/change_detection/exceptions.ts @@ -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; } } diff --git a/modules/angular2/src/core/application_common.ts b/modules/angular2/src/core/application_common.ts index cba050c0cc..d9870b5985 100644 --- a/modules/angular2/src/core/application_common.ts +++ b/modules/angular2/src/core/application_common.ts @@ -128,7 +128,7 @@ function _injectorBindings(appComponentType): List> { DirectiveResolver, Parser, Lexer, - bind(ExceptionHandler).toFactory(() => new ExceptionHandler(DOM)), + bind(ExceptionHandler).toFactory(() => new ExceptionHandler(DOM), []), bind(XHR).toValue(new XHRImpl()), ComponentUrlMapper, UrlResolver, diff --git a/modules/angular2/src/core/compiler/element_injector.ts b/modules/angular2/src/core/compiler/element_injector.ts index 0ebfe7d633..bf9657da26 100644 --- a/modules/angular2/src/core/compiler/element_injector.ts +++ b/modules/angular2/src/core/compiler/element_injector.ts @@ -496,10 +496,9 @@ export class ElementInjector extends TreeNode 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 implements Depend getComponent(): any { return this._strategy.getComponent(); } + getInjector(): Injector { return this._injector; } + getElementRef(): ElementRef { return this._preBuiltObjects.elementRef; } getViewContainerRef(): ViewContainerRef { diff --git a/modules/angular2/src/core/compiler/view.ts b/modules/angular2/src/core/compiler/view.ts index 216514374a..454366976e 100644 --- a/modules/angular2/src/core/compiler/view.ts +++ b/modules/angular2/src/core/compiler/view.ts @@ -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 { + 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 { + var res = {}; + var c = locals; + while (isPresent(c)) { + res = StringMapWrapper.merge(res, MapWrapper.toStringMap(c.current)); + c = c.parent; + } + return res; +} + /** * */ diff --git a/modules/angular2/src/core/exception_handler.ts b/modules/angular2/src/core/exception_handler.ts index cb07765da8..b329a004c1 100644 --- a/modules/angular2/src/core/exception_handler.ts +++ b/modules/angular2/src/core/exception_handler.ts @@ -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)) { diff --git a/modules/angular2/src/di/exceptions.ts b/modules/angular2/src/di/exceptions.ts index 933053bdce..d16fa2c60d 100644 --- a/modules/angular2/src/di/exceptions.ts +++ b/modules/angular2/src/di/exceptions.ts @@ -105,9 +105,7 @@ export class InstantiationError extends AbstractBindingError { constructor(injector: Injector, originalException, originalStack, key: Key) { super(injector, key, function(keys: List) { 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; diff --git a/modules/angular2/src/facade/collection.dart b/modules/angular2/src/facade/collection.dart index 43a617b5da..f9c1fc2a74 100644 --- a/modules/angular2/src/facade/collection.dart +++ b/modules/angular2/src/facade/collection.dart @@ -34,7 +34,13 @@ class IterableMap extends IterableBase { 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; diff --git a/modules/angular2/src/facade/collection.ts b/modules/angular2/src/facade/collection.ts index ccedbd894b..8f4e19d7a3 100644 --- a/modules/angular2/src/facade/collection.ts +++ b/modules/angular2/src/facade/collection.ts @@ -62,6 +62,11 @@ export class MapWrapper { } return result; } + static toStringMap(m: Map): StringMap { + var r = {}; + m.forEach((v, k) => r[k] = v); + return r; + } static createFromPairs(pairs: List): Map { return createMapFromPairs(pairs); } static forEach(m: Map, fn: /*(V, K) => void*/ Function) { m.forEach(fn); } static get(map: Map, key: K): V { return map.get(key); } diff --git a/modules/angular2/src/transform/template_compiler/change_detector_codegen.dart b/modules/angular2/src/transform/template_compiler/change_detector_codegen.dart index ddfab4b1b2..49f5ff635b 100644 --- a/modules/angular2/src/transform/template_compiler/change_detector_codegen.dart +++ b/modules/angular2/src/transform/template_compiler/change_detector_codegen.dart @@ -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'; diff --git a/modules/angular2/src/web-workers/worker/application_common.ts b/modules/angular2/src/web-workers/worker/application_common.ts index e912db0f2f..89c96189be 100644 --- a/modules/angular2/src/web-workers/worker/application_common.ts +++ b/modules/angular2/src/web-workers/worker/application_common.ts @@ -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, diff --git a/modules/angular2/test/change_detection/change_detector_spec.ts b/modules/angular2/test/change_detection/change_detector_spec.ts index 5427ae2ea0..882ac95378 100644 --- a/modules/angular2/test/change_detection/change_detector_spec.ts +++ b/modules/angular2/test/change_detection/change_detector_spec.ts @@ -1043,6 +1043,10 @@ class TestDispatcher implements ChangeDispatcher { notifyOnAllChangesDone() { this.onAllChangesDoneCalled = true; } + getDebugContext(a, b) { + return {} + } + _asString(value) { return (isBlank(value) ? 'null' : value.toString()); } } diff --git a/modules/angular2/test/core/compiler/integration_spec.ts b/modules/angular2/test/core/compiler/integration_spec.ts index 199bd6418d..5c1c2cd507 100644 --- a/modules/angular2/test/core/compiler/integration_spec.ts +++ b/modules/angular2/test/core/compiler/integration_spec.ts @@ -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: ``})); + + 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, diff --git a/modules/angular2/test/transform/integration/two_annotations_files/expected/bar.ng_deps.dart b/modules/angular2/test/transform/integration/two_annotations_files/expected/bar.ng_deps.dart index fe52a195db..e20ef1745f 100644 --- a/modules/angular2/test/transform/integration/two_annotations_files/expected/bar.ng_deps.dart +++ b/modules/angular2/test/transform/integration/two_annotations_files/expected/bar.ng_deps.dart @@ -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) {