From 390aea5ee4318855584911afb2ce4a2b86fc718c Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Mon, 3 Feb 2014 16:08:53 -0800 Subject: [PATCH] feat(Scope): Brand new scope implementation which takes advantage of the new change detection --- demo/bouncing_balls/bouncy_balls.dart | 4 +- lib/change_detection/ast.dart | 12 +- lib/change_detection/change_detection.dart | 69 +- .../dirty_checking_change_detector.dart | 412 +++- lib/change_detection/watch_group.dart | 11 +- lib/core/directive.dart | 8 +- lib/core/interpolate.dart | 14 +- lib/core/module.dart | 21 +- lib/core/scope.dart | 1483 +++++------- lib/core/scope2.dart | 775 ------- lib/core/zone.dart | 2 +- lib/core_dom/block_factory.dart | 28 +- lib/core_dom/common.dart | 3 +- lib/core_dom/compiler.dart | 79 +- lib/core_dom/ng_mustache.dart | 47 +- lib/directive/input_select.dart | 26 +- lib/directive/module.dart | 2 + lib/directive/ng_a.dart | 5 +- lib/directive/ng_class.dart | 38 +- lib/directive/ng_control.dart | 4 +- lib/directive/ng_events.dart | 2 +- lib/directive/ng_form.dart | 4 +- lib/directive/ng_if.dart | 20 +- lib/directive/ng_include.dart | 4 +- lib/directive/ng_model.dart | 61 +- lib/directive/ng_pluralize.dart | 4 +- lib/directive/ng_repeat.dart | 90 +- lib/directive/ng_style.dart | 27 +- lib/directive/ng_switch.dart | 4 +- lib/introspection.dart | 10 +- lib/mock/debug.dart | 2 +- lib/mock/probe.dart | 4 +- lib/mock/test_bed.dart | 3 + lib/routing/ng_view.dart | 13 +- lib/utils.dart | 2 +- perf/dom/compile_perf.dart | 4 +- perf/scope_perf.dart | 220 +- .../dirty_checking_change_detector_spec.dart | 213 +- test/change_detection/watch_group_spec.dart | 68 + test/core/core_directive_spec.dart | 2 +- test/core/interpolate_spec.dart | 6 +- test/core/parser/parser_spec.dart | 156 +- test/core/scope2_spec.dart | 1065 --------- test/core/scope_spec.dart | 1997 +++++++---------- test/core/templateurl_spec.dart | 8 +- test/core_dom/block_spec.dart | 4 +- test/core_dom/compiler_spec.dart | 244 +- test/core_dom/http_spec.dart | 4 +- test/core_dom/ng_mustache_spec.dart | 52 +- test/directive/input_select_spec.dart | 566 ++--- test/directive/ng_a_spec.dart | 18 +- test/directive/ng_bind_html_spec.dart | 8 +- test/directive/ng_bind_spec.dart | 16 +- test/directive/ng_bind_template_spec.dart | 10 +- test/directive/ng_class_spec.dart | 136 +- test/directive/ng_events_spec.dart | 4 +- test/directive/ng_form_spec.dart | 98 +- test/directive/ng_if_spec.dart | 56 +- test/directive/ng_include_spec.dart | 14 +- test/directive/ng_model_spec.dart | 302 +-- test/directive/ng_model_validators_spec.dart | 160 +- test/directive/ng_non_bindable_spec.dart | 6 +- test/directive/ng_pluralize_spec.dart | 100 +- test/directive/ng_repeat_spec.dart | 215 +- test/directive/ng_show_hide_spec.dart | 16 +- test/directive/ng_src_boolean_spec.dart | 122 +- test/directive/ng_style_spec.dart | 22 +- test/directive/ng_switch_spec.dart | 100 +- test/filter/json_spec.dart | 6 +- test/filter/limit_to_spec.dart | 58 +- test/filter/lowercase_spec.dart | 6 +- test/filter/order_by_spec.dart | 76 +- test/filter/uppercase_spec.dart | 6 +- test/mock/test_bed_spec.dart | 8 +- test/routing/ng_bind_route_spec.dart | 6 +- test/routing/ng_view_spec.dart | 2 +- test/routing/routing_spec.dart | 4 +- 77 files changed, 3868 insertions(+), 5609 deletions(-) delete mode 100644 lib/core/scope2.dart delete mode 100644 test/core/scope2_spec.dart diff --git a/demo/bouncing_balls/bouncy_balls.dart b/demo/bouncing_balls/bouncy_balls.dart index a1734d203..4fefe5d27 100644 --- a/demo/bouncing_balls/bouncy_balls.dart +++ b/demo/bouncing_balls/bouncy_balls.dart @@ -71,7 +71,7 @@ class BounceController { timeDigest() { var start = window.performance.now(); - scope.$evalAsync(() { + scope.runAsync(() { digestTime = (window.performance.now() - start).round(); }, outsideDigest: true); } @@ -109,7 +109,7 @@ class BallPositionDirective { set position(BallModel model) { element.style.backgroundColor = model.color; - scope.$watch(() { + scope.watch(() { element.style.left = '${model.x + 10}px'; element.style.top = '${model.y + 10}px'; }); diff --git a/lib/change_detection/ast.dart b/lib/change_detection/ast.dart index 1f8d1bf65..e6b9d970b 100644 --- a/lib/change_detection/ast.dart +++ b/lib/change_detection/ast.dart @@ -37,8 +37,10 @@ class ContextReferenceAST extends AST { class ConstantAST extends AST { final constant; - ConstantAST(dynamic constant): - super(constant is String ? '"$constant"' : '$constant'), + ConstantAST(dynamic constant, [String expression]): + super(expression == null + ? (constant is String ? '"$constant"' : '$constant') + : expression), constant = constant; WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) @@ -107,9 +109,9 @@ class MethodAST extends AST { class CollectionAST extends AST { final AST valueAST; - CollectionAST(valueAST): - super('#collection($valueAST)'), - valueAST = valueAST; + CollectionAST(valueAST) + : super('#collection($valueAST)'), + valueAST = valueAST; WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) { return watchGroup.addCollectionWatch(valueAST); diff --git a/lib/change_detection/change_detection.dart b/lib/change_detection/change_detection.dart index 1c41631a0..6acc152fd 100644 --- a/lib/change_detection/change_detection.dart +++ b/lib/change_detection/change_detection.dart @@ -112,7 +112,64 @@ abstract class ChangeRecord extends Record { } /** - * If [ChangeDetector] is watching a collection (an [Iterable]) then the + * If [ChangeDetector] is watching a an [Map] then the + * [currentValue] of [Record] will contain this object. The object contains a + * summary of changes to the map since the last execution. The changes + * are reported as a list of [MapKeyValue]s which contain the current + * and previous value in the list as well as the key. + */ +abstract class MapChangeRecord { + /// The underlying iterable object + Map get map; + + /// A list of [CollectionKeyValue]s which are in the iteration order. */ + KeyValue get mapHead; + /// A list of changed items. + ChangedKeyValue get changesHead; + /// A list of new added items. + AddedKeyValue get additionsHead; + /// A list of removed items + RemovedKeyValue get removalsHead; + + void forEachChange(void f(ChangedKeyValue change)); + void forEachAddition(void f(AddedKeyValue addition)); + void forEachRemoval(void f(RemovedKeyValue removal)); +} + +/** + * Each item in map is wrapped in [MapKeyValue], which can track + * the [item]s [currentValue] and [previousValue] location. + */ +abstract class MapKeyValue { + /// The item. + K get key; + + /// Previous item location in the list or [null] if addition. + V get previousValue; + + /// Current item location in the list or [null] if removal. + V get currentValue; +} + +abstract class KeyValue extends MapKeyValue { + KeyValue get nextKeyValue; +} + +abstract class AddedKeyValue extends MapKeyValue { + AddedKeyValue get nextAddedKeyValue; +} + +abstract class RemovedKeyValue extends MapKeyValue { + RemovedKeyValue get nextRemovedKeyValue; +} + +abstract class ChangedKeyValue extends MapKeyValue { + ChangedKeyValue get nextChangedKeyValue; +} + + +/** + * If [ChangeDetector] is watching a an [Iterable] then the * [currentValue] of [Record] will contain this object. The object contains a * summary of changes to the collection since the last execution. The changes * are reported as a list of [CollectionChangeItem]s which contain the current @@ -130,18 +187,22 @@ abstract class CollectionChangeRecord { MovedItem get movesHead; /** A list of [RemovedItem]s. */ RemovedItem get removalsHead; + + void forEachAddition(void f(AddedItem addition)); + void forEachMove(void f(MovedItem move)); + void forEachRemoval(void f(RemovedItem removal)); } /** * Each item in collection is wrapped in [CollectionChangeItem], which can track * the [item]s [currentKey] and [previousKey] location. */ -abstract class CollectionChangeItem { +abstract class CollectionChangeItem { // TODO(misko): change to since K is int. /** Previous item location in the list or [null] if addition. */ - K get previousKey; + K get previousKey; // TODO(misko): rename to previousIndex /** Current item location in the list or [null] if removal. */ - K get currentKey; + K get currentKey; // TODO(misko): rename to CurrentIndex /** The item. */ V get item; diff --git a/lib/change_detection/dirty_checking_change_detector.dart b/lib/change_detection/dirty_checking_change_detector.dart index 6a144acff..6f67b6154 100644 --- a/lib/change_detection/dirty_checking_change_detector.dart +++ b/lib/change_detection/dirty_checking_change_detector.dart @@ -313,13 +313,17 @@ class DirtyCheckingRecord implements ChangeRecord, WatchRecord { } else if (field == null) { _instanceMirror = null; if (obj is Map) { - _mode = _MODE_MAP_; - assert('implement' == false); - currentValue = null; //new _MapChangeRecord(); + if (_mode != _MODE_MAP_) { + // Last one was collection as well, don't reset state. + _mode = _MODE_MAP_; + currentValue = new _MapChangeRecord(); + } } else if (obj is Iterable) { - if (_mode == _MODE_ITERABLE_) return; // Last one was collection as well, don't reset state. - _mode = _MODE_ITERABLE_; - currentValue = new _CollectionChangeRecord(); + if (_mode != _MODE_ITERABLE_) { + // Last one was collection as well, don't reset state. + _mode = _MODE_ITERABLE_; + currentValue = new _CollectionChangeRecord(); + } } else { _mode = _MODE_IDENTITY_; } @@ -356,9 +360,9 @@ class DirtyCheckingRecord implements ChangeRecord, WatchRecord { current = object; break; case _MODE_MAP_: - return mapCheck(object) ? this : null; + return (currentValue as _MapChangeRecord)._check(object) ? this : null; case _MODE_ITERABLE_: - return iterableCheck(object) ? this : null; + return (currentValue as _CollectionChangeRecord)._check(object) ? this : null; default: assert(false); } @@ -381,76 +385,271 @@ class DirtyCheckingRecord implements ChangeRecord, WatchRecord { return null; } - mapCheck(Map map) { - assert('TODO: implement!' == true); - /* - _MapChangeRecord mapChangeRecord = currentValue as _MapChangeRecord; - ItemRecord record = mapChangeRecord._collectionHead; - mapChangeRecord.truncate(record); - map.forEach((key, value) { - if (record == null || !identical(value, record.item)) { } - }); - return mapChangeRecord.isDirty; - */ + + remove() { + _group._recordRemove(this); } + toString() => '${_MODE_NAMES[_mode]}[$field]'; +} - /** - * Check the [Iterable] [collection] for changes. - */ - iterableCheck(Iterable collection) { - _CollectionChangeRecord collectionChangeRecord = - currentValue as _CollectionChangeRecord; - collectionChangeRecord._reset(); - ItemRecord record = collectionChangeRecord._collectionHead; - bool maybeDirty = false; - if ((collection is UnmodifiableListView) && - identical(collectionChangeRecord._iterable, collection)) { - // Short circuit and assume that the list has not been modified. - return false; - } else if (collection is List) { - List list = collection; - for(int index = 0, length = list.length; index < length; index++) { - var item = list[index]; - if (record == null || !identical(item, record.item)) { - record = collectionChangeRecord.mismatch(record, item, index); - maybeDirty = true; - } else if (maybeDirty) { - // TODO(misko): can we limit this to duplicates only? - record = collectionChangeRecord.verifyReinsertion(record, item, index); +final Object _INITIAL_ = new Object(); + +class _MapChangeRecord implements MapChangeRecord { + final Map _records = new Map(); + Map _map; + KeyValueRecord _mapHead; + KeyValueRecord _changesHead, _changesTail; + KeyValueRecord _additionsHead, _additionsTail; + KeyValueRecord _removalsHead, _removalsTail; + + Map get map => _map; + KeyValue get mapHead => _mapHead; + ChangedKeyValue get changesHead => _changesHead; + AddedKeyValue get additionsHead => _additionsHead; + RemovedKeyValue get removalsHead => _removalsHead; + + get isDirty => _additionsHead != null || + _changesHead != null || + _removalsHead != null; + + void forEachChange(void f(ChangedKeyValue change)) { + KeyValueRecord record = _changesHead; + while(record != null) { + f(record); + record = record._nextChangedKeyValue; + } + } + + void forEachAddition(void f(AddedKeyValue addition)){ + KeyValueRecord record = _additionsHead; + while(record != null) { + f(record); + record = record._nextAddedKeyValue; + } + } + + void forEachRemoval(void f(RemovedKeyValue removal)){ + KeyValueRecord record = _removalsHead; + while(record != null) { + f(record); + record = record._nextRemovedKeyValue; + } + } + + + _check(Map map) { + _reset(); + _map = map; + Map records = _records; + KeyValueRecord oldSeqRecord = _mapHead; + KeyValueRecord lastOldSeqRecord; + KeyValueRecord lastNewSeqRecord; + var seqChanged = false; + map.forEach((key, value) { + var newSeqRecord; + if (oldSeqRecord != null && key == oldSeqRecord.key) { + newSeqRecord = oldSeqRecord; + if (!identical(value, oldSeqRecord._currentValue)) { + oldSeqRecord._previousValue = oldSeqRecord._currentValue; + oldSeqRecord._currentValue = value; + _addToChanges(oldSeqRecord); + } + } else { + seqChanged = true; + if (oldSeqRecord != null) { + oldSeqRecord._nextKeyValue = null; + _removeFromSeq(lastOldSeqRecord, oldSeqRecord); + _addToRemovals(oldSeqRecord); + } + if (records.containsKey(key)) { + newSeqRecord = records[key]; + } else { + newSeqRecord = records[key] = new KeyValueRecord(key); + newSeqRecord._currentValue = value; + _addToAdditions(newSeqRecord); } - record = record._nextRec; } - } else { - int index = 0; - for(var item in collection) { - if (record == null || !identical(item, record.item)) { - record = collectionChangeRecord.mismatch(record, item, index); - maybeDirty = true; - } else if (maybeDirty) { - // TODO(misko): can we limit this to duplicates only? - record = collectionChangeRecord.verifyReinsertion(record, item, index); + + if (seqChanged) { + if (_isInRemovals(newSeqRecord)) { + _removeFromRemovals(newSeqRecord); + } + if (lastNewSeqRecord == null) { + _mapHead = newSeqRecord; + } else { + lastNewSeqRecord._nextKeyValue = newSeqRecord; } - record = record._nextRec; - index++; } + lastOldSeqRecord = oldSeqRecord; + lastNewSeqRecord = newSeqRecord; + oldSeqRecord = oldSeqRecord == null ? null : oldSeqRecord._nextKeyValue; + }); + _truncate(lastOldSeqRecord, oldSeqRecord); + return isDirty; + } + + void _reset() { + var record = _changesHead; + while (record != null) { + record._previousValue = record._currentValue; + record = record._nextChangedKeyValue; + } + + record = _additionsHead; + while (record != null) { + record._previousValue = record._currentValue; + record = record._nextAddedKeyValue; } - collectionChangeRecord.truncate(record); - collectionChangeRecord._iterable = collection; - return collectionChangeRecord.isDirty; + + assert((() { + var record = _changesHead; + while (record != null) { + var nextRecord = record._nextChangedKeyValue; + record._nextChangedKeyValue = null; + record = nextRecord; + } + + record = _additionsHead; + while (record != null) { + var nextRecord = record._nextAddedKeyValue; + record._nextAddedKeyValue = null; + record = nextRecord; + } + + record = _removalsHead; + while (record != null) { + var nextRecord = record._nextRemovedKeyValue; + record._nextRemovedKeyValue = null; + record = nextRecord; + } + + return true; + })()); + _changesHead = _changesTail = null; + _additionsHead = _additionsTail = null; + _removalsHead = _removalsTail = null; } - remove() { - _group._recordRemove(this); + void _truncate(KeyValueRecord lastRecord, KeyValueRecord record) { + while(record != null) { + if (lastRecord == null) { + _mapHead = null; + } else { + lastRecord._nextKeyValue = null; + } + var nextRecord = record._nextKeyValue; + assert((() { + record._nextKeyValue = null; + return true; + })()); + _addToRemovals(record); + lastRecord = record; + record = nextRecord; + } + + record = _removalsHead; + while (record != null) { + record._previousValue = record._currentValue; + record._currentValue = null; + _records.remove(record.key); + record = record._nextRemovedKeyValue; + } } - toString() => '${_MODE_NAMES[_mode]}[$field]'; + bool _isInRemovals(KeyValueRecord record) { + return record == _removalsHead || + record._nextRemovedKeyValue != null || + record._prevRemovedKeyValue != null; + } + + void _addToRemovals(KeyValueRecord record) { + assert(record._nextKeyValue == null); + assert(record._nextAddedKeyValue == null); + assert(record._nextChangedKeyValue == null); + assert(record._nextRemovedKeyValue == null); + assert(record._prevRemovedKeyValue == null); + if (_removalsHead == null) { + _removalsHead = _removalsTail = record; + } else { + _removalsTail._nextRemovedKeyValue = record; + record._prevRemovedKeyValue = _removalsTail; + _removalsTail = record; + } + } + + void _removeFromSeq(KeyValueRecord prev, KeyValueRecord record) { + KeyValueRecord next = record._nextKeyValue; + if (prev == null) _mapHead = next; else prev._nextKeyValue = next; + assert((() { + record._nextKeyValue = null; + return true; + })()); + } + + void _removeFromRemovals(KeyValueRecord record) { + assert(record._nextKeyValue == null); + assert(record._nextAddedKeyValue == null); + assert(record._nextChangedKeyValue == null); + + var prev = record._prevRemovedKeyValue; + var next = record._nextRemovedKeyValue; + if (prev == null) _removalsHead = next; else prev._nextRemovedKeyValue = next; + if (next == null) _removalsTail = prev; else next._prevRemovedKeyValue = prev; + record._prevRemovedKeyValue = record._nextRemovedKeyValue = null; + } + + void _addToAdditions(KeyValueRecord record) { + assert(record._nextKeyValue == null); + assert(record._nextAddedKeyValue == null); + assert(record._nextChangedKeyValue == null); + assert(record._nextRemovedKeyValue == null); + assert(record._prevRemovedKeyValue == null); + if (_additionsHead == null) { + _additionsHead = _additionsTail = record; + } else { + _additionsTail._nextAddedKeyValue = record; + _additionsTail = record; + } + } + + void _addToChanges(KeyValueRecord record) { + assert(record._nextAddedKeyValue == null); + assert(record._nextChangedKeyValue == null); + assert(record._nextRemovedKeyValue == null); + assert(record._prevRemovedKeyValue == null); + if (_changesHead == null) { + _changesHead = _changesTail = record; + } else { + _changesTail._nextChangedKeyValue = record; + _changesTail = record; + } + } } -final Object _INITIAL_ = new Object(); +class KeyValueRecord implements KeyValue, AddedKeyValue, RemovedKeyValue, ChangedKeyValue { + final K key; + V _previousValue, _currentValue; + + KeyValueRecord _nextKeyValue; + KeyValueRecord _nextAddedKeyValue; + KeyValueRecord _nextRemovedKeyValue, _prevRemovedKeyValue; + KeyValueRecord _nextChangedKeyValue; + + KeyValueRecord(this.key); + + V get previousValue => _previousValue; + V get currentValue => _currentValue; + KeyValue get nextKeyValue => _nextKeyValue; + AddedKeyValue get nextAddedKeyValue => _nextAddedKeyValue; + RemovedKeyValue get nextRemovedKeyValue => _nextRemovedKeyValue; + ChangedKeyValue get nextChangedKeyValue => _nextChangedKeyValue; + + toString() { + return _previousValue == _currentValue ? key : '$key[$_previousValue -> $_currentValue]'; + } +} -//class _MapChangeRecord implements CollectionChangeRecord { -//} class _CollectionChangeRecord implements CollectionChangeRecord { Iterable _iterable; @@ -470,8 +669,73 @@ class _CollectionChangeRecord implements CollectionChangeRecord { CollectionChangeItem get movesHead => _movesHead; CollectionChangeItem get removalsHead => _removalsHead; + void forEachAddition(void f(AddedItem addition)){ + ItemRecord record = _additionsHead; + while(record != null) { + f(record); + record = record._nextAddedRec; + } + } + + void forEachMove(void f(MovedItem change)) { + ItemRecord record = _changesHead; + while(record != null) { + f(record); + record = record._nextMovedRec; + } + } + + void forEachRemoval(void f(RemovedItem removal)){ + ItemRecord record = _removalsHead; + while(record != null) { + f(record); + record = record._nextRemovedRec; + } + } + + Iterable get iterable => _iterable; + _check(Iterable collection) { + _reset(); + ItemRecord record = _collectionHead; + bool maybeDirty = false; + if ((collection is UnmodifiableListView) && + identical(_iterable, collection)) { + // Short circuit and assume that the list has not been modified. + return false; + } else if (collection is List) { + List list = collection; + for(int index = 0, length = list.length; index < length; index++) { + var item = list[index]; + if (record == null || !identical(item, record.item)) { + record = mismatch(record, item, index); + maybeDirty = true; + } else if (maybeDirty) { + // TODO(misko): can we limit this to duplicates only? + record = verifyReinsertion(record, item, index); + } + record = record._nextRec; + } + } else { + int index = 0; + for(var item in collection) { + if (record == null || !identical(item, record.item)) { + record = mismatch(record, item, index); + maybeDirty = true; + } else if (maybeDirty) { + // TODO(misko): can we limit this to duplicates only? + record = verifyReinsertion(record, item, index); + } + record = record._nextRec; + index++; + } + } + _truncate(record); + _iterable = collection; + return isDirty; + } + /** * Reset the state of the change objects to show no changes. This means set * previousKey to currentKey, and clear all of the queues (additions, moves, @@ -490,24 +754,22 @@ class _CollectionChangeRecord implements CollectionChangeRecord { record = _movesHead; while(record != null) { record.previousKey = record.currentKey; - record = record._nextMovedRec; + var nextRecord = record._nextMovedRec; + assert((record._nextMovedRec = null) == null); + record = nextRecord; } _movesHead = _movesTail = null; - - record = _removalsHead; - while(record != null) { - record.previousKey = record.currentKey; - record = record._nextRemovedRec; - } _removalsHead = _removalsTail = null; + assert(isDirty == false); } /** * A [_CollectionChangeRecord] is considered dirty if it has additions, moves * or removals. */ - get isDirty => _additionsHead != null || _movesHead != null || - _removalsHead != null; + get isDirty => _additionsHead != null || + _movesHead != null || + _removalsHead != null; /** * This is the core function which handles differences between collections. @@ -526,7 +788,7 @@ class _CollectionChangeRecord implements CollectionChangeRecord { return record..item = item; } - // find the previous record os that we know where to insert after. + // find the previous record so that we know where to insert after. ItemRecord prev = record == null ? _collectionTail : record._prevRec; // Remove the record from the collection since we know it does not match the item. @@ -592,7 +854,7 @@ class _CollectionChangeRecord implements CollectionChangeRecord { * * - [record] The first excess [ItemRecord]. */ - void truncate(ItemRecord record) { + void _truncate(ItemRecord record) { // Anything after that needs to be removed; while(record != null) { ItemRecord nextRecord = record._nextRec; @@ -700,12 +962,12 @@ class _CollectionChangeRecord implements CollectionChangeRecord { } ItemRecord _moves_add(ItemRecord record) { + assert(record._nextMovedRec == null); if (_movesTail == null) { assert(_movesHead == null); _movesTail = _movesHead = record; } else { assert(_movesTail._nextMovedRec == null); - assert(record._nextMovedRec == null); _movesTail = _movesTail._nextMovedRec = record; } diff --git a/lib/change_detection/watch_group.dart b/lib/change_detection/watch_group.dart index 196c930b3..3783bcf60 100644 --- a/lib/change_detection/watch_group.dart +++ b/lib/change_detection/watch_group.dart @@ -334,7 +334,7 @@ class RootWatchGroup extends WatchGroup { * Each step is called in sequence. ([ReactionFn]s are not called until all previous steps are * completed). */ - int detectChanges({ExceptionHandler exceptionHandler, ChangeLog changeLog}) { + int detectChanges({EvalExceptionHandler exceptionHandler, ChangeLog changeLog}) { // Process the ChangeRecords from the change detector ChangeRecord<_Handler> changeRecord = (_changeDetector as ChangeDetector<_Handler>).collectChanges(exceptionHandler); @@ -369,6 +369,7 @@ class RootWatchGroup extends WatchGroup { count++; try { dirtyWatch.invoke(); + } catch (e, s) { if (exceptionHandler == null) rethrow; else exceptionHandler(e, s); } @@ -576,7 +577,13 @@ class _InvokeHandler extends _Handler implements _ArgHandlerList { _InvokeHandler(watchGrp, expression): super(watchGrp, expression); - acceptValue(dynamic object) => watchRecord.object = object; + acceptValue(dynamic object) { + return watchRecord.object = object; + } + + onChange(ChangeRecord<_Handler> record) { + super.onChange(record); + } _releaseWatch() => (watchRecord as _EvalWatchRecord).remove(); diff --git a/lib/core/directive.dart b/lib/core/directive.dart index a5f32c04c..63fd69a0f 100644 --- a/lib/core/directive.dart +++ b/lib/core/directive.dart @@ -132,7 +132,7 @@ abstract class NgAnnotation { /** * Use the list to specify a expressions which are evaluated dynamically - * (ex. via [Scope.$eval]) and are otherwise not statically discoverable. + * (ex. via [Scope.eval]) and are otherwise not statically discoverable. */ final List exportExpressions; @@ -167,7 +167,7 @@ abstract class NgAnnotation { * Components can implement [NgAttachAware], [NgDetachAware], * [NgShadowRootAware] and declare these optional methods: * - * * `attach()` - Called on first [Scope.$digest()]. + * * `attach()` - Called on first [Scope.apply()]. * * `detach()` - Called on when owning scope is destroyed. * * `onShadowRoot(ShadowRoot shadowRoot)` - Called when [ShadowRoot] is loaded. */ @@ -261,7 +261,7 @@ RegExp _ATTR_NAME = new RegExp(r'\[([^\]]+)\]$'); * Directives can implement [NgAttachAware], [NgDetachAware] and * declare these optional methods: * - * * `attach()` - Called on first [Scope.$digest()]. + * * `attach()` - Called on first [Scope.apply()]. * * `detach()` - Called on when owning scope is destroyed. */ class NgDirective extends NgAnnotation { @@ -304,7 +304,7 @@ class NgDirective extends NgAnnotation { * Controllers can implement [NgAttachAware], [NgDetachAware] and * declare these optional methods: * - * * `attach()` - Called on first [Scope.$digest()]. + * * `attach()` - Called on first [Scope.apply()]. * * `detach()` - Called on when owning scope is destroyed. */ class NgController extends NgDirective { diff --git a/lib/core/interpolate.dart b/lib/core/interpolate.dart index aac6c7aed..9a976019d 100644 --- a/lib/core/interpolate.dart +++ b/lib/core/interpolate.dart @@ -3,12 +3,13 @@ part of angular.core; class Interpolation { final String template; final List seperators; - final List watchExpressions; + final List expressions; Function setter = (_) => _; - Interpolation(this.template, this.seperators, this.watchExpressions); + Interpolation(this.template, this.seperators, this.expressions); - String call(List parts, [_, __]) { + String call(List parts, [_]) { + if (parts == null) return seperators.join(''); var str = []; for(var i = 0, ii = parts.length; i < ii; i++) { str.add(seperators[i]); @@ -58,15 +59,14 @@ class Interpolate { bool shouldAddSeparator = true; String exp; List separators = []; - List watchExpressions = []; + List expressions = []; while(index < length) { if ( ((startIndex = template.indexOf(startSymbol, index)) != -1) && ((endIndex = template.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) ) { separators.add(template.substring(index, startIndex)); exp = template.substring(startIndex + startSymbolLength, endIndex); - Expression expression = _parse(exp); - watchExpressions.add(expression.eval); + expressions.add(exp); index = endIndex + endSymbolLength; hasInterpolation = true; } else { @@ -80,7 +80,7 @@ class Interpolate { separators.add(''); } return (!mustHaveExpression || hasInterpolation) - ? new Interpolation(template, separators, watchExpressions) + ? new Interpolation(template, separators, expressions) : null; } } diff --git a/lib/core/module.dart b/lib/core/module.dart index 58a479f07..70ee0c5ae 100644 --- a/lib/core/module.dart +++ b/lib/core/module.dart @@ -2,11 +2,9 @@ library angular.core; import 'dart:async' as async; import 'dart:collection'; -import 'dart:convert' show JSON; import 'dart:mirrors'; import 'package:di/di.dart'; -import 'package:perf_api/perf_api.dart'; import 'package:angular/core/parser/parser.dart'; import 'package:angular/core/parser/lexer.dart'; @@ -15,6 +13,13 @@ import 'package:angular/utils.dart'; import 'package:angular/core/service.dart'; export 'package:angular/core/service.dart'; +import 'package:angular/change_detection/watch_group.dart'; +export 'package:angular/change_detection/watch_group.dart'; +import 'package:angular/change_detection/change_detection.dart'; +import 'package:angular/change_detection/dirty_checking_change_detector.dart'; +import 'package:angular/core/parser/utils.dart'; +import 'package:angular/core/parser/syntax.dart'; + part "cache.dart"; part "directive.dart"; part "exception_handler.dart"; @@ -34,7 +39,17 @@ class NgCoreModule extends Module { type(ExceptionHandler); type(FilterMap); type(Interpolate); - type(Scope); + type(RootScope); + value(GetterCache, new GetterCache({})); + value(Object, {}); // RootScope context + factory(Scope, (injector) { +// try { throw null; } +// catch (e, s) { +// print('DEPRECATED reference to Scope:\n$s'); +// } + return injector.get(RootScope); + }); + type(AstParser); type(NgZone); type(Parser, implementedBy: DynamicParser); diff --git a/lib/core/scope.dart b/lib/core/scope.dart index f3f9971ed..53da59a04 100644 --- a/lib/core/scope.dart +++ b/lib/core/scope.dart @@ -1,56 +1,68 @@ part of angular.core; +NOT_IMPLEMENTED() { + throw new StateError('Not Implemented'); +} + +typedef EvalFunction0(); +typedef EvalFunction1(dynamic context); /** - * Injected into the listener function within [Scope.$on] to provide event-specific + * Injected into the listener function within [Scope.on] to provide event-specific * details to the scope listener. */ class ScopeEvent { + static final String DESTROY = 'ng-destroy'; + + final dynamic data; /** * The name of the intercepted scope event. */ - String name; + final String name; /** - * The origin scope that triggered the event (via $broadcast or $emit). + * The origin scope that triggered the event (via broadcast or emit). */ - Scope targetScope; + final Scope targetScope; /** * The destination scope that intercepted the event. */ - Scope currentScope; + Scope get currentScope => _currentScope; + Scope _currentScope; /** * true or false depending on if stopPropagation() was executed. */ - bool propagationStopped = false; + bool get propagationStopped => _propagationStopped; + bool _propagationStopped = false; /** * true or false depending on if preventDefault() was executed. */ - bool defaultPrevented = false; + bool get defaultPrevented => _defaultPrevented; + bool _defaultPrevented = false; /** ** [name] - The name of the scope event. ** [targetScope] - The destination scope that is listening on the event. */ - ScopeEvent(this.name, this.targetScope); + ScopeEvent(this.name, this.targetScope, this.data); /** * Prevents the intercepted event from propagating further to successive scopes. */ - stopPropagation () => propagationStopped = true; + stopPropagation () => _propagationStopped = true; /** * Sets the defaultPrevented flag to true. */ - preventDefault() => defaultPrevented = true; + preventDefault() => _defaultPrevented = true; } /** - * Allows the configuration of [Scope.$digest] iteration maximum time-to-live + * Allows the configuration of [Scope.digest] iteration maximum time-to-live * value. Digest keeps checking the state of the watcher getters until it * can execute one full iteration with no watchers triggering. TTL is used * to prevent an infinite loop where watch A triggers watch B which in turn @@ -64,976 +76,707 @@ class ScopeDigestTTL { ScopeDigestTTL.value(num this.ttl); } -/** - * Scope has two responsibilities. 1) to keep track af watches and 2) - * to keep references to the model so that they are available for - * data-binding. - */ -@proxy -@NgInjectableService() -class Scope implements Map { - final ExceptionHandler _exceptionHandler; - final Parser _parser; - final NgZone _zone; - final num _ttl; - final Map _properties = {}; - final _WatchList _watchers = new _WatchList(); - final Map> _listeners = {}; - final bool _isolate; - final bool _lazy; - final Profiler _perf; - final FilterMap _filters; - - /** - * The direct parent scope that created this scope (this can also be the $rootScope) - */ - final Scope $parent; +//TODO(misko): I don't think this should be in scope. +class ScopeLocals implements Map { + static wrapper(dynamic scope, Map locals) => new ScopeLocals(scope, locals); - /** - * The auto-incremented ID of the scope - */ - String $id; + Map _scope; + Map _locals; - /** - * The topmost scope of the application (same as $rootScope). - */ - Scope $root; - num _nextId = 0; - String _phase; - List _innerAsyncQueue; - List _outerAsyncQueue; - Scope _nextSibling, _prevSibling, _childHead, _childTail; - bool _skipAutoDigest = false; - bool _disabled = false; - - _set$Properties() { - _properties[r'this'] = this; - _properties[r'$id'] = this.$id; - _properties[r'$parent'] = this.$parent; - _properties[r'$root'] = this.$root; - } + ScopeLocals(this._scope, this._locals); - Scope(this._exceptionHandler, this._parser, ScopeDigestTTL ttl, - this._zone, this._perf, this._filters): - $parent = null, _isolate = false, _lazy = false, _ttl = ttl.ttl { - $root = this; - $id = '_${$root._nextId++}'; - _innerAsyncQueue = []; - _outerAsyncQueue = []; - - // Set up the zone to auto digest this scope. - _zone.onTurnDone = _autoDigestOnTurnDone; - _zone.onError = (e, s, ls) => _exceptionHandler(e, s); - _set$Properties(); - } + operator []=(String name, value) => _scope[name] = value; + operator [](String name) => (_locals.containsKey(name) ? _locals : _scope)[name]; - Scope._child(Scope parent, bool this._isolate, bool this._lazy, this._perf, filters) - : $parent = parent, _ttl = parent._ttl, _parser = parent._parser, - _exceptionHandler = parent._exceptionHandler, _zone = parent._zone, - _filters = filters == null ? parent._filters : filters { - $root = $parent.$root; - $id = '_${$root._nextId++}'; - _innerAsyncQueue = $parent._innerAsyncQueue; - _outerAsyncQueue = $parent._outerAsyncQueue; - - _prevSibling = $parent._childTail; - if ($parent._childHead != null) { - $parent._childTail._nextSibling = this; - $parent._childTail = this; - } else { - $parent._childHead = $parent._childTail = this; - } - _set$Properties(); - } + get isEmpty => _scope.isEmpty && _locals.isEmpty; + get isNotEmpty => _scope.isNotEmpty || _locals.isNotEmpty; + get keys => _scope.keys; + get values => _scope.values; + get length => _scope.length; + + forEach(fn) => _scope.forEach(fn); + remove(key) => _scope.remove(key); + clear() => _scope.clear; + containsKey(key) => _scope.containsKey(key); + containsValue(key) => _scope.containsValue(key); + addAll(map) => _scope.addAll(map); + putIfAbsent(key, fn) => _scope.putIfAbsent(key, fn); +} - _autoDigestOnTurnDone() { - if ($root._skipAutoDigest) { - $root._skipAutoDigest = false; - } else { - $digest(); +class Scope { + final dynamic context; + final RootScope rootScope; + Scope _parentScope; + Scope get parentScope => _parentScope; + + final WatchGroup watchGroup; + final WatchGroup observeGroup; + final int _depth; + final int _index; + + Scope _childHead, _childTail, _next, _prev; + _Streams _streams; + int _nextChildIndex = 0; + + Scope(Object this.context, this.rootScope, this._parentScope, + this._depth, this._index, + this.watchGroup, this.observeGroup); + + // TODO(misko): this is a hack and should be removed + // A better way to do this is to remove the praser from the scope. + Watch watchSet(List exprs, Function reactionFn) { + var expr = '{{${exprs.join('}}?{{')}}}'; + List items = exprs.map(rootScope._parse).toList(); + AST ast = new PureFunctionAST(expr, new ArrayFn(), items); + return watchGroup.watch(ast, reactionFn); + } + + Watch watch(expression, ReactionFn reactionFn) { + // Todo(misko): remove the parser from here. It should only take AST. + assert(expression != null); + AST ast = expression is AST ? expression : rootScope._parse(expression); + return watchGroup.watch(ast, reactionFn); + } + + Watch observe(expression, ReactionFn reactionFn) { + // Todo(misko): remove the parser from here. It should only take AST. + assert(expression != null); + AST ast = expression is AST ? expression : rootScope._parse(expression); + return observeGroup.watch(ast, reactionFn); + } + + dynamic eval(expression, [Map locals]) { + assert(expression == null || + expression is String || + expression is Function); + if (expression is String && expression.isNotEmpty) { + var obj = locals == null ? context : new ScopeLocals(context, locals); + return rootScope._parser(expression).eval(obj); + } else if (expression is EvalFunction1) { + assert(locals == null); + return expression(context); + } else if (expression is EvalFunction0) { + assert(locals == null); + return expression(); } } - _identical(a, b) => - identical(a, b) || - (a is String && b is String && a == b) || - (a is num && b is num && a.isNaN && b.isNaN); + dynamic applyInZone([expression, Map locals]) + => rootScope._zone.run(() => apply(expression, locals)); - containsKey(String name) { - for (var scope = this; scope != null; scope = scope.$parent) { - if (scope._properties.containsKey(name)) { - return true; - } else if(scope._isolate) { - break; - } + dynamic apply([expression, Map locals]) { + rootScope._transitionState(null, RootScope.STATE_APPLY); + try { + return eval(expression, locals); + } catch (e, s) { + rootScope._exceptionHandler(e, s); + } finally { + rootScope._transitionState(RootScope.STATE_APPLY, null); + rootScope.digest(); + rootScope.flush(); } - return false; } - remove(String name) => this._properties.remove(name); - operator []=(String name, value) => _properties[name] = value; - operator [](String name) { - for (var scope = this; scope != null; scope = scope.$parent) { - if (scope._properties.containsKey(name)) { - return scope._properties[name]; - } else if(scope._isolate) { - break; - } - } - return null; - } - noSuchMethod(Invocation invocation) { - var name = MirrorSystem.getName(invocation.memberName); - if (invocation.isGetter) { - return this[name]; - } else if (invocation.isSetter) { - var value = invocation.positionalArguments[0]; - name = name.substring(0, name.length - 1); - this[name] = value; - return value; - } else { - if (this[name] is Function) { - return this[name](); - } else { - super.noSuchMethod(invocation); - } - } - } + ScopeEvent emit(String name, [data]) => _Streams.emit(this, name, data); + ScopeEvent broadcast(String name, [data]) => _Streams.broadcast(this, name, data); + ScopeStream on(String name) => _Streams.on(this, rootScope._exceptionHandler, name); + Scope createChild([Object childContext]) { + if (childContext == null) childContext = context; + Scope child = new Scope(childContext, rootScope, this, + _depth + 1, _nextChildIndex++, + watchGroup.newGroup(childContext), + observeGroup.newGroup(childContext)); + var next = null; + var prev = _childTail; + child._next = next; + child._prev = prev; + if (prev == null) _childHead = child; else prev._next = child; + if (next == null) _childTail = child; else next._prev = child; + return child; + } - /** - * Create a new child [Scope]. - * - * * [isolate] - If set to true the child scope does not inherit properties from the parent scope. - * This in essence creates an independent (isolated) view for the users of the scope. - * * [lazy] - If set to true the scope digest will only run if the scope is marked as [$dirty]. - * This is usefull if we expect that the bindings in the scope are constant and there is no need - * to check them on each digest. The digest can be forced by marking it [$dirty]. - */ - $new({bool isolate: false, bool lazy: false, FilterMap filters}) => - new Scope._child(this, isolate, lazy, _perf, filters); + void destroy() { + var prev = this._prev; + var next = this._next; + if (prev == null) _parentScope._childHead = next; else prev._next = next; + if (next == null) _parentScope._childTail = prev; else next._prev = prev; - /** - * *EXPERIMENTAL:* This feature is experimental. We reserve the right to change or delete it. - * - * A dissabled scope will not be part of the [$digest] cycle until it is re-enabled. - */ - set $disabled(value) => this._disabled = value; - get $disabled => this._disabled; + this._next = this._prev = null; - /** - * Registers a listener callback to be executed whenever the [watchExpression] changes. - * - * The watchExpression is called on every call to [$digest] and should return the value that - * will be watched. (Since [$digest] reruns when it detects changes the watchExpression can - * execute multiple times per [$digest] and should be idempotent.) - * - * The listener is called only when the value from the current [watchExpression] and the - * previous call to [watchExpression] are not identical (with the exception of the initial run, - * see below). - * - * The watch listener may change the model, which may trigger other listeners to fire. This is - * achieved by rerunning the watchers until no changes are detected. The rerun iteration limit - * is 10 to prevent an infinite loop deadlock. - * If you want to be notified whenever [$digest] is called, you can register a [watchExpression] - * function with no listener. (Since [watchExpression] can execute multiple times per [$digest] - * cycle when a change is detected, be prepared for multiple calls to your listener.) - * - * After a watcher is registered with the scope, the listener fn is called asynchronously - * (via [$evalAsync]) to initialize the watcher. In rare cases, this is undesirable because the - * listener is called when the result of [watchExpression] didn't change. To detect this - * scenario within the listener fn, you can compare the newVal and oldVal. If these two values - * are identical then the listener was called due to initialization. - * - * * [watchExpression] - can be any one of these: a [Function] - `(Scope scope) => ...;` or a - * [String] - `expression` which is compiled with [Parser] service into a function - * * [listener] - A [Function] `(currentValue, previousValue, Scope scope) => ...;` - * * [watchStr] - Used as a debbuging hint to easier identify which expression is associated with - * this watcher. - */ - $watch(watchExpression, [Function listener, String watchStr]) { - if (watchStr == null) { - watchStr = watchExpression.toString(); + watchGroup.remove(); + observeGroup.remove(); + _Streams.destroy(this); - // Keep prod fast - assert((() { - watchStr = _source(watchExpression); - return true; - })()); - } - var watcher = new _Watch(_compileToFn(listener), _initWatchVal, - _compileToFn(watchExpression), watchStr); - _watchers.addLast(watcher); - return () => _watchers.remove(watcher); + _parentScope = null; + broadcast(ScopeEvent.DESTROY); } +} - /** - * A variant of [$watch] where it watches a collection of [watchExpressios]. If any - * one expression in the collection changes the [listener] is executed. - * - * * [watcherExpressions] - `List` - * * [Listener] - `(List newValues, List previousValues, Scope scope)` - */ - $watchSet(List watchExpressions, [Function listener, String watchStr]) { - if (watchExpressions.length == 0) return () => null; - - var lastValues = new List(watchExpressions.length); - var currentValues = new List(watchExpressions.length); - - if (watchExpressions.length == 1) { - // Special case size of one. - return $watch(watchExpressions[0], (value, oldValue, scope) { - currentValues[0] = value; - lastValues[0] = oldValue; - listener(currentValues, lastValues, scope); - }); - } - var deregesterFns = []; - var changeCount = 0; - for(var i = 0, ii = watchExpressions.length; i < ii; i++) { - deregesterFns.add($watch(watchExpressions[i], (value, oldValue, __) { - currentValues[i] = value; - lastValues[i] = oldValue; - changeCount++; - })); - } - deregesterFns.add($watch((s) => changeCount, (c, o, scope) { - listener(currentValues, lastValues, scope); - })); - return () { - for(var i = 0, ii = deregesterFns.length; i < ii; i++) { - deregesterFns[i](); - } - }; - } - /** - * Shallow watches the properties of an object and fires whenever any of the properties change - * (for arrays, this implies watching the array items; for object maps, this implies watching - * the properties). If a change is detected, the listener callback is fired. - * - * The obj collection is observed via standard [$watch] operation and is examined on every call - * to [$digest] to see if any items have been added, removed, or moved. - * - * The listener is called whenever anything within the obj has changed. Examples include - * adding, removing, and moving items belonging to an object or array. - */ - $watchCollection(obj, listener, [String expression, bool shallow=false]) { - var oldValue; - var newValue; - int changeDetected = 0; - Function objGetter = relaxFnArgs2(_compileToFn(obj)); - List internalArray = []; - Map internalMap = {}; - int oldLength = 0; - int newLength; - var key; - List keysToRemove = []; - Function detectNewKeys = (key, value) { - newLength++; - if (oldValue.containsKey(key)) { - if (!_identical(oldValue[key], value)) { - changeDetected++; - oldValue[key] = value; - } - } else { - oldLength++; - oldValue[key] = value; - changeDetected++; - } - }; - Function findMissingKeys = (key, _) { - if (!newValue.containsKey(key)) { - oldLength--; - keysToRemove.add(key); - } - }; +class RootScope extends Scope { + static final STATE_APPLY = 'apply'; + static final STATE_DIGEST = 'digest'; + static final STATE_FLUSH = 'digest'; - Function removeMissingKeys = (k) => oldValue.remove(k); + final ExceptionHandler _exceptionHandler; + final Parser _parser; + final ScopeDigestTTL _ttl; + final ExpressionVisitor visitor = new ExpressionVisitor(); // TODO(misko): delete me + final NgZone _zone; - var $watchCollectionWatch; + _FunctionChain _runAsyncHead, _runAsyncTail; + _FunctionChain _domWriteHead, _domWriteTail; + _FunctionChain _domReadHead, _domReadTail; + + String _state; + + RootScope(Object context, Parser this._parser, GetterCache cacheGetter, + FilterMap filterMap, ExceptionHandler this._exceptionHandler, + ScopeDigestTTL this._ttl, this._zone) + : super(context, null, null, 0, 0, + new RootWatchGroup(new DirtyCheckingChangeDetector(cacheGetter), context), + new RootWatchGroup(new DirtyCheckingChangeDetector(cacheGetter), context)) + { + _zone.onTurnDone = () { + digest(); + flush(); + }; + } - if (shallow) { - $watchCollectionWatch = (_) { - newValue = objGetter(this, _filters); - newLength = newValue == null ? 0 : newValue.length; - if (newLength != oldLength) { - oldLength = newLength; - changeDetected++; - } - if (!identical(oldValue, newValue)) { - oldValue = newValue; - changeDetected++; - } - return changeDetected; - }; - } else { - $watchCollectionWatch = (_) { - newValue = objGetter(this, _filters); + RootScope get rootScope => this; - if (newValue is! Map && newValue is! Iterable) { - if (!_identical(oldValue, newValue)) { - oldValue = newValue; - changeDetected++; - } - } else if (newValue is Iterable) { - if (!_identical(oldValue, internalArray)) { - // we are transitioning from something which was not an array into array. - oldValue = internalArray; - oldLength = oldValue.length = 0; - changeDetected++; + void digest() { + _transitionState(null, STATE_DIGEST); + try { + RootWatchGroup rootWatchGroup = (watchGroup as RootWatchGroup); + + int digestTTL = _ttl.ttl; + const int logCount = 3; + List log; + List digestLog; + var count; + ChangeLog changeLog; + do { + while(_runAsyncHead != null) { + try { + _runAsyncHead.fn(); + } catch (e, s) { + _exceptionHandler(e, s); } + _runAsyncHead = _runAsyncHead._next; + } - newLength = newValue.length; - - if (oldLength != newLength) { - // if lengths do not match we need to trigger change notification - changeDetected++; - oldValue.length = oldLength = newLength; - } - // copy the items to oldValue and look for changes. - for (var i = 0; i < newLength; i++) { - if (!_identical(oldValue[i], newValue.elementAt(i))) { - changeDetected++; - oldValue[i] = newValue.elementAt(i); - } - } - } else { // Map - if (!_identical(oldValue, internalMap)) { - // we are transitioning from something which was not an object into object. - oldValue = internalMap = {}; - oldLength = 0; - changeDetected++; - } - // copy the items to oldValue and look for changes. - newLength = 0; - newValue.forEach(detectNewKeys); - if (oldLength > newLength) { - // we used to have more keys, need to find them and destroy them. - changeDetected++; - oldValue.forEach(findMissingKeys); - keysToRemove.forEach(removeMissingKeys); - keysToRemove.clear(); + digestTTL--; + count = rootWatchGroup.detectChanges( + exceptionHandler: _exceptionHandler, + changeLog: changeLog); + + if (digestTTL <= logCount) { + if (changeLog == null) { + log = []; + digestLog = []; + changeLog = (value) => digestLog.add(value); + } else { + log.add(digestLog.join(', ')); + digestLog.clear(); } } - return changeDetected; - }; + if (digestTTL == 0) { + throw 'Model did not stabilize in ${_ttl.ttl} digests. ' + + 'Last $logCount iterations:\n${log.join('\n')}'; + } + } while (count > 0); + } finally { + _transitionState(STATE_DIGEST, null); } - - var $watchCollectionAction = (_, __, ___) { - relaxFnApply(listener, [newValue, oldValue, this]); - }; - - return this.$watch($watchCollectionWatch, - $watchCollectionAction, - expression == null ? obj : expression); - } - - - /** - * Add this function to your code if you want to add a $digest - * and want to assert that the digest will be called on this turn. - * This method will be deleted when we are comfortable with - * auto-digesting scope. - */ - $$verifyDigestWillRun() { - assert(!$root._skipAutoDigest); - _zone.assertInTurn(); } - /** - * *EXPERIMENTAL:* This feature is experimental. We reserve the right to change or delete it. - * - * Marks a scope as dirty. If the scope is lazy (see [$new]) then the scope will be included - * in the next [$digest]. - * - * NOTE: This has no effect for non-lazy scopes. - */ - $dirty() { - this._disabled = false; - } - - /** - * Processes all of the watchers of the current scope and its children. - * Because a watcher's listener can change the model, the `$digest()` operation keeps calling - * the watchers no further response data has changed. This means that it is possible to get - * into an infinite loop. This function will throw `'Maximum iteration limit exceeded.'` - * if the number of iterations exceeds 10. - * - * There should really be no need to call $digest() in production code since everything is - * handled behind the scenes with zones and object mutation events. However, in testing - * both $digest and [$apply] are useful to control state and simulate the scope life cycle in - * a step-by-step manner. - * - * Refer to [$watch], [$watchSet] or [$watchCollection] to see how to register watchers that - * are executed during the digest cycle. - */ - $digest() { + void flush() { + _transitionState(null, STATE_FLUSH); + RootWatchGroup observeGroup = this.observeGroup as RootWatchGroup; + bool runObservers = true; try { - _beginPhase('\$digest'); - _digestWhileDirtyLoop(); - } catch (e, s) { - _exceptionHandler(e, s); + do { + while(_domWriteHead != null) { + try { _domWriteHead.fn(); } + catch (e, s) { _exceptionHandler(e, s); } + _domWriteHead = _domWriteHead._next; + } + if (runObservers) { + runObservers = false; + observeGroup.detectChanges(exceptionHandler:_exceptionHandler); + } + while(_domReadHead != null) { + try { _domReadHead.fn(); } + catch (e, s) { _exceptionHandler(e, s); } + _domReadHead = _domReadHead._next; + } + } while (_domWriteHead != null || _domReadHead != null); + assert((() { + var watchLog = []; + var observeLog = []; + (watchGroup as RootWatchGroup).detectChanges(changeLog: watchLog.add); + (observeGroup as RootWatchGroup).detectChanges(changeLog: observeLog.add); + if (watchLog.isNotEmpty || observeLog.isNotEmpty) { + throw 'Observer reaction functions should not change model. \n' + 'These watch changes were detected: ${watchLog.join('; ')}\n' + 'These observe changes were detected: ${observeLog.join('; ')}'; + } + return true; + })()); } finally { - _clearPhase(); + _transitionState(STATE_FLUSH, null); } - } - - _digestWhileDirtyLoop() { - _digestHandleQueue('ng.innerAsync', _innerAsyncQueue); - - int timerId; - assert((timerId = _perf.startTimer('ng.dirty_check', 0)) != false); - _Watch lastDirtyWatch = _digestComputeLastDirty(); - assert(_perf.stopTimer(timerId) != false); + } - if (lastDirtyWatch == null) { - _digestHandleQueue('ng.outerAsync', _outerAsyncQueue); - return; + // QUEUES + void runAsync(fn()) { + var chain = new _FunctionChain(fn); + if (_runAsyncHead == null) { + _runAsyncHead = _runAsyncTail = chain; + } else { + _runAsyncTail = _runAsyncTail._next = chain; } + } - List> watchLog = []; - for (int iteration = 1, ttl = _ttl; iteration < ttl; iteration++) { - _Watch stopWatch = _digestHandleQueue('ng.innerAsync', _innerAsyncQueue) - ? null // Evaluating async work requires re-evaluating all watchers. - : lastDirtyWatch; - lastDirtyWatch = null; - - List expressionLog; - if (ttl - iteration <= 3) { - expressionLog = []; - watchLog.add(expressionLog); - } - - int timerId; - assert((timerId = _perf.startTimer('ng.dirty_check', iteration)) != false); - lastDirtyWatch = _digestComputeLastDirtyUntil(stopWatch, expressionLog); - assert(_perf.stopTimer(timerId) != false); - - if (lastDirtyWatch == null) { - _digestComputePerfCounters(); - _digestHandleQueue('ng.outerAsync', _outerAsyncQueue); - return; - } + void domWrite(fn()) { + var chain = new _FunctionChain(fn); + if (_domWriteHead == null) { + _domWriteHead = _domWriteTail = chain; + } else { + _domWriteTail = _domWriteTail._next = chain; } + } - // I've seen things you people wouldn't believe. Attack ships on fire - // off the shoulder of Orion. I've watched C-beams glitter in the dark - // near the Tannhauser Gate. All those moments will be lost in time, - // like tears in rain. Time to die. - throw '$_ttl \$digest() iterations reached. Aborting!\n' - 'Watchers fired in the last ${watchLog.length} iterations: ' - '${_toJson(watchLog)}'; + void domRead(fn()) { + var chain = new _FunctionChain(fn); + if (_domReadHead == null) { + _domReadHead = _domReadTail = chain; + } else { + _domReadTail = _domReadTail._next = chain; + } } - bool _digestHandleQueue(String timerName, List queue) { - if (queue.isEmpty) { - return false; + AST _parse(expression) => visitor.visit(_parser.call(expression)); + void destroy() {} + + void _transitionState(String from, String to) { + if (_state != from) { + throw "$_state already in progress can not enter $to."; } - do { - var timerId; - try { - var workFn = queue.removeAt(0); - assert((timerId = _perf.startTimer(timerName, _source(workFn))) != false); - $root.$eval(workFn); - } catch (e, s) { - _exceptionHandler(e, s); - } finally { - assert(_perf.stopTimer(timerId) != false); - } - } while (queue.isNotEmpty); - return true; + _state = to; } +} - - _Watch _digestComputeLastDirty() { - int watcherCount = 0; - int scopeCount = 0; - Scope scope = this; - do { - _WatchList watchers = scope._watchers; - watcherCount += watchers.length; - scopeCount++; - for (_Watch watch = watchers.head; watch != null; watch = watch.next) { - var last = watch.last; - var value = watch.get(scope, scope._filters); - if (!_identical(value, last)) { - return _digestHandleDirty(scope, watch, last, value, null); +/** + * Keeps track of Streams for each Scope. When emitting events + * we would need to walk the whole tree. Its faster if we can prune + * the Scopes we have to visit. + * + * Scope with no [_ScopeStreams] has no events registered on itself or children + * + * We keep track of [Stream]s, and also child scope [Stream]s. To save + * memory we use the same stream object on all of our parents if they don't + * have one. But that means that we have to keep track if the stream belongs + * to the node. + * + * Scope with [_ScopeStreams] but who's [_scope] dose not match the scope + * is only inherited + * + * Only [Scope] with [_ScopeStreams] who's [_scope] matches the [Scope] + * instance is the actual scope. + * + * Once the [Stream] is created it can not be removed even if all listeners + * are canceled. That is because we don't know if someone still has reference + * to it. + */ +class _Streams { + final ExceptionHandler _exceptionHandler; + /// Scope we belong to. + final Scope _scope; + /// [Stream]s for [_scope] only + final Map _streams = new Map(); + /// Child [Scope] event counts. + final Map _typeCounts; + + _Streams(this._scope, this._exceptionHandler, _Streams inheritStreams) + : _typeCounts = inheritStreams == null + ? new Map() + : new Map.from(inheritStreams._typeCounts); + + static ScopeEvent emit(Scope scope, String name, data) { + ScopeEvent event = new ScopeEvent(name, scope, data); + Scope scopeCursor = scope; + while(scopeCursor != null) { + if (scopeCursor._streams._scope == scopeCursor) { + ScopeStream stream = scopeCursor._streams._streams[name]; + if (stream != null) { + event._currentScope = scopeCursor; + stream._fire(event); + if (event.propagationStopped) return event; } } - } while ((scope = _digestComputeNextScope(scope)) != null); - _digestUpdatePerfCounters(watcherCount, scopeCount); - return null; + scopeCursor = scopeCursor._parentScope; + } + return event; } - - _Watch _digestComputeLastDirtyUntil(_Watch stopWatch, List log) { - int watcherCount = 0; - int scopeCount = 0; - Scope scope = this; - do { - _WatchList watchers = scope._watchers; - watcherCount += watchers.length; - scopeCount++; - for (_Watch watch = watchers.head; watch != null; watch = watch.next) { - if (identical(stopWatch, watch)) return null; - var last = watch.last; - var value = watch.get(scope, scope._filters); - if (!_identical(value, last)) { - return _digestHandleDirty(scope, watch, last, value, log); + static ScopeEvent broadcast(Scope scope, String name, data) { + _Streams scopeStreams = scope._streams; + ScopeEvent event = new ScopeEvent(name, scope, data); + if (scopeStreams != null && scopeStreams._typeCounts.containsKey(name)) { + Queue queue = new Queue(); + queue.addFirst(scopeStreams._scope); + while(queue.isNotEmpty) { + scope = queue.removeFirst(); + scopeStreams = scope._streams; + assert(scopeStreams._scope == scope); + assert(scopeStreams._streams.containsKey(name)); + var stream = scopeStreams._streams[name]; + event._currentScope = scope; + stream._fire(event); + // Reverse traversal so that when the queue is read it is correct order. + var childScope = scope._childTail; + while(childScope != null) { + scopeStreams = childScope._streams; + if (scopeStreams != null) { + queue.addFirst(scopeStreams._scope); + } + childScope = childScope._prev; } } - } while ((scope = _digestComputeNextScope(scope)) != null); - return null; + } + return event; } - - _Watch _digestHandleDirty(Scope scope, _Watch watch, last, value, List log) { - _Watch lastDirtyWatch; - while (true) { - if (!_identical(value, last)) { - lastDirtyWatch = watch; - if (log != null) log.add(watch.exp == null ? '[unknown]' : watch.exp); - watch.last = value; - var fireTimer; - assert((fireTimer = _perf.startTimer('ng.fire', watch.exp)) != false); - watch.fn(value, identical(_initWatchVal, last) ? value : last, scope); - assert(_perf.stopTimer(fireTimer) != false); + static ScopeStream on(Scope scope, ExceptionHandler _exceptionHandler, String name) { + var scopeStream = scope._streams; + if (scopeStream == null || scopeStream._scope != scope) { + // We either don't have [_ScopeStreams] or it is inherited. + var newStreams = new _Streams(scope, _exceptionHandler, scopeStream); + var scopeCursor = scope; + while (scopeCursor != null && scopeCursor._streams == scopeStream) { + scopeCursor._streams = newStreams; + scopeCursor = scopeCursor._parentScope; } - watch = watch.next; - while (watch == null) { - scope = _digestComputeNextScope(scope); - if (scope == null) return lastDirtyWatch; - watch = scope._watchers.head; - } - last = watch.last; - value = watch.get(scope, scope._filters); + scopeStream = newStreams; } + return scopeStream._get(scope, name); } - - Scope _digestComputeNextScope(Scope scope) { - // Insanity Warning: scope depth-first traversal - // yes, this code is a bit crazy, but it works and we have tests to prove it! - // this piece should be kept in sync with the traversal in $broadcast - Scope target = this; - Scope childHead = scope._childHead; - while (childHead != null && childHead._disabled) { - childHead = childHead._nextSibling; + static void destroy(Scope scope) { + var toBeDeletedStreams = scope._streams; + if (toBeDeletedStreams == null) return; + scope = scope._parentScope; // skip current state as not to delete listeners + while (scope != null && + scope._streams == toBeDeletedStreams) { + scope._streams = null; + scope = scope._parentScope; } - if (childHead == null) { - if (scope == target) { - return null; - } else { - Scope next = scope._nextSibling; - if (next == null) { - while (scope != target && (next = scope._nextSibling) == null) { - scope = scope.$parent; - } + if (scope == null) return; + var parentStreams = scope._streams; + assert(parentStreams != toBeDeletedStreams); + toBeDeletedStreams._typeCounts.forEach( + (name, count) => parentStreams._addCount(name, -count)); + } + + async.Stream _get(Scope scope, String name) { + assert(scope._streams == this); + assert(scope._streams._scope == scope); + assert(_exceptionHandler != null); + return _streams.putIfAbsent(name, () => new ScopeStream(this, _exceptionHandler, name)); + } + + void _addCount(String name, int amount) { + // decrement the counters on all parent scopes + _Streams lastStreams = null; + Scope scope = _scope; + while (scope != null) { + if (lastStreams != scope._streams) { + // we have a transition, need to decrement it + lastStreams = scope._streams; + int count = lastStreams._typeCounts[name]; + count = count == null ? amount : count + amount; + assert(count >= 0); + if (count == 0) { + lastStreams._typeCounts.remove(name); + } else { + lastStreams._typeCounts[name] = count; } - return next; } - } else { - if (childHead._lazy) childHead._disabled = true; - return childHead; + scope = scope._parentScope; } } +} - - void _digestComputePerfCounters() { - int watcherCount = 0, scopeCount = 0; - Scope scope = this; - do { - scopeCount++; - watcherCount += scope._watchers.length; - } while ((scope = _digestComputeNextScope(scope)) != null); - _digestUpdatePerfCounters(watcherCount, scopeCount); - } - - - void _digestUpdatePerfCounters(int watcherCount, int scopeCount) { - _perf.counters['ng.scope.watchers'] = watcherCount; - _perf.counters['ng.scopes'] = scopeCount; - } - - - /** - * Removes the current scope (and all of its children) from the parent scope. Removal implies - * that calls to $digest() will no longer propagate to the current scope and its children. - * Removal also implies that the current scope is eligible for garbage collection. - * - * The `$destroy()` operation is usually used within directives that perform transclusion on - * multiple child elements (like ngRepeat) which create multiple child scopes. - * - * Just before a scope is destroyed, a `$destroy` event is broadcasted on this scope. This is - * a great way for child scopes (such as shared directives or controllers) to detect to and - * perform any necessary cleanup before the scope is removed from the application. - * - * Note that, in AngularDart, there is also a `$destroy` jQuery DOM event, which can be used to - * clean up DOM bindings before an element is removed from the DOM. - */ - $destroy() { - if ($root == this) return; // we can't remove the root node; - - $broadcast(r'$destroy'); - - if ($parent._childHead == this) $parent._childHead = _nextSibling; - if ($parent._childTail == this) $parent._childTail = _prevSibling; - if (_prevSibling != null) _prevSibling._nextSibling = _nextSibling; - if (_nextSibling != null) _nextSibling._prevSibling = _prevSibling; +class ScopeStream extends async.Stream { + final ExceptionHandler _exceptionHandler; + final _Streams _streams; + final String _name; + final List subscriptions = []; + + ScopeStream(this._streams, this._exceptionHandler, this._name); + + ScopeStreamSubscription listen(void onData(ScopeEvent event), + { Function onError, + void onDone(), + bool cancelOnError }) { + if (subscriptions.isEmpty) { + _streams._addCount(_name, 1); + } + ScopeStreamSubscription subscription = new ScopeStreamSubscription(this, onData); + subscriptions.add(subscription); + return subscription; } - - /** - * Evaluates the expression against the current scope and returns the result. Note that, the - * expression data is relative to the data within the scope. Therefore an expression such as - * `a + b` will deference variables `a` and `b` and return a result so long as `a` and `b` - * exist on the scope. - * - * * [expr] - The expression that will be evaluated. This can be both a Function or a String. - * * [locals] - An optional Map of key/value data that will override any matching scope members - * for the purposes of the evaluation. - */ - $eval(expr, [locals]) { - return relaxFnArgs(_compileToFn(expr))(locals == null ? this : new ScopeLocals(this, locals), _filters); + _fire(ScopeEvent event) { + for(ScopeStreamSubscription subscription in subscriptions) { + try { + subscription._onData(event); + } catch (e, s) { + _exceptionHandler(e, s); + } + } } - - /** - * Evaluates the expression against the current scope at a later point in time. The $evalAsync - * operation may not get run right away (depending if an existing digest cycle is going on) and - * may therefore be issued later on (by a follow-up digest cycle). Note that at least one digest - * cycle will be performed after the expression is evaluated. However, If triggering an additional - * digest cycle is not desired then this can be avoided by placing `{outsideDigest: true}` as - * the 2nd parameter to the function. - * - * * [expr] - The expression that will be evaluated. This can be both a Function or a String. - * * [outsideDigest] - Whether or not to trigger a follow-up digest after evaluation. - */ - $evalAsync(expr, {outsideDigest: false}) { - if (outsideDigest) { - _outerAsyncQueue.add(expr); + _remove(ScopeStreamSubscription subscription) { + assert(subscription._scopeStream == this); + if (subscriptions.remove(subscription)) { + if (subscriptions.isEmpty) { + _streams._addCount(_name, -1); + } } else { - _innerAsyncQueue.add(expr); + throw new StateError('AlreadyCanceled'); } + return null; } +} +class ScopeStreamSubscription implements async.StreamSubscription { + final ScopeStream _scopeStream; + final Function _onData; + ScopeStreamSubscription(this._scopeStream, this._onData); - /** - * Skip running a $digest at the end of this turn. - * The primary use case is to skip the digest in the current VM turn because - * you just scheduled or are otherwise certain of an impending VM turn and the - * digest at the end of that turn is sufficient. You should be able to answer - * "No" to the question "Is there any other code that is aware that this VM - * turn occurred and therefore expected a digest?". If your answer is "Yes", - * then you run the risk that the very next VM turn is not for your event and - * now that other code runs in that turn and sees stale values. - * - * You might call this function, for instance, from an event listener where, - * though the event occurred, you need to wait for another event before you can - * perform something meaningful. You might schedule that other event, - * set a flag for the handler of the other event to recognize, etc. and then - * call this method to skip the digest this cycle. Note that you should call - * this function *after* you have successfully confirmed that the expected VM - * turn will occur (perhaps by scheduling it) to ensure that the digest - * actually does take place on that turn. - */ - $skipAutoDigest() { - _zone.assertInTurn(); - $root._skipAutoDigest = true; - } + async.Future cancel() => _scopeStream._remove(this); + void onData(void handleData(ScopeEvent data)) => NOT_IMPLEMENTED(); + void onError(Function handleError) => NOT_IMPLEMENTED(); + void onDone(void handleDone()) => NOT_IMPLEMENTED(); + void pause([async.Future resumeSignal]) => NOT_IMPLEMENTED(); + void resume() => NOT_IMPLEMENTED(); + bool get isPaused => NOT_IMPLEMENTED(); + async.Future asFuture([var futureValue]) => NOT_IMPLEMENTED(); +} - /** - * Triggers a digest operation much like [$digest] does, however, also accepts an - * optional expression to evaluate alongside the digest operation. The result of that - * expression will be returned afterwards. Much like with $digest, $apply should only be - * used within unit tests to simulate the life cycle of a scope. See [$digest] to learn - * more. - * - * * [expr] - optional expression which will be evaluated after the digest is performed. See [$eval] - * to learn more about expressions. - */ - $apply([expr]) { - return _zone.run(() { - var timerId; - try { - assert((timerId = _perf.startTimer('ng.\$apply', _source(expr))) != false); - return $eval(expr); - } catch (e, s) { - _exceptionHandler(e, s); - } finally { - assert(_perf.stopTimer(timerId) != false); - } - }); +class _FunctionChain { + final Function fn; + _FunctionChain _next; + + _FunctionChain(fn()) + : fn = fn + { + assert(fn != null); } +} +class AstParser { + final Parser _parser; + int _id = 0; + ExpressionVisitor _visitor = new ExpressionVisitor(); - /** - * Registers a scope-based event listener to intercept events triggered by - * [$broadcast] (from any parent scopes) or [$emit] (from child scopes) that - * match the given event name. $on accepts two arguments: - * - * * [name] - Refers to the event name that the scope will listen on. - * * [listener] - Refers to the callback function which is executed when the event - * is intercepted. - * - * - * When the listener function is executed, an instance of [ScopeEvent] will be passed - * as the first parameter to the function. - * - * Any additional parameters available within the listener callback function are those that - * are set by the $broadcast or $emit scope methods (which are set by the origin scope which - * is the scope that first triggered the scope event). - */ - $on(name, listener) { - var namedListeners = _listeners[name]; - if (!_listeners.containsKey(name)) { - _listeners[name] = namedListeners = []; - } - namedListeners.add(listener); + AstParser(this._parser); - return () { - namedListeners.remove(listener); - }; + AST call(String exp, { FilterMap filters, + bool collection:false, + Object context:null }) { + _visitor.filters = filters; + AST contextRef = _visitor.contextRef; + try { + if (context != null) { + _visitor.contextRef = new ConstantAST(context, '#${_id++}'); + } + var ast = _parser(exp); + return collection ? _visitor.visitCollection(ast) : _visitor.visit(ast); + } finally { + _visitor.contextRef = contextRef; + _visitor.filters = null; + } } +} +class ExpressionVisitor implements Visitor { + static final ContextReferenceAST scopeContextRef = new ContextReferenceAST(); + AST contextRef = scopeContextRef; - /** - * Triggers a scope event referenced by the [name] parameters upwards towards the root of the - * scope tree. If intercepted, by a parent scope containing a matching scope event listener - * (which is registered via the [$on] scope method), then the event listener callback function - * will be executed. - * - * * [name] - The scope event name that will be triggered. - * * [args] - An optional list of arguments that will be fed into the listener callback function - * for any event listeners that are registered via [$on]. - */ - $emit(name, [List args]) { - var empty = [], - namedListeners, - scope = this, - event = new ScopeEvent(name, this), - listenerArgs = [event], - i; - - if (args != null) { - listenerArgs.addAll(args); + AST ast; + FilterMap filters; + + AST visit(Expression exp) { + exp.accept(this); + assert(this.ast != null); + try { + return ast; + } finally { + ast = null; } + } - do { - namedListeners = scope._listeners[name]; - if (namedListeners != null) { - event.currentScope = scope; - i = 0; - for (var length = namedListeners.length; i new CollectionAST(visit(exp)); + AST _mapToAst(Expression expression) => visit(expression); - return event; - } + List _toAst(List expressions) => expressions.map(_mapToAst).toList(); + visitCallScope(CallScope exp) => ast = new MethodAST(contextRef, exp.name, _toAst(exp.arguments)); + visitCallMember(CallMember exp) => ast = new MethodAST(visit(exp.object), exp.name, _toAst(exp.arguments)); - /** - * Triggers a scope event referenced by the [name] parameters dowards towards the leaf nodes of the - * scope tree. If intercepted, by a child scope containing a matching scope event listener - * (which is registered via the [$on] scope method), then the event listener callback function - * will be executed. - * - * * [name] - The scope event name that will be triggered. - * * [listenerArgs] - An optional list of arguments that will be fed into the listener callback function - * for any event listeners that are registered via [$on]. - */ - $broadcast(String name, [List listenerArgs]) { - var target = this, - current = target, - next = target, - event = new ScopeEvent(name, this); - - //down while you can, then up and next sibling or up and next sibling until back at root - if (listenerArgs == null) { - listenerArgs = []; - } - listenerArgs.insert(0, event); - do { - current = next; - event.currentScope = current; - if (current._listeners.containsKey(name)) { - current._listeners[name].forEach((listener) { - try { - relaxFnApply(listener, listenerArgs); - } catch(e, s) { - _exceptionHandler(e, s); - } - }); - } + visitAccessScope(AccessScope exp) => ast = new FieldReadAST(contextRef, exp.name); + visitAccessMember(AccessMember exp) => ast = new FieldReadAST(visit(exp.object), exp.name); + visitBinary(Binary exp) => ast = new PureFunctionAST(exp.operation, + _operationToFunction(exp.operation), + [visit(exp.left), visit(exp.right)]); + visitPrefix(Prefix exp) => ast = new PureFunctionAST(exp.operation, + _operationToFunction(exp.operation), + [visit(exp.expression)]); + visitConditional(Conditional exp) => ast = new PureFunctionAST('?:', _operation_ternary, + [visit(exp.condition), visit(exp.yes), visit(exp.no)]); + visitAccessKeyed(AccessKeyed exp) => ast = new PureFunctionAST('[]', _operation_bracket, + [visit(exp.object), visit(exp.key)]); - // Insanity Warning: scope depth-first traversal - // yes, this code is a bit crazy, but it works and we have tests to prove it! - // this piece should be kept in sync with the traversal in $broadcast - if (current._childHead == null) { - if (current == target) { - next = null; - } else { - next = current._nextSibling; - if (next == null) { - while(current != target && (next = current._nextSibling) == null) { - current = current.$parent; - } - } - } - } else { - next = current._childHead; - } - } while ((current = next) != null); + visitLiteralPrimitive(LiteralPrimitive exp) => ast = new ConstantAST(exp.value); + visitLiteralString(LiteralString exp) => ast = new ConstantAST(exp.value); - return event; + visitLiteralArray(LiteralArray exp) { + List items = _toAst(exp.elements); + ast = new PureFunctionAST('[${items.join(', ')}]', new ArrayFn(), items); } - _beginPhase(phase) { - if ($root._phase != null) { - // TODO(deboer): Remove the []s when dartbug.com/11999 is fixed. - throw ['${$root._phase} already in progress']; + visitLiteralObject(LiteralObject exp) { + List keys = exp.keys; + List values = _toAst(exp.values); + assert(keys.length == values.length); + List kv = []; + for(var i = 0; i < keys.length; i++) { + kv.add('${keys[i]}: ${values[i]}'); } - assert(_perf.startTimer('ng.phase.${phase}') != false); - - $root._phase = phase; + ast = new PureFunctionAST('{${kv.join(', ')}}', new MapFn(keys), values); } - _clearPhase() { - assert(_perf.stopTimer('ng.phase.${$root._phase}') != false); - $root._phase = null; + visitFilter(Filter exp) { + Function filterFunction = filters(exp.name); + List args = [visitCollection(exp.expression)]; + args.addAll(_toAst(exp.arguments).map((ast) => new CollectionAST(ast))); + ast = new PureFunctionAST('|${exp.name}', new _FilterWrapper(filterFunction, args.length), args); } - Function _compileToFn(exp) { - if (exp == null) { - return () => null; - } else if (exp is String) { - Expression expression = _parser(exp); - return expression.eval; - } else if (exp is Function) { - return exp; - } else { - throw 'Expecting String or Function'; - } + // TODO(misko): this is a corner case. Choosing not to implement for now. + visitCallFunction(CallFunction exp) => _notSupported("function's returing functions"); + visitAssign(Assign exp) => _notSupported('assignement'); + visitLiteral(Literal exp) => _notSupported('literal'); + visitExpression(Expression exp) => _notSupported('?'); + visitChain(Chain exp) => _notSupported(';'); + + _notSupported(String name) { + throw new StateError("Can not watch expression containing '$name'."); } } -@proxy -class ScopeLocals implements Scope, Map { - static wrapper(dynamic scope, Map locals) => new ScopeLocals(scope, locals); - - dynamic _scope; - Map _locals; - - ScopeLocals(this._scope, this._locals); - - operator []=(String name, value) => _scope[name] = value; - operator [](String name) => (_locals.containsKey(name) ? _locals : _scope)[name]; - - noSuchMethod(Invocation invocation) => mirror.reflect(_scope).delegate(invocation); +_operationToFunction(String operation) { + switch(operation) { + case '!' : return _operation_negate; + case '+' : return _operation_add; + case '-' : return _operation_subtract; + case '*' : return _operation_multiply; + case '/' : return _operation_divide; + case '~/' : return _operation_divide_int; + case '%' : return _operation_remainder; + case '==' : return _operation_equals; + case '!=' : return _operation_not_equals; + case '<' : return _operation_less_then; + case '>' : return _operation_greater_then; + case '<=' : return _operation_less_or_equals_then; + case '>=' : return _operation_greater_or_equals_then; + case '^' : return _operation_power; + case '&' : return _operation_bitwise_and; + case '&&' : return _operation_logical_and; + case '||' : return _operation_logical_or; + default: throw new StateError(operation); + } } -class _InitWatchVal { const _InitWatchVal(); } -const _initWatchVal = const _InitWatchVal(); - -class _Watch { - final Function fn; - final Function get; - final String exp; - var last; - - _Watch previous; - _Watch next; - - _Watch(fn, this.last, getFn, this.exp) - : this.fn = relaxFnArgs3(fn) - , this.get = relaxFnArgs2(getFn); +_operation_negate(value) => !toBool(value); +_operation_add(left, right) => autoConvertAdd(left, right); +_operation_subtract(left, right) => left - right; +_operation_multiply(left, right) => left * right; +_operation_divide(left, right) => left / right; +_operation_divide_int(left, right) => left ~/ right; +_operation_remainder(left, right) => left % right; +_operation_equals(left, right) => left == right; +_operation_not_equals(left, right) => left != right; +_operation_less_then(left, right) => left < right; +_operation_greater_then(left, right) => left > right; +_operation_less_or_equals_then(left, right) => left <= right; +_operation_greater_or_equals_then(left, right) => left >= right; +_operation_power(left, right) => left ^ right; +_operation_bitwise_and(left, right) => left & right; +// TODO(misko): these should short circuit the evaluation. +_operation_logical_and(left, right) => toBool(left) && toBool(right); +_operation_logical_or(left, right) => toBool(left) || toBool(right); + +_operation_ternary(condition, yes, no) => toBool(condition) ? yes : no; +_operation_bracket(obj, key) => obj == null ? null : obj[key]; + +class ArrayFn extends FunctionApply { + // TODO(misko): figure out why do we need to make a copy? + apply(List args) => new List.from(args); } -class _WatchList { - int length = 0; - _Watch head; - _Watch tail; +class MapFn extends FunctionApply { + final List keys; - void addLast(_Watch watch) { - assert(watch.previous == null); - assert(watch.next == null); - if (tail == null) { - tail = head = watch; - } else { - watch.previous = tail; - tail.next = watch; - tail = watch; - } - length++; - } + MapFn(this.keys); - void remove(_Watch watch) { - if (watch == head) { - _Watch next = watch.next; - if (next == null) tail = null; - else next.previous = null; - head = next; - } else if (watch == tail) { - _Watch previous = watch.previous; - previous.next = null; - tail = previous; - } else { - _Watch next = watch.next; - _Watch previous = watch.previous; - previous.next = next; - next.previous = previous; + apply(List values) { + // TODO(misko): figure out why do we need to make a copy instead of reusing instance? + Map map = {}; + assert(values.length == keys.length); + for(var i = 0; i < keys.length; i++) { + map[keys[i]] = values[i]; } - length--; + return map; } } -_toJson(obj) { - try { - return JSON.encode(obj); - } catch(e) { - var ret = "NOT-JSONABLE"; - // Keep prod fast. - assert(() { - var mirror = reflect(obj); - if (mirror is ClosureMirror) { - // work-around dartbug.com/14130 - try { - ret = mirror.function.source; - } on NoSuchMethodError catch (e) { - } on UnimplementedError catch (e) { - } - } - return true; - }); - return ret; - } -} - -String _source(obj) { - if (obj is Function) { - var m = reflect(obj); - if (m is ClosureMirror) { - // work-around dartbug.com/14130 - try { - return "FN: ${m.function.source}"; - } on NoSuchMethodError catch (e) { - } on UnimplementedError catch (e) { +class _FilterWrapper extends FunctionApply { + final Function filterFn; + final List args; + final List argsWatches; + _FilterWrapper(this.filterFn, length): + args = new List(length), + argsWatches = new List(length); + + apply(List values) { + for(var i=0; i < values.length; i++) { + var value = values[i]; + var lastValue = args[i]; + if (!identical(value, lastValue)) { + if (value is CollectionChangeRecord) { + args[i] = (value as CollectionChangeRecord).iterable; + } else { + args[i] = value; + } } } + var value = Function.apply(filterFn, args); + if (value is Iterable) { + // Since filters are pure we can guarantee that this well never change. + // By wrapping in UnmodifiableListView we can hint to the dirty checker and + // short circuit the iterator. + value = new UnmodifiableListView(value); + } + return value; } - return '$obj'; } diff --git a/lib/core/scope2.dart b/lib/core/scope2.dart deleted file mode 100644 index 8943f3b8b..000000000 --- a/lib/core/scope2.dart +++ /dev/null @@ -1,775 +0,0 @@ -part of angular.core; - -NOT_IMPLEMENTED() { - throw new StateError('Not Implemented'); -} - -typedef EvalFunction0(); -typedef EvalFunction1(dynamic context); - -/** - * Injected into the listener function within [Scope.on] to provide event-specific - * details to the scope listener. - */ -class ScopeEvent { - static final String DESTROY = 'ng-destroy'; - - final dynamic data; - - /** - * The name of the intercepted scope event. - */ - final String name; - - /** - * The origin scope that triggered the event (via $broadcast or $emit). - */ - final Scope targetScope; - - /** - * The destination scope that intercepted the event. - */ - Scope get currentScope => _currentScope; - Scope _currentScope; - - /** - * true or false depending on if stopPropagation() was executed. - */ - bool get propagationStopped => _propagationStopped; - bool _propagationStopped = false; - - /** - * true or false depending on if preventDefault() was executed. - */ - bool get defaultPrevented => _defaultPrevented; - bool _defaultPrevented = false; - - /** - ** [name] - The name of the scope event. - ** [targetScope] - The destination scope that is listening on the event. - */ - ScopeEvent(this.name, this.targetScope, this.data); - - /** - * Prevents the intercepted event from propagating further to successive scopes. - */ - stopPropagation () => _propagationStopped = true; - - /** - * Sets the defaultPrevented flag to true. - */ - preventDefault() => _defaultPrevented = true; -} - -/** - * Allows the configuration of [Scope.digest] iteration maximum time-to-live - * value. Digest keeps checking the state of the watcher getters until it - * can execute one full iteration with no watchers triggering. TTL is used - * to prevent an infinite loop where watch A triggers watch B which in turn - * triggers watch A. If the system does not stabilize in TTL iteration then - * an digest is stop an an exception is thrown. - */ -@NgInjectableService() -class ScopeDigestTTL { - final num ttl; - ScopeDigestTTL(): ttl = 5; - ScopeDigestTTL.value(num this.ttl); -} - -//TODO(misko): I don't think this should be in scope. -class ScopeLocals implements Map { - static wrapper(dynamic scope, Map locals) => new ScopeLocals(scope, locals); - - Map _scope; - Map _locals; - - ScopeLocals(this._scope, this._locals); - - operator []=(String name, value) => _scope[name] = value; - operator [](String name) => (_locals.containsKey(name) ? _locals : _scope)[name]; - - get isEmpty => _scope.isEmpty && _locals.isEmpty; - get isNotEmpty => _scope.isNotEmpty || _locals.isNotEmpty; - get keys => _scope.keys; - get values => _scope.values; - get length => _scope.length; - - forEach(fn) => _scope.forEach(fn); - remove(key) => _scope.remove(key); - clear() => _scope.clear; - containsKey(key) => _scope.containsKey(key); - containsValue(key) => _scope.containsValue(key); - addAll(map) => _scope.addAll(map); - putIfAbsent(key, fn) => _scope.putIfAbsent(key, fn); -} - -class Scope { - final dynamic context; - final RootScope rootScope; - Scope _parentScope; - Scope get parentScope => _parentScope; - - final WatchGroup watchGroup; - final WatchGroup observeGroup; - final int _depth; - final int _index; - - Scope _childHead, _childTail, _next, _prev; - _Streams _streams; - int _nextChildIndex = 0; - - Scope(Object this.context, this.rootScope, this._parentScope, - this._depth, this._index, - this.watchGroup, this.observeGroup); - - // TODO(misko): this is a hack and should be removed - // A better way to do this is to remove the praser from the scope. - Watch watchSet(List exprs, Function reactionFn) { - var expr = '{{${exprs.join('}}?{{')}}}'; - List items = exprs.map(rootScope._parse).toList(); - AST ast = new PureFunctionAST(expr, new ArrayFn(), items); - return watchGroup.watch(ast, reactionFn); - } - - Watch watch(expression, ReactionFn reactionFn) { - // Todo(misko): remove the parser from here. It should only take AST. - assert(expression != null); - AST ast = expression is AST ? expression : rootScope._parse(expression); - return watchGroup.watch(ast, reactionFn); - } - - Watch observe(expression, ReactionFn reactionFn) { - // Todo(misko): remove the parser from here. It should only take AST. - assert(expression != null); - AST ast = expression is AST ? expression : rootScope._parse(expression); - return observeGroup.watch(ast, reactionFn); - } - - dynamic eval(expression, [Map locals]) { - assert(expression == null || - expression is String || - expression is Function); - if (expression is String && expression.isNotEmpty) { - var obj = locals == null ? context : new ScopeLocals(context, locals); - return rootScope._parser(expression).eval(obj); - } else if (expression is EvalFunction1) { - assert(locals == null); - return expression(context); - } else if (expression is EvalFunction0) { - assert(locals == null); - return expression(); - } - } - - dynamic applyInZone([expression, Map locals]) - => rootScope._zone.run(() => apply(expression, locals)); - - dynamic apply([expression, Map locals]) { - rootScope._transitionState(null, RootScope.STATE_APPLY); - try { - return eval(expression, locals); - } catch (e, s) { - rootScope._exceptionHandler(e, s); - } finally { - rootScope._transitionState(RootScope.STATE_APPLY, null); - rootScope.digest(); - rootScope.flush(); - } - } - - - ScopeEvent emit(String name, [data]) => _Streams.emit(this, name, data); - ScopeEvent broadcast(String name, [data]) => _Streams.broadcast(this, name, data); - ScopeStream on(String name) => _Streams.on(this, rootScope._exceptionHandler, name); - - Scope createChild([Object childContext]) { - if (childContext == null) childContext = context; - Scope child = new Scope(childContext, rootScope, this, - _depth + 1, _nextChildIndex++, - watchGroup.newGroup(childContext), - observeGroup.newGroup(childContext)); - var next = null; - var prev = _childTail; - child._next = next; - child._prev = prev; - if (prev == null) _childHead = child; else prev._next = child; - if (next == null) _childTail = child; else next._prev = child; - return child; - } - - void destroy() { - var prev = this._prev; - var next = this._next; - if (prev == null) _parentScope._childHead = next; else prev._next = next; - if (next == null) _parentScope._childTail = prev; else next._prev = prev; - - this._next = this._prev = null; - - watchGroup.remove(); - observeGroup.remove(); - _Streams.destroy(this); - - _parentScope = null; - broadcast(ScopeEvent.DESTROY); - } -} - - -class RootScope extends Scope { - static final STATE_APPLY = 'apply'; - static final STATE_DIGEST = 'digest'; - static final STATE_FLUSH = 'digest'; - - final ExceptionHandler _exceptionHandler; - final Parser _parser; - final ScopeDigestTTL _ttl; - final ExpressionVisitor visitor = new ExpressionVisitor(); // TODO(misko): delete me - final NgZone _zone; - - _FunctionChain _runAsyncHead, _runAsyncTail; - _FunctionChain _domWriteHead, _domWriteTail; - _FunctionChain _domReadHead, _domReadTail; - - String _state; - - RootScope(Object context, Parser this._parser, GetterCache cacheGetter, - FilterMap filterMap, ExceptionHandler this._exceptionHandler, - ScopeDigestTTL this._ttl, this._zone) - : super(context, null, null, 0, 0, - new RootWatchGroup(new DirtyCheckingChangeDetector(cacheGetter), context), - new RootWatchGroup(new DirtyCheckingChangeDetector(cacheGetter), context)) - { - _zone.onTurnDone = () { - digest(); - flush(); - }; - } - - RootScope get rootScope => this; - - void digest() { - _transitionState(null, STATE_DIGEST); - try { - RootWatchGroup rootWatchGroup = (watchGroup as RootWatchGroup); - - int digestTTL = _ttl.ttl; - const int logCount = 3; - List log; - List digestLog; - var count; - ChangeLog changeLog; - do { - while(_runAsyncHead != null) { - try { _runAsyncHead.fn(); } - catch (e, s) { _exceptionHandler(e, s); } - _runAsyncHead = _runAsyncHead._next; - } - - digestTTL--; - count = rootWatchGroup.detectChanges( - exceptionHandler: _exceptionHandler, - changeLog: changeLog); - - if (digestTTL <= logCount) { - if (changeLog == null) { - log = []; - digestLog = []; - changeLog = (value) => digestLog.add(value); - } else { - log.add(digestLog.join(', ')); - digestLog.clear(); - } - } - if (digestTTL == 0) { - throw 'Model did not stabilize in ${_ttl.ttl} digests. ' + - 'Last $logCount iterations:\n${log.join('\n')}'; - } - } while (count > 0); - } finally { - _transitionState(STATE_DIGEST, null); - } - } - - void flush() { - _transitionState(null, STATE_FLUSH); - RootWatchGroup observeGroup = this.observeGroup as RootWatchGroup; - bool runObservers = true; - try { - do { - while(_domWriteHead != null) { - try { _domWriteHead.fn(); } - catch (e, s) { _exceptionHandler(e, s); } - _domWriteHead = _domWriteHead._next; - } - if (runObservers) { - runObservers = false; - observeGroup.detectChanges(exceptionHandler:_exceptionHandler); - } - while(_domReadHead != null) { - try { _domReadHead.fn(); } - catch (e, s) { _exceptionHandler(e, s); } - _domReadHead = _domReadHead._next; - } - } while (_domWriteHead != null || _domReadHead != null); - assert((() { - var watchLog = []; - var observeLog = []; - (watchGroup as RootWatchGroup).detectChanges(changeLog: watchLog.add); - (observeGroup as RootWatchGroup).detectChanges(changeLog: observeLog.add); - if (watchLog.isNotEmpty || observeLog.isNotEmpty) { - throw 'Observer reaction functions should not change model. \n' - 'These watch changes were detected: ${watchLog.join('; ')}\n' - 'These observe changes were detected: ${observeLog.join('; ')}'; - } - return true; - })()); - } finally { - _transitionState(STATE_FLUSH, null); - } - - } - - // QUEUES - void runAsync(Function fn) { - var chain = new _FunctionChain(fn); - if (_runAsyncHead == null) { - _runAsyncHead = _runAsyncTail = chain; - } else { - _runAsyncTail = _runAsyncTail._next = chain; - } - } - - void domWrite(Function fn) { - var chain = new _FunctionChain(fn); - if (_domWriteHead == null) { - _domWriteHead = _domWriteTail = chain; - } else { - _domWriteTail = _domWriteTail._next = chain; - } - } - - void domRead(Function fn) { - var chain = new _FunctionChain(fn); - if (_domReadHead == null) { - _domReadHead = _domReadTail = chain; - } else { - _domReadTail = _domReadTail._next = chain; - } - } - - - AST _parse(expression) => visitor.visit(_parser.call(expression)); - void destroy() {} - - void _transitionState(String from, String to) { - if (_state != from) { - throw "$_state already in progress can not enter $to."; - } - _state = to; - } -} - -/** - * Keeps track of Streams for each Scope. When emitting events - * we would need to walk the whole tree. Its faster if we can prune - * the Scopes we have to visit. - * - * Scope with no [_ScopeStreams] has no events registered on itself or children - * - * We keep track of [Stream]s, and also child scope [Stream]s. To save - * memory we use the same stream object on all of our parents if they don't - * have one. But that means that we have to keep track if the stream belongs - * to the node. - * - * Scope with [_ScopeStreams] but who's [_scope] dose not match the scope - * is only inherited - * - * Only [Scope] with [_ScopeStreams] who's [_scope] matches the [Scope] - * instance is the actual scope. - * - * Once the [Stream] is created it can not be removed even if all listeners - * are canceled. That is because we don't know if someone still has reference - * to it. - */ -class _Streams { - final ExceptionHandler _exceptionHandler; - /// Scope we belong to. - final Scope _scope; - /// [Stream]s for [_scope] only - final Map _streams = new Map(); - /// Child [Scope] event counts. - final Map _typeCounts; - - _Streams(this._scope, this._exceptionHandler, _Streams inheritStreams) - : _typeCounts = inheritStreams == null - ? new Map() - : new Map.from(inheritStreams._typeCounts); - - static ScopeEvent emit(Scope scope, String name, data) { - ScopeEvent event = new ScopeEvent(name, scope, data); - Scope scopeCursor = scope; - while(scopeCursor != null) { - if (scopeCursor._streams._scope == scopeCursor) { - ScopeStream stream = scopeCursor._streams._streams[name]; - if (stream != null) { - event._currentScope = scopeCursor; - stream._fire(event); - if (event.propagationStopped) return event; - } - } - scopeCursor = scopeCursor._parentScope; - } - return event; - } - - static ScopeEvent broadcast(Scope scope, String name, data) { - _Streams scopeStreams = scope._streams; - ScopeEvent event = new ScopeEvent(name, scope, data); - if (scopeStreams != null && scopeStreams._typeCounts.containsKey(name)) { - Queue queue = new Queue(); - queue.addFirst(scopeStreams._scope); - while(queue.isNotEmpty) { - scope = queue.removeFirst(); - scopeStreams = scope._streams; - assert(scopeStreams._scope == scope); - assert(scopeStreams._streams.containsKey(name)); - var stream = scopeStreams._streams[name]; - event._currentScope = scope; - stream._fire(event); - // Reverse traversal so that when the queue is read it is correct order. - var childScope = scope._childTail; - while(childScope != null) { - scopeStreams = childScope._streams; - if (scopeStreams != null) { - queue.addFirst(scopeStreams._scope); - } - childScope = childScope._prev; - } - } - } - return event; - } - - static ScopeStream on(Scope scope, ExceptionHandler _exceptionHandler, String name) { - var scopeStream = scope._streams; - if (scopeStream == null || scopeStream._scope != scope) { - // We either don't have [_ScopeStreams] or it is inherited. - var newStreams = new _Streams(scope, _exceptionHandler, scopeStream); - var scopeCursor = scope; - while (scopeCursor != null && scopeCursor._streams == scopeStream) { - scopeCursor._streams = newStreams; - scopeCursor = scopeCursor._parentScope; - } - scopeStream = newStreams; - } - return scopeStream._get(scope, name); - } - - static void destroy(Scope scope) { - var toBeDeletedStreams = scope._streams; - if (toBeDeletedStreams == null) return; - scope = scope._parentScope; // skip current state as not to delete listeners - while (scope != null && - scope._streams == toBeDeletedStreams) { - scope._streams = null; - scope = scope._parentScope; - } - if (scope == null) return; - var parentStreams = scope._streams; - assert(parentStreams != toBeDeletedStreams); - toBeDeletedStreams._typeCounts.forEach( - (name, count) => parentStreams._addCount(name, -count)); - } - - async.Stream _get(Scope scope, String name) { - assert(scope._streams == this); - assert(scope._streams._scope == scope); - assert(_exceptionHandler != null); - return _streams.putIfAbsent(name, () => new ScopeStream(this, _exceptionHandler, name)); - } - - void _addCount(String name, int amount) { - // decrement the counters on all parent scopes - _Streams lastStreams = null; - Scope scope = _scope; - while (scope != null) { - if (lastStreams != scope._streams) { - // we have a transition, need to decrement it - lastStreams = scope._streams; - int count = lastStreams._typeCounts[name]; - count = count == null ? amount : count + amount; - assert(count >= 0); - if (count == 0) { - lastStreams._typeCounts.remove(name); - } else { - lastStreams._typeCounts[name] = count; - } - } - scope = scope._parentScope; - } - } -} - -class ScopeStream extends async.Stream { - final ExceptionHandler _exceptionHandler; - final _Streams _streams; - final String _name; - final List subscriptions = []; - - ScopeStream(this._streams, this._exceptionHandler, this._name); - - ScopeStreamSubscription listen(void onData(ScopeEvent event), - { Function onError, - void onDone(), - bool cancelOnError }) { - if (subscriptions.isEmpty) { - _streams._addCount(_name, 1); - } - ScopeStreamSubscription subscription = new ScopeStreamSubscription(this, onData); - subscriptions.add(subscription); - return subscription; - } - - _fire(ScopeEvent event) { - for(ScopeStreamSubscription subscription in subscriptions) { - try { - subscription._onData(event); - } catch (e, s) { - _exceptionHandler(e, s); - } - } - } - - _remove(ScopeStreamSubscription subscription) { - assert(subscription._scopeStream == this); - if (subscriptions.remove(subscription)) { - if (subscriptions.isEmpty) { - _streams._addCount(_name, -1); - } - } else { - throw new StateError('AlreadyCanceled'); - } - return null; - } -} - -class ScopeStreamSubscription implements async.StreamSubscription { - final ScopeStream _scopeStream; - final Function _onData; - ScopeStreamSubscription(this._scopeStream, this._onData); - - async.Future cancel() => _scopeStream._remove(this); - - void onData(void handleData(ScopeEvent data)) => NOT_IMPLEMENTED(); - void onError(Function handleError) => NOT_IMPLEMENTED(); - void onDone(void handleDone()) => NOT_IMPLEMENTED(); - void pause([async.Future resumeSignal]) => NOT_IMPLEMENTED(); - void resume() => NOT_IMPLEMENTED(); - bool get isPaused => NOT_IMPLEMENTED(); - async.Future asFuture([var futureValue]) => NOT_IMPLEMENTED(); -} - -class _FunctionChain { - final Function fn; - _FunctionChain _next; - - _FunctionChain(this.fn); -} - -class AstParser { - final Parser _parser; - int _id = 0; - ExpressionVisitor _visitor = new ExpressionVisitor(); - - AstParser(this._parser); - - AST call(String exp, { FilterMap filters, - bool collection:false, - Object context:null }) { - _visitor.filters = filters; - AST contextRef = _visitor.contextRef; - try { - if (context != null) { - _visitor.contextRef = new ConstantAST(context, '#${_id++}'); - } - var ast = _parser(exp); - return collection ? _visitor.visitCollection(ast) : _visitor.visit(ast); - } finally { - _visitor.contextRef = contextRef; - _visitor.filters = null; - } - } -} - -class ExpressionVisitor implements Visitor { - static final ContextReferenceAST scopeContextRef = new ContextReferenceAST(); - AST contextRef = scopeContextRef; - - AST ast; - FilterMap filters; - - AST visit(Expression exp) { - exp.accept(this); - assert(this.ast != null); - try { - return ast; - } finally { - ast = null; - } - } - - AST visitCollection(Expression exp) => new CollectionAST(visit(exp)); - AST _mapToAst(Expression expression) => visit(expression); - - List _toAst(List expressions) => expressions.map(_mapToAst).toList(); - - visitCallScope(CallScope exp) => ast = new MethodAST(contextRef, exp.name, _toAst(exp.arguments)); - visitCallMember(CallMember exp) => ast = new MethodAST(visit(exp.object), exp.name, _toAst(exp.arguments)); - - visitAccessScope(AccessScope exp) => ast = new FieldReadAST(contextRef, exp.name); - visitAccessMember(AccessMember exp) => ast = new FieldReadAST(visit(exp.object), exp.name); - visitBinary(Binary exp) => ast = new PureFunctionAST(exp.operation, - _operationToFunction(exp.operation), - [visit(exp.left), visit(exp.right)]); - visitPrefix(Prefix exp) => ast = new PureFunctionAST(exp.operation, - _operationToFunction(exp.operation), - [visit(exp.expression)]); - visitConditional(Conditional exp) => ast = new PureFunctionAST('?:', _operation_ternary, - [visit(exp.condition), visit(exp.yes), visit(exp.no)]); - visitAccessKeyed(AccessKeyed exp) => ast = new PureFunctionAST('[]', _operation_bracket, - [visit(exp.object), visit(exp.key)]); - - visitLiteralPrimitive(LiteralPrimitive exp) => ast = new ConstantAST(exp.value); - visitLiteralString(LiteralString exp) => ast = new ConstantAST(exp.value); - - visitLiteralArray(LiteralArray exp) { - List items = _toAst(exp.elements); - ast = new PureFunctionAST('[${items.join(', ')}]', new ArrayFn(), items); - } - - visitLiteralObject(LiteralObject exp) { - List keys = exp.keys; - List values = _toAst(exp.values); - assert(keys.length == values.length); - List kv = []; - for(var i = 0; i < keys.length; i++) { - kv.add('${keys[i]}: ${values[i]}'); - } - ast = new PureFunctionAST('{${kv.join(', ')}}', new MapFn(keys), values); - } - - visitFilter(Filter exp) { - Function filterFunction = filters(exp.name); - List args = [visitCollection(exp.expression)]; - args.addAll(_toAst(exp.arguments).map((ast) => new CollectionAST(ast))); - ast = new PureFunctionAST('|${exp.name}', new _FilterWrapper(filterFunction, args.length), args); - } - - // TODO(misko): this is a corner case. Choosing not to implement for now. - visitCallFunction(CallFunction exp) => _notSupported("function's returing functions"); - visitAssign(Assign exp) => _notSupported('assignement'); - visitLiteral(Literal exp) => _notSupported('literal'); - visitExpression(Expression exp) => _notSupported('?'); - visitChain(Chain exp) => _notSupported(';'); - - _notSupported(String name) { - throw new StateError("Can not watch expression containing '$name'."); - } -} - -_operationToFunction(String operation) { - switch(operation) { - case '!' : return _operation_negate; - case '+' : return _operation_add; - case '-' : return _operation_subtract; - case '*' : return _operation_multiply; - case '/' : return _operation_divide; - case '~/' : return _operation_divide_int; - case '%' : return _operation_remainder; - case '==' : return _operation_equals; - case '!=' : return _operation_not_equals; - case '<' : return _operation_less_then; - case '>' : return _operation_greater_then; - case '<=' : return _operation_less_or_equals_then; - case '>=' : return _operation_greater_or_equals_then; - case '^' : return _operation_power; - case '&' : return _operation_bitwise_and; - case '&&' : return _operation_logical_and; - case '||' : return _operation_logical_or; - default: throw new StateError(operation); - } -} - -_operation_negate(value) => !toBool(value); -_operation_add(left, right) => autoConvertAdd(left, right); -_operation_subtract(left, right) => left - right; -_operation_multiply(left, right) => left * right; -_operation_divide(left, right) => left / right; -_operation_divide_int(left, right) => left ~/ right; -_operation_remainder(left, right) => left % right; -_operation_equals(left, right) => left == right; -_operation_not_equals(left, right) => left != right; -_operation_less_then(left, right) => left < right; -_operation_greater_then(left, right) => left > right; -_operation_less_or_equals_then(left, right) => left <= right; -_operation_greater_or_equals_then(left, right) => left >= right; -_operation_power(left, right) => left ^ right; -_operation_bitwise_and(left, right) => left & right; -// TODO(misko): these should short circuit the evaluation. -_operation_logical_and(left, right) => toBool(left) && toBool(right); -_operation_logical_or(left, right) => toBool(left) || toBool(right); - -_operation_ternary(condition, yes, no) => toBool(condition) ? yes : no; -_operation_bracket(obj, key) => obj == null ? null : obj[key]; - -class ArrayFn extends FunctionApply { - // TODO(misko): figure out why do we need to make a copy? - apply(List args) => new List.from(args); -} - -class MapFn extends FunctionApply { - final List keys; - - MapFn(this.keys); - - apply(List values) { - // TODO(misko): figure out why do we need to make a copy instead of reusing instance? - Map map = {}; - assert(values.length == keys.length); - for(var i = 0; i < keys.length; i++) { - map[keys[i]] = values[i]; - } - return map; - } -} - -class _FilterWrapper extends FunctionApply { - final Function filterFn; - final List args; - final List argsWatches; - _FilterWrapper(this.filterFn, length): - args = new List(length), - argsWatches = new List(length); - - apply(List values) { - for(var i=0; i < values.length; i++) { - var value = values[i]; - var lastValue = args[i]; - if (!identical(value, lastValue)) { - if (value is CollectionChangeRecord) { - args[i] = (value as CollectionChangeRecord).iterable; - } else { - args[i] = value; - } - } - } - var value = Function.apply(filterFn, args); - if (value is Iterable) { - // Since filters are pure we can guarantee that this well never change. - // By wrapping in UnmodifiableListView we can hint to the dirty checker and - // short circuit the iterator. - value = new UnmodifiableListView(value); - } - return value; - } -} diff --git a/lib/core/zone.dart b/lib/core/zone.dart index 346724b3d..3c443beaf 100644 --- a/lib/core/zone.dart +++ b/lib/core/zone.dart @@ -82,7 +82,7 @@ class NgZone { _inFinishTurn = true; try { // Two loops here: the inner one runs all queued microtasks, - // the outer runs onTurnDone (e.g. scope.$digest) and then + // the outer runs onTurnDone (e.g. scope.digest) and then // any microtasks which may have been queued from onTurnDone. do { while (!_asyncQueue.isEmpty) { diff --git a/lib/core_dom/block_factory.dart b/lib/core_dom/block_factory.dart index 957bd880f..45f06e762 100644 --- a/lib/core_dom/block_factory.dart +++ b/lib/core_dom/block_factory.dart @@ -120,7 +120,7 @@ class BlockFactory { NgAnnotation annotation = ref.annotation; var visibility = _elementOnly; if (ref.annotation is NgController) { - scope = scope.$new(); + scope = scope.createChild({}); nodeModule.value(Scope, scope); } if (ref.annotation.visibility == NgDirective.CHILDREN_VISIBILITY) { @@ -132,7 +132,8 @@ class BlockFactory { nodeModule.factory(NgTextMustacheDirective, (Injector injector) { return new NgTextMustacheDirective( node, ref.value, injector.get(Interpolate), injector.get(Scope), - injector.get(TextChangeListener)); + injector.get(TextChangeListener), injector.get(AstParser), + injector.get(FilterMap)); }); } else if (ref.type == NgAttrMustacheDirective) { if (nodesAttrsDirectives == null) { @@ -141,7 +142,8 @@ class BlockFactory { var scope = injector.get(Scope); var interpolate = injector.get(Interpolate); for(var ref in nodesAttrsDirectives) { - new NgAttrMustacheDirective(nodeAttrs, ref.value, interpolate, scope); + new NgAttrMustacheDirective(nodeAttrs, ref.value, interpolate, + scope, injector.get(AstParser), injector.get(FilterMap)); } }); } @@ -194,23 +196,25 @@ class BlockFactory { assert((linkMapTimer = _perf.startTimer('ng.block.link.map', ref.type)) != false); var shadowScope = (fctrs != null && fctrs.containsKey(ref.type)) ? fctrs[ref.type].shadowScope : null; if (ref.annotation is NgController) { - scope[(ref.annotation as NgController).publishAs] = controller; + scope.context[(ref.annotation as NgController).publishAs] = controller; } else if (ref.annotation is NgComponent) { - shadowScope[(ref.annotation as NgComponent).publishAs] = controller; + shadowScope.context[(ref.annotation as NgComponent).publishAs] = controller; } if (nodeAttrs == null) nodeAttrs = new _AnchorAttrs(ref); for(var map in ref.mappings) { map(nodeAttrs, scope, controller, filters); } if (controller is NgAttachAware) { - var removeWatcher; - removeWatcher = scope.$watch(() { - removeWatcher(); - controller.attach(); - }); + Watch watch; + watch = scope.watch( + '1', // Cheat a bit. + (_, __) { + watch.remove(); + controller.attach(); + }); } if (controller is NgDetachAware) { - scope.$on(r'$destroy', controller.detach); + scope.on(ScopeEvent.DESTROY).listen((_) => controller.detach()); } assert(_perf.stopTimer(linkMapTimer) != false); } finally { @@ -303,7 +307,7 @@ class _ComponentFactory { shadowDom.applyAuthorStyles = component.applyAuthorStyles; shadowDom.resetStyleInheritance = component.resetStyleInheritance; - shadowScope = scope.$new(isolate: true); + shadowScope = scope.createChild({}); // Isolate // TODO(pavelgj): fetching CSS with Http is mainly an attempt to // work around an unfiled Chrome bug when reloading same CSS breaks // styles all over the page. We shouldn't be doing browsers work, diff --git a/lib/core_dom/common.dart b/lib/core_dom/common.dart index 013a6ddd1..df7e56f2c 100644 --- a/lib/core_dom/common.dart +++ b/lib/core_dom/common.dart @@ -29,8 +29,7 @@ class DirectiveRef { */ Injector forceNewDirectivesAndFilters(Injector injector, List modules) { modules.add(new Module() - ..factory(Scope, - (i) => i.parent.get(Scope).$new(filters: i.get(FilterMap)))); + ..factory(Scope, (i) => i.parent.get(Scope).createChild())); return injector.createChild(modules, forceNewInstances: [DirectiveMap, FilterMap]); } diff --git a/lib/core_dom/compiler.dart b/lib/core_dom/compiler.dart index 7e597ce4e..9d6835400 100644 --- a/lib/core_dom/compiler.dart +++ b/lib/core_dom/compiler.dart @@ -4,9 +4,10 @@ part of angular.core.dom; class Compiler { final Profiler _perf; final Parser _parser; + final AstParser _astParser; final Expando _expando; - Compiler(this._perf, this._parser, this._expando); + Compiler(this._perf, this._parser, this._astParser, this._expando); _compileBlock(NodeCursor domCursor, NodeCursor templateCursor, List useExistingDirectiveRefs, @@ -139,67 +140,75 @@ class Compiler { var mode = match[1]; var dstPath = match[2]; - Expression dstPathFn = _parser(dstPath.isEmpty ? attrName : dstPath); + String dstExpression = dstPath.isEmpty ? attrName : dstPath; + Expression dstPathFn = _parser(dstExpression); if (!dstPathFn.isAssignable) { throw "Expression '$dstPath' is not assignable in mapping '$mapping' for attribute '$attrName'."; } ApplyMapping mappingFn; switch (mode) { case '@': - mappingFn = (NodeAttrs attrs, Scope scope, Object dst, FilterMap filters) { - attrs.observe(attrName, (value) => dstPathFn.assign(dst, value)); + mappingFn = (NodeAttrs attrs, Scope scope, Object controller, FilterMap filters) { + attrs.observe(attrName, (value) => dstPathFn.assign(controller, value)); }; break; case '<=>': - mappingFn = (NodeAttrs attrs, Scope scope, Object dst, FilterMap filters) { + mappingFn = (NodeAttrs attrs, Scope scope, Object controller, FilterMap filters) { if (attrs[attrName] == null) return; - Expression attrExprFn = _parser(attrs[attrName]); - var shadowValue = null; - scope.$watch( - () => attrExprFn.eval(scope, filters), - (v) => dstPathFn.assign(dst, shadowValue = v), - attrs[attrName]); - if (attrExprFn.isAssignable) { - scope.$watch( - () => dstPathFn.eval(dst), - (v) { - if (shadowValue != v) { - shadowValue = v; - attrExprFn.assign(scope, v); + String expression = attrs[attrName]; + Expression expressionFn = _parser(expression); + var blockOutbound = false; + var blockInbound = false; + scope.watch( + _astParser(expression, filters: filters), + (inboundValue, _) { + if (!blockInbound) { + blockOutbound = true; + scope.rootScope.runAsync(() => blockOutbound = false); + return dstPathFn.assign(controller, inboundValue); + } + } + ); + if (expressionFn.isAssignable) { + scope.watch( + _astParser(dstExpression, context: controller, filters: filters), + (outboundValue, _) { + if(!blockOutbound) { + blockInbound = true; + scope.rootScope.runAsync(() => blockInbound = false); + expressionFn.assign(scope.context, outboundValue); } - }, - dstPath); + } + ); } }; break; case '=>': - mappingFn = (NodeAttrs attrs, Scope scope, Object dst, FilterMap filters) { + mappingFn = (NodeAttrs attrs, Scope scope, Object controller, FilterMap filters) { if (attrs[attrName] == null) return; Expression attrExprFn = _parser(attrs[attrName]); - scope.$watch( - () => attrExprFn.eval(scope, filters), - (v) => dstPathFn.assign(dst, v), - attrs[attrName]); + var shadowValue = null; + scope.watch(_astParser(attrs[attrName], filters: filters), + (v, _) => dstPathFn.assign(controller, shadowValue = v)); }; break; case '=>!': - mappingFn = (NodeAttrs attrs, Scope scope, Object dst, FilterMap filters) { + mappingFn = (NodeAttrs attrs, Scope scope, Object controller, FilterMap filters) { if (attrs[attrName] == null) return; Expression attrExprFn = _parser(attrs[attrName]); - var stopWatching; - stopWatching = scope.$watch( - () => attrExprFn.eval(scope, filters), - (value) { - if (dstPathFn.assign(dst, value) != null) { - stopWatching(); + var watch; + watch = scope.watch( + _astParser(attrs[attrName], filters: filters), + (value, _) { + if (dstPathFn.assign(controller, value) != null) { + watch.remove(); } - }, - attrs[attrName]); + }); }; break; case '&': mappingFn = (NodeAttrs attrs, Scope scope, Object dst, FilterMap filters) { - dstPathFn.assign(dst, _parser(attrs[attrName]).bind(scope, ScopeLocals.wrapper)); + dstPathFn.assign(dst, _parser(attrs[attrName]).bind(scope.context, ScopeLocals.wrapper)); }; break; } diff --git a/lib/core_dom/ng_mustache.dart b/lib/core_dom/ng_mustache.dart index 1dabaf917..315e0bcda 100644 --- a/lib/core_dom/ng_mustache.dart +++ b/lib/core_dom/ng_mustache.dart @@ -7,14 +7,20 @@ class NgTextMustacheDirective { String markup, Interpolate interpolate, Scope scope, - TextChangeListener listener) { + TextChangeListener listener, + AstParser parser, + FilterMap filters) { Interpolation interpolation = interpolate(markup); interpolation.setter = (text) { element.text = text; if (listener != null) listener.call(text); }; - interpolation.setter(''); - scope.$watchSet(interpolation.watchExpressions, interpolation.call, markup.trim()); + + List items = interpolation.expressions.map((exp) { + return parser(exp, filters:filters); + }).toList(); + AST ast = new PureFunctionAST(markup, new ArrayFn(), items); + scope.observe(ast, interpolation.call); } } @@ -22,14 +28,43 @@ class NgTextMustacheDirective { @NgDirective(selector: r'[*=/{{.*}}/]') class NgAttrMustacheDirective { // This Directive is special and does not go through injection. - NgAttrMustacheDirective(NodeAttrs attrs, String markup, Interpolate interpolate, Scope scope) { + NgAttrMustacheDirective(NodeAttrs attrs, + String markup, + Interpolate interpolate, + Scope scope, + AstParser parser, + FilterMap filters) { var eqPos = markup.indexOf('='); var attrName = markup.substring(0, eqPos); var attrValue = markup.substring(eqPos + 1); Interpolation interpolation = interpolate(attrValue); - interpolation.setter = (text) => attrs[attrName] = text; + var lastValue = markup; + interpolation.setter = (text) { + if (lastValue != text) { + lastValue = attrs[attrName] = text; + } + }; + // TODO(misko): figure out how to remove call to setter. It slows down + // Block instantiation interpolation.setter(''); - scope.$watchSet(interpolation.watchExpressions, interpolation.call, markup.trim()); + + List items = interpolation.expressions.map((exp) { + return parser(exp, filters:filters); + }).toList(); + AST ast = new PureFunctionAST(markup, new ArrayFn(), items); + /* + Attribute bindings are tricky. They need to be resolved on digest + inline with components so that any bindings to component can + be resolved before the component attach method. But once the + component is attached we need to run on the flush cycle rather + then digest cycle. + */ + Watch watch; + watch = scope.watch(ast, (value, _) { + watch.remove(); + interpolation.call(value); + scope.observe(ast, interpolation.call); + }); } } diff --git a/lib/directive/input_select.dart b/lib/directive/input_select.dart index fffbb52de..2ef2be4ba 100644 --- a/lib/directive/input_select.dart +++ b/lib/directive/input_select.dart @@ -61,19 +61,31 @@ class InputSelectDirective implements NgAttachAware { }); _selectElement.onChange.listen((event) => _mode.onViewChange(event)); - _model.render = (value) => _mode.onModelChange(value); + _model.render = (value) { + // TODO(misko): this hack need to delay the rendering until after domRead + // becouse the modelChange reads from the DOM. We should be able to render + // without DOM changes. + _scope.rootScope.domRead(() { + _scope.rootScope.domWrite(() => _mode.onModelChange(value)); + }); + }; } /** * This method invalidates the current state of the selector and forces a - * re-rendering of the options using the [Scope.$evalAsync]. + * re-rendering of the options using the [Scope.evalAsync]. */ dirty() { if (!_dirty) { _dirty = true; - _scope.$evalAsync(() { - _dirty = false; - _mode.onModelChange(_model.viewValue); + // TODO(misko): this hack need to delay the rendering until after domRead + // becouse the modelChange reads from the DOM. We should be able to render + // without DOM changes. + _scope.rootScope.domRead(() { + _scope.rootScope.domWrite(() { + _dirty = false; + _mode.onModelChange(_model.viewValue); + }); }); } } @@ -200,8 +212,8 @@ class _SingleSelectMode extends _SelectMode { class _MultipleSelectionMode extends _SelectMode { _MultipleSelectionMode(Expando expando, dom.SelectElement select, - NgModel model - ): super(expando, select, model); + NgModel model) + : super(expando, select, model); onViewChange(event) { var selected = []; diff --git a/lib/directive/module.dart b/lib/directive/module.dart index a84b47756..6bd55d707 100644 --- a/lib/directive/module.dart +++ b/lib/directive/module.dart @@ -7,6 +7,8 @@ import 'package:angular/core/module.dart'; import 'package:angular/core/parser/parser.dart'; import 'package:angular/core_dom/module.dart'; import 'package:angular/utils.dart'; +import 'package:angular/change_detection/watch_group.dart'; +import 'package:angular/change_detection/change_detection.dart'; part 'ng_a.dart'; part 'ng_bind.dart'; diff --git a/lib/directive/ng_a.dart b/lib/directive/ng_a.dart index ee4ea8458..91b7b5590 100644 --- a/lib/directive/ng_a.dart +++ b/lib/directive/ng_a.dart @@ -7,7 +7,7 @@ part of angular.directive; * * @description * Modifies the default behavior of the html A tag so that the default action is prevented when - * the href attribute is empty. + * the a href is empty or it contains `ng-click` directive. * * This change permits the easy creation of action links with the `ngClick` directive * without changing the location or causing page reloads, e.g.: @@ -18,7 +18,8 @@ class NgADirective { final dom.Element element; NgADirective(this.element) { - if (element.attributes["href"] == "") { + if (element.attributes["href"] == "" || + element.attributes.containsKey('ng-click')) { element.onClick.listen((event) { if (element.attributes["href"] == "") { event.preventDefault(); diff --git a/lib/directive/ng_class.dart b/lib/directive/ng_class.dart index 0aed9a7fc..74c216182 100644 --- a/lib/directive/ng_class.dart +++ b/lib/directive/ng_class.dart @@ -67,8 +67,8 @@ part of angular.directive; map: const {'ng-class': '@valueExpression'}, exportExpressionAttrs: const ['ng-class']) class NgClassDirective extends _NgClassBase { - NgClassDirective(dom.Element element, Scope scope, NodeAttrs attrs) - : super(element, scope, null, attrs); + NgClassDirective(dom.Element element, Scope scope, NodeAttrs attrs, AstParser parser) + : super(element, scope, null, attrs, parser); } /** @@ -102,8 +102,8 @@ class NgClassDirective extends _NgClassBase { map: const {'ng-class-odd': '@valueExpression'}, exportExpressionAttrs: const ['ng-class-odd']) class NgClassOddDirective extends _NgClassBase { - NgClassOddDirective(dom.Element element, Scope scope, NodeAttrs attrs) - : super(element, scope, 0, attrs); + NgClassOddDirective(dom.Element element, Scope scope, NodeAttrs attrs, AstParser parser) + : super(element, scope, 0, attrs, parser); } /** @@ -137,8 +137,8 @@ class NgClassOddDirective extends _NgClassBase { map: const {'ng-class-even': '@valueExpression'}, exportExpressionAttrs: const ['ng-class-even']) class NgClassEvenDirective extends _NgClassBase { - NgClassEvenDirective(dom.Element element, Scope scope, NodeAttrs attrs) - : super(element, scope, 1, attrs); + NgClassEvenDirective(dom.Element element, Scope scope, NodeAttrs attrs, AstParser parser) + : super(element, scope, 1, attrs, parser); } abstract class _NgClassBase { @@ -146,16 +146,17 @@ abstract class _NgClassBase { final Scope scope; final int mode; final NodeAttrs nodeAttrs; + final AstParser _parser; var previousSet = []; var currentSet = []; - _NgClassBase(this.element, this.scope, this.mode, this.nodeAttrs) { + _NgClassBase(this.element, this.scope, this.mode, this.nodeAttrs, this._parser) { var prevClass; nodeAttrs.observe('class', (String newValue) { if (prevClass != newValue) { prevClass = newValue; - _handleChange(scope[r'$index']); + _handleChange(scope.context[r'$index']); } }); } @@ -163,12 +164,15 @@ abstract class _NgClassBase { set valueExpression(currentExpression) { // this should be called only once, so we don't worry about cleaning up // watcher registrations. - scope.$watchCollection(currentExpression, (current) { - currentSet = _flatten(current); - _handleChange(scope[r'$index']); - }); + scope.observe( + _parser(currentExpression, collection: true), + (current, _) { + currentSet = _flatten(current); + _handleChange(scope.context[r'$index']); + } + ); if (mode != null) { - scope.$watch(r'$index', (index, oldIndex) { + scope.observe(_parser(r'$index'), (index, oldIndex) { var mod = index % 2; if (oldIndex == null || mod != oldIndex % 2) { if (mod == mode) { @@ -191,14 +195,20 @@ abstract class _NgClassBase { static List _flatten(classes) { if (classes == null) return []; + if (classes is CollectionChangeRecord) { + classes = (classes as CollectionChangeRecord).iterable.toList(); + } if (classes is List) { return classes.where((String e) => e != null && e.isNotEmpty) .toList(growable: false); } + if (classes is MapChangeRecord) { + classes = (classes as MapChangeRecord).map; + } if (classes is Map) { return classes.keys.where((key) => toBool(classes[key])).toList(); } if (classes is String) return classes.split(' '); - throw 'ng-class expects expression value to be List, Map or String.'; + throw 'ng-class expects expression value to be List, Map or String, got $classes'; } } diff --git a/lib/directive/ng_control.dart b/lib/directive/ng_control.dart index 9565ba572..51628aaa7 100644 --- a/lib/directive/ng_control.dart +++ b/lib/directive/ng_control.dart @@ -26,7 +26,7 @@ abstract class NgControl implements NgDetachAware { : _parentControl = injector.parent.get(NgControl) { pristine = true; - _scope.$on('submitNgControl', (e, data) => _onSubmit(data)); + _scope.on('submitNgControl').listen((e) => _onSubmit(e.data)); } detach() { @@ -36,7 +36,7 @@ abstract class NgControl implements NgDetachAware { } reset() { - _scope.$broadcast('resetNgModel'); + _scope.broadcast('resetNgModel'); } _onSubmit(bool valid) { diff --git a/lib/directive/ng_events.dart b/lib/directive/ng_events.dart index 80f1ce755..33164434b 100644 --- a/lib/directive/ng_events.dart +++ b/lib/directive/ng_events.dart @@ -151,7 +151,7 @@ class NgEventDirective { int key = stream.hashCode; if (!listeners.containsKey(key)) { listeners[key] = handler; - stream.listen((event) => scope.$apply(() {handler({r"$event": event});})); + stream.listen((event) => scope.apply(() {handler({r"$event": event});})); } } diff --git a/lib/directive/ng_form.dart b/lib/directive/ng_form.dart index f859e5590..2712be93f 100644 --- a/lib/directive/ng_form.dart +++ b/lib/directive/ng_form.dart @@ -38,7 +38,7 @@ class NgForm extends NgControl implements Map { if (!element.attributes.containsKey('action')) { element.onSubmit.listen((event) { event.preventDefault(); - _scope.$broadcast('submitNgControl', [valid == null ? false : valid]); + _scope.broadcast('submitNgControl', valid == null ? false : valid); }); } } @@ -47,7 +47,7 @@ class NgForm extends NgControl implements Map { get name => _name; set name(value) { super.name = value; - _scope[name] = this; + _scope.context[name] = this; } //FIXME: fix this reflection bug that shows up when Map is implemented diff --git a/lib/directive/ng_if.dart b/lib/directive/ng_if.dart index 255651af6..22120439e 100644 --- a/lib/directive/ng_if.dart +++ b/lib/directive/ng_if.dart @@ -14,7 +14,7 @@ abstract class _NgUnlessIfAttrDirectiveBase { * The new child scope. This child scope is recreated whenever the `ng-if` * subtree is inserted into the DOM and destroyed when it's removed from the * DOM. Refer - * https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-Inheritance prototypal inheritance + * https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-prototypical-Inheritance prototypical inheritance */ Scope _childScope; @@ -26,16 +26,22 @@ abstract class _NgUnlessIfAttrDirectiveBase { void _ensureBlockExists() { if (_block == null) { - _childScope = _scope.$new(); + _childScope = _scope.createChild(new PrototypeMap(_scope.context)); _block = _boundBlockFactory(_childScope); - _block.insertAfter(_blockHole); + var insertBlock = _block; + _scope.rootScope.domWrite(() { + insertBlock.insertAfter(_blockHole); + }); } } void _ensureBlockDestroyed() { if (_block != null) { - _block.remove(); - _childScope.$destroy(); + var removeBlock = _block; + _scope.rootScope.domWrite(() { + removeBlock.remove(); + }); + _childScope.destroy(); _block = null; _childScope = null; } @@ -57,7 +63,7 @@ abstract class _NgUnlessIfAttrDirectiveBase { * Whenever the subtree is inserted into the DOM, it always gets a new child * scope. This child scope is destroyed when the subtree is removed from the * DOM. Refer - * https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-Inheritance prototypal inheritance + * https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-prototypical-Inheritance prototypical inheritance * * This has an important implication when `ng-model` is used inside an `ng-if` * to bind to a javascript primitive defined in the parent scope. In such a @@ -117,7 +123,7 @@ class NgIfDirective extends _NgUnlessIfAttrDirectiveBase { * Whenever the subtree is inserted into the DOM, it always gets a new child * scope. This child scope is destroyed when the subtree is removed from the * DOM. Refer - * https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-Inheritance prototypal inheritance + * https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-prototypical-Inheritance prototypical inheritance * * This has an important implication when `ng-model` is used inside an * `ng-unless` to bind to a javascript primitive defined in the parent scope. diff --git a/lib/directive/ng_include.dart b/lib/directive/ng_include.dart index 25c06866a..f0e35e078 100644 --- a/lib/directive/ng_include.dart +++ b/lib/directive/ng_include.dart @@ -35,7 +35,7 @@ class NgIncludeDirective { if (_previousBlock == null) return; _previousBlock.remove(); - _previousScope.$destroy(); + _previousScope.destroy(); element.innerHtml = ''; _previousBlock = null; @@ -44,7 +44,7 @@ class NgIncludeDirective { _updateContent(createBlock) { // create a new scope - _previousScope = scope.$new(); + _previousScope = scope.createChild(new PrototypeMap(scope.context)); _previousBlock = createBlock(injector.createChild([new Module() ..value(Scope, _previousScope)])); diff --git a/lib/directive/ng_model.dart b/lib/directive/ng_model.dart index 7621a0c2e..b4dc3140b 100644 --- a/lib/directive/ng_model.dart +++ b/lib/directive/ng_model.dart @@ -12,6 +12,9 @@ part of angular.directive; */ @NgDirective(selector: '[ng-model]') class NgModel extends NgControl implements NgAttachAware { + final NgForm _form; + final AstParser _parser; + BoundGetter getter = ([_]) => null; BoundSetter setter = (_, [__]) => null; @@ -19,23 +22,26 @@ class NgModel extends NgControl implements NgAttachAware { String _exp; final _validators = []; + Watch _removeWatch; bool _watchCollection; - Function _removeWatch = () => null; Function render = (value) => null; - NgModel(Scope scope, NodeAttrs attrs, dom.Element element, Injector injector) : - super(scope, element, injector) { - _exp = 'ng-model=${attrs["ng-model"]}'; + NgModel(Scope _scope, dom.Element _element, Injector injector, + NgForm this._form, this._parser, NodeAttrs attrs) + : super(_scope, _element, injector) + { + _exp = attrs["ng-model"]; + watchCollection = false; } - process(value) { - render(value); + process(value, [_]) { validate(); + _scope.rootScope.domWrite(() => render(value)); } attach() { watchCollection = false; - _scope.$on('resetNgModel', reset); + _scope.on('resetNgModel').listen((e) => reset()); } reset() { @@ -49,24 +55,31 @@ class NgModel extends NgControl implements NgAttachAware { _parentControl.addControl(this); } + // TODO(misko): could we get rid of watch collection, and just always watch the collection? get watchCollection => _watchCollection; set watchCollection(value) { if (_watchCollection == value) return; _watchCollection = value; - _removeWatch(); + if (_removeWatch!=null) _removeWatch.remove(); if (_watchCollection) { - _removeWatch = _scope.$watchCollection((s) => getter(), (value) => process(value), _exp); - } else { - _removeWatch = _scope.$watch((s) => getter(), (value) => process(value), _exp); + _removeWatch = _scope.watch( + _parser(_exp, collection: true), + (changeRecord, _) { + var value = changeRecord is CollectionChangeRecord ? changeRecord.iterable: changeRecord; + process(value); + }); + } else if (_exp != null) { + _removeWatch = _scope.watch(_exp, process); } } + // TODO(misko): getters/setters need to go. We need AST here. @NgCallback('ng-model') set model(BoundExpression boundExpression) { getter = boundExpression; setter = boundExpression.assign; - _scope.$evalAsync((value) { + _scope.rootScope.runAsync(() { _lastValue = modelValue; }); } @@ -140,12 +153,10 @@ class InputCheckboxDirective { inputElement.checked = ngTrueValue.isValue(inputElement, value); }; inputElement.onChange.listen((value) { - scope.$apply(() { - ngModel.dirty = true; - ngModel.viewValue = inputElement.checked - ? ngTrueValue.readValue(inputElement) - : ngFalseValue.readValue(inputElement); - }); + ngModel.dirty = true; + ngModel.viewValue = inputElement.checked + ? ngTrueValue.readValue(inputElement) + : ngFalseValue.readValue(inputElement); }); } } @@ -191,17 +202,15 @@ class InputTextLikeDirective { } }; inputElement - ..onChange.listen(relaxFnArgs(processValue)) - ..onInput.listen((e) { - processValue(); - }); + ..onChange.listen(processValue) + ..onInput.listen(processValue); } - processValue() { + processValue([_]) { var value = typedValue; if (value != ngModel.viewValue) { ngModel.dirty = true; - scope.$apply(() => ngModel.viewValue = value); + ngModel.viewValue = value; } ngModel.validate(); } @@ -261,7 +270,7 @@ class InputNumberLikeDirective { num value = typedValue; if (value != ngModel.viewValue) { ngModel.dirty = true; - scope.$apply(() => ngModel.viewValue = value); + scope.eval(() => ngModel.viewValue = value); } ngModel.validate(); } @@ -391,7 +400,7 @@ class InputRadioDirective { radioButtonElement.onClick.listen((_) { if (radioButtonElement.checked) { ngModel.dirty = true; - scope.$apply(() => ngModel.viewValue = ngValue.readValue(radioButtonElement)); + ngModel.viewValue = ngValue.readValue(radioButtonElement); } }); } diff --git a/lib/directive/ng_pluralize.dart b/lib/directive/ng_pluralize.dart index 972d6243f..b7cd21e44 100644 --- a/lib/directive/ng_pluralize.dart +++ b/lib/directive/ng_pluralize.dart @@ -101,7 +101,7 @@ class NgPluralizeDirective { NodeAttrs attributes) { Map whens = attributes['when'] == null ? {} : - scope.$eval(attributes['when']); + scope.eval(attributes['when']); offset = attributes['offset'] == null ? 0 : int.parse(attributes['offset']); element.attributes.keys.where((k) => IS_WHEN.hasMatch(k)).forEach((k) { @@ -156,6 +156,6 @@ class NgPluralizeDirective { var interpolation = interpolate(expression, false, '\${', '}'); interpolation.setter = (text) => element.text = text; interpolation.setter(expression); - scope.$watchSet(interpolation.watchExpressions, interpolation.call); + scope.watchSet(interpolation.expressions, interpolation.call); } } diff --git a/lib/directive/ng_repeat.dart b/lib/directive/ng_repeat.dart index 3d9c1072d..8fc0e0b04 100644 --- a/lib/directive/ng_repeat.dart +++ b/lib/directive/ng_repeat.dart @@ -83,11 +83,10 @@ class _Row { class NgRepeatDirective extends AbstractNgRepeatDirective { NgRepeatDirective(BlockHole blockHole, BoundBlockFactory boundBlockFactory, + Scope scope, Parser parser, - Scope scope) - : super(blockHole, boundBlockFactory, parser, scope); - - get _shallow => false; + AstParser astParser) + : super(blockHole, boundBlockFactory, scope, parser, astParser); } /** @@ -112,13 +111,17 @@ class NgRepeatDirective extends AbstractNgRepeatDirective { children: NgAnnotation.TRANSCLUDE_CHILDREN, selector: '[ng-shallow-repeat]', map: const {'.': '@expression'}) +//TODO(misko): delete me, since we can no longer do shallow digest. class NgShallowRepeatDirective extends AbstractNgRepeatDirective { NgShallowRepeatDirective(BlockHole blockHole, - BoundBlockFactory boundBlockFactory, - Parser parser, - Scope scope) - : super(blockHole, boundBlockFactory, parser, scope); - get _shallow => true; + BoundBlockFactory boundBlockFactory, + Scope scope, + Parser parser, + AstParser astParser) + : super(blockHole, boundBlockFactory, scope, parser, astParser) + { + print('DEPRICATED: [ng-shallow-repeat] use [ng-repeat]'); + } } abstract class AbstractNgRepeatDirective { @@ -127,8 +130,9 @@ abstract class AbstractNgRepeatDirective { final BlockHole _blockHole; final BoundBlockFactory _boundBlockFactory; - final Parser _parser; final Scope _scope; + final Parser _parser; + final AstParser _astParser; String _expression; String _valueIdentifier; @@ -136,23 +140,22 @@ abstract class AbstractNgRepeatDirective { String _listExpr; Map _rows = {}; Function _trackByIdFn = (key, value, index) => value; - Function _removeWatch = () => null; + Watch _watch = null; Iterable _lastCollection; - AbstractNgRepeatDirective(this._blockHole, this._boundBlockFactory, this._parser, this._scope); - - get _shallow; + AbstractNgRepeatDirective(this._blockHole, this._boundBlockFactory, + this._scope, this._parser, this._astParser); set expression(value) { _expression = value; - _removeWatch(); + if (_watch != null) _watch.remove(); Match match = _SYNTAX.firstMatch(_expression); if (match == null) { throw "[NgErr7] ngRepeat error! Expected expression in form of '_item_ " "in _collection_[ track by _id_]' but got '$_expression'."; } _listExpr = match.group(2); - var trackByExpr = match.group(3); + var trackByExpr = match.group(4); if (trackByExpr != null) { Expression trackBy = _parser(trackByExpr); _trackByIdFn = ((key, value, index) { @@ -162,7 +165,7 @@ abstract class AbstractNgRepeatDirective { ..[_valueIdentifier] = value ..[r'$index'] = index ..[r'$id'] = (obj) => obj; - return relaxFnArgs(trackBy.eval)(new ScopeLocals(_scope, trackByLocals)); + return relaxFnArgs(trackBy.eval)(new ScopeLocals(_scope.context, trackByLocals)); }); } var assignExpr = match.group(1); @@ -176,8 +179,13 @@ abstract class AbstractNgRepeatDirective { if (_valueIdentifier == null) _valueIdentifier = match.group(1); _keyIdentifier = match.group(2); - _removeWatch = _scope.$watchCollection(_listExpr, _onCollectionChange, - value, _shallow); + _watch = _scope.watch( + _astParser(_listExpr, collection: true), + (CollectionChangeRecord collection, _) { + //TODO(misko): we should take advantage of the CollectionChangeRecord! + _onCollectionChange(collection == null ? [] : collection.iterable); + } + ); } List<_Row> _computeNewRows(Iterable collection, trackById) { @@ -212,26 +220,23 @@ abstract class AbstractNgRepeatDirective { // remove existing items _rows.forEach((key, row){ row.block.remove(); - row.scope.$destroy(); + row.scope.destroy(); }); _rows = newRows; return newRowOrder; } _onCollectionChange(Iterable collection) { - var previousNode = _blockHole.elements[0], // current position of the node - nextNode, - childScope, - trackById, - cursor = _blockHole, - arrayChange = _lastCollection != collection; - - if (arrayChange) _lastCollection = collection; - if (collection is! Iterable) collection = []; + dom.Node previousNode = _blockHole.elements[0]; // current position of the node + dom.Node nextNode; + Scope childScope; + Map childContext; + Scope trackById; + ElementWrapper cursor = _blockHole; List<_Row> newRowOrder = _computeNewRows(collection, trackById); - for (var index = 0; index < collection.length; index++) { + for (var index = 0, length = collection.length; index < length; index++) { var value = collection.elementAt(index); _Row row = newRowOrder[index]; @@ -239,6 +244,7 @@ abstract class AbstractNgRepeatDirective { // if we have already seen this object, then we need to reuse the // associated scope/element childScope = row.scope; + childContext = childScope.context as Map; nextNode = previousNode; do { @@ -250,21 +256,21 @@ abstract class AbstractNgRepeatDirective { previousNode = row.endNode; } else { // new item which we don't know about - childScope = _scope.$new(lazy: _shallow); + childScope = _scope.createChild(childContext = new PrototypeMap(_scope.context)); } - if (!identical(childScope[_valueIdentifier], value)) { - childScope[_valueIdentifier] = value; - childScope.$dirty(); + if (!identical(childScope.context[_valueIdentifier], value)) { + childContext[_valueIdentifier] = value; } - childScope - ..[r'$index'] = index - ..[r'$first'] = (index == 0) - ..[r'$last'] = (index == (collection.length - 1)) - ..[r'$middle'] = !(childScope.$first || childScope.$last) - ..[r'$odd'] = index & 1 == 1 - ..[r'$even'] = index & 1 == 0; - if (arrayChange && _shallow) childScope.$dirty(); + var first = (index == 0); + var last = (index == (length - 1)); + childContext + ..[r'$index'] = index + ..[r'$first'] = (index == 0) + ..[r'$last'] = (index == (length - 1)) + ..[r'$middle'] = !first && !last + ..[r'$odd'] = index & 1 == 1 + ..[r'$even'] = index & 1 == 0; if (row.startNode == null) { var block = _boundBlockFactory(childScope); diff --git a/lib/directive/ng_style.dart b/lib/directive/ng_style.dart index 944bf1288..1bfe9b1ea 100644 --- a/lib/directive/ng_style.dart +++ b/lib/directive/ng_style.dart @@ -13,13 +13,12 @@ part of angular.directive; class NgStyleDirective { final dom.Element _element; final Scope _scope; + final AstParser _parser; String _styleExpression; + Watch _watch; - NgStyleDirective(this._element, this._scope); - - Function _removeWatch = () => null; - var _lastStyles; + NgStyleDirective(this._element, this._scope, this._parser); /** * ng-style attribute takes an expression which evaluates to an @@ -28,19 +27,19 @@ class NgStyleDirective { */ set styleExpression(String value) { _styleExpression = value; - _removeWatch(); - _removeWatch = _scope.$watchCollection(_styleExpression, _onStyleChange); + if (_watch != null) _watch.remove(); + _watch = _scope.watch(_parser(_styleExpression, collection: true), _onStyleChange); } - _onStyleChange(Map newStyles) { - dom.CssStyleDeclaration css = _element.style; - if (_lastStyles != null) { - _lastStyles.forEach((val, style) { css.setProperty(val, ''); }); - } - _lastStyles = newStyles; + _onStyleChange(MapChangeRecord mapChangeRecord, _) { + if (mapChangeRecord != null) { + dom.CssStyleDeclaration css = _element.style; + fn(MapKeyValue kv) => css.setProperty(kv.key, kv.currentValue == null ? '' : kv.currentValue); - if (newStyles != null) { - newStyles.forEach((val, style) { css.setProperty(val, style); }); + mapChangeRecord + ..forEachRemoval(fn) + ..forEachChange(fn) + ..forEachAddition(fn); } } } diff --git a/lib/directive/ng_switch.dart b/lib/directive/ng_switch.dart index d9f38981c..a66d5c404 100644 --- a/lib/directive/ng_switch.dart +++ b/lib/directive/ng_switch.dart @@ -75,14 +75,14 @@ class NgSwitchDirective { currentBlocks ..forEach((_BlockScopePair pair) { pair.block.remove(); - pair.scope.$destroy(); + pair.scope.destroy(); }) ..clear(); val = '!$val'; (cases.containsKey(val) ? cases[val] : cases['?']) .forEach((_Case caze) { - Scope childScope = scope.$new(); + Scope childScope = scope.createChild(new PrototypeMap(scope.context)); var block = caze.blockFactory(childScope)..insertAfter(caze.anchor); currentBlocks.add(new _BlockScopePair(block, childScope)); }); diff --git a/lib/introspection.dart b/lib/introspection.dart index e2d27235a..4ac02ec3e 100644 --- a/lib/introspection.dart +++ b/lib/introspection.dart @@ -96,10 +96,12 @@ js.JsObject _jsInjector(Injector injector) => js.JsObject _jsScope(Scope scope) { return new js.JsObject.jsify({ - "\$apply": scope.$apply, - "\$digest": scope.$digest, - "get": (name) => scope[name], - "set": (name, value) => scope[name] = value + "apply": scope.apply, + "digest": scope.digest, + "flush": scope.flush, + "context": scope.context, + "get": (name) => scope.context[name], + "set": (name, value) => scope.context[name] = value })..['_dart_'] = scope; } diff --git a/lib/mock/debug.dart b/lib/mock/debug.dart index 1088f9051..d04308b96 100644 --- a/lib/mock/debug.dart +++ b/lib/mock/debug.dart @@ -29,7 +29,7 @@ dump([p1, p2, p3, p4, p5, p6, p7, p8, p9, p10]) { if (p8 != null) log.add(STRINGIFY(p8)); if (p9 != null) log.add(STRINGIFY(p9)); if (p10 != null) log.add(STRINGIFY(p10)); - js.context['console'].callMethod('log', log); + js.context['console'].callMethod('log', [log.join(', ')]); } STRINGIFY(obj) { diff --git a/lib/mock/probe.dart b/lib/mock/probe.dart index b146bf63d..6eb742474 100644 --- a/lib/mock/probe.dart +++ b/lib/mock/probe.dart @@ -17,10 +17,10 @@ class Probe implements NgDetachAware { final NodeAttrs _attrs; Probe(this.scope, this.injector, this.element, this._attrs) { - scope.$root[_attrs['probe']] = this; + scope.rootScope.context[_attrs['probe']] = this; } - detach() => scope.$root[_attrs['probe']] = null; + detach() => scope.rootScope.context[_attrs['probe']] = null; /** * Retrieve a Directive at the current element. diff --git a/lib/mock/test_bed.dart b/lib/mock/test_bed.dart index e3de2b03b..d0e23b49b 100644 --- a/lib/mock/test_bed.dart +++ b/lib/mock/test_bed.dart @@ -75,6 +75,8 @@ class TestBed { */ triggerEvent(element, name, [type='MouseEvent']) { element.dispatchEvent(new Event.eventType(type, name)); + // Since we are manually triggering event we need to simpulate apply(); + rootScope.apply(); } /** @@ -84,5 +86,6 @@ class TestBed { selectOption(element, text) { element.querySelectorAll('option').forEach((o) => o.selected = o.text == text); triggerEvent(element, 'change'); + rootScope.apply(); } } diff --git a/lib/routing/ng_view.dart b/lib/routing/ng_view.dart index 45c905766..2e3d0077b 100644 --- a/lib/routing/ng_view.dart +++ b/lib/routing/ng_view.dart @@ -65,14 +65,19 @@ class NgViewDirective implements NgDetachAware, RouteProvider { final BlockCache blockCache; final Injector injector; final Element element; + final Scope scope; RouteHandle _route; Block _previousBlock; Scope _previousScope; Route _viewRoute; - NgViewDirective(this.element, this.blockCache, Injector injector, Router router) - : injector = injector, locationService = injector.get(NgRoutingHelper) { + NgViewDirective(this.element, this.blockCache, + Injector injector, Router router, + this.scope) + : injector = injector, + locationService = injector.get(NgRoutingHelper) + { RouteProvider routeProvider = injector.parent.get(NgViewDirective); _route = routeProvider != null ? routeProvider.route.newHandle() : @@ -112,7 +117,7 @@ class NgViewDirective implements NgDetachAware, RouteProvider { var newDirectives = viewInjector.get(DirectiveMap); blockCache.fromUrl(templateUrl, newDirectives).then((blockFactory) { _cleanUp(); - _previousScope = viewInjector.get(Scope).$new(); + _previousScope = scope.createChild(new PrototypeMap(scope.context)); _previousBlock = blockFactory( viewInjector.createChild( [new Module()..value(Scope, _previousScope)])); @@ -125,7 +130,7 @@ class NgViewDirective implements NgDetachAware, RouteProvider { if (_previousBlock == null) return; _previousBlock.remove(); - _previousScope.$destroy(); + _previousScope.destroy(); _previousBlock = null; _previousScope = null; diff --git a/lib/utils.dart b/lib/utils.dart index 3eb2b4109..6c35b862e 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -1,6 +1,6 @@ library angular.util; -toBool(x) { +bool toBool(x) { if (x is bool) return x; if (x is num) return x != 0; return false; diff --git a/perf/dom/compile_perf.dart b/perf/dom/compile_perf.dart index 56dcd900a..0698b1e68 100644 --- a/perf/dom/compile_perf.dart +++ b/perf/dom/compile_perf.dart @@ -12,8 +12,8 @@ main() => describe('compiler', () { tb.rootScope.classFor = (item) => 'ng-${item["done"]}'; time('create 100 blocks', - () => tb.rootScope.$apply(() => tb.rootScope.items = items), - cleanUp: () => tb.rootScope.$apply(() => tb.rootScope.items = empty), + () => tb.rootScope.apply(() => tb.rootScope.items = items), + cleanUp: () => tb.rootScope.apply(() => tb.rootScope.items = empty), verify: () => expect(tb.rootElement.querySelectorAll('li').length).toEqual(100)); })); }); diff --git a/perf/scope_perf.dart b/perf/scope_perf.dart index bacc8807f..7701c30ed 100644 --- a/perf/scope_perf.dart +++ b/perf/scope_perf.dart @@ -33,28 +33,28 @@ _fieldRead() { var o = obj; return (s) => fn(o); }; - scope.$watch(parse('a'), reactionFn); - scope.$watch(parse('b'), reactionFn); - scope.$watch(parse('c'), reactionFn); - scope.$watch(parse('d'), reactionFn); - scope.$watch(parse('e'), reactionFn); - scope.$watch(parse('f'), reactionFn); - scope.$watch(parse('g'), reactionFn); - scope.$watch(parse('h'), reactionFn); - scope.$watch(parse('i'), reactionFn); - scope.$watch(parse('j'), reactionFn); - scope.$watch(parse('k'), reactionFn); - scope.$watch(parse('l'), reactionFn); - scope.$watch(parse('m'), reactionFn); - scope.$watch(parse('n'), reactionFn); - scope.$watch(parse('o'), reactionFn); - scope.$watch(parse('p'), reactionFn); - scope.$watch(parse('q'), reactionFn); - scope.$watch(parse('r'), reactionFn); - scope.$watch(parse('s'), reactionFn); - scope.$watch(parse('t'), reactionFn); - scope.$digest(); - time('fieldRead', () => scope.$digest()); + scope.watch(parse('a'), reactionFn); + scope.watch(parse('b'), reactionFn); + scope.watch(parse('c'), reactionFn); + scope.watch(parse('d'), reactionFn); + scope.watch(parse('e'), reactionFn); + scope.watch(parse('f'), reactionFn); + scope.watch(parse('g'), reactionFn); + scope.watch(parse('h'), reactionFn); + scope.watch(parse('i'), reactionFn); + scope.watch(parse('j'), reactionFn); + scope.watch(parse('k'), reactionFn); + scope.watch(parse('l'), reactionFn); + scope.watch(parse('m'), reactionFn); + scope.watch(parse('n'), reactionFn); + scope.watch(parse('o'), reactionFn); + scope.watch(parse('p'), reactionFn); + scope.watch(parse('q'), reactionFn); + scope.watch(parse('r'), reactionFn); + scope.watch(parse('s'), reactionFn); + scope.watch(parse('t'), reactionFn); + scope.apply(); + time('fieldRead', () => scope.apply()); } _mapRead() { @@ -68,28 +68,28 @@ _mapRead() { var scope = injector.get(Scope); map.forEach((k, v) => scope[k] = v); - scope.$watch('a', reactionFn); - scope.$watch('b', reactionFn); - scope.$watch('c', reactionFn); - scope.$watch('d', reactionFn); - scope.$watch('e', reactionFn); - scope.$watch('f', reactionFn); - scope.$watch('g', reactionFn); - scope.$watch('h', reactionFn); - scope.$watch('i', reactionFn); - scope.$watch('j', reactionFn); - scope.$watch('k', reactionFn); - scope.$watch('l', reactionFn); - scope.$watch('m', reactionFn); - scope.$watch('n', reactionFn); - scope.$watch('o', reactionFn); - scope.$watch('p', reactionFn); - scope.$watch('q', reactionFn); - scope.$watch('r', reactionFn); - scope.$watch('s', reactionFn); - scope.$watch('t', reactionFn); - scope.$digest(); - time('mapRead', () => scope.$digest()); + scope.watch('a', reactionFn); + scope.watch('b', reactionFn); + scope.watch('c', reactionFn); + scope.watch('d', reactionFn); + scope.watch('e', reactionFn); + scope.watch('f', reactionFn); + scope.watch('g', reactionFn); + scope.watch('h', reactionFn); + scope.watch('i', reactionFn); + scope.watch('j', reactionFn); + scope.watch('k', reactionFn); + scope.watch('l', reactionFn); + scope.watch('m', reactionFn); + scope.watch('n', reactionFn); + scope.watch('o', reactionFn); + scope.watch('p', reactionFn); + scope.watch('q', reactionFn); + scope.watch('r', reactionFn); + scope.watch('s', reactionFn); + scope.watch('t', reactionFn); + scope.apply(); + time('mapRead', () => scope.apply()); } _methodInvoke0() { @@ -98,28 +98,28 @@ _methodInvoke0() { var obj = new _Obj(); var scope = injector.get(Scope); scope.a = context; - scope.$watch('a.methodA()', reactionFn); - scope.$watch('a.methodB()', reactionFn); - scope.$watch('a.methodC()', reactionFn); - scope.$watch('a.methodD()', reactionFn); - scope.$watch('a.methodE()', reactionFn); - scope.$watch('a.methodF()', reactionFn); - scope.$watch('a.methodG()', reactionFn); - scope.$watch('a.methodH()', reactionFn); - scope.$watch('a.methodI()', reactionFn); - scope.$watch('a.methodJ()', reactionFn); - scope.$watch('a.methodK()', reactionFn); - scope.$watch('a.methodL()', reactionFn); - scope.$watch('a.methodM()', reactionFn); - scope.$watch('a.methodN()', reactionFn); - scope.$watch('a.methodO()', reactionFn); - scope.$watch('a.methodP()', reactionFn); - scope.$watch('a.methodQ()', reactionFn); - scope.$watch('a.methodR()', reactionFn); - scope.$watch('a.methodS()', reactionFn); - scope.$watch('a.methodT()', reactionFn); - scope.$digest(); - time('obj.method?()', () => scope.$digest()); + scope.watch('a.methodA()', reactionFn); + scope.watch('a.methodB()', reactionFn); + scope.watch('a.methodC()', reactionFn); + scope.watch('a.methodD()', reactionFn); + scope.watch('a.methodE()', reactionFn); + scope.watch('a.methodF()', reactionFn); + scope.watch('a.methodG()', reactionFn); + scope.watch('a.methodH()', reactionFn); + scope.watch('a.methodI()', reactionFn); + scope.watch('a.methodJ()', reactionFn); + scope.watch('a.methodK()', reactionFn); + scope.watch('a.methodL()', reactionFn); + scope.watch('a.methodM()', reactionFn); + scope.watch('a.methodN()', reactionFn); + scope.watch('a.methodO()', reactionFn); + scope.watch('a.methodP()', reactionFn); + scope.watch('a.methodQ()', reactionFn); + scope.watch('a.methodR()', reactionFn); + scope.watch('a.methodS()', reactionFn); + scope.watch('a.methodT()', reactionFn); + scope.apply(); + time('obj.method?()', () => scope.apply()); } _methodInvoke1() { @@ -128,28 +128,28 @@ _methodInvoke1() { var obj = new _Obj(); var scope = injector.get(Scope); scope.a = context; - scope.$watch('a.methodA(a)', reactionFn); - scope.$watch('a.methodB(a)', reactionFn); - scope.$watch('a.methodC(a)', reactionFn); - scope.$watch('a.methodD(a)', reactionFn); - scope.$watch('a.methodE(a)', reactionFn); - scope.$watch('a.methodF(a)', reactionFn); - scope.$watch('a.methodG(a)', reactionFn); - scope.$watch('a.methodH(a)', reactionFn); - scope.$watch('a.methodI(a)', reactionFn); - scope.$watch('a.methodJ(a)', reactionFn); - scope.$watch('a.methodK(a)', reactionFn); - scope.$watch('a.methodL(a)', reactionFn); - scope.$watch('a.methodM(a)', reactionFn); - scope.$watch('a.methodN(a)', reactionFn); - scope.$watch('a.methodO(a)', reactionFn); - scope.$watch('a.methodP(a)', reactionFn); - scope.$watch('a.methodQ(a)', reactionFn); - scope.$watch('a.methodR(a)', reactionFn); - scope.$watch('a.methodS(a)', reactionFn); - scope.$watch('a.methodT(a)', reactionFn); - scope.$digest(); - time('obj.method?(obj)', () => scope.$digest()); + scope.watch('a.methodA(a)', reactionFn); + scope.watch('a.methodB(a)', reactionFn); + scope.watch('a.methodC(a)', reactionFn); + scope.watch('a.methodD(a)', reactionFn); + scope.watch('a.methodE(a)', reactionFn); + scope.watch('a.methodF(a)', reactionFn); + scope.watch('a.methodG(a)', reactionFn); + scope.watch('a.methodH(a)', reactionFn); + scope.watch('a.methodI(a)', reactionFn); + scope.watch('a.methodJ(a)', reactionFn); + scope.watch('a.methodK(a)', reactionFn); + scope.watch('a.methodL(a)', reactionFn); + scope.watch('a.methodM(a)', reactionFn); + scope.watch('a.methodN(a)', reactionFn); + scope.watch('a.methodO(a)', reactionFn); + scope.watch('a.methodP(a)', reactionFn); + scope.watch('a.methodQ(a)', reactionFn); + scope.watch('a.methodR(a)', reactionFn); + scope.watch('a.methodS(a)', reactionFn); + scope.watch('a.methodT(a)', reactionFn); + scope.apply(); + time('obj.method?(obj)', () => scope.apply()); } _function2() { @@ -164,28 +164,28 @@ _function2() { var o = obj; return (s) => fn(o) + fn(o); }; - scope.$watch(add(), reactionFn); - scope.$watch(add(), reactionFn); - scope.$watch(add(), reactionFn); - scope.$watch(add(), reactionFn); - scope.$watch(add(), reactionFn); - scope.$watch(add(), reactionFn); - scope.$watch(add(), reactionFn); - scope.$watch(add(), reactionFn); - scope.$watch(add(), reactionFn); - scope.$watch(add(), reactionFn); - scope.$watch(add(), reactionFn); - scope.$watch(add(), reactionFn); - scope.$watch(add(), reactionFn); - scope.$watch(add(), reactionFn); - scope.$watch(add(), reactionFn); - scope.$watch(add(), reactionFn); - scope.$watch(add(), reactionFn); - scope.$watch(add(), reactionFn); - scope.$watch(add(), reactionFn); - scope.$watch(add(), reactionFn); - scope.$digest(); - time('add?(a, a)', () => scope.$digest()); + scope.watch(add(), reactionFn); + scope.watch(add(), reactionFn); + scope.watch(add(), reactionFn); + scope.watch(add(), reactionFn); + scope.watch(add(), reactionFn); + scope.watch(add(), reactionFn); + scope.watch(add(), reactionFn); + scope.watch(add(), reactionFn); + scope.watch(add(), reactionFn); + scope.watch(add(), reactionFn); + scope.watch(add(), reactionFn); + scope.watch(add(), reactionFn); + scope.watch(add(), reactionFn); + scope.watch(add(), reactionFn); + scope.watch(add(), reactionFn); + scope.watch(add(), reactionFn); + scope.watch(add(), reactionFn); + scope.watch(add(), reactionFn); + scope.watch(add(), reactionFn); + scope.watch(add(), reactionFn); + scope.apply(); + time('add?(a, a)', () => scope.apply()); } class _Obj { diff --git a/test/change_detection/dirty_checking_change_detector_spec.dart b/test/change_detection/dirty_checking_change_detector_spec.dart index 2ee2b8fc1..d6da96cac 100644 --- a/test/change_detection/dirty_checking_change_detector_spec.dart +++ b/test/change_detection/dirty_checking_change_detector_spec.dart @@ -309,51 +309,75 @@ main() => describe('DirtyCheckingChangeDetector', () { hiddenList[0] = 2; expect(detector.collectChanges()).toEqual(null); }); + + it('should bug', () { + var list = [1, 2, 3, 4]; + var record = detector.watch(list, null, 'handler'); + expect(detector.collectChanges().currentValue, toEqualCollectionRecord( + collection: ['1[null -> 0]', '2[null -> 1]', '3[null -> 2]', '4[null -> 3]'], + additions: ['1[null -> 0]', '2[null -> 1]', '3[null -> 2]', '4[null -> 3]'], + moves: [], + removals: [])); + detector.collectChanges(); + + list.removeRange(0, 1); + expect(detector.collectChanges().currentValue, toEqualCollectionRecord( + collection: ['2[1 -> 0]', '3[2 -> 1]', '4[3 -> 2]'], + additions: [], + moves: ['2[1 -> 0]', '3[2 -> 1]', '4[3 -> 2]'], + removals: ['1[0 -> null]'])); + + list.insert(0, 1); + expect(detector.collectChanges().currentValue, toEqualCollectionRecord( + collection: ['1[null -> 0]', '2[0 -> 1]', '3[1 -> 2]', '4[2 -> 3]'], + additions: ['1[null -> 0]'], + moves: ['2[0 -> 1]', '3[1 -> 2]', '4[2 -> 3]'], + removals: [])); + }); }); describe('map watching', () { - xit('should do basic map watching', () { + it('should do basic map watching', () { var map = {}; var record = detector.watch(map, null, 'handler'); expect(detector.collectChanges()).toEqual(null); map['a'] = 'A'; - expect(detector.collectChanges().currentValue, toEqualCollectionRecord( - collection: ['a[null -> 0]'], - additions: ['a[null -> 0]'], - moves: [], + expect(detector.collectChanges().currentValue, toEqualMapRecord( + map: ['a[null -> A]'], + additions: ['a[null -> A]'], + changes: [], removals: [])); map['b'] = 'B'; - expect(detector.collectChanges().currentValue, toEqualCollectionRecord( - collection: ['a', 'b[null -> 1]'], - additions: ['b[null -> 1]'], - moves: [], + expect(detector.collectChanges().currentValue, toEqualMapRecord( + map: ['a', 'b[null -> B]'], + additions: ['b[null -> B]'], + changes: [], removals: [])); - map['c'] = 'C'; + map['b'] = 'BB'; map['d'] = 'D'; - expect(detector.collectChanges().currentValue, toEqualCollectionRecord( - collection: ['a', 'b', 'c[null -> 2]', 'd[null -> 3]'], - additions: ['c[null -> 2]', 'd[null -> 3]'], - moves: [], + expect(detector.collectChanges().currentValue, toEqualMapRecord( + map: ['a', 'b[B -> BB]', 'd[null -> D]'], + additions: ['d[null -> D]'], + changes: ['b[B -> BB]'], removals: [])); - map.remove('c'); - expect(map).toEqual(['a', 'b', 'd']); - expect(detector.collectChanges().currentValue, toEqualCollectionRecord( - collection: ['a', 'b', 'd[3 -> 2]'], + map.remove('b'); + expect(map).toEqual({'a': 'A', 'd':'D'}); + expect(detector.collectChanges().currentValue, toEqualMapRecord( + map: ['a', 'd'], additions: [], - moves: ['d[3 -> 2]'], - removals: ['c[2 -> null]'])); + changes: [], + removals: ['b[BB -> null]'])); map.clear(); - map.addAll(['d', 'c', 'b', 'a']); - expect(detector.collectChanges().currentValue, toEqualCollectionRecord( - collection: ['d[2 -> 0]', 'c[null -> 1]', 'b[1 -> 2]', 'a[0 -> 3]'], - additions: ['c[null -> 1]'], - moves: ['d[2 -> 0]', 'b[1 -> 2]', 'a[0 -> 3]'], - removals: [])); + expect(detector.collectChanges().currentValue, toEqualMapRecord( + map: [], + additions: [], + changes: [], + removals: ['a[A -> null]', 'd[D -> null]'])); }); }); @@ -403,6 +427,9 @@ class _User { Matcher toEqualCollectionRecord({collection, additions, moves, removals}) => new CollectionRecordMatcher(collection:collection, additions:additions, moves:moves, removals:removals); +Matcher toEqualMapRecord({map, additions, changes, removals}) => + new MapRecordMatcher(map:map, additions:additions, + changes:changes, removals:removals); Matcher toEqualChanges(List changes) => new ChangeMatcher(changes); class ChangeMatcher extends Matcher { @@ -564,3 +591,137 @@ class CollectionRecordMatcher extends Matcher { return equals; } } + +class MapRecordMatcher extends Matcher { + List map; + List additions; + List changes; + List removals; + + MapRecordMatcher({this.map, this.additions, this.changes, this.removals}); + + Description describeMismatch(changes, Description mismatchDescription, Map matchState, bool verbose) { + List diffs = matchState['diffs']; + return mismatchDescription..add(diffs.join('\n')); + } + + Description describe(Description description) { + add(name, map) { + if (map != null) { + description.add('$name: ${map.join(', ')}\n '); + } + } + + add('map', map); + add('additions', additions); + add('changes', changes); + add('removals', removals); + return description; + } + + bool matches(MapChangeRecord changeRecord, Map matchState) { + List diffs = matchState['diffs'] = []; + var equals = true; + equals = equals && checkMap(changeRecord, diffs); + equals = equals && checkAdditions(changeRecord, diffs); + equals = equals && checkChanges(changeRecord, diffs); + equals = equals && checkRemovals(changeRecord, diffs); + return equals; + } + + checkMap(MapChangeRecord changeRecord, List diffs) { + var equals = true; + if (map != null) { + KeyValue mapKeyValue = changeRecord.mapHead; + for(var item in map) { + if (mapKeyValue == null) { + equals = false; + diffs.add('map too short: $item'); + } else { + if (mapKeyValue.toString() != item) { + equals = false; + diffs.add('map mismatch: $mapKeyValue != $item'); + } + mapKeyValue = mapKeyValue.nextKeyValue; + } + } + if (mapKeyValue != null) { + diffs.add('map too long: $mapKeyValue'); + equals = false; + } + } + return equals; + } + + checkAdditions(MapChangeRecord changeRecord, List diffs) { + var equals = true; + if (additions != null) { + AddedKeyValue addedKeyValue = changeRecord.additionsHead; + for(var item in additions) { + if (addedKeyValue == null) { + equals = false; + diffs.add('additions too short: $item'); + } else { + if (addedKeyValue.toString() != item) { + equals = false; + diffs.add('additions mismatch: $addedKeyValue != $item'); + } + addedKeyValue = addedKeyValue.nextAddedKeyValue; + } + } + if (addedKeyValue != null) { + equals = false; + diffs.add('additions too long: $addedKeyValue'); + } + } + return equals; + } + + checkChanges(MapChangeRecord changeRecord, List diffs) { + var equals = true; + if (changes != null) { + ChangedKeyValue movedKeyValue = changeRecord.changesHead; + for(var item in changes) { + if (movedKeyValue == null) { + equals = false; + diffs.add('changes too short: $item'); + } else { + if (movedKeyValue.toString() != item) { + equals = false; + diffs.add('changes too mismatch: $movedKeyValue != $item'); + } + movedKeyValue = movedKeyValue.nextChangedKeyValue; + } + } + if (movedKeyValue != null) { + equals = false; + diffs.add('changes too long: $movedKeyValue'); + } + } + return equals; + } + + checkRemovals(MapChangeRecord changeRecord, List diffs) { + var equals = true; + if (removals != null) { + RemovedKeyValue removedKeyValue = changeRecord.removalsHead; + for(var item in removals) { + if (removedKeyValue == null) { + equals = false; + diffs.add('rechanges too short: $item'); + } else { + if (removedKeyValue.toString() != item) { + equals = false; + diffs.add('rechanges too mismatch: $removedKeyValue != $item'); + } + removedKeyValue = removedKeyValue.nextRemovedKeyValue; + } + } + if (removedKeyValue != null) { + equals = false; + diffs.add('rechanges too long: $removedKeyValue'); + } + } + return equals; + } +} diff --git a/test/change_detection/watch_group_spec.dart b/test/change_detection/watch_group_spec.dart index e36cdc25b..cbe9dfd26 100644 --- a/test/change_detection/watch_group_spec.dart +++ b/test/change_detection/watch_group_spec.dart @@ -153,6 +153,11 @@ main() => describe('WatchGroup', () { watchGrp.detectChanges(); expect(logger).toEqual([[1], null]); + logger.clear(); + + context['a'] = {'val': 2}; + watchGrp.detectChanges(); + expect(logger).toEqual([[2]]); }); @@ -449,6 +454,48 @@ main() => describe('WatchGroup', () { expect(watchGrp.collectionCost).toEqual(0); expect(watchGrp.evalCost).toEqual(0); }); + + it('should watch literal arrays made of expressions', () { + context['a'] = 1; + var ast = new CollectionAST( + new PureFunctionAST('[a]', new ArrayFn(), [parse('a')]) + ); + var watch = watchGrp.watch(ast, (v, p) => logger(v)); + watchGrp.detectChanges(); + expect(logger[0], toEqualCollectionRecord( + collection: ['1[null -> 0]'], + additions: ['1[null -> 0]'], + moves: [], + removals: [])); + logger.clear(); + + context['a'] = 2; + watchGrp.detectChanges(); + expect(logger[0], toEqualCollectionRecord( + collection: ['2[null -> 0]'], + additions: ['2[null -> 0]'], + moves: [], + removals: ['1[0 -> null]'])); + logger.clear(); + }); + + it('should watch pure function whose result goes to pure function', () { + context['a'] = 1; + var ast = new PureFunctionAST( + '-', + (v) => -v, + [new PureFunctionAST('++', (v) => v + 1, [parse('a')])] + ); + var watch = watchGrp.watch(ast, (v, p) => logger(v)); + + expect(watchGrp.detectChanges()).not.toBe(null); + expect(logger).toEqual([-2]); + logger.clear(); + + context['a'] = 2; + expect(watchGrp.detectChanges()).not.toBe(null); + expect(logger).toEqual([-3]); + }); }); describe('child group', () { @@ -534,6 +581,27 @@ main() => describe('WatchGroup', () { ra = watchGrp.watch(countMethod, (v, p) => logger('a'));; expectOrder(['a', 'b']); }); + + + it('should watch children', () { + var childContext = new PrototypeMap(context); + context['a'] = 'OK'; + context['b'] = 'BAD'; + childContext['b'] = 'OK'; + watchGrp.watch(parse('a'), (v, p) => logger(v)); + watchGrp.newGroup(childContext).watch(parse('b'), (v, p) => logger(v)); + + watchGrp.detectChanges(); + expect(logger).toEqual(['OK', 'OK']); + logger.clear(); + + context['a'] = 'A'; + childContext['b'] = 'B'; + + watchGrp.detectChanges(); + expect(logger).toEqual(['A', 'B']); + logger.clear(); + }); }); }); diff --git a/test/core/core_directive_spec.dart b/test/core/core_directive_spec.dart index 6262783aa..2e1927fc8 100644 --- a/test/core/core_directive_spec.dart +++ b/test/core/core_directive_spec.dart @@ -83,7 +83,7 @@ main() => describe('DirectiveMap', () { }) class AnnotatedIoComponent { AnnotatedIoComponent(Scope scope) { - scope.$root.ioComponent = this; + scope.rootScope.context['ioComponent'] = this; } @NgAttr('attr') diff --git a/test/core/interpolate_spec.dart b/test/core/interpolate_spec.dart index 4a1e912a9..818cde2ab 100644 --- a/test/core/interpolate_spec.dart +++ b/test/core/interpolate_spec.dart @@ -28,8 +28,8 @@ main() { })); - it('should return interpolation function', inject((Interpolate $interpolate, Scope $rootScope) { - $rootScope.name = 'Misko'; + it('should return interpolation function', inject((Interpolate $interpolate, Scope rootScope) { + rootScope.context['name'] = 'Misko'; var fn = $interpolate('Hello {{name}}!'); expect(fn(['Misko'])).toEqual('Hello Misko!'); })); @@ -40,7 +40,7 @@ main() { })); - it('should use toString to conver objects to string', inject((Interpolate $interpolate, Scope $rootScope) { + it('should use toString to conver objects to string', inject((Interpolate $interpolate, Scope rootScope) { expect($interpolate("Hello, {{obj}}!")([new ToStringableObject()])).toEqual('Hello, World!'); })); diff --git a/test/core/parser/parser_spec.dart b/test/core/parser/parser_spec.dart index e2db3e2fc..437fa66b6 100644 --- a/test/core/parser/parser_spec.dart +++ b/test/core/parser/parser_spec.dart @@ -44,7 +44,7 @@ toBool(x) => (x is num) ? x != 0 : x == true; main() { describe('parse', () { - var scope; + Map context; Parser parser; FilterMap filters; beforeEach(module((Module module) { @@ -56,10 +56,11 @@ main() { filters = injectedFilters; })); - eval(String text, [FilterMap f]) => parser(text).eval(scope, f); + eval(String text, [FilterMap f]) + => parser(text).eval(context, f == null ? filters : f); expectEval(String expr) => expect(() => eval(expr)); - beforeEach(inject((Scope rootScope) { scope = rootScope; })); + beforeEach((){ context = {}; }); describe('expressions', () { it('should parse numerical expressions', () { @@ -125,11 +126,11 @@ main() { expect(eval("true||false?10:20")).toEqual(true||false?10:20); expect(eval("true&&false?10:20")).toEqual(true&&false?10:20); expect(eval("true?a=10:a=20")).toEqual(true?a=10:a=20); - expect([scope['a'], a]).toEqual([10, 10]); - scope['a'] = a = null; + expect([context['a'], a]).toEqual([10, 10]); + context['a'] = a = null; expect(eval("b=true?a=false?11:c=12:a=13")).toEqual( b=true?a=false?11:c=12:a=13); - expect([scope['a'], scope['b'], scope['c']]).toEqual([a, b, c]); + expect([context['a'], context['b'], context['c']]).toEqual([a, b, c]); expect([a, b, c]).toEqual([12, 12, 12]); }); @@ -143,7 +144,7 @@ main() { }); it('should allow keyed access on non-maps', () { - scope['nonmap'] = new BracketButNotMap(); + context['nonmap'] = new BracketButNotMap(); expect(eval("nonmap['hello']")).toEqual('hello'); expect(eval("nonmap['hello']=3")).toEqual(3); }); @@ -269,7 +270,7 @@ main() { it('should let null be null', () { - scope['map'] = {}; + context['map'] = {}; expect(eval('null')).toBe(null); expect(eval('map.null')).toBe(null); @@ -321,74 +322,74 @@ main() { describe('setters', () { it('should set a field in a map', () { - scope['map'] = {}; + context['map'] = {}; eval('map["square"] = 6'); eval('map.dot = 7'); - expect(scope['map']['square']).toEqual(6); - expect(scope['map']['dot']).toEqual(7); + expect(context['map']['square']).toEqual(6); + expect(context['map']['dot']).toEqual(7); }); it('should set a field in a list', () { - scope['list'] = []; + context['list'] = []; eval('list[3] = 2'); - expect(scope['list'].length).toEqual(4); - expect(scope['list'][3]).toEqual(2); + expect(context['list'].length).toEqual(4); + expect(context['list'][3]).toEqual(2); }); it('should set a field on an object', () { - scope['obj'] = new SetterObject(); + context['obj'] = new SetterObject(); eval('obj.field = 1'); - expect(scope['obj'].field).toEqual(1); + expect(context['obj'].field).toEqual(1); }); it('should set a setter on an object', () { - scope['obj'] = new SetterObject(); + context['obj'] = new SetterObject(); eval('obj.setter = 2'); - expect(scope['obj'].setterValue).toEqual(2); + expect(context['obj'].setterValue).toEqual(2); }); it('should set a []= on an object', () { - scope['obj'] = new OverloadObject(); + context['obj'] = new OverloadObject(); eval('obj.overload = 7'); - expect(scope['obj'].overloadValue).toEqual(7); + expect(context['obj'].overloadValue).toEqual(7); }); it('should set a field in a nested map on an object', () { - scope['obj'] = new SetterObject(); + context['obj'] = new SetterObject(); eval('obj.map.mapKey = 3'); - expect(scope['obj'].map['mapKey']).toEqual(3); + expect(context['obj'].map['mapKey']).toEqual(3); }); it('should set a field in a nested object on an object', () { - scope['obj'] = new SetterObject(); + context['obj'] = new SetterObject(); eval('obj.nested.field = 1'); - expect(scope['obj'].nested.field).toEqual(1); + expect(context['obj'].nested.field).toEqual(1); }); it('should create a map for dotted acces', () { - scope['obj'] = new SetterObject(); + context['obj'] = new SetterObject(); eval('obj.field.key = 4'); - expect(scope['obj'].field['key']).toEqual(4); + expect(context['obj'].field['key']).toEqual(4); }); xit('should throw a nice error for type mismatch', () { - scope['obj'] = new SetterObject(); + context['obj'] = new SetterObject(); expect(() { eval('obj.integer = "hello"'); }).toThrow("Eval Error: Caught type 'String' is not a subtype of type 'int' of 'value'. while evaling [obj.integer = \"hello\"]"); @@ -429,11 +430,11 @@ main() { it('should parse ternary', () { - var returnTrue = scope['returnTrue'] = () => true; - var returnFalse = scope['returnFalse'] = () => false; - var returnString = scope['returnString'] = () => 'asd'; - var returnInt = scope['returnInt'] = () => 123; - var identity = scope['identity'] = (x) => x; + var returnTrue = context['returnTrue'] = () => true; + var returnFalse = context['returnFalse'] = () => false; + var returnString = context['returnString'] = () => 'asd'; + var returnInt = context['returnInt'] = () => 123; + var identity = context['identity'] = (x) => x; var B = toBool; // Simple. @@ -504,8 +505,8 @@ main() { it('should access scope', () { - scope['a'] = 123; - scope['b'] = {'c': 456}; + context['a'] = 123; + context['b'] = {'c': 456}; expect(eval("a")).toEqual(123); expect(eval("b.c")).toEqual(456); expect(eval("x.y.z")).toEqual(null); @@ -513,27 +514,27 @@ main() { it('should access classes on scope', () { - scope['ident'] = new Ident(); + context['ident'] = new Ident(); expect(eval('ident.id(6)')).toEqual(6); expect(eval('ident.doubleId(4,5)')).toEqual([4, 5]); }); it('should resolve deeply nested paths (important for CSP mode)', () { - scope['a'] = {'b': {'c': {'d': {'e': {'f': {'g': {'h': {'i': {'j': {'k': {'l': {'m': {'n': 'nooo!'}}}}}}}}}}}}}; + context['a'] = {'b': {'c': {'d': {'e': {'f': {'g': {'h': {'i': {'j': {'k': {'l': {'m': {'n': 'nooo!'}}}}}}}}}}}}}; expect(eval("a.b.c.d.e.f.g.h.i.j.k.l.m.n")).toBe('nooo!'); }); it('should be forgiving', () { - scope = {'a': {'b': 23}}; + context = {'a': {'b': 23}}; expect(eval('b')).toBeNull(); expect(eval('a.x')).toBeNull(); }); it('should catch NoSuchMethod', () { - scope = {'a': {'b': 23}}; + context = {'a': {'b': 23}}; expect(() => eval('a.b.c.d')).toThrow('NoSuchMethod'); }); @@ -544,20 +545,20 @@ main() { it('should evaluate assignments', () { - scope = {'g': 4, 'arr': [3,4]}; + context = {'g': 4, 'arr': [3,4]}; expect(eval("a=12")).toEqual(12); - expect(scope["a"]).toEqual(12); + expect(context["a"]).toEqual(12); expect(eval("arr[c=1]")).toEqual(4); - expect(scope["c"]).toEqual(1); + expect(context["c"]).toEqual(1); expect(eval("x.y.z=123;")).toEqual(123); - expect(scope["x"]["y"]["z"]).toEqual(123); + expect(context["x"]["y"]["z"]).toEqual(123); expect(eval("a=123; b=234")).toEqual(234); - expect(scope["a"]).toEqual(123); - expect(scope["b"]).toEqual(234); + expect(context["a"]).toEqual(123); + expect(context["b"]).toEqual(234); }); // TODO: assignment to an arr[c] @@ -566,18 +567,18 @@ main() { it('should evaluate function call without arguments', () { - scope['constN'] = () => 123; + context['constN'] = () => 123; expect(eval("constN()")).toEqual(123); }); it('should access a protected keyword on scope', () { - scope['const'] = 3; + context['const'] = 3; expect(eval('const')).toEqual(3); }); it('should evaluate function call with arguments', () { - scope["add"] = (a,b) { + context["add"] = (a,b) { return a+b; }; expect(eval("add(1,2)")).toEqual(3); @@ -585,31 +586,31 @@ main() { it('should evaluate function call from a return value', () { - scope["val"] = 33; - scope["getter"] = () { return () { return scope["val"]; };}; + context["val"] = 33; + context["getter"] = () { return () { return context["val"]; };}; expect(eval("getter()()")).toBe(33); }); it('should evaluate methods on object', () { - scope['obj'] = ['ABC']; + context['obj'] = ['ABC']; var fn = parser("obj.elementAt(0)").eval; - expect(fn(scope)).toEqual('ABC'); - expect(scope.$eval(fn)).toEqual('ABC'); + expect(fn(context)).toEqual('ABC'); }); it('should only check locals on first dereference', () { - scope['a'] = {'b': 1}; + context['a'] = {'b': 1}; + context['this'] = context; var locals = {'b': 2}; - var fn = parser("this['a'].b").bind(scope, ScopeLocals.wrapper); + var fn = parser("this['a'].b").bind(context, ScopeLocals.wrapper); expect(fn(locals)).toEqual(1); }); it('should evaluate multiplication and division', () { - scope["taxRate"] = 8; - scope["subTotal"] = 100; + context["taxRate"] = 8; + context["subTotal"] = 100; expect(eval("taxRate / 100 * subTotal")).toEqual(8); expect(eval("taxRate ~/ 100 * subTotal")).toEqual(0); expect(eval("subTotal * taxRate / 100")).toEqual(8); @@ -663,20 +664,13 @@ main() { it('should evaluate objects on scope context', () { - scope["a"] = "abc"; + context["a"] = "abc"; expect(eval("{a:a}")["a"]).toEqual("abc"); }); - it('should evalulate objects on Scope', inject((Scope scope) { - expect(eval(r'$id')).toEqual(scope.$id); - expect(eval(r'$root')).toEqual(scope.$root); - expect(eval(r'$parent')).toEqual(scope.$parent); - })); - - it('should evaluate field access on function call result', () { - scope["a"] = () { + context["a"] = () { return {'name':'misko'}; }; expect(eval("a().name")).toEqual("misko"); @@ -684,13 +678,13 @@ main() { it('should evaluate field access after array access', () { - scope["items"] = [{}, {'name':'misko'}]; + context["items"] = [{}, {'name':'misko'}]; expect(eval('items[1].name')).toEqual("misko"); }); it('should evaluate array assignment', () { - scope["items"] = []; + context["items"] = []; expect(eval('items[1] = "abc"')).toEqual("abc"); expect(eval('items[1]')).toEqual("abc"); @@ -746,20 +740,20 @@ main() { it('should evaluate undefined', () { expect(eval("undefined")).toBeNull(); expect(eval("a=undefined")).toBeNull(); - expect(scope["a"]).toBeNull(); + expect(context["a"]).toBeNull(); }); it('should allow assignment after array dereference', () { - scope["obj"] = [{}]; + context["obj"] = [{}]; eval('obj[0].name=1'); // can not be expressed in Dart expect(scope["obj"]["name"]).toBeNull(); - expect(scope["obj"][0]["name"]).toEqual(1); + expect(context["obj"][0]["name"]).toEqual(1); }); it('should short-circuit AND operator', () { - scope["run"] = () { + context["run"] = () { throw "IT SHOULD NOT HAVE RUN"; }; expect(eval('false && run()')).toBe(false); @@ -767,7 +761,7 @@ main() { it('should short-circuit OR operator', () { - scope["run"] = () { + context["run"] = () { throw "IT SHOULD NOT HAVE RUN"; }; expect(eval('true || run()')).toBe(true); @@ -775,9 +769,9 @@ main() { it('should support method calls on primitive types', () { - scope["empty"] = ''; - scope["zero"] = 0; - scope["bool"] = false; + context["empty"] = ''; + context["zero"] = 0; + context["bool"] = false; // DOES NOT WORK. String.substring is not reflected. Or toString // expect(eval('empty.substring(0)')).toEqual(''); @@ -871,8 +865,8 @@ main() { it('should work with scopes', inject((Scope scope) { - scope.a = {'b': 6}; - expect(parser('a.b').bind(scope, ScopeLocals.wrapper)({'a': {'b':1}})).toEqual(1); + scope.context['a'] = {'b': 6}; + expect(parser('a.b').bind(scope.context, ScopeLocals.wrapper)({'a': {'b':1}})).toEqual(1); })); it('should expose assignment function', () { @@ -899,15 +893,15 @@ main() { it('should parse filters', () { expect(() { - scope.$eval("1|nonexistent"); + eval("1|nonexistent"); }).toThrow('No NgFilter: nonexistent found!'); expect(() { eval("1|nonexistent", filters); }).toThrow('No NgFilter: nonexistent found!'); - scope.offset = 3; - expect(scope.$eval("'abcd'|substring:1:offset")).toEqual("bc"); - expect(scope.$eval("'abcd'|substring:1:3|uppercase")).toEqual("BC"); + context['offset'] = 3; + expect(eval("'abcd'|substring:1:offset")).toEqual("bc"); + expect(eval("'abcd'|substring:1:3|uppercase")).toEqual("BC"); }); it('should only use filters that are passed as an argument', inject((Injector injector) { diff --git a/test/core/scope2_spec.dart b/test/core/scope2_spec.dart deleted file mode 100644 index bd4dbe4f0..000000000 --- a/test/core/scope2_spec.dart +++ /dev/null @@ -1,1065 +0,0 @@ -library scope2_spec; - -import '../_specs.dart'; -import 'package:angular/change_detection/change_detection.dart' hide ExceptionHandler; -import 'package:angular/change_detection/dirty_checking_change_detector.dart'; - -main() => describe('scope2', () { - beforeEach(module((Module module) { - Map context = {}; - module.value(GetterCache, new GetterCache({})); - module.type(ChangeDetector, implementedBy: DirtyCheckingChangeDetector); - module.value(Object, context); - module.value(Map, context); - module.type(RootScope); - module.type(_MultiplyFilter); - module.type(_ListHeadFilter); - module.type(_ListTailFilter); - module.type(_SortFilter); - })); - - describe('AST Bridge', () { - it('should watch field', inject((Logger logger, Map context, RootScope rootScope) { - context['field'] = 'Worked!'; - rootScope.watch('field', (value, previous) => logger([value, previous])); - expect(logger).toEqual([]); - rootScope.digest(); - expect(logger).toEqual([['Worked!', null]]); - rootScope.digest(); - expect(logger).toEqual([['Worked!', null]]); - })); - - it('should watch field path', inject((Logger logger, Map context, RootScope rootScope) { - context['a'] = {'b': 'AB'}; - rootScope.watch('a.b', (value, previous) => logger(value)); - rootScope.digest(); - expect(logger).toEqual(['AB']); - context['a']['b'] = '123'; - rootScope.digest(); - expect(logger).toEqual(['AB', '123']); - context['a'] = {'b': 'XYZ'}; - rootScope.digest(); - expect(logger).toEqual(['AB', '123', 'XYZ']); - })); - - it('should watch math operations', inject((Logger logger, Map context, RootScope rootScope) { - context['a'] = 1; - context['b'] = 2; - rootScope.watch('a + b + 1', (value, previous) => logger(value)); - rootScope.digest(); - expect(logger).toEqual([4]); - context['a'] = 3; - rootScope.digest(); - expect(logger).toEqual([4, 6]); - context['b'] = 5; - rootScope.digest(); - expect(logger).toEqual([4, 6, 9]); - })); - - - it('should watch literals', inject((Logger logger, Map context, RootScope rootScope) { - context['a'] = 1; - rootScope.watch('1', (value, previous) => logger(value)); - rootScope.watch('"str"', (value, previous) => logger(value)); - rootScope.watch('[a, 2, 3]', (value, previous) => logger(value)); - rootScope.watch('{a:a, b:2}', (value, previous) => logger(value)); - rootScope.digest(); - expect(logger).toEqual([1, 'str', [1, 2, 3], {'a': 1, 'b': 2}]); - logger.clear(); - context['a'] = 3; - rootScope.digest(); - expect(logger).toEqual([[3, 2, 3], {'a': 3, 'b': 2}]); - })); - - it('should invoke closures', inject((Logger logger, Map context, RootScope rootScope) { - context['fn'] = () { - logger('fn'); - return 1; - }; - context['a'] = {'fn': () { - logger('a.fn'); - return 2; - }}; - rootScope.watch('fn()', (value, previous) => logger('=> $value')); - rootScope.watch('a.fn()', (value, previous) => logger('-> $value')); - rootScope.digest(); - expect(logger).toEqual(['fn', 'a.fn', '=> 1', '-> 2', - /* second loop*/ 'fn', 'a.fn']); - logger.clear(); - rootScope.digest(); - expect(logger).toEqual(['fn', 'a.fn']); - })); - - it('should perform conditionals', inject((Logger logger, Map context, RootScope rootScope) { - context['a'] = 1; - context['b'] = 2; - context['c'] = 3; - rootScope.watch('a?b:c', (value, previous) => logger(value)); - rootScope.digest(); - expect(logger).toEqual([2]); - logger.clear(); - context['a'] = 0; - rootScope.digest(); - expect(logger).toEqual([3]); - })); - - - xit('should call function', inject((Logger logger, Map context, RootScope rootScope) { - context['a'] = () { - return () { return 123; }; - }; - rootScope.watch('a()()', (value, previous) => logger(value)); - rootScope.digest(); - expect(logger).toEqual([123]); - logger.clear(); - rootScope.digest(); - expect(logger).toEqual([]); - })); - - it('should access bracket', inject((Logger logger, Map context, RootScope rootScope) { - context['a'] = {'b': 123}; - rootScope.watch('a["b"]', (value, previous) => logger(value)); - rootScope.digest(); - expect(logger).toEqual([123]); - logger.clear(); - rootScope.digest(); - expect(logger).toEqual([]); - })); - - - it('should prefix', inject((Logger logger, Map context, RootScope rootScope) { - context['a'] = true; - rootScope.watch('!a', (value, previous) => logger(value)); - rootScope.digest(); - expect(logger).toEqual([false]); - logger.clear(); - context['a'] = false; - rootScope.digest(); - expect(logger).toEqual([true]); - })); - - it('should support filters', inject((Logger logger, Map context, - RootScope rootScope, AstParser parser, - FilterMap filters) { - context['a'] = 123; - context['b'] = 2; - rootScope.watch( - parser('a | multiply:b', filters: filters), - (value, previous) => logger(value)); - rootScope.digest(); - expect(logger).toEqual([246]); - logger.clear(); - rootScope.digest(); - expect(logger).toEqual([]); - logger.clear(); - })); - - it('should support arrays in filters', inject((Logger logger, Map context, - RootScope rootScope, - AstParser parser, - FilterMap filters) { - context['a'] = [1]; - rootScope.watch( - parser('a | sort | listHead:"A" | listTail:"B"', filters: filters), - (value, previous) => logger(value)); - rootScope.digest(); - expect(logger).toEqual(['sort', 'listHead', 'listTail', ['A', 1, 'B']]); - logger.clear(); - - rootScope.digest(); - expect(logger).toEqual([]); - logger.clear(); - - context['a'].add(2); - rootScope.digest(); - expect(logger).toEqual(['sort', 'listHead', 'listTail', ['A', 1, 2, 'B']]); - logger.clear(); - - // We change the order, but sort should change it to same one and it should not - // call subsequent filters. - context['a'] = [2, 1]; - rootScope.digest(); - expect(logger).toEqual(['sort']); - logger.clear(); - })); - }); - - describe('properties', () { - describe('root', () { - it('should point to itself', inject((RootScope rootScope) { - expect(rootScope.rootScope).toEqual(rootScope); - })); - - it('children should point to root', inject((RootScope rootScope) { - var child = rootScope.createChild(); - expect(child.rootScope).toEqual(rootScope); - expect(child.createChild().rootScope).toEqual(rootScope); - })); - }); - - - describe('parent', () { - it('should not have parent', inject((RootScope rootScope) { - expect(rootScope.parentScope).toEqual(null); - })); - - - it('should point to parent', inject((RootScope rootScope) { - var child = rootScope.createChild(); - expect(rootScope.parentScope).toEqual(null); - expect(child.parentScope).toEqual(rootScope); - expect(child.createChild().parentScope).toEqual(child); - })); - }); - }); - - describe(r'events', () { - - describe('on', () { - it(r'should add listener for both emit and broadcast events', inject((RootScope rootScope) { - var log = '', - child = rootScope.createChild(); - - eventFn(event) { - expect(event).not.toEqual(null); - log += 'X'; - } - - child.on('abc').listen(eventFn); - expect(log).toEqual(''); - - child.emit('abc'); - expect(log).toEqual('X'); - - child.broadcast('abc'); - expect(log).toEqual('XX'); - })); - - - it(r'should return a function that deregisters the listener', inject((RootScope rootScope) { - var log = ''; - var child = rootScope.createChild(); - var subscription; - - eventFn(e) { - log += 'X'; - } - - subscription = child.on('abc').listen(eventFn); - expect(log).toEqual(''); - expect(subscription).toBeDefined(); - - child.emit(r'abc'); - child.broadcast('abc'); - expect(log).toEqual('XX'); - - log = ''; - expect(subscription.cancel()).toBe(null); - child.emit(r'abc'); - child.broadcast('abc'); - expect(log).toEqual(''); - })); - }); - - - describe('emit', () { - var log, child, grandChild, greatGrandChild; - - logger(event) { - log.add(event.currentScope.context['id']); - } - - beforeEach(module(() { - return (RootScope rootScope) { - log = []; - child = rootScope.createChild({'id': 1}); - grandChild = child.createChild({'id': 2}); - greatGrandChild = grandChild.createChild({'id': 3}); - - rootScope.context['id'] = 0; - - rootScope.on('myEvent').listen(logger); - child.on('myEvent').listen(logger); - grandChild.on('myEvent').listen(logger); - greatGrandChild.on('myEvent').listen(logger); - }; - })); - - it(r'should bubble event up to the root scope', inject((RootScope rootScope) { - grandChild.emit(r'myEvent'); - expect(log.join('>')).toEqual('2>1>0'); - })); - - - it(r'should dispatch exceptions to the exceptionHandler', () { - module((Module module) { - module.type(ExceptionHandler, implementedBy: LoggingExceptionHandler); - }); - inject((ExceptionHandler e) { - LoggingExceptionHandler exceptionHandler = e; - child.on('myEvent').listen((e) { throw 'bubbleException'; }); - grandChild.emit(r'myEvent'); - expect(log.join('>')).toEqual('2>1>0'); - expect(exceptionHandler.errors[0].error).toEqual('bubbleException'); - }); - }); - - - it(r'should allow stopping event propagation', inject((RootScope rootScope) { - child.on('myEvent').listen((event) { event.stopPropagation(); }); - grandChild.emit(r'myEvent'); - expect(log.join('>')).toEqual('2>1'); - })); - - - it(r'should forward method arguments', inject((RootScope rootScope) { - var eventName; - var eventData; - child.on('abc').listen((event) { - eventName = event.name; - eventData = event.data; - }); - child.emit('abc', ['arg1', 'arg2']); - expect(eventName).toEqual('abc'); - expect(eventData).toEqual(['arg1', 'arg2']); - })); - - - describe(r'event object', () { - it(r'should have methods/properties', inject((RootScope rootScope) { - var event; - child.on('myEvent').listen((e) { - expect(e.targetScope).toBe(grandChild); - expect(e.currentScope).toBe(child); - expect(e.name).toBe('myEvent'); - event = e; - }); - grandChild.emit(r'myEvent'); - expect(event).toBeDefined(); - })); - - - it(r'should have preventDefault method and defaultPrevented property', inject((RootScope rootScope) { - var event = grandChild.emit(r'myEvent'); - expect(event.defaultPrevented).toBe(false); - - child.on('myEvent').listen((event) { - event.preventDefault(); - }); - event = grandChild.emit(r'myEvent'); - expect(event.defaultPrevented).toBe(true); - })); - }); - }); - - - describe('broadcast', () { - describe(r'event propagation', () { - var log, child1, child2, child3, grandChild11, grandChild21, grandChild22, grandChild23, - greatGrandChild211; - - logger(event) { - log.add(event.currentScope.context['id']); - } - - beforeEach(inject((RootScope rootScope) { - log = []; - child1 = rootScope.createChild({}); - child2 = rootScope.createChild({}); - child3 = rootScope.createChild({}); - grandChild11 = child1.createChild({}); - grandChild21 = child2.createChild({}); - grandChild22 = child2.createChild({}); - grandChild23 = child2.createChild({}); - greatGrandChild211 = grandChild21.createChild({}); - - rootScope.context['id'] = 0; - child1.context['id'] = 1; - child2.context['id'] = 2; - child3.context['id'] = 3; - grandChild11.context['id'] = 11; - grandChild21.context['id'] = 21; - grandChild22.context['id'] = 22; - grandChild23.context['id'] = 23; - greatGrandChild211.context['id'] = 211; - - rootScope.on('myEvent').listen(logger); - child1.on('myEvent').listen(logger); - child2.on('myEvent').listen(logger); - child3.on('myEvent').listen(logger); - grandChild11.on('myEvent').listen(logger); - grandChild21.on('myEvent').listen(logger); - grandChild22.on('myEvent').listen(logger); - grandChild23.on('myEvent').listen(logger); - greatGrandChild211.on('myEvent').listen(logger); - - // R - // / | \ - // 1 2 3 - // / / | \ - // 11 21 22 23 - // | - // 211 - })); - - - it(r'should broadcast an event from the root scope', inject((RootScope rootScope) { - rootScope.broadcast('myEvent'); - expect(log.join('>')).toEqual('0>1>11>2>21>211>22>23>3'); - })); - - - it(r'should broadcast an event from a child scope', inject((RootScope rootScope) { - child2.broadcast('myEvent'); - expect(log.join('>')).toEqual('2>21>211>22>23'); - })); - - - it(r'should broadcast an event from a leaf scope with a sibling', inject((RootScope rootScope) { - grandChild22.broadcast('myEvent'); - expect(log.join('>')).toEqual('22'); - })); - - - it(r'should broadcast an event from a leaf scope without a sibling', inject((RootScope rootScope) { - grandChild23.broadcast('myEvent'); - expect(log.join('>')).toEqual('23'); - })); - - - it(r'should not not fire any listeners for other events', inject((RootScope rootScope) { - rootScope.broadcast('fooEvent'); - expect(log.join('>')).toEqual(''); - })); - - - it(r'should return event object', inject((RootScope rootScope) { - var result = child1.broadcast('some'); - - expect(result).toBeDefined(); - expect(result.name).toBe('some'); - expect(result.targetScope).toBe(child1); - })); - }); - - - describe(r'listener', () { - it(r'should receive event object', inject((RootScope rootScope) { - var scope = rootScope, - child = scope.createChild({}), - event; - - child.on('fooEvent').listen((e) { - event = e; - }); - scope.broadcast('fooEvent'); - - expect(event.name).toBe('fooEvent'); - expect(event.targetScope).toBe(scope); - expect(event.currentScope).toBe(child); - })); - - it(r'should support passing messages as varargs', inject((RootScope rootScope) { - var scope = rootScope, - child = scope.createChild({}), - args; - - child.on('fooEvent').listen((e) { - args = e.data; - }); - scope.broadcast('fooEvent', ['do', 're', 'me', 'fa']); - - expect(args.length).toBe(4); - expect(args).toEqual(['do', 're', 'me', 'fa']); - })); - }); - }); - }); - - describe(r'$destroy', () { - var first = null, middle = null, last = null, log = null; - - beforeEach(inject((RootScope rootScope) { - log = ''; - - first = rootScope.createChild({"check": (n) { log+= '$n'; return n;}}); - middle = rootScope.createChild({"check": (n) { log+= '$n'; return n;}}); - last = rootScope.createChild({"check": (n) { log+= '$n'; return n;}}); - - first.watch('check(1)', (v, l) {}); - middle.watch('check(2)', (v, l) {}); - last.watch('check(3)', (v, l) {}); - - first.on(ScopeEvent.DESTROY).listen((e) { log += 'destroy:first;'; }); - - rootScope.digest(); - log = ''; - })); - - - it(r'should ignore remove on root', inject((RootScope rootScope) { - rootScope.destroy(); - rootScope.digest(); - expect(log).toEqual('123'); - })); - - - it(r'should remove first', inject((RootScope rootScope) { - first.destroy(); - rootScope.digest(); - expect(log).toEqual('destroy:first;23'); - })); - - - it(r'should remove middle', inject((RootScope rootScope) { - middle.destroy(); - rootScope.digest(); - expect(log).toEqual('13'); - })); - - - it(r'should remove last', inject((RootScope rootScope) { - last.destroy(); - rootScope.digest(); - expect(log).toEqual('12'); - })); - - - it(r'should broadcast the $destroy event', inject((RootScope rootScope) { - var log = []; - first.on(ScopeEvent.DESTROY).listen((s) => log.add('first')); - first.createChild({}).on(ScopeEvent.DESTROY).listen((s) => log.add('first-child')); - - first.destroy(); - expect(log).toEqual(['first', 'first-child']); - })); - }); - - describe('digest lifecycle', () { - it(r'should apply expression with full lifecycle', inject((RootScope rootScope) { - var log = ''; - var child = rootScope.createChild({"parent": rootScope.context}); - rootScope.watch('a', (a, _) { log += '1'; }); - child.apply('parent.a = 0'); - expect(log).toEqual('1'); - })); - - - it(r'should catch exceptions', () { - module((Module module) => module.type(ExceptionHandler, implementedBy: LoggingExceptionHandler)); - inject((RootScope rootScope, ExceptionHandler e) { - LoggingExceptionHandler $exceptionHandler = e; - var log = []; - var child = rootScope.createChild({}); - rootScope.watch('a', (a, _) => log.add('1')); - rootScope.context['a'] = 0; - child.apply(() { throw 'MyError'; }); - expect(log.join(',')).toEqual('1'); - expect($exceptionHandler.errors[0].error).toEqual('MyError'); - $exceptionHandler.errors.removeAt(0); - $exceptionHandler.assertEmpty(); - }); - }); - - - describe(r'exceptions', () { - var log; - beforeEach(module((Module module) { - return module.type(ExceptionHandler, implementedBy: LoggingExceptionHandler); - })); - beforeEach(inject((RootScope rootScope) { - rootScope.context['log'] = () { log += 'digest;'; return null; }; - log = ''; - rootScope.watch('log()', (v, o) => null); - rootScope.digest(); - log = ''; - })); - - - it(r'should execute and return value and update', inject( - (RootScope rootScope, ExceptionHandler e) { - LoggingExceptionHandler $exceptionHandler = e; - rootScope.context['name'] = 'abc'; - expect(rootScope.apply((context) => context['name'])).toEqual('abc'); - expect(log).toEqual('digest;digest;'); - $exceptionHandler.assertEmpty(); - })); - - - it(r'should execute and return value and update', inject((RootScope rootScope) { - rootScope.context['name'] = 'abc'; - expect(rootScope.apply('name', {'name': 123})).toEqual(123); - })); - - - it(r'should catch exception and update', inject((RootScope rootScope, ExceptionHandler e) { - LoggingExceptionHandler $exceptionHandler = e; - var error = 'MyError'; - rootScope.apply(() { throw error; }); - expect(log).toEqual('digest;digest;'); - expect($exceptionHandler.errors[0].error).toEqual(error); - })); - }); - - it(r'should proprely reset phase on exception', inject((RootScope rootScope) { - var error = 'MyError'; - expect(() => rootScope.apply(() { throw error; })).toThrow(error); - expect(() => rootScope.apply(() { throw error; })).toThrow(error); - })); - }); - - - describe('flush lifecycle', () { - it(r'should apply expression with full lifecycle', inject((RootScope rootScope) { - var log = ''; - var child = rootScope.createChild({"parent": rootScope.context}); - rootScope.observe('a', (a, _) { log += '1'; }); - child.apply('parent.a = 0'); - expect(log).toEqual('1'); - })); - - - it(r'should schedule domWrites and domReads', inject((RootScope rootScope) { - var log = ''; - var child = rootScope.createChild({"parent": rootScope.context}); - rootScope.observe('a', (a, _) { log += '1'; }); - child.apply('parent.a = 0'); - expect(log).toEqual('1'); - })); - - - it(r'should catch exceptions', () { - module((Module module) => module.type(ExceptionHandler, implementedBy: LoggingExceptionHandler)); - inject((RootScope rootScope, ExceptionHandler e) { - LoggingExceptionHandler $exceptionHandler = e; - var log = []; - var child = rootScope.createChild({}); - rootScope.observe('a', (a, _) => log.add('1')); - rootScope.context['a'] = 0; - child.apply(() { throw 'MyError'; }); - expect(log.join(',')).toEqual('1'); - expect($exceptionHandler.errors[0].error).toEqual('MyError'); - $exceptionHandler.errors.removeAt(0); - $exceptionHandler.assertEmpty(); - }); - }); - - - describe(r'exceptions', () { - var log; - beforeEach(module((Module module) { - return module.type(ExceptionHandler, implementedBy: LoggingExceptionHandler); - })); - beforeEach(inject((RootScope rootScope) { - rootScope.context['log'] = () { log += 'digest;'; return null; }; - log = ''; - rootScope.observe('log()', (v, o) => null); - rootScope.digest(); - log = ''; - })); - - - it(r'should execute and return value and update', inject( - (RootScope rootScope, ExceptionHandler e) { - LoggingExceptionHandler $exceptionHandler = e; - rootScope.context['name'] = 'abc'; - expect(rootScope.apply((context) => context['name'])).toEqual('abc'); - expect(log).toEqual('digest;digest;'); - $exceptionHandler.assertEmpty(); - })); - - it(r'should execute and return value and update', inject((RootScope rootScope) { - rootScope.context['name'] = 'abc'; - expect(rootScope.apply('name', {'name': 123})).toEqual(123); - })); - - it(r'should catch exception and update', inject((RootScope rootScope, ExceptionHandler e) { - LoggingExceptionHandler $exceptionHandler = e; - var error = 'MyError'; - rootScope.apply(() { throw error; }); - expect(log).toEqual('digest;digest;'); - expect($exceptionHandler.errors[0].error).toEqual(error); - })); - - it(r'should throw assertion when model changes in flush', inject((RootScope rootScope, Logger log) { - var retValue = 1; - rootScope.context['logger'] = (name) { log(name); return retValue; }; - - rootScope.watch('logger("watch")', (n, v) => null); - rootScope.observe('logger("flush")', (n, v) => null); - - // clear watches - rootScope.digest(); - log.clear(); - - rootScope.flush(); - expect(log).toEqual(['flush', /*assertion*/ 'watch', 'flush']); - - retValue = 2; - expect(rootScope.flush). - toThrow('Observer reaction functions should not change model. \n' - 'These watch changes were detected: logger("watch")\n' - 'These observe changes were detected: '); - })); - }); - - }); - - - describe('ScopeLocals', () { - it('should read from locals', inject((RootScope scope) { - scope.context['a'] = 'XXX'; - scope.context['c'] = 'C'; - var scopeLocal = new ScopeLocals(scope.context, {'a': 'A', 'b': 'B'}); - expect(scopeLocal['a']).toEqual('A'); - expect(scopeLocal['b']).toEqual('B'); - expect(scopeLocal['c']).toEqual('C'); - })); - - it('should write to Scope', inject((RootScope scope) { - scope.context['a'] = 'XXX'; - scope.context['c'] = 'C'; - var scopeLocal = new ScopeLocals(scope.context, {'a': 'A', 'b': 'B'}); - - scopeLocal['a'] = 'aW'; - scopeLocal['b'] = 'bW'; - scopeLocal['c'] = 'cW'; - - expect(scope.context['a']).toEqual('aW'); - expect(scope.context['b']).toEqual('bW'); - expect(scope.context['c']).toEqual('cW'); - - expect(scopeLocal['a']).toEqual('A'); - expect(scopeLocal['b']).toEqual('B'); - expect(scopeLocal['c']).toEqual('cW'); - })); - }); - - - describe(r'watch/digest', () { - it(r'should watch and fire on simple property change', inject((RootScope rootScope) { - var log; - - rootScope.watch('name', (a, b) { - log = [a, b]; - }); - rootScope.digest(); - log = null; - - expect(log).toEqual(null); - rootScope.digest(); - expect(log).toEqual(null); - rootScope.context['name'] = 'misko'; - rootScope.digest(); - expect(log).toEqual(['misko', null]); - })); - - - it(r'should watch and fire on expression change', inject((RootScope rootScope) { - var log; - - rootScope.watch('name.first', (a, b) => log = [a, b]); - rootScope.digest(); - log = null; - - rootScope.context['name'] = {}; - expect(log).toEqual(null); - rootScope.digest(); - expect(log).toEqual(null); - rootScope.context['name']['first'] = 'misko'; - rootScope.digest(); - expect(log).toEqual(['misko', null]); - })); - - - it(r'should delegate exceptions', () { - module((Module module) { - module.type(ExceptionHandler, implementedBy: LoggingExceptionHandler); - }); - inject((RootScope rootScope, ExceptionHandler e) { - LoggingExceptionHandler $exceptionHandler = e; - rootScope.watch('a', (n, o) {throw 'abc';}); - rootScope.context['a'] = 1; - rootScope.digest(); - expect($exceptionHandler.errors.length).toEqual(1); - expect($exceptionHandler.errors[0].error).toEqual('abc'); - }); - }); - - - it(r'should fire watches in order of addition', inject((RootScope rootScope) { - // this is not an external guarantee, just our own sanity - var log = ''; - rootScope.watch('a', (a, b) { log += 'a'; }); - rootScope.watch('b', (a, b) { log += 'b'; }); - rootScope.watch('c', (a, b) { log += 'c'; }); - rootScope.context['a'] = rootScope.context['b'] = rootScope.context['c'] = 1; - rootScope.digest(); - expect(log).toEqual('abc'); - })); - - - it(r'should call child watchers in addition order', inject((RootScope rootScope) { - // this is not an external guarantee, just our own sanity - var log = ''; - var childA = rootScope.createChild({}); - var childB = rootScope.createChild({}); - var childC = rootScope.createChild({}); - childA.watch('a', (a, b) { log += 'a'; }); - childB.watch('b', (a, b) { log += 'b'; }); - childC.watch('c', (a, b) { log += 'c'; }); - childA.context['a'] = childB.context['b'] = childC.context['c'] = 1; - rootScope.digest(); - expect(log).toEqual('abc'); - })); - - - it(r'should run digest multiple times', inject( - (RootScope rootScope) { - // tests a traversal edge case which we originally missed - var log = []; - var childA = rootScope.createChild({'log': log}); - var childB = rootScope.createChild({'log': log}); - - rootScope.context['log'] = log; - - rootScope.watch("log.add('r')", (_, __) => null); - childA.watch("log.add('a')", (_, __) => null); - childB.watch("log.add('b')", (_, __) => null); - - // init - rootScope.digest(); - expect(log.join('')).toEqual('rabrab'); - })); - - - it(r'should repeat watch cycle while model changes are identified', inject((RootScope rootScope) { - var log = ''; - rootScope.watch('c', (v, b) {rootScope.context['d'] = v; log+='c'; }); - rootScope.watch('b', (v, b) {rootScope.context['c'] = v; log+='b'; }); - rootScope.watch('a', (v, b) {rootScope.context['b'] = v; log+='a'; }); - rootScope.digest(); - log = ''; - rootScope.context['a'] = 1; - rootScope.digest(); - expect(rootScope.context['b']).toEqual(1); - expect(rootScope.context['c']).toEqual(1); - expect(rootScope.context['d']).toEqual(1); - expect(log).toEqual('abc'); - })); - - - it(r'should repeat watch cycle from the root element', inject((RootScope rootScope) { - var log = []; - rootScope.context['log'] = log; - var child = rootScope.createChild({'log':log}); - rootScope.watch("log.add('a')", (_, __) => null); - child.watch("log.add('b')", (_, __) => null); - rootScope.digest(); - expect(log.join('')).toEqual('abab'); - })); - - - it(r'should not fire upon watch registration on initial digest', inject((RootScope rootScope) { - var log = ''; - rootScope.context['a'] = 1; - rootScope.watch('a', (a, b) { log += 'a'; }); - rootScope.watch('b', (a, b) { log += 'b'; }); - rootScope.digest(); - log = ''; - rootScope.digest(); - expect(log).toEqual(''); - })); - - - it(r'should prevent digest recursion', inject((RootScope rootScope) { - var callCount = 0; - rootScope.watch('name', (a, b) { - expect(() { - rootScope.digest(); - }).toThrow(r'digest already in progress'); - callCount++; - }); - rootScope.context['name'] = 'a'; - rootScope.digest(); - expect(callCount).toEqual(1); - })); - - - it(r'should return a function that allows listeners to be unregistered', inject( - (RootScope rootScope) { - var listener = jasmine.createSpy('watch listener'); - var watch; - - watch = rootScope.watch('foo', listener); - rootScope.digest(); //init - expect(listener).toHaveBeenCalled(); - expect(watch).toBeDefined(); - - listener.reset(); - rootScope.context['foo'] = 'bar'; - rootScope.digest(); //triger - expect(listener).toHaveBeenCalledOnce(); - - listener.reset(); - rootScope.context['foo'] = 'baz'; - watch.remove(); - rootScope.digest(); //trigger - expect(listener).not.toHaveBeenCalled(); - })); - - - it(r'should not infinitely digest when current value is NaN', inject((RootScope rootScope) { - rootScope.context['nan'] = double.NAN; - rootScope.watch('nan', (_, __) => null); - - expect(() { - rootScope.digest(); - }).not.toThrow(); - })); - - - it(r'should prevent infinite digest and should log firing expressions', inject((RootScope rootScope) { - rootScope.context['a'] = 0; - rootScope.context['b'] = 0; - rootScope.watch('a', (a, __) => rootScope.context['a'] = a + 1); - rootScope.watch('b', (b, __) => rootScope.context['b'] = b + 1); - - expect(() { - rootScope.digest(); - }).toThrow('Model did not stabilize in 5 digests. ' - 'Last 3 iterations:\n' - 'a, b\n' - 'a, b\n' - 'a, b'); - })); - - - it(r'should always call the watchr with newVal and oldVal equal on the first run', - inject((RootScope rootScope) { - var log = []; - var logger = (newVal, oldVal) { - var val = (newVal == oldVal || (newVal != oldVal && oldVal != newVal)) ? newVal : 'xxx'; - log.add(val); - }; - - rootScope.context['nanValue'] = double.NAN; - rootScope.context['nullValue'] = null; - rootScope.context['emptyString'] = ''; - rootScope.context['falseValue'] = false; - rootScope.context['numberValue'] = 23; - - rootScope.watch('nanValue', logger); - rootScope.watch('nullValue', logger); - rootScope.watch('emptyString', logger); - rootScope.watch('falseValue', logger); - rootScope.watch('numberValue', logger); - - rootScope.digest(); - expect(log.removeAt(0).isNaN).toEqual(true); //jasmine's toBe and toEqual don't work well with NaNs - expect(log).toEqual([null, '', false, 23]); - log = []; - rootScope.digest(); - expect(log).toEqual([]); - })); - }); - - - describe('runAsync', () { - it(r'should run callback before watch', inject((RootScope rootScope) { - var log = ''; - rootScope.runAsync(() { log += 'parent.async;'; }); - rootScope.watch('value', (_, __) { log += 'parent.digest;'; }); - rootScope.digest(); - expect(log).toEqual('parent.async;parent.digest;'); - })); - - it(r'should cause a $digest rerun', inject((RootScope rootScope) { - rootScope.context['log'] = ''; - rootScope.context['value'] = 0; - // NOTE(deboer): watch listener string functions not yet supported - //rootScope.watch('value', 'log = log + ".";'); - rootScope.watch('value', (_, __) { rootScope.context['log'] += "."; }); - rootScope.watch('init', (_, __) { - rootScope.runAsync(() => rootScope.eval('value = 123; log = log + "=" ')); - expect(rootScope.context['value']).toEqual(0); - }); - rootScope.digest(); - expect(rootScope.context['log']).toEqual('.=.'); - })); - - it(r'should run async in the same order as added', inject((RootScope rootScope) { - rootScope.context['log'] = ''; - rootScope.runAsync(() => rootScope.eval("log = log + 1")); - rootScope.runAsync(() => rootScope.eval("log = log + 2")); - rootScope.digest(); - expect(rootScope.context['log']).toEqual('12'); - })); - }); - - - - describe('domRead/domWrite', () { - it(r'should run writes before reads', () { - module((Module module) { - module.type(ExceptionHandler, implementedBy: LoggingExceptionHandler); - }); - inject((RootScope rootScope, Logger logger, ExceptionHandler e) { - LoggingExceptionHandler exceptionHandler = e as LoggingExceptionHandler; - rootScope.domWrite(() { - logger('write1'); - rootScope.domWrite(() => logger('write2')); - throw 'write1'; - }); - rootScope.domRead(() { - logger('read1'); - rootScope.domRead(() => logger('read2')); - rootScope.domWrite(() => logger('write3')); - throw 'read1'; - }); - rootScope.observe('value', (_, __) => logger('observe')); - rootScope.flush(); - expect(logger).toEqual(['write1', 'write2', 'observe', 'read1', 'read2', 'write3']); - expect(exceptionHandler.errors.length).toEqual(2); - expect(exceptionHandler.errors[0].error).toEqual('write1'); - expect(exceptionHandler.errors[1].error).toEqual('read1'); - }); - }); - }); -}); - -@NgFilter(name: 'multiply') -class _MultiplyFilter { - call(a, b) => a * b; -} - -@NgFilter(name: 'listHead') -class _ListHeadFilter { - Logger logger; - _ListHeadFilter(Logger this.logger); - call(list, head) { - logger('listHead'); - return [head]..addAll(list); - } -} - - -@NgFilter(name: 'listTail') -class _ListTailFilter { - Logger logger; - _ListTailFilter(Logger this.logger); - call(list, tail) { - logger('listTail'); - return new List.from(list)..add(tail); - } -} - -@NgFilter(name: 'sort') -class _SortFilter { - Logger logger; - _SortFilter(Logger this.logger); - call(list) { - logger('sort'); - return new List.from(list)..sort(); - } -} diff --git a/test/core/scope_spec.dart b/test/core/scope_spec.dart index 075237145..6182c440f 100644 --- a/test/core/scope_spec.dart +++ b/test/core/scope_spec.dart @@ -1,1374 +1,1067 @@ -library scope_spec; +library scope2_spec; import '../_specs.dart'; -import 'dart:convert' show JSON; - - -main() { - describe(r'Scope', () { - NgZone zone; - - noop() {} - - beforeEach(module(() { - return (NgZone _zone) { - zone = _zone; - zone.onError = (e, s, l) => null; - }; +import 'package:angular/change_detection/change_detection.dart' hide ExceptionHandler; +import 'package:angular/change_detection/dirty_checking_change_detector.dart'; + +main() => describe('scope', () { + beforeEach(module((Module module) { + Map context = {}; + module.value(GetterCache, new GetterCache({})); + module.type(ChangeDetector, implementedBy: DirtyCheckingChangeDetector); + module.value(Object, context); + module.value(Map, context); + module.type(RootScope); + module.type(_MultiplyFilter); + module.type(_ListHeadFilter); + module.type(_ListTailFilter); + module.type(_SortFilter); + })); + + describe('AST Bridge', () { + it('should watch field', inject((Logger logger, Map context, RootScope rootScope) { + context['field'] = 'Worked!'; + rootScope.watch('field', (value, previous) => logger([value, previous])); + expect(logger).toEqual([]); + rootScope.digest(); + expect(logger).toEqual([['Worked!', null]]); + rootScope.digest(); + expect(logger).toEqual([['Worked!', null]]); })); - describe(r'$root', () { - it(r'should point to itself', inject((Scope $rootScope) { - expect($rootScope.$root).toEqual($rootScope); - expect($rootScope.$root).toEqual($rootScope); - expect($rootScope.$root).toBeTruthy(); - })); + it('should watch field path', inject((Logger logger, Map context, RootScope rootScope) { + context['a'] = {'b': 'AB'}; + rootScope.watch('a.b', (value, previous) => logger(value)); + rootScope.digest(); + expect(logger).toEqual(['AB']); + context['a']['b'] = '123'; + rootScope.digest(); + expect(logger).toEqual(['AB', '123']); + context['a'] = {'b': 'XYZ'}; + rootScope.digest(); + expect(logger).toEqual(['AB', '123', 'XYZ']); + })); + it('should watch math operations', inject((Logger logger, Map context, RootScope rootScope) { + context['a'] = 1; + context['b'] = 2; + rootScope.watch('a + b + 1', (value, previous) => logger(value)); + rootScope.digest(); + expect(logger).toEqual([4]); + context['a'] = 3; + rootScope.digest(); + expect(logger).toEqual([4, 6]); + context['b'] = 5; + rootScope.digest(); + expect(logger).toEqual([4, 6, 9]); + })); - it(r'should not have $root on children, but should inherit', inject((Scope $rootScope) { - var child = $rootScope.$new(); - expect(child.$root).toEqual($rootScope); - expect(child._$root).toBeFalsy(); - })); - }); + it('should watch literals', inject((Logger logger, Map context, RootScope rootScope) { + context['a'] = 1; + rootScope.watch('1', (value, previous) => logger(value)); + rootScope.watch('"str"', (value, previous) => logger(value)); + rootScope.watch('[a, 2, 3]', (value, previous) => logger(value)); + rootScope.watch('{a:a, b:2}', (value, previous) => logger(value)); + rootScope.digest(); + expect(logger).toEqual([1, 'str', [1, 2, 3], {'a': 1, 'b': 2}]); + logger.clear(); + context['a'] = 3; + rootScope.digest(); + expect(logger).toEqual([[3, 2, 3], {'a': 3, 'b': 2}]); + })); + it('should invoke closures', inject((Logger logger, Map context, RootScope rootScope) { + context['fn'] = () { + logger('fn'); + return 1; + }; + context['a'] = {'fn': () { + logger('a.fn'); + return 2; + }}; + rootScope.watch('fn()', (value, previous) => logger('=> $value')); + rootScope.watch('a.fn()', (value, previous) => logger('-> $value')); + rootScope.digest(); + expect(logger).toEqual(['fn', 'a.fn', '=> 1', '-> 2', + /* second loop*/ 'fn', 'a.fn']); + logger.clear(); + rootScope.digest(); + expect(logger).toEqual(['fn', 'a.fn']); + })); - describe(r'$parent', () { - it(r'should point to itself in root', inject((Scope $rootScope) { - expect($rootScope.$root).toEqual($rootScope); - })); + it('should perform conditionals', inject((Logger logger, Map context, RootScope rootScope) { + context['a'] = 1; + context['b'] = 2; + context['c'] = 3; + rootScope.watch('a?b:c', (value, previous) => logger(value)); + rootScope.digest(); + expect(logger).toEqual([2]); + logger.clear(); + context['a'] = 0; + rootScope.digest(); + expect(logger).toEqual([3]); + })); - it(r'should point to parent', inject((Scope $rootScope) { - var child = $rootScope.$new(); - expect($rootScope.$parent).toEqual(null); - expect(child.$parent).toEqual($rootScope); - expect(child.$new().$parent).toEqual(child); - })); - }); + xit('should call function', inject((Logger logger, Map context, RootScope rootScope) { + context['a'] = () { + return () { return 123; }; + }; + rootScope.watch('a()()', (value, previous) => logger(value)); + rootScope.digest(); + expect(logger).toEqual([123]); + logger.clear(); + rootScope.digest(); + expect(logger).toEqual([]); + })); + it('should access bracket', inject((Logger logger, Map context, RootScope rootScope) { + context['a'] = {'b': 123}; + rootScope.watch('a["b"]', (value, previous) => logger(value)); + rootScope.digest(); + expect(logger).toEqual([123]); + logger.clear(); + rootScope.digest(); + expect(logger).toEqual([]); + })); - describe(r'$id', () { - it(r'should have a unique id', inject((Scope $rootScope) { - expect($rootScope.$id != $rootScope.$new().$id).toBe(true); - })); - }); + it('should prefix', inject((Logger logger, Map context, RootScope rootScope) { + context['a'] = true; + rootScope.watch('!a', (value, previous) => logger(value)); + rootScope.digest(); + expect(logger).toEqual([false]); + logger.clear(); + context['a'] = false; + rootScope.digest(); + expect(logger).toEqual([true]); + })); - describe(r'this', () { - it('should have a \'this\'', inject((Scope $rootScope) { - expect($rootScope['this']).toEqual($rootScope); - })); - }); + it('should support filters', inject((Logger logger, Map context, + RootScope rootScope, AstParser parser, + FilterMap filters) { + context['a'] = 123; + context['b'] = 2; + rootScope.watch( + parser('a | multiply:b', filters: filters), + (value, previous) => logger(value)); + rootScope.digest(); + expect(logger).toEqual([246]); + logger.clear(); + rootScope.digest(); + expect(logger).toEqual([]); + logger.clear(); + })); + it('should support arrays in filters', inject((Logger logger, Map context, + RootScope rootScope, + AstParser parser, + FilterMap filters) { + context['a'] = [1]; + rootScope.watch( + parser('a | sort | listHead:"A" | listTail:"B"', filters: filters), + (value, previous) => logger(value)); + rootScope.digest(); + expect(logger).toEqual(['sort', 'listHead', 'listTail', ['A', 1, 'B']]); + logger.clear(); + + rootScope.digest(); + expect(logger).toEqual([]); + logger.clear(); + + context['a'].add(2); + rootScope.digest(); + expect(logger).toEqual(['sort', 'listHead', 'listTail', ['A', 1, 2, 'B']]); + logger.clear(); + + // We change the order, but sort should change it to same one and it should not + // call subsequent filters. + context['a'] = [2, 1]; + rootScope.digest(); + expect(logger).toEqual(['sort']); + logger.clear(); + })); + }); - describe(r'$new()', () { - it(r'should create a child scope', inject((Scope $rootScope) { - var child = $rootScope.$new(); - $rootScope.a = 123; - expect(child.a).toEqual(123); + describe('properties', () { + describe('root', () { + it('should point to itself', inject((RootScope rootScope) { + expect(rootScope.rootScope).toEqual(rootScope); })); - it(r'should create a non prototypically inherited child scope', inject((Scope $rootScope) { - var child = $rootScope.$new(isolate: true); - $rootScope.a = 123; - expect(child.a).toEqual(null); - expect(child.$parent).toEqual($rootScope); - expect(child.$root).toBe($rootScope); + it('children should point to root', inject((RootScope rootScope) { + var child = rootScope.createChild(); + expect(child.rootScope).toEqual(rootScope); + expect(child.createChild().rootScope).toEqual(rootScope); })); }); - describe(r'auto digest', () { - it(r'should auto digest at the end of the turn', inject((Scope $rootScope) { - var digestedValue = 0; - $rootScope.a = 1; - $rootScope.$watch('a', (newValue, oldValue, _this) { - digestedValue = newValue; - }); - expect(digestedValue).toEqual(0); - zone.run(noop); - expect(digestedValue).toEqual(1); + describe('parent', () { + it('should not have parent', inject((RootScope rootScope) { + expect(rootScope.parentScope).toEqual(null); })); - it(r'should skip auto digest if requested', inject((Scope $rootScope) { - var digestedValue = 0; - $rootScope.a = 1; - $rootScope.$watch('a', (newValue, oldValue, _this) { - digestedValue = newValue; - }); - expect(digestedValue).toEqual(0); - zone.run(() { - $rootScope.$skipAutoDigest(); - }); - expect(digestedValue).toEqual(0); - zone.run(noop); - expect(digestedValue).toEqual(1); - })); - it(r'should skip auto digest if requested on any scope', inject((Scope $rootScope) { - var scope = $rootScope.$new(); - var digestedValue = 0; - scope.a = 1; - scope.$watch('a', (newValue, oldValue, _this) { - digestedValue = newValue; - }); - expect(digestedValue).toEqual(0); - zone.run(() { - scope.$skipAutoDigest(); - }); - expect(digestedValue).toEqual(0); - zone.run(noop); - expect(digestedValue).toEqual(1); - })); - - it(r'should throw exception if asked to skip auto digest outside of a turn', - inject((Scope $rootScope) { - var digestedValue = 0; - $rootScope.a = 1; - $rootScope.$watch('a', (newValue, oldValue, _this) { - digestedValue = newValue; - }); - expect(digestedValue).toEqual(0); - expect($rootScope.$skipAutoDigest).toThrow(); + it('should point to parent', inject((RootScope rootScope) { + var child = rootScope.createChild(); + expect(rootScope.parentScope).toEqual(null); + expect(child.parentScope).toEqual(rootScope); + expect(child.createChild().parentScope).toEqual(child); })); }); + }); + describe(r'events', () { - describe(r'$watch/$digest', () { - it(r'should watch and fire on simple property change', inject((Scope $rootScope) { - var log; - - $rootScope.$watch('name', (a, b, c) { - log = [a, b, c]; - }); - $rootScope.$digest(); - log = null; - - expect(log).toEqual(null); - $rootScope.$digest(); - expect(log).toEqual(null); - $rootScope.name = 'misko'; - $rootScope.$digest(); - expect(log).toEqual(['misko', null, $rootScope]); - })); - - - it(r'should watch and fire on expression change', inject((Scope $rootScope) { - var log; - - $rootScope.$watch('name.first', (a, b, c) { - log = [a, b, c]; - }); - $rootScope.$digest(); - log = null; - - $rootScope.name = {}; - expect(log).toEqual(null); - $rootScope.$digest(); - expect(log).toEqual(null); - $rootScope.name['first'] = 'misko'; - $rootScope.$digest(); - expect(log).toEqual(['misko', null, $rootScope]); - })); + describe('on', () { + it(r'should add listener for both emit and broadcast events', inject((RootScope rootScope) { + var log = '', + child = rootScope.createChild(); + eventFn(event) { + expect(event).not.toEqual(null); + log += 'X'; + } - it(r'should delegate exceptions', () { - module((Module module) { - module.type(ExceptionHandler, implementedBy: LoggingExceptionHandler); - }); - inject((Scope $rootScope, ExceptionHandler e) { - LoggingExceptionHandler $exceptionHandler = e; - $rootScope.$watch('a', () {throw 'abc';}); - $rootScope.a = 1; - $rootScope.$digest(); - expect($exceptionHandler.errors.length).toEqual(1); - expect($exceptionHandler.errors[0].error).toEqual('abc'); - }); - }); + child.on('abc').listen(eventFn); + expect(log).toEqual(''); + child.emit('abc'); + expect(log).toEqual('X'); - it(r'should fire watches in order of addition', inject((Scope $rootScope) { - // this is not an external guarantee, just our own sanity - var log = ''; - $rootScope.$watch('a', (a, b, c) { log += 'a'; }); - $rootScope.$watch('b', (a, b, c) { log += 'b'; }); - $rootScope.$watch('c', (a, b, c) { log += 'c'; }); - $rootScope.a = $rootScope.b = $rootScope.c = 1; - $rootScope.$digest(); - expect(log).toEqual('abc'); + child.broadcast('abc'); + expect(log).toEqual('XX'); })); - it(r'should call child $watchers in addition order', inject((Scope $rootScope) { - // this is not an external guarantee, just our own sanity + it(r'should return a function that deregisters the listener', inject((RootScope rootScope) { var log = ''; - var childA = $rootScope.$new(); - var childB = $rootScope.$new(); - var childC = $rootScope.$new(); - childA.$watch('a', (a, b, c) { log += 'a'; }); - childB.$watch('b', (a, b, c) { log += 'b'; }); - childC.$watch('c', (a, b, c) { log += 'c'; }); - childA.a = childB.b = childC.c = 1; - $rootScope.$digest(); - expect(log).toEqual('abc'); - })); + var child = rootScope.createChild(); + var subscription; + eventFn(e) { + log += 'X'; + } - it(r'should allow $digest on a child scope with and without a right sibling', inject( - (Scope $rootScope) { - // tests a traversal edge case which we originally missed - var log = [], - childA = $rootScope.$new(), - childB = $rootScope.$new(); + subscription = child.on('abc').listen(eventFn); + expect(log).toEqual(''); + expect(subscription).toBeDefined(); - $rootScope.$watch((a) { log.add('r'); }); - childA.$watch((a) { log.add('a'); }); - childB.$watch((a) { log.add('b'); }); + child.emit(r'abc'); + child.broadcast('abc'); + expect(log).toEqual('XX'); - // init - $rootScope.$digest(); - expect(log.join('')).toEqual('rabra'); + log = ''; + expect(subscription.cancel()).toBe(null); + child.emit(r'abc'); + child.broadcast('abc'); + expect(log).toEqual(''); + })); + }); - log.removeWhere((e) => true); - childA.$digest(); - expect(log.join('')).toEqual('a'); - log.removeWhere((e) => true); - childB.$digest(); - expect(log.join('')).toEqual('b'); - })); + describe('emit', () { + var log, child, grandChild, greatGrandChild; + logger(event) { + log.add(event.currentScope.context['id']); + } - it(r'should repeat watch cycle while model changes are identified', inject((Scope $rootScope) { - var log = ''; - $rootScope.$watch('c', (v, b, c) {$rootScope.d = v; log+='c'; }); - $rootScope.$watch('b', (v, b, c) {$rootScope.c = v; log+='b'; }); - $rootScope.$watch('a', (v, b, c) {$rootScope.b = v; log+='a'; }); - $rootScope.$digest(); - log = ''; - $rootScope.a = 1; - $rootScope.$digest(); - expect($rootScope.b).toEqual(1); - expect($rootScope.c).toEqual(1); - expect($rootScope.d).toEqual(1); - expect(log).toEqual('abc'); - })); + beforeEach(module(() { + return (RootScope rootScope) { + log = []; + child = rootScope.createChild({'id': 1}); + grandChild = child.createChild({'id': 2}); + greatGrandChild = grandChild.createChild({'id': 3}); + rootScope.context['id'] = 0; - it(r'should repeat watch cycle from the root element', inject((Scope $rootScope) { - var log = ''; - var child = $rootScope.$new(); - $rootScope.$watch((a) { log += 'a'; }); - child.$watch((a) { log += 'b'; }); - $rootScope.$digest(); - expect(log).toEqual('aba'); + rootScope.on('myEvent').listen(logger); + child.on('myEvent').listen(logger); + grandChild.on('myEvent').listen(logger); + greatGrandChild.on('myEvent').listen(logger); + }; })); - - it(r'should not fire upon $watch registration on initial $digest', inject((Scope $rootScope) { - var log = ''; - $rootScope.a = 1; - $rootScope.$watch('a', (a, b, c) { log += 'a'; }); - $rootScope.$watch('b', (a, b, c) { log += 'b'; }); - $rootScope.$digest(); - log = ''; - $rootScope.$digest(); - expect(log).toEqual(''); + it(r'should bubble event up to the root scope', inject((RootScope rootScope) { + grandChild.emit(r'myEvent'); + expect(log.join('>')).toEqual('2>1>0'); })); - it(r'should watch functions', () { + it(r'should dispatch exceptions to the exceptionHandler', () { module((Module module) { module.type(ExceptionHandler, implementedBy: LoggingExceptionHandler); }); - inject((Scope $rootScope, ExceptionHandler e) { + inject((ExceptionHandler e) { LoggingExceptionHandler exceptionHandler = e; - $rootScope.fn = () {return 'a';}; - $rootScope.$watch('fn', (fn, a, b) { - exceptionHandler.errors.add(fn()); - }); - $rootScope.$digest(); - expect(exceptionHandler.errors).toEqual(['a']); - $rootScope.fn = () {return 'b';}; - $rootScope.$digest(); - expect(exceptionHandler.errors).toEqual(['a', 'b']); + child.on('myEvent').listen((e) { throw 'bubbleException'; }); + grandChild.emit(r'myEvent'); + expect(log.join('>')).toEqual('2>1>0'); + expect(exceptionHandler.errors[0].error).toEqual('bubbleException'); }); }); - it(r'should prevent $digest recursion', inject((Scope $rootScope) { - var callCount = 0; - $rootScope.$watch('name', (a, b, c) { - expect(() { - $rootScope.$digest(); - }).toThrow(r'$digest already in progress'); - callCount++; - }); - $rootScope.name = 'a'; - $rootScope.$digest(); - expect(callCount).toEqual(1); + it(r'should allow stopping event propagation', inject((RootScope rootScope) { + child.on('myEvent').listen((event) { event.stopPropagation(); }); + grandChild.emit(r'myEvent'); + expect(log.join('>')).toEqual('2>1'); })); - it(r'should return a function that allows listeners to be unregistered', inject( - (Scope $rootScope) { - var listener = jasmine.createSpy('watch listener'), - listenerRemove; - - listenerRemove = $rootScope.$watch('foo', listener); - $rootScope.$digest(); //init - expect(listener).toHaveBeenCalled(); - expect(listenerRemove).toBeDefined(); - - listener.reset(); - $rootScope.foo = 'bar'; - $rootScope.$digest(); //triger - expect(listener).toHaveBeenCalledOnce(); - - listener.reset(); - $rootScope.foo = 'baz'; - listenerRemove(); - $rootScope.$digest(); //trigger - expect(listener).not.toHaveBeenCalled(); + it(r'should forward method arguments', inject((RootScope rootScope) { + var eventName; + var eventData; + child.on('abc').listen((event) { + eventName = event.name; + eventData = event.data; + }); + child.emit('abc', ['arg1', 'arg2']); + expect(eventName).toEqual('abc'); + expect(eventData).toEqual(['arg1', 'arg2']); })); - it(r'should not infinitely digest when current value is NaN', inject((Scope $rootScope) { - $rootScope.$watch((a) { return double.NAN;}); - - expect(() { - $rootScope.$digest(); - }).not.toThrow(); - })); - + describe(r'event object', () { + it(r'should have methods/properties', inject((RootScope rootScope) { + var event; + child.on('myEvent').listen((e) { + expect(e.targetScope).toBe(grandChild); + expect(e.currentScope).toBe(child); + expect(e.name).toBe('myEvent'); + event = e; + }); + grandChild.emit(r'myEvent'); + expect(event).toBeDefined(); + })); - it(r'should prevent infinite digest and should log firing expressions', inject((Scope $rootScope) { - $rootScope['a'] = 0; - $rootScope['b'] = 0; - $rootScope.$watch('a = a + 1'); - $rootScope.$watch('b = b + 1'); - expect(() { - $rootScope.$digest(); - }).toThrow('Watchers fired in the last 3 iterations: [' - '["a = a + 1","b = b + 1"],' - '["a = a + 1","b = b + 1"],' - '["a = a + 1","b = b + 1"]' - ']'); - })); + it(r'should have preventDefault method and defaultPrevented property', inject((RootScope rootScope) { + var event = grandChild.emit(r'myEvent'); + expect(event.defaultPrevented).toBe(false); + child.on('myEvent').listen((event) { + event.preventDefault(); + }); + event = grandChild.emit(r'myEvent'); + expect(event.defaultPrevented).toBe(true); + })); + }); + }); - it(r'should always call the watchr with newVal and oldVal equal on the first run', - inject((Scope $rootScope) { - var log = []; - var logger = (scope, newVal, oldVal) { - var val = (newVal == oldVal || (newVal != oldVal && oldVal != newVal)) ? newVal : 'xxx'; - log.add(val); - }; - $rootScope.$watch((s) { return double.NAN;}, logger); - $rootScope.$watch((s) { return null;}, logger); - $rootScope.$watch((s) { return '';}, logger); - $rootScope.$watch((s) { return false;}, logger); - $rootScope.$watch((s) { return 23;}, logger); - - $rootScope.$digest(); - expect(log.removeAt(0).isNaN).toEqual(true); //jasmine's toBe and toEqual don't work well with NaNs - expect(log).toEqual([null, '', false, 23]); - log = []; - $rootScope.$digest(); - expect(log).toEqual([]); - })); + describe('broadcast', () { + describe(r'event propagation', () { + var log, child1, child2, child3, grandChild11, grandChild21, grandChild22, grandChild23, + greatGrandChild211; - describe('lazy digest', () { - var rootScope, lazyScope, eagerScope; + logger(event) { + log.add(event.currentScope.context['id']); + } - beforeEach(inject((Scope root) { - rootScope = root; - lazyScope = root.$new(lazy: true); - eagerScope = root.$new(); + beforeEach(inject((RootScope rootScope) { + log = []; + child1 = rootScope.createChild({}); + child2 = rootScope.createChild({}); + child3 = rootScope.createChild({}); + grandChild11 = child1.createChild({}); + grandChild21 = child2.createChild({}); + grandChild22 = child2.createChild({}); + grandChild23 = child2.createChild({}); + greatGrandChild211 = grandChild21.createChild({}); + + rootScope.context['id'] = 0; + child1.context['id'] = 1; + child2.context['id'] = 2; + child3.context['id'] = 3; + grandChild11.context['id'] = 11; + grandChild21.context['id'] = 21; + grandChild22.context['id'] = 22; + grandChild23.context['id'] = 23; + greatGrandChild211.context['id'] = 211; + + rootScope.on('myEvent').listen(logger); + child1.on('myEvent').listen(logger); + child2.on('myEvent').listen(logger); + child3.on('myEvent').listen(logger); + grandChild11.on('myEvent').listen(logger); + grandChild21.on('myEvent').listen(logger); + grandChild22.on('myEvent').listen(logger); + grandChild23.on('myEvent').listen(logger); + greatGrandChild211.on('myEvent').listen(logger); + + // R + // / | \ + // 1 2 3 + // / / | \ + // 11 21 22 23 + // | + // 211 })); - it('should digest initially', () { - var log = ''; - lazyScope.$watch(() {log += 'lazy;';}); - eagerScope.$watch(() {log += 'eager;';}); - rootScope.$digest(); - expect(log).toEqual('lazy;eager;'); + it(r'should broadcast an event from the root scope', inject((RootScope rootScope) { + rootScope.broadcast('myEvent'); + expect(log.join('>')).toEqual('0>1>11>2>21>211>22>23>3'); + })); - rootScope.$digest(); - expect(log).toEqual('lazy;eager;eager;'); - lazyScope.$dirty(); - rootScope.$digest(); - expect(log).toEqual('lazy;eager;eager;lazy;eager;'); - }); - }); + it(r'should broadcast an event from a child scope', inject((RootScope rootScope) { + child2.broadcast('myEvent'); + expect(log.join('>')).toEqual('2>21>211>22>23'); + })); - describe('disabled digest', () { - var rootScope, childScope; - beforeEach(inject((Scope root) { - rootScope = root; - childScope = root.$new(); + it(r'should broadcast an event from a leaf scope with a sibling', inject((RootScope rootScope) { + grandChild22.broadcast('myEvent'); + expect(log.join('>')).toEqual('22'); })); - it('should disable digest', () { - var log = ''; - childScope.$watch(() {log += 'digest;';}); - rootScope.$digest(); - expect(log).toEqual('digest;'); + it(r'should broadcast an event from a leaf scope without a sibling', inject((RootScope rootScope) { + grandChild23.broadcast('myEvent'); + expect(log.join('>')).toEqual('23'); + })); - childScope.$disabled = true; - expect(childScope.$disabled).toEqual(true); - rootScope.$digest(); - expect(log).toEqual('digest;'); - childScope.$disabled = false; - expect(childScope.$disabled).toEqual(false); - rootScope.$digest(); - expect(log).toEqual('digest;digest;'); - }); - }); - }); + it(r'should not not fire any listeners for other events', inject((RootScope rootScope) { + rootScope.broadcast('fooEvent'); + expect(log.join('>')).toEqual(''); + })); - describe(r'$watchSet', () { - var scope; - beforeEach(inject((Scope s) => scope = s)); + it(r'should return event object', inject((RootScope rootScope) { + var result = child1.broadcast('some'); - it('should skip empty sets', () { - expect(scope.$watchSet([], null)()).toBe(null); + expect(result).toBeDefined(); + expect(result.name).toBe('some'); + expect(result.targetScope).toBe(child1); + })); }); - it('should treat set of 1 as direct watch', () { - var lastValues = ['foo']; - var log = ''; - var clean = scope.$watchSet(['a'], (values, oldValues, s) { - log += values.join(',') + ';'; - expect(s).toBe(scope); - expect(oldValues).toEqual(lastValues); - lastValues = new List.from(values); - }); - - scope.a = 'foo'; - scope.$digest(); - expect(log).toEqual('foo;'); - scope.$digest(); - expect(log).toEqual('foo;'); - - scope.a = 'bar'; - scope.$digest(); - expect(log).toEqual('foo;bar;'); - - clean(); - scope.a = 'xxx'; - scope.$digest(); - expect(log).toEqual('foo;bar;'); - }); - - it('should detect a change to any one in a set', () { - var lastValues = ['foo', 'bar']; - var log = ''; - var clean = scope.$watchSet(['a', 'b'], (values, oldValues, s) { - log += values.join(',') + ';'; - expect(oldValues).toEqual(lastValues); - lastValues = new List.from(values); - }); + describe(r'listener', () { + it(r'should receive event object', inject((RootScope rootScope) { + var scope = rootScope, + child = scope.createChild({}), + event; - scope.a = 'foo'; - scope.b = 'bar'; - scope.$digest(); - expect(log).toEqual('foo,bar;'); + child.on('fooEvent').listen((e) { + event = e; + }); + scope.broadcast('fooEvent'); - scope.$digest(); - expect(log).toEqual('foo,bar;'); + expect(event.name).toBe('fooEvent'); + expect(event.targetScope).toBe(scope); + expect(event.currentScope).toBe(child); + })); - scope.a = 'a'; - scope.$digest(); - expect(log).toEqual('foo,bar;a,bar;'); + it(r'should support passing messages as varargs', inject((RootScope rootScope) { + var scope = rootScope, + child = scope.createChild({}), + args; - scope.a = 'A'; - scope.b = 'B'; - scope.$digest(); - expect(log).toEqual('foo,bar;a,bar;A,B;'); + child.on('fooEvent').listen((e) { + args = e.data; + }); + scope.broadcast('fooEvent', ['do', 're', 'me', 'fa']); - clean(); - scope.a = 'xxx'; - scope.$digest(); - expect(log).toEqual('foo,bar;a,bar;A,B;'); + expect(args.length).toBe(4); + expect(args).toEqual(['do', 're', 'me', 'fa']); + })); }); }); + }); + describe(r'$destroy', () { + var first = null, middle = null, last = null, log = null; - describe(r'$destroy', () { - var first = null, middle = null, last = null, log = null; - - beforeEach(inject((Scope $rootScope) { - log = ''; - - first = $rootScope.$new(); - middle = $rootScope.$new(); - last = $rootScope.$new(); + beforeEach(inject((RootScope rootScope) { + log = ''; - first.$watch((s) { log += '1';}); - middle.$watch((s) { log += '2';}); - last.$watch((s) { log += '3';}); + first = rootScope.createChild({"check": (n) { log+= '$n'; return n;}}); + middle = rootScope.createChild({"check": (n) { log+= '$n'; return n;}}); + last = rootScope.createChild({"check": (n) { log+= '$n'; return n;}}); - $rootScope.$digest(); - log = ''; - })); + first.watch('check(1)', (v, l) {}); + middle.watch('check(2)', (v, l) {}); + last.watch('check(3)', (v, l) {}); + first.on(ScopeEvent.DESTROY).listen((e) { log += 'destroy:first;'; }); - it(r'should ignore remove on root', inject((Scope $rootScope) { - $rootScope.$destroy(); - $rootScope.$digest(); - expect(log).toEqual('123'); - })); - - - it(r'should remove first', inject((Scope $rootScope) { - first.$destroy(); - $rootScope.$digest(); - expect(log).toEqual('23'); - })); + rootScope.digest(); + log = ''; + })); - it(r'should remove middle', inject((Scope $rootScope) { - middle.$destroy(); - $rootScope.$digest(); - expect(log).toEqual('13'); - })); + it(r'should ignore remove on root', inject((RootScope rootScope) { + rootScope.destroy(); + rootScope.digest(); + expect(log).toEqual('123'); + })); - it(r'should remove last', inject((Scope $rootScope) { - last.$destroy(); - $rootScope.$digest(); - expect(log).toEqual('12'); - })); + it(r'should remove first', inject((RootScope rootScope) { + first.destroy(); + rootScope.digest(); + expect(log).toEqual('destroy:first;23'); + })); - it(r'should broadcast the $destroy event', inject((Scope $rootScope) { - var log = []; - first.$on(r'$destroy', (s) => log.add('first')); - first.$new().$on(r'$destroy', (s) => log.add('first-child')); + it(r'should remove middle', inject((RootScope rootScope) { + middle.destroy(); + rootScope.digest(); + expect(log).toEqual('13'); + })); - first.$destroy(); - expect(log).toEqual(['first', 'first-child']); - })); - }); + it(r'should remove last', inject((RootScope rootScope) { + last.destroy(); + rootScope.digest(); + expect(log).toEqual('12'); + })); - describe(r'$eval', () { - it(r'should eval an expression', inject((Scope $rootScope) { - expect($rootScope.$eval('a=1')).toEqual(1); - expect($rootScope.a).toEqual(1); - $rootScope.$eval((self, locals) {self.b=2;}); - expect($rootScope.b).toEqual(2); - })); + it(r'should broadcast the $destroy event', inject((RootScope rootScope) { + var log = []; + first.on(ScopeEvent.DESTROY).listen((s) => log.add('first')); + first.createChild({}).on(ScopeEvent.DESTROY).listen((s) => log.add('first-child')); + first.destroy(); + expect(log).toEqual(['first', 'first-child']); + })); + }); + + describe('digest lifecycle', () { + it(r'should apply expression with full lifecycle', inject((RootScope rootScope) { + var log = ''; + var child = rootScope.createChild({"parent": rootScope.context}); + rootScope.watch('a', (a, _) { log += '1'; }); + child.apply('parent.a = 0'); + expect(log).toEqual('1'); + })); - it(r'should allow passing locals to the expression', inject((Scope $rootScope) { - expect($rootScope.$eval('a+1', {"a": 2})).toBe(3); - $rootScope.$eval((scope) { - scope['c'] = scope['b'] + 4; - }, {"b": 3}); - expect($rootScope.c).toBe(7); - })); + it(r'should catch exceptions', () { + module((Module module) => module.type(ExceptionHandler, implementedBy: LoggingExceptionHandler)); + inject((RootScope rootScope, ExceptionHandler e) { + LoggingExceptionHandler $exceptionHandler = e; + var log = []; + var child = rootScope.createChild({}); + rootScope.watch('a', (a, _) => log.add('1')); + rootScope.context['a'] = 0; + child.apply(() { throw 'MyError'; }); + expect(log.join(',')).toEqual('1'); + expect($exceptionHandler.errors[0].error).toEqual('MyError'); + $exceptionHandler.errors.removeAt(0); + $exceptionHandler.assertEmpty(); + }); }); - describe(r'$evalAsync', () { - - it(r'should run callback before $watch', inject((Scope $rootScope) { - var log = ''; - var child = $rootScope.$new(); - $rootScope.$evalAsync((scope, _) { log += 'parent.async;'; }); - $rootScope.$watch('value', (_, _0, _1) { log += 'parent.\$digest;'; }); - child.$evalAsync((scope, _) { log += 'child.async;'; }); - child.$watch('value', (_, _0, _1) { log += 'child.\$digest;'; }); - $rootScope.$digest(); - expect(log).toEqual('parent.async;child.async;parent.\$digest;child.\$digest;'); - })); - - it(r'should cause a $digest rerun', inject((Scope $rootScope) { - $rootScope.log = ''; - $rootScope.value = 0; - // NOTE(deboer): watch listener string functions not yet supported - //$rootScope.$watch('value', 'log = log + ".";'); - $rootScope.$watch('value', (__, _, scope) { scope.log = scope.log + "."; }); - $rootScope.$watch('init', (_, __, _0) { - $rootScope.$evalAsync('value = 123; log = log + "=" '); - expect($rootScope.value).toEqual(0); - }); - $rootScope.$digest(); - expect($rootScope.log).toEqual('.=.'); + describe(r'exceptions', () { + var log; + beforeEach(module((Module module) { + return module.type(ExceptionHandler, implementedBy: LoggingExceptionHandler); })); - - it(r'should run async in the same order as added', inject((Scope $rootScope) { - $rootScope.log = ''; - $rootScope.$evalAsync("log = log + 1"); - $rootScope.$evalAsync("log = log + 2"); - $rootScope.$digest(); - expect($rootScope.log).toEqual('12'); + beforeEach(inject((RootScope rootScope) { + rootScope.context['log'] = () { log += 'digest;'; return null; }; + log = ''; + rootScope.watch('log()', (v, o) => null); + rootScope.digest(); + log = ''; })); - it(r'should allow running after digest', inject((Scope $rootScope) { - $rootScope.log = ''; - $rootScope.$evalAsync(() => $rootScope.log += 'eval;', outsideDigest: true); - $rootScope.$watch(() { $rootScope.log += 'digest;'; }); - $rootScope.$digest(); - expect($rootScope.log).toEqual('digest;eval;'); - })); - it(r'should allow running after digest in issolate scope', inject((Scope $rootScope) { - var isolateScope = $rootScope.$new(isolate: true); - isolateScope.log = ''; - isolateScope.$evalAsync(() => isolateScope.log += 'eval;', outsideDigest: true); - isolateScope.$watch(() { isolateScope.log += 'digest;'; }); - isolateScope.$digest(); - expect(isolateScope.log).toEqual('digest;eval;'); + it(r'should execute and return value and update', inject( + (RootScope rootScope, ExceptionHandler e) { + LoggingExceptionHandler $exceptionHandler = e; + rootScope.context['name'] = 'abc'; + expect(rootScope.apply((context) => context['name'])).toEqual('abc'); + expect(log).toEqual('digest;digest;'); + $exceptionHandler.assertEmpty(); })); - }); - - describe(r'$apply', () { - it(r'should apply expression with full lifecycle', inject((Scope $rootScope) { - var log = ''; - var child = $rootScope.$new(); - $rootScope.$watch('a', (a, _, __) { log += '1'; }); - child.$apply(r'$parent.a=0'); - expect(log).toEqual('1'); + it(r'should execute and return value and update', inject((RootScope rootScope) { + rootScope.context['name'] = 'abc'; + expect(rootScope.apply('name', {'name': 123})).toEqual(123); })); - it(r'should catch exceptions', () { - module((Module module) => module.type(ExceptionHandler, implementedBy: LoggingExceptionHandler)); - inject((Scope $rootScope, ExceptionHandler e) { - LoggingExceptionHandler $exceptionHandler = e; - var log = []; - var child = $rootScope.$new(); - $rootScope.$watch('a', (a, _, __) => log.add('1')); - $rootScope.a = 0; - child.$apply((_, __) { throw 'MyError'; }); - expect(log.join(',')).toEqual('1'); - expect($exceptionHandler.errors[0].error).toEqual('MyError'); - $exceptionHandler.errors.removeAt(0); - $exceptionHandler.assertEmpty(); - }); - }); - - - describe(r'exceptions', () { - var log; - beforeEach(module((Module module) { - return module.type(ExceptionHandler, implementedBy: LoggingExceptionHandler); - })); - beforeEach(inject((Scope $rootScope) { - log = ''; - $rootScope.$watch(() { log += '\$digest;'; }); - $rootScope.$digest(); - log = ''; - })); - - - it(r'should execute and return value and update', inject( - (Scope $rootScope, ExceptionHandler e) { - LoggingExceptionHandler $exceptionHandler = e; - $rootScope.name = 'abc'; - expect($rootScope.$apply((scope) => scope.name)).toEqual('abc'); - expect(log).toEqual(r'$digest;'); - $exceptionHandler.assertEmpty(); - })); - - - it(r'should catch exception and update', inject((Scope $rootScope, ExceptionHandler e) { - LoggingExceptionHandler $exceptionHandler = e; - var error = 'MyError'; - $rootScope.$apply(() { throw error; }); - expect(log).toEqual(r'$digest;'); - expect($exceptionHandler.errors[0].error).toEqual(error); - })); - }); - - it(r'should proprely reset phase on exception', inject((Scope $rootScope) { + it(r'should catch exception and update', inject((RootScope rootScope, ExceptionHandler e) { + LoggingExceptionHandler $exceptionHandler = e; var error = 'MyError'; - expect(() =>$rootScope.$apply(() { throw error; })).toThrow(error); - expect(() =>$rootScope.$apply(() { throw error; })).toThrow(error); + rootScope.apply(() { throw error; }); + expect(log).toEqual('digest;digest;'); + expect($exceptionHandler.errors[0].error).toEqual(error); })); }); - - describe(r'events', () { - - describe(r'$on', () { - - it(r'should add listener for both $emit and $broadcast events', inject((Scope $rootScope) { - var log = '', - child = $rootScope.$new(); - - eventFn() { - log += 'X'; - } - - child.$on('abc', eventFn); - expect(log).toEqual(''); - - child.$emit(r'abc'); - expect(log).toEqual('X'); - - child.$broadcast('abc'); - expect(log).toEqual('XX'); - })); - - - it(r'should return a function that deregisters the listener', inject((Scope $rootScope) { - var log = '', - child = $rootScope.$new(), - listenerRemove; - - eventFn() { - log += 'X'; - } - - listenerRemove = child.$on('abc', eventFn); - expect(log).toEqual(''); - expect(listenerRemove).toBeDefined(); - - child.$emit(r'abc'); - child.$broadcast('abc'); - expect(log).toEqual('XX'); - - log = ''; - listenerRemove(); - child.$emit(r'abc'); - child.$broadcast('abc'); - expect(log).toEqual(''); - })); - }); - - - describe(r'$emit', () { - var log, child, grandChild, greatGrandChild; - - logger(event) { - log.add(event.currentScope.id); - } - - beforeEach(module((Module module) { - return module.type(ExceptionHandler, implementedBy: LoggingExceptionHandler); - })); - beforeEach(inject((Scope $rootScope) { - log = []; - child = $rootScope.$new(); - grandChild = child.$new(); - greatGrandChild = grandChild.$new(); - - $rootScope.id = 0; - child.id = 1; - grandChild.id = 2; - greatGrandChild.id = 3; - - $rootScope.$on('myEvent', logger); - child.$on('myEvent', logger); - grandChild.$on('myEvent', logger); - greatGrandChild.$on('myEvent', logger); - })); - - it(r'should bubble event up to the root scope', () { - grandChild.$emit(r'myEvent'); - expect(log.join('>')).toEqual('2>1>0'); - }); - - - it(r'should dispatch exceptions to the $exceptionHandler', - inject((ExceptionHandler e) { - LoggingExceptionHandler $exceptionHandler = e; - child.$on('myEvent', () { throw 'bubbleException'; }); - grandChild.$emit(r'myEvent'); - expect(log.join('>')).toEqual('2>1>0'); - expect($exceptionHandler.errors[0].error).toEqual('bubbleException'); - })); - - - it(r'should allow stopping event propagation', () { - child.$on('myEvent', (event) { event.stopPropagation(); }); - grandChild.$emit(r'myEvent'); - expect(log.join('>')).toEqual('2>1'); - }); - - - it(r'should forward method arguments', () { - child.$on('abc', (event, arg1, arg2) { - expect(event.name).toBe('abc'); - expect(arg1).toBe('arg1'); - expect(arg2).toBe('arg2'); - }); - child.$emit(r'abc', ['arg1', 'arg2']); - }); - - - describe(r'event object', () { - it(r'should have methods/properties', () { - var event; - child.$on('myEvent', (e) { - expect(e.targetScope).toBe(grandChild); - expect(e.currentScope).toBe(child); - expect(e.name).toBe('myEvent'); - event = e; - }); - grandChild.$emit(r'myEvent'); - expect(event).toBeDefined(); - }); - - - it(r'should have preventDefault method and defaultPrevented property', () { - var event = grandChild.$emit(r'myEvent'); - expect(event.defaultPrevented).toBe(false); - - child.$on('myEvent', (event) { - event.preventDefault(); - }); - event = grandChild.$emit(r'myEvent'); - expect(event.defaultPrevented).toBe(true); - }); - }); - }); - - - describe(r'$broadcast', () { - describe(r'event propagation', () { - var log, child1, child2, child3, grandChild11, grandChild21, grandChild22, grandChild23, - greatGrandChild211; - - logger(event) { - log.add(event.currentScope.id); - } - - beforeEach(inject((Scope $rootScope) { - log = []; - child1 = $rootScope.$new(); - child2 = $rootScope.$new(); - child3 = $rootScope.$new(); - grandChild11 = child1.$new(); - grandChild21 = child2.$new(); - grandChild22 = child2.$new(); - grandChild23 = child2.$new(); - greatGrandChild211 = grandChild21.$new(); - - $rootScope.id = 0; - child1.id = 1; - child2.id = 2; - child3.id = 3; - grandChild11.id = 11; - grandChild21.id = 21; - grandChild22.id = 22; - grandChild23.id = 23; - greatGrandChild211.id = 211; - - $rootScope.$on('myEvent', logger); - child1.$on('myEvent', logger); - child2.$on('myEvent', logger); - child3.$on('myEvent', logger); - grandChild11.$on('myEvent', logger); - grandChild21.$on('myEvent', logger); - grandChild22.$on('myEvent', logger); - grandChild23.$on('myEvent', logger); - greatGrandChild211.$on('myEvent', logger); - - // R - // / | \ - // 1 2 3 - // / / | \ - // 11 21 22 23 - // | - // 211 - })); - - - it(r'should broadcast an event from the root scope', inject((Scope $rootScope) { - $rootScope.$broadcast('myEvent'); - expect(log.join('>')).toEqual('0>1>11>2>21>211>22>23>3'); - })); - - - it(r'should broadcast an event from a child scope', () { - child2.$broadcast('myEvent'); - expect(log.join('>')).toEqual('2>21>211>22>23'); - }); - - - it(r'should broadcast an event from a leaf scope with a sibling', () { - grandChild22.$broadcast('myEvent'); - expect(log.join('>')).toEqual('22'); - }); - - - it(r'should broadcast an event from a leaf scope without a sibling', () { - grandChild23.$broadcast('myEvent'); - expect(log.join('>')).toEqual('23'); - }); - - - it(r'should not not fire any listeners for other events', inject((Scope $rootScope) { - $rootScope.$broadcast('fooEvent'); - expect(log.join('>')).toEqual(''); - })); - - - it(r'should return event object', () { - var result = child1.$broadcast('some'); - - expect(result).toBeDefined(); - expect(result.name).toBe('some'); - expect(result.targetScope).toBe(child1); - }); - }); - - - describe(r'listener', () { - it(r'should receive event object', inject((Scope $rootScope) { - var scope = $rootScope, - child = scope.$new(), - event; - - child.$on('fooEvent', (e) { - event = e; - }); - scope.$broadcast('fooEvent'); - - expect(event.name).toBe('fooEvent'); - expect(event.targetScope).toBe(scope); - expect(event.currentScope).toBe(child); - })); + it(r'should proprely reset phase on exception', inject((RootScope rootScope) { + var error = 'MyError'; + expect(() => rootScope.apply(() { throw error; })).toThrow(error); + expect(() => rootScope.apply(() { throw error; })).toThrow(error); + })); + }); + + + describe('flush lifecycle', () { + it(r'should apply expression with full lifecycle', inject((RootScope rootScope) { + var log = ''; + var child = rootScope.createChild({"parent": rootScope.context}); + rootScope.observe('a', (a, _) { log += '1'; }); + child.apply('parent.a = 0'); + expect(log).toEqual('1'); + })); - it(r'should support passing messages as varargs', inject((Scope $rootScope) { - var scope = $rootScope, - child = scope.$new(), - args; + it(r'should schedule domWrites and domReads', inject((RootScope rootScope) { + var log = ''; + var child = rootScope.createChild({"parent": rootScope.context}); + rootScope.observe('a', (a, _) { log += '1'; }); + child.apply('parent.a = 0'); + expect(log).toEqual('1'); + })); - child.$on('fooEvent', (a, b, c, d, e) { - args = [a, b, c, d, e]; - }); - scope.$broadcast('fooEvent', ['do', 're', 'me', 'fa']); - expect(args.length).toBe(5); - expect(args.sublist(1)).toEqual(['do', 're', 'me', 'fa']); - })); - }); + it(r'should catch exceptions', () { + module((Module module) => module.type(ExceptionHandler, implementedBy: LoggingExceptionHandler)); + inject((RootScope rootScope, ExceptionHandler e) { + LoggingExceptionHandler $exceptionHandler = e; + var log = []; + var child = rootScope.createChild({}); + rootScope.observe('a', (a, _) => log.add('1')); + rootScope.context['a'] = 0; + child.apply(() { throw 'MyError'; }); + expect(log.join(',')).toEqual('1'); + expect($exceptionHandler.errors[0].error).toEqual('MyError'); + $exceptionHandler.errors.removeAt(0); + $exceptionHandler.assertEmpty(); }); }); - describe('\$watchCollection', () { - var log, $rootScope, deregister; - - beforeEach(inject((Scope _$rootScope_) { - log = []; - $rootScope = _$rootScope_; - deregister = $rootScope.$watchCollection('obj', (obj) { - log.add(JSON.encode(obj)); - }); + describe(r'exceptions', () { + var log; + beforeEach(module((Module module) { + return module.type(ExceptionHandler, implementedBy: LoggingExceptionHandler); })); - - - it('should not trigger if nothing change', inject((Scope $rootScope) { - $rootScope.$digest(); - expect(log).toEqual(['null']); - - $rootScope.$digest(); - expect(log).toEqual(['null']); + beforeEach(inject((RootScope rootScope) { + rootScope.context['log'] = () { log += 'digest;'; return null; }; + log = ''; + rootScope.observe('log()', (v, o) => null); + rootScope.digest(); + log = ''; })); - it('should allow deregistration', inject((Scope $rootScope) { - $rootScope.obj = []; - $rootScope.$digest(); - - expect(log).toEqual(['[]']); + it(r'should execute and return value and update', inject( + (RootScope rootScope, ExceptionHandler e) { + LoggingExceptionHandler $exceptionHandler = e; + rootScope.context['name'] = 'abc'; + expect(rootScope.apply((context) => context['name'])).toEqual('abc'); + expect(log).toEqual('digest;digest;'); + $exceptionHandler.assertEmpty(); + })); - $rootScope.obj.add('a'); - deregister(); + it(r'should execute and return value and update', inject((RootScope rootScope) { + rootScope.context['name'] = 'abc'; + expect(rootScope.apply('name', {'name': 123})).toEqual(123); + })); - $rootScope.$digest(); - expect(log).toEqual(['[]']); + it(r'should catch exception and update', inject((RootScope rootScope, ExceptionHandler e) { + LoggingExceptionHandler $exceptionHandler = e; + var error = 'MyError'; + rootScope.apply(() { throw error; }); + expect(log).toEqual('digest;digest;'); + expect($exceptionHandler.errors[0].error).toEqual(error); })); + it(r'should throw assertion when model changes in flush', inject((RootScope rootScope, Logger log) { + var retValue = 1; + rootScope.context['logger'] = (name) { log(name); return retValue; }; - describe('array', () { - it('should trigger when property changes into array', () { - $rootScope.obj = 'test'; - $rootScope.$digest(); - expect(log).toEqual(['"test"']); + rootScope.watch('logger("watch")', (n, v) => null); + rootScope.observe('logger("flush")', (n, v) => null); - $rootScope.obj = []; - $rootScope.$digest(); - expect(log).toEqual(['"test"', '[]']); - }); + // clear watches + rootScope.digest(); + log.clear(); + rootScope.flush(); + expect(log).toEqual(['flush', /*assertion*/ 'watch', 'flush']); - it('should not trigger change when object in collection changes', () { - $rootScope.obj = [{}]; - $rootScope.$digest(); - expect(log).toEqual(['[{}]']); + retValue = 2; + expect(rootScope.flush). + toThrow('Observer reaction functions should not change model. \n' + 'These watch changes were detected: logger("watch")\n' + 'These observe changes were detected: '); + })); + }); - $rootScope.obj[0]['name'] = 'foo'; - $rootScope.$digest(); - expect(log).toEqual(['[{}]']); - }); + }); - it('should watch array properties', () { - $rootScope.obj = []; - $rootScope.$digest(); - expect(log).toEqual(['[]']); + describe('ScopeLocals', () { + it('should read from locals', inject((RootScope scope) { + scope.context['a'] = 'XXX'; + scope.context['c'] = 'C'; + var scopeLocal = new ScopeLocals(scope.context, {'a': 'A', 'b': 'B'}); + expect(scopeLocal['a']).toEqual('A'); + expect(scopeLocal['b']).toEqual('B'); + expect(scopeLocal['c']).toEqual('C'); + })); - $rootScope.obj.add('a'); - $rootScope.$digest(); - expect(log).toEqual(['[]', '["a"]']); + it('should write to Scope', inject((RootScope scope) { + scope.context['a'] = 'XXX'; + scope.context['c'] = 'C'; + var scopeLocal = new ScopeLocals(scope.context, {'a': 'A', 'b': 'B'}); - $rootScope.obj[0] = 'b'; - $rootScope.$digest(); - expect(log).toEqual(['[]', '["a"]', '["b"]']); + scopeLocal['a'] = 'aW'; + scopeLocal['b'] = 'bW'; + scopeLocal['c'] = 'cW'; - $rootScope.obj.add([]); - $rootScope.obj.add({}); - log = []; - $rootScope.$digest(); - expect(log).toEqual(['["b",[],{}]']); + expect(scope.context['a']).toEqual('aW'); + expect(scope.context['b']).toEqual('bW'); + expect(scope.context['c']).toEqual('cW'); - var temp = $rootScope.obj[1]; - $rootScope.obj[1] = $rootScope.obj[2]; - $rootScope.obj[2] = temp; - $rootScope.$digest(); - expect(log).toEqual([ '["b",[],{}]', '["b",{},[]]' ]); + expect(scopeLocal['a']).toEqual('A'); + expect(scopeLocal['b']).toEqual('B'); + expect(scopeLocal['c']).toEqual('cW'); + })); + }); - $rootScope.obj.removeAt(0); - log = []; - $rootScope.$digest(); - expect(log).toEqual([ '[{},[]]' ]); - }); - }); + describe(r'watch/digest', () { + it(r'should watch and fire on simple property change', inject((RootScope rootScope) { + var log; - it('should watch iterable properties', () { - $rootScope.obj = _toJsonableIterable([]); - $rootScope.$digest(); - expect(log).toEqual(['[]']); + rootScope.watch('name', (a, b) { + log = [a, b]; + }); + rootScope.digest(); + log = null; + + expect(log).toEqual(null); + rootScope.digest(); + expect(log).toEqual(null); + rootScope.context['name'] = 'misko'; + rootScope.digest(); + expect(log).toEqual(['misko', null]); + })); - $rootScope.obj = _toJsonableIterable(['a']); - $rootScope.$digest(); - expect(log).toEqual(['[]', '["a"]']); - $rootScope.obj = _toJsonableIterable(['b']); - $rootScope.$digest(); - expect(log).toEqual(['[]', '["a"]', '["b"]']); + it(r'should watch and fire on expression change', inject((RootScope rootScope) { + var log; - $rootScope.obj = _toJsonableIterable(['b', [], {}]); - log = []; - $rootScope.$digest(); - expect(log).toEqual(['["b",[],{}]']); + rootScope.watch('name.first', (a, b) => log = [a, b]); + rootScope.digest(); + log = null; + + rootScope.context['name'] = {}; + expect(log).toEqual(null); + rootScope.digest(); + expect(log).toEqual(null); + rootScope.context['name']['first'] = 'misko'; + rootScope.digest(); + expect(log).toEqual(['misko', null]); + })); - $rootScope.obj = _toJsonableIterable(['a']); - log = []; - $rootScope.$digest(); - expect(log).toEqual(['["a"]']); - $rootScope.obj = _toJsonableIterable(['a']); - $rootScope.$digest(); - expect(log).toEqual(['["a"]']); + it(r'should delegate exceptions', () { + module((Module module) { + module.type(ExceptionHandler, implementedBy: LoggingExceptionHandler); }); + inject((RootScope rootScope, ExceptionHandler e) { + LoggingExceptionHandler $exceptionHandler = e; + rootScope.watch('a', (n, o) {throw 'abc';}); + rootScope.context['a'] = 1; + rootScope.digest(); + expect($exceptionHandler.errors.length).toEqual(1); + expect($exceptionHandler.errors[0].error).toEqual('abc'); + }); + }); - describe('objects', () { - it('should trigger when property changes into object', () { - $rootScope.obj = 'test'; - $rootScope.$digest(); - expect(log).toEqual(['"test"']); + it(r'should fire watches in order of addition', inject((RootScope rootScope) { + // this is not an external guarantee, just our own sanity + var log = ''; + rootScope.watch('a', (a, b) { log += 'a'; }); + rootScope.watch('b', (a, b) { log += 'b'; }); + rootScope.watch('c', (a, b) { log += 'c'; }); + rootScope.context['a'] = rootScope.context['b'] = rootScope.context['c'] = 1; + rootScope.digest(); + expect(log).toEqual('abc'); + })); - $rootScope.obj = {}; - $rootScope.$digest(); - expect(log).toEqual(['"test"', '{}']); - }); + it(r'should call child watchers in addition order', inject((RootScope rootScope) { + // this is not an external guarantee, just our own sanity + var log = ''; + var childA = rootScope.createChild({}); + var childB = rootScope.createChild({}); + var childC = rootScope.createChild({}); + childA.watch('a', (a, b) { log += 'a'; }); + childB.watch('b', (a, b) { log += 'b'; }); + childC.watch('c', (a, b) { log += 'c'; }); + childA.context['a'] = childB.context['b'] = childC.context['c'] = 1; + rootScope.digest(); + expect(log).toEqual('abc'); + })); - it('should not trigger change when object in collection changes', () { - $rootScope.obj = {'name': {}}; - $rootScope.$digest(); - expect(log).toEqual(['{"name":{}}']); - $rootScope.obj['name']['bar'] = 'foo'; - $rootScope.$digest(); - expect(log).toEqual(['{"name":{}}']); - }); + it(r'should run digest multiple times', inject( + (RootScope rootScope) { + // tests a traversal edge case which we originally missed + var log = []; + var childA = rootScope.createChild({'log': log}); + var childB = rootScope.createChild({'log': log}); + rootScope.context['log'] = log; - it('should watch object properties', () { - $rootScope.obj = {}; - $rootScope.$digest(); - expect(log).toEqual(['{}']); + rootScope.watch("log.add('r')", (_, __) => null); + childA.watch("log.add('a')", (_, __) => null); + childB.watch("log.add('b')", (_, __) => null); - $rootScope.obj['a']= 'A'; - $rootScope.$digest(); - expect(log).toEqual(['{}', '{"a":"A"}']); + // init + rootScope.digest(); + expect(log.join('')).toEqual('rabrab'); + })); - $rootScope.obj['a'] = 'B'; - $rootScope.$digest(); - expect(log).toEqual(['{}', '{"a":"A"}', '{"a":"B"}']); - $rootScope.obj['b'] = []; - $rootScope.obj['c'] = {}; - log = []; - $rootScope.$digest(); - expect(log).toEqual(['{"a":"B","b":[],"c":{}}']); + it(r'should repeat watch cycle while model changes are identified', inject((RootScope rootScope) { + var log = ''; + rootScope.watch('c', (v, b) {rootScope.context['d'] = v; log+='c'; }); + rootScope.watch('b', (v, b) {rootScope.context['c'] = v; log+='b'; }); + rootScope.watch('a', (v, b) {rootScope.context['b'] = v; log+='a'; }); + rootScope.digest(); + log = ''; + rootScope.context['a'] = 1; + rootScope.digest(); + expect(rootScope.context['b']).toEqual(1); + expect(rootScope.context['c']).toEqual(1); + expect(rootScope.context['d']).toEqual(1); + expect(log).toEqual('abc'); + })); - var temp = $rootScope.obj['a']; - $rootScope.obj['a'] = $rootScope.obj['b']; - $rootScope.obj['c'] = temp; - $rootScope.$digest(); - expect(log).toEqual([ '{"a":"B","b":[],"c":{}}', '{"a":[],"b":[],"c":"B"}' ]); - $rootScope.obj.remove('a'); - log = []; - $rootScope.$digest(); - expect(log).toEqual([ '{"b":[],"c":"B"}' ]); - }); - }); - }); + it(r'should repeat watch cycle from the root element', inject((RootScope rootScope) { + var log = []; + rootScope.context['log'] = log; + var child = rootScope.createChild({'log':log}); + rootScope.watch("log.add('a')", (_, __) => null); + child.watch("log.add('b')", (_, __) => null); + rootScope.digest(); + expect(log.join('')).toEqual('abab'); + })); - describe('perf', () { - describe('counters', () { + it(r'should not fire upon watch registration on initial digest', inject((RootScope rootScope) { + var log = ''; + rootScope.context['a'] = 1; + rootScope.watch('a', (a, b) { log += 'a'; }); + rootScope.watch('b', (a, b) { log += 'b'; }); + rootScope.digest(); + log = ''; + rootScope.digest(); + expect(log).toEqual(''); + })); - it('should expose scope count', inject((Profiler perf, Scope scope) { - scope.$digest(); - expect(perf.counters['ng.scopes']).toEqual(1); - scope.$new(); - scope.$new(); - var lastChild = scope.$new(); - scope.$digest(); - expect(perf.counters['ng.scopes']).toEqual(4); + it(r'should prevent digest recursion', inject((RootScope rootScope) { + var callCount = 0; + rootScope.watch('name', (a, b) { + expect(() { + rootScope.digest(); + }).toThrow(r'digest already in progress'); + callCount++; + }); + rootScope.context['name'] = 'a'; + rootScope.digest(); + expect(callCount).toEqual(1); + })); - // Create a child scope and make sure it's counted as well. - lastChild.$new(); - scope.$digest(); - expect(perf.counters['ng.scopes']).toEqual(5); - })); + it(r'should return a function that allows listeners to be unregistered', inject( + (RootScope rootScope) { + var listener = jasmine.createSpy('watch listener'); + var watch; - it('should update scope count when scope destroyed', - inject((Profiler perf, Scope scope) { + watch = rootScope.watch('foo', listener); + rootScope.digest(); //init + expect(listener).toHaveBeenCalled(); + expect(watch).toBeDefined(); - var child = scope.$new(); - scope.$digest(); - expect(perf.counters['ng.scopes']).toEqual(2); + listener.reset(); + rootScope.context['foo'] = 'bar'; + rootScope.digest(); //triger + expect(listener).toHaveBeenCalledOnce(); - child.$destroy(); - scope.$digest(); - expect(perf.counters['ng.scopes']).toEqual(1); + listener.reset(); + rootScope.context['foo'] = 'baz'; + watch.remove(); + rootScope.digest(); //trigger + expect(listener).not.toHaveBeenCalled(); })); - it('should expose watcher count', inject((Profiler perf, Scope scope) { - scope.$digest(); - expect(perf.counters['ng.scope.watchers']).toEqual(0); + it(r'should not infinitely digest when current value is NaN', inject((RootScope rootScope) { + rootScope.context['nan'] = double.NAN; + rootScope.watch('nan', (_, __) => null); - scope.$watch(() => 0, (_) {}); - scope.$watch(() => 0, (_) {}); - scope.$watch(() => 0, (_) {}); - scope.$digest(); - expect(perf.counters['ng.scope.watchers']).toEqual(3); - - // Create a child scope and make sure it's counted as well. - scope.$new().$watch(() => 0, (_) {}); - scope.$digest(); - expect(perf.counters['ng.scope.watchers']).toEqual(4); - })); + expect(() { + rootScope.digest(); + }).not.toThrow(); + })); - it('should update watcher count when watcher removed', - inject((Profiler perf, Scope scope) { + it(r'should prevent infinite digest and should log firing expressions', inject((RootScope rootScope) { + rootScope.context['a'] = 0; + rootScope.context['b'] = 0; + rootScope.watch('a', (a, __) => rootScope.context['a'] = a + 1); + rootScope.watch('b', (b, __) => rootScope.context['b'] = b + 1); - var unwatch = scope.$new().$watch(() => 0, (_) {}); - scope.$digest(); - expect(perf.counters['ng.scope.watchers']).toEqual(1); + expect(() { + rootScope.digest(); + }).toThrow('Model did not stabilize in 5 digests. ' + 'Last 3 iterations:\n' + 'a, b\n' + 'a, b\n' + 'a, b'); + })); - unwatch(); - scope.$digest(); - expect(perf.counters['ng.scope.watchers']).toEqual(0); - })); - }); - }); + it(r'should always call the watchr with newVal and oldVal equal on the first run', + inject((RootScope rootScope) { + var log = []; + var logger = (newVal, oldVal) { + var val = (newVal == oldVal || (newVal != oldVal && oldVal != newVal)) ? newVal : 'xxx'; + log.add(val); + }; - describe('optimizations', () { - var scope; - var log; - beforeEach(inject((Scope _scope, Logger _log) { - scope = _scope; - log = _log; - scope['a'] = 1; - scope['b'] = 2; - scope['c'] = 3; - scope.$watch(() {log('a'); return scope['a'];}, (value) => log('fire:a')); - scope.$watch(() {log('b'); return scope['b'];}, (value) => log('fire:b')); - scope.$watch(() {log('c'); return scope['c'];}, (value) {log('fire:c'); scope['b']++; }); - scope.$digest(); - log.clear(); - })); + rootScope.context['nanValue'] = double.NAN; + rootScope.context['nullValue'] = null; + rootScope.context['emptyString'] = ''; + rootScope.context['falseValue'] = false; + rootScope.context['numberValue'] = 23; + + rootScope.watch('nanValue', logger); + rootScope.watch('nullValue', logger); + rootScope.watch('emptyString', logger); + rootScope.watch('falseValue', logger); + rootScope.watch('numberValue', logger); + + rootScope.digest(); + expect(log.removeAt(0).isNaN).toEqual(true); //jasmine's toBe and toEqual don't work well with NaNs + expect(log).toEqual([null, '', false, 23]); + log = []; + rootScope.digest(); + expect(log).toEqual([]); + })); + }); - it('should loop once on no dirty', () { - scope.$digest(); - expect(log.result()).toEqual('a; b; c'); - }); + + describe('runAsync', () { + it(r'should run callback before watch', inject((RootScope rootScope) { + var log = ''; + rootScope.runAsync(() { log += 'parent.async;'; }); + rootScope.watch('value', (_, __) { log += 'parent.digest;'; }); + rootScope.digest(); + expect(log).toEqual('parent.async;parent.digest;'); + })); - it('should exit early on second loop', () { - scope['b']++; - scope.$digest(); - expect(log.result()).toEqual('a; b; fire:b; c; a'); + it(r'should cause a $digest rerun', inject((RootScope rootScope) { + rootScope.context['log'] = ''; + rootScope.context['value'] = 0; + // NOTE(deboer): watch listener string functions not yet supported + //rootScope.watch('value', 'log = log + ".";'); + rootScope.watch('value', (_, __) { rootScope.context['log'] += "."; }); + rootScope.watch('init', (_, __) { + rootScope.runAsync(() => rootScope.eval('value = 123; log = log + "=" ')); + expect(rootScope.context['value']).toEqual(0); }); + rootScope.digest(); + expect(rootScope.context['log']).toEqual('.=.'); + })); - it('should continue checking if second loop dirty', () { - scope['c']++; - scope.$digest(); - expect(log.result()).toEqual('a; b; c; fire:c; a; b; fire:b; c; a'); - }); - }); + it(r'should run async in the same order as added', inject((RootScope rootScope) { + rootScope.context['log'] = ''; + rootScope.runAsync(() => rootScope.eval("log = log + 1")); + rootScope.runAsync(() => rootScope.eval("log = log + 2")); + rootScope.digest(); + expect(rootScope.context['log']).toEqual('12'); + })); + }); - describe('ScopeLocals', () { - var scope; - beforeEach(inject((Scope _scope) => scope = _scope)); - it('should read from locals', () { - scope['a'] = 'XXX'; - scope['c'] = 'C'; - var scopeLocal = new ScopeLocals(scope, {'a': 'A', 'b': 'B'}); - expect(scopeLocal['a']).toEqual('A'); - expect(scopeLocal['b']).toEqual('B'); - expect(scopeLocal['c']).toEqual('C'); + describe('domRead/domWrite', () { + it(r'should run writes before reads', () { + module((Module module) { + module.type(ExceptionHandler, implementedBy: LoggingExceptionHandler); }); - - it('should write to Scope', () { - scope['a'] = 'XXX'; - scope['c'] = 'C'; - var scopeLocal = new ScopeLocals(scope, {'a': 'A', 'b': 'B'}); - - scopeLocal['a'] = 'aW'; - scopeLocal['b'] = 'bW'; - scopeLocal['c'] = 'cW'; - - expect(scope['a']).toEqual('aW'); - expect(scope['b']).toEqual('bW'); - expect(scope['c']).toEqual('cW'); - - expect(scopeLocal['a']).toEqual('A'); - expect(scopeLocal['b']).toEqual('B'); - expect(scopeLocal['c']).toEqual('cW'); + inject((RootScope rootScope, Logger logger, ExceptionHandler e) { + LoggingExceptionHandler exceptionHandler = e as LoggingExceptionHandler; + rootScope.domWrite(() { + logger('write1'); + rootScope.domWrite(() => logger('write2')); + throw 'write1'; + }); + rootScope.domRead(() { + logger('read1'); + rootScope.domRead(() => logger('read2')); + rootScope.domWrite(() => logger('write3')); + throw 'read1'; + }); + rootScope.observe('value', (_, __) => logger('observe')); + rootScope.flush(); + expect(logger).toEqual(['write1', 'write2', 'observe', 'read1', 'read2', 'write3']); + expect(exceptionHandler.errors.length).toEqual(2); + expect(exceptionHandler.errors[0].error).toEqual('write1'); + expect(exceptionHandler.errors[1].error).toEqual('read1'); }); }); - - describe('filters', () { - - it('should use filters from correct scope when digesting scope trees', inject((Scope rootScope, Injector injector) { - var withFilterOne = injector.createChild([new Module()..type(FilterOne)], - forceNewInstances: [FilterMap]).get(FilterMap); - var withFilterTwo = injector.createChild([new Module()..type(FilterTwo)], - forceNewInstances: [FilterMap]).get(FilterMap); - - var childScopeOne = rootScope.$new(filters: withFilterOne); - var childScopeTwo = rootScope.$new(filters: withFilterTwo); - - var valueOne; - var valueTwo; - childScopeOne.$watch('"str" | newFilter', (val) => valueOne = val); - childScopeTwo.$watch('"str" | newFilter', (val) => valueTwo = val); - - rootScope.$digest(); - - expect(valueOne).toEqual('str 1'); - expect(valueTwo).toEqual('str 2'); - })); - - }); }); -} - -_toJsonableIterable(Iterable source) => new _JsonableIterableWrapper(source); - -class _JsonableIterableWrapper implements Iterable { - final Iterable source; - - _JsonableIterableWrapper(this.source); - - bool any(bool test(T element)) => source.any(test); - - bool contains(Object element) => source.contains(element); - - T elementAt(int index) => source.elementAt(index); - - bool every(bool test(T element)) => source.every(test); - - Iterable expand(Iterable f(T element)) => source.expand(f); - - T get first => source.first; - - T firstWhere(bool test(T element), {T orElse()}) => - source.firstWhere(test, orElse: orElse); +}); - fold(initialValue, combine(previousValue, T element)) => - source.fold(initialValue, combine); - - void forEach(void f(T element)) => source.forEach(f); - - bool get isEmpty => source.isEmpty; - - bool get isNotEmpty => source.isNotEmpty; - - Iterator get iterator => source.iterator; - - String join([String separator = ""]) => source.join(separator); - - T get last => source.last; - - T lastWhere(bool test(T element), {T orElse()}) => - source.lastWhere(test, orElse: orElse); - - int get length => source.length; - - Iterable map(f(T element)) => source.map(f); - - T reduce(T combine(T value, T element)) => source.reduce(combine); - - T get single => source.single; - - T singleWhere(bool test(T element)) => source.singleWhere(test); - - Iterable skip(int n) => source.skip(n); - - Iterable skipWhile(bool test(T value)) => source.skipWhile(test); - - Iterable take(int n) => source.take(n); - - Iterable takeWhile(bool test(T value)) => source.takeWhile(test); +@NgFilter(name: 'multiply') +class _MultiplyFilter { + call(a, b) => a * b; +} - List toList({bool growable: true}) => source.toList(growable: growable); +@NgFilter(name: 'listHead') +class _ListHeadFilter { + Logger logger; + _ListHeadFilter(Logger this.logger); + call(list, head) { + logger('listHead'); + return [head]..addAll(list); + } +} - Set toSet() => source.toSet(); - Iterable where(bool test(T element)) => source.where(test); +@NgFilter(name: 'listTail') +class _ListTailFilter { + Logger logger; + _ListTailFilter(Logger this.logger); + call(list, tail) { + logger('listTail'); + return new List.from(list)..add(tail); + } +} - toJson() => source.toList(); +@NgFilter(name: 'sort') +class _SortFilter { + Logger logger; + _SortFilter(Logger this.logger); + call(list) { + logger('sort'); + return new List.from(list)..sort(); + } } @NgFilter(name:'newFilter') diff --git a/test/core/templateurl_spec.dart b/test/core/templateurl_spec.dart index c6ad4a38f..e24043a5c 100644 --- a/test/core/templateurl_spec.dart +++ b/test/core/templateurl_spec.dart @@ -97,7 +97,7 @@ main() => describe('template url', () { microLeap(); expect(renderedText(element)).toEqual('Simple!'); - $rootScope.$digest(); + $rootScope.apply(); // Note: There is no ordering. It is who ever comes off the wire first! expect(log.result()).toEqual('LOG; SIMPLE'); }))); @@ -118,7 +118,7 @@ main() => describe('template url', () { microLeap(); expect(renderedText(element)).toEqual('Simple!Simple!'); - $rootScope.$digest(); + $rootScope.apply(); // Note: There is no ordering. It is who ever comes off the wire first! expect(log.result()).toEqual('LOG; LOG; SIMPLE; SIMPLE'); }))); @@ -140,7 +140,7 @@ main() => describe('template url', () { expect(element[0].nodes[0].shadowRoot.innerHtml).toEqual( '
Simple!
' ); - $rootScope.$digest(); + $rootScope.apply(); // Note: There is no ordering. It is who ever comes off the wire first! expect(log.result()).toEqual('LOG; SIMPLE'); }))); @@ -226,7 +226,7 @@ main() => describe('template url', () { expect(element[0].nodes[0].shadowRoot.innerHtml).toEqual( '
Simple!
' ); - $rootScope.$digest(); + $rootScope.apply(); // Note: There is no ordering. It is who ever comes off the wire first! expect(log.result()).toEqual('LOG; SIMPLE'); }))); diff --git a/test/core_dom/block_spec.dart b/test/core_dom/block_spec.dart index c94fee0ed..3c7559fc4 100644 --- a/test/core_dom/block_spec.dart +++ b/test/core_dom/block_spec.dart @@ -243,7 +243,7 @@ main() { Compiler compiler = rootInjector.get(Compiler); DirectiveMap directives = rootInjector.get(DirectiveMap); compiler(es('{{\'a\' | filterA}}'), directives)(rootInjector); - rootScope.$digest(); + rootScope.apply(); expect(log.log, equals(['ADirective', 'AFilter'])); @@ -257,7 +257,7 @@ main() { DirectiveMap newDirectives = childInjector.get(DirectiveMap); compiler(es('{{\'a\' | filterA}}' '{{\'b\' | filterB}}'), newDirectives)(childInjector); - rootScope.$digest(); + rootScope.apply(); expect(log.log, equals(['ADirective', 'AFilter', 'ADirective', 'BDirective', 'BFilter'])); }); diff --git a/test/core_dom/compiler_spec.dart b/test/core_dom/compiler_spec.dart index fa8c8bcd3..71cc70a91 100644 --- a/test/core_dom/compiler_spec.dart +++ b/test/core_dom/compiler_spec.dart @@ -7,7 +7,7 @@ main() => describe('dte.compiler', () { Compiler $compile; DirectiveMap directives; Injector injector; - Scope $rootScope; + Scope rootScope; beforeEach(module((Module module) { module @@ -24,7 +24,7 @@ main() => describe('dte.compiler', () { injector = _injector; $compile = injector.get(Compiler); directives = injector.get(DirectiveMap); - $rootScope = injector.get(Scope); + rootScope = injector.get(Scope); }; })); @@ -32,11 +32,11 @@ main() => describe('dte.compiler', () { var element = $('
'); var template = $compile(element, directives); - $rootScope['name'] = 'angular'; + rootScope.context['name'] = 'angular'; template(injector, element); expect(element.text()).toEqual(''); - $rootScope.$digest(); + rootScope.apply(); expect(element.text()).toEqual('angular'); })); @@ -48,13 +48,13 @@ main() => describe('dte.compiler', () { var element = $('
'); var template = $compile(element, directives); - $rootScope['name'] = 'angular'; + rootScope.context['name'] = 'angular'; template(injector, element); expect(element.text()).toEqual(''); - $rootScope.$digest(); + rootScope.apply(); expect(element.text()).toEqual('angular'); })); @@ -62,17 +62,17 @@ main() => describe('dte.compiler', () { var element = $('
'); var template = $compile(element, directives); - $rootScope.items = ['A', 'b']; + rootScope.context['items'] = ['A', 'b']; template(injector, element); expect(element.text()).toEqual(''); // TODO(deboer): Digest twice until we have dirty checking in the scope. - $rootScope.$digest(); - $rootScope.$digest(); + rootScope.apply(); + rootScope.apply(); expect(element.text()).toEqual('Ab'); - $rootScope.items = []; - $rootScope.$digest(); + rootScope.context['items'] = []; + rootScope.apply(); expect(element.html()).toEqual(''); })); @@ -80,17 +80,17 @@ main() => describe('dte.compiler', () { var element = $('
'); var template = $compile(element, directives); - $rootScope.items = ['A', 'b']; + rootScope.context['items'] = ['A', 'b']; template(injector, element); expect(element.text()).toEqual(''); // TODO(deboer): Digest twice until we have dirty checking in the scope. - $rootScope.$digest(); - $rootScope.$digest(); + rootScope.apply(); + rootScope.apply(); expect(element.text()).toEqual('Ab'); - $rootScope.items = []; - $rootScope.$digest(); + rootScope.context['items'] = []; + rootScope.apply(); expect(element.html()).toEqual(''); })); @@ -100,13 +100,12 @@ main() => describe('dte.compiler', () { var template = $compile(element, directives); - $rootScope.name = 'OK'; + rootScope.context['name'] = 'OK'; var block = template(injector, element); element = $(block.elements); - expect(element.text()).toEqual('!'); - $rootScope.$digest(); + rootScope.apply(); expect(element.text()).toEqual('OK!'); })); @@ -119,11 +118,10 @@ main() => describe('dte.compiler', () { ''); var template = $compile(element, directives); - $rootScope.uls = [['A'], ['b']]; + rootScope.context['uls'] = [['A'], ['b']]; template(injector, element); - expect(element.text()).toEqual(''); - $rootScope.$digest(); + rootScope.apply(); expect(element.text()).toEqual('Ab'); })); @@ -132,7 +130,7 @@ main() => describe('dte.compiler', () { var template = $compile(element, directives); template(injector, element); - $rootScope.$digest(); + rootScope.apply(); expect(log).toEqual(['OneOfTwo', 'TwoOfTwo']); })); @@ -144,10 +142,10 @@ main() => describe('dte.compiler', () { var element = $('
'); var template = $compile(element, directives); - $rootScope.name = 'angular'; + rootScope.context['name'] = 'angular'; template(injector, element); - $rootScope.$digest(); + rootScope.apply(); expect(element.attr('test')).toEqual('angular'); })); @@ -155,11 +153,10 @@ main() => describe('dte.compiler', () { var element = $('
{{name}}
'); var template = $compile(element, directives); - $rootScope.name = 'angular'; + rootScope.context['name'] = 'angular'; template(injector, element); - expect(element.text()).toEqual(''); - $rootScope.$digest(); + rootScope.apply(); expect(element.text()).toEqual('angular'); })); }); @@ -193,13 +190,12 @@ main() => describe('dte.compiler', () { }); microLeap(); - expect(element.textWithShadow()).toEqual('INNER_1()'); + expect(element.textWithShadow()).toEqual('INNER()'); }))); it('should create a simple component', async(inject((NgZone zone) { - $rootScope.name = 'OUTTER'; - $rootScope.sep = '-'; - var element = $(r'
{{name}}{{sep}}{{$id}}:{{name}}{{sep}}{{$id}}
'); + rootScope.context['name'] = 'OUTTER'; + var element = $(r'
{{name}}:{{name}}
'); zone.run(() { BlockFactory blockFactory = $compile(element, directives); @@ -207,12 +203,12 @@ main() => describe('dte.compiler', () { }); microLeap(); - expect(element.textWithShadow()).toEqual('OUTTER-_0:INNER_1(OUTTER-_0)'); + expect(element.textWithShadow()).toEqual('OUTTER:INNER(OUTTER)'); }))); it('should create a component that can access parent scope', async(inject((NgZone zone) { - $rootScope.fromParent = "should not be used"; - $rootScope.val = "poof"; + rootScope.context['fromParent'] = "should not be used"; + rootScope.context['val'] = "poof"; var element = $(''); zone.run(() => @@ -232,7 +228,7 @@ main() => describe('dte.compiler', () { }))); it('should behave nicely if a mapped attribute evals to null', async(inject((NgZone zone) { - $rootScope.val = null; + rootScope.context['val'] = null; var element = $(''); zone.run(() => $compile(element, directives)(injector, element)); @@ -246,44 +242,49 @@ main() => describe('dte.compiler', () { $compile(element, directives)(injector, element); microLeap(); - $rootScope.name = 'misko'; - $rootScope.$apply(); - var component = $rootScope.ioComponent; - expect(component.scope.name).toEqual(null); - expect(component.scope.attr).toEqual('A'); - expect(component.scope.expr).toEqual('misko'); - component.scope.expr = 'angular'; - $rootScope.$apply(); - expect($rootScope.name).toEqual('angular'); - expect($rootScope.done).toEqual(null); - component.scope.ondone(); - expect($rootScope.done).toEqual(true); + rootScope.context['name'] = 'misko'; + rootScope.apply(); + var component = rootScope.context['ioComponent']; + expect(component.scope.context['name']).toEqual(null); + expect(component.scope.context['attr']).toEqual('A'); + expect(component.scope.context['expr']).toEqual('misko'); + component.scope.context['expr'] = 'angular'; + rootScope.apply(); + expect(rootScope.context['name']).toEqual('angular'); + expect(rootScope.context['done']).toEqual(null); + component.scope.context['ondone'](); + expect(rootScope.context['done']).toEqual(true); }))); it('should should not create any watchers if no attributes are specified', async(inject((Profiler perf) { var element = $(r'
'); $compile(element, directives)(injector, element); microLeap(); - injector.get(Scope).$digest(); - expect(perf.counters['ng.scope.watchers']).toEqual(0); + injector.get(Scope).apply(); + expect(rootScope.watchGroup.totalFieldCost).toEqual(0); + expect(rootScope.watchGroup.totalCollectionCost).toEqual(0); + expect(rootScope.watchGroup.totalEvalCost).toEqual(0); + expect(rootScope.observeGroup.totalFieldCost).toEqual(0); + expect(rootScope.observeGroup.totalCollectionCost).toEqual(0); + expect(rootScope.observeGroup.totalEvalCost).toEqual(0); }))); it('should create a component with I/O and "=" binding value should be available', async(inject(() { - $rootScope.name = 'misko'; + rootScope.context['name'] = 'misko'; var element = $(r'
'); $compile(element, directives)(injector, element); microLeap(); - var component = $rootScope.ioComponent; - $rootScope.$apply(); - expect(component.scope.expr).toEqual('misko'); - component.scope.expr = 'angular'; - $rootScope.$apply(); - expect($rootScope.name).toEqual('angular'); + var component = rootScope.context['ioComponent']; + rootScope.apply(); + expect(component.scope.context['expr']).toEqual('misko'); + component.scope.context['expr'] = 'angular'; + rootScope.apply(); + expect(rootScope.context['name']).toEqual('angular'); }))); it('should create a component with I/O bound to controller and "=" binding value should be available', async(inject(() { - $rootScope.done = false; + rootScope.context['done'] = false; var element = $(r'
'); @@ -291,30 +292,30 @@ main() => describe('dte.compiler', () { $compile(element, directives)(injector, element); microLeap(); - IoControllerComponent component = $rootScope.ioComponent; + IoControllerComponent component = rootScope.context['ioComponent']; expect(component.expr).toEqual(null); expect(component.exprOnce).toEqual(null); expect(component.attr).toEqual('A'); - $rootScope.$apply(); + rootScope.apply(); - $rootScope.name = 'misko'; - $rootScope.$apply(); + rootScope.context['name'] = 'misko'; + rootScope.apply(); expect(component.expr).toEqual('misko'); expect(component.exprOnce).toEqual('misko'); - $rootScope.name = 'igor'; - $rootScope.$apply(); + rootScope.context['name'] = 'igor'; + rootScope.apply(); expect(component.expr).toEqual('igor'); expect(component.exprOnce).toEqual('misko'); component.expr = 'angular'; - $rootScope.$apply(); - expect($rootScope.name).toEqual('angular'); + rootScope.apply(); + expect(rootScope.context['name']).toEqual('angular'); - expect($rootScope.done).toEqual(false); + expect(rootScope.context['done']).toEqual(false); component.onDone(); - expect($rootScope.done).toEqual(true); + expect(rootScope.context['done']).toEqual(true); // Should be noop component.onOptional(); @@ -325,35 +326,35 @@ main() => describe('dte.compiler', () { $compile(element, directives)(injector, element); microLeap(); - IoControllerComponent component = $rootScope.ioComponent; + IoControllerComponent component = rootScope.context['ioComponent']; - $rootScope.name = 'misko'; - $rootScope.$apply(); + rootScope.context['name'] = 'misko'; + rootScope.apply(); expect(component.attr).toEqual('misko'); - $rootScope.name = 'james'; - $rootScope.$apply(); + rootScope.context['name'] = 'james'; + rootScope.apply(); expect(component.attr).toEqual('james'); }))); it('should create a unpublished component with I/O bound to controller and "=" binding value should be available', async(inject(() { - $rootScope.name = 'misko'; - $rootScope.done = false; + rootScope.context['name'] = 'misko'; + rootScope.context['done'] = false; var element = $(r'
'); $compile(element, directives)(injector, element); microLeap(); - UnpublishedIoControllerComponent component = $rootScope.ioComponent; - $rootScope.$apply(); + UnpublishedIoControllerComponent component = rootScope.context['ioComponent']; + rootScope.apply(); expect(component.attr).toEqual('A'); expect(component.expr).toEqual('misko'); component.expr = 'angular'; - $rootScope.$apply(); - expect($rootScope.name).toEqual('angular'); + rootScope.apply(); + expect(rootScope.context['name']).toEqual('angular'); - expect($rootScope.done).toEqual(false); + expect(rootScope.context['done']).toEqual(false); component.onDone(); - expect($rootScope.done).toEqual(true); + expect(rootScope.context['done']).toEqual(true); // Should be noop component.onOptional(); @@ -369,8 +370,8 @@ main() => describe('dte.compiler', () { it('should support filters in attribute expressions', async(inject(() { var element = $(r''''''); $compile(element, directives)(injector, element); - ExprAttrComponent component = $rootScope['exprAttrComponent']; - $rootScope.$digest(); + ExprAttrComponent component = rootScope.context['exprAttrComponent']; + rootScope.apply(); expect(component.expr).toEqual('Hello, Misko!'); expect(component.oneWay).toEqual('Hello, James!'); expect(component.exprOnce).toEqual('Hello, Chirayu!'); @@ -387,12 +388,12 @@ main() => describe('dte.compiler', () { var element = $(''); $compile(element, directives)(injector, element); microLeap(); - $rootScope.$apply(); - var componentScope = $rootScope.camelCase; - expect(componentScope.camelCase).toEqual('G'); + rootScope.apply(); + var componentScope = rootScope.context['camelCase']; + expect(componentScope.context['camelCase']).toEqual('G'); }))); - it('should throw an exception if required directive is missing', async(inject((Compiler $compile, Scope $rootScope, Injector injector) { + it('should throw an exception if required directive is missing', async(inject((Compiler $compile, Scope rootScope, Injector injector) { try { var element = $(''); $compile(element, directives)(injector, element); @@ -432,7 +433,7 @@ main() => describe('dte.compiler', () { it('should allow repeaters over controllers', async(inject((Logger logger) { var element = $(r''); $compile(element, directives)(injector, element); - $rootScope.$apply(); + rootScope.apply(); microLeap(); expect(logger.length).toEqual(2); @@ -449,25 +450,33 @@ main() => describe('dte.compiler', () { it('should fire onTemplate method', async(inject((Logger logger, MockHttpBackend backend) { backend.whenGET('some/template.url').respond('
WORKED
'); - var scope = $rootScope.$new(); - scope['isReady'] = 'ready'; - scope['logger'] = logger; + var scope = rootScope.createChild({}); + scope.context['isReady'] = 'ready'; + scope.context['logger'] = logger; var element = $('{{logger("inner")}}'); $compile(element, directives)(injector.createChild([new Module()..value(Scope, scope)]), element); expect(logger).toEqual(['new']); expect(logger).toEqual(['new']); - $rootScope.$digest(); - expect(logger).toEqual(['new', 'attach:@ready; =>ready', 'inner']); + rootScope.apply(); + var expected = ['new', 'attach:@ready; =>ready', 'inner']; + assert((() { + // there is an assertion in flush which double checks that + // flushes do not change model. This assertion creates one + // more 'inner'; + expected.add('inner'); + return true; + })()); + expect(logger).toEqual(expected); logger.clear(); backend.flush(); microLeap(); - expect(logger).toEqual(['templateLoaded', scope.shadowRoot]); + expect(logger).toEqual(['templateLoaded', rootScope.context['shadowRoot']]); logger.clear(); - scope.$destroy(); + scope.destroy(); expect(logger).toEqual(['detach']); expect(element.textWithShadow()).toEqual('WORKED'); }))); @@ -499,7 +508,7 @@ main() => describe('dte.compiler', () { describe('controller scoping', () { - it('should make controllers available to sibling and child controllers', async(inject((Compiler $compile, Scope $rootScope, Logger log, Injector injector) { + it('should make controllers available to sibling and child controllers', async(inject((Compiler $compile, Scope rootScope, Logger log, Injector injector) { var element = $(''); $compile(element, directives)(injector, element); microLeap(); @@ -507,12 +516,12 @@ main() => describe('dte.compiler', () { expect(log.result()).toEqual('TabComponent-0; LocalAttrDirective-0; PaneComponent-1; LocalAttrDirective-0; PaneComponent-2; LocalAttrDirective-0'); }))); - it('should reuse controllers for transclusions', async(inject((Compiler $compile, Scope $rootScope, Logger log, Injector injector) { + it('should reuse controllers for transclusions', async(inject((Compiler $compile, Scope rootScope, Logger log, Injector injector) { var element = $('
block
'); $compile(element, directives)(injector, element); microLeap(); - $rootScope.$apply(); + rootScope.apply(); expect(log.result()).toEqual('IncludeTransclude; SimpleTransclude'); }))); }); @@ -520,10 +529,10 @@ main() => describe('dte.compiler', () { describe('NgDirective', () { it('should allow creation of a new scope', inject((TestBed _) { - _.rootScope.name = 'cover me'; + _.rootScope.context['name'] = 'cover me'; _.compile('
{{name}}
'); - _.rootScope.$digest(); - expect(_.rootScope.name).toEqual('cover me'); + _.rootScope.apply(); + expect(_.rootScope.context['name']).toEqual('cover me'); expect(_.rootElement.text).toEqual('MyController'); })); }); @@ -571,8 +580,8 @@ class LocalAttrDirective { selector: '[simple-transclude-in-attach]', visibility: NgDirective.CHILDREN_VISIBILITY, children: NgAnnotation.TRANSCLUDE_CHILDREN) class SimpleTranscludeInAttachAttrDirective { - SimpleTranscludeInAttachAttrDirective(BlockHole blockHole, BoundBlockFactory boundBlockFactory, Logger log, Scope scope) { - scope.$evalAsync(() { + SimpleTranscludeInAttachAttrDirective(BlockHole blockHole, BoundBlockFactory boundBlockFactory, Logger log, RootScope scope) { + scope.runAsync(() { var block = boundBlockFactory(scope); block.insertAfter(blockHole); log('SimpleTransclude'); @@ -616,11 +625,11 @@ class PublishTypesAttrDirective implements PublishTypesDirectiveSuperType { @NgComponent( selector: 'simple', - template: r'{{name}}{{sep}}{{$id}}(SHADOW-CONTENT)' + template: r'{{name}}(SHADOW-CONTENT)' ) class SimpleComponent { SimpleComponent(Scope scope) { - scope.name = 'INNER'; + scope.context['name'] = 'INNER'; } } @@ -628,16 +637,17 @@ class SimpleComponent { selector: 'io', template: r'', map: const { - 'attr': '@scope.attr', - 'expr': '<=>scope.expr', - 'ondone': '&scope.ondone', + 'attr': '@scope.context.attr', + 'expr': '<=>scope.context.expr', + 'ondone': '&scope.context.ondone', } ) class IoComponent { Scope scope; IoComponent(Scope scope) { this.scope = scope; - scope.$root.ioComponent = this; + scope.rootScope.context['ioComponent'] = this; + scope.context['expr'] = 'initialExpr'; } } @@ -662,7 +672,7 @@ class IoControllerComponent { var onOptional; IoControllerComponent(Scope scope) { this.scope = scope; - scope.$root.ioComponent = this; + scope.rootScope.context['ioComponent'] = this; } } @@ -685,7 +695,7 @@ class UnpublishedIoControllerComponent { var onOptional; UnpublishedIoControllerComponent(Scope scope) { this.scope = scope; - scope.$root.ioComponent = this; + scope.rootScope.context['ioComponent'] = this; } } @@ -704,13 +714,13 @@ class NonAssignableMappingComponent { } @NgComponent( selector: 'camel-case-map', map: const { - 'camel-case': '@scope.camelCase', + 'camel-case': '@scope.context.camelCase', } ) class CamelCaseMapComponent { Scope scope; CamelCaseMapComponent(Scope this.scope) { - scope.$root.camelCase = scope; + scope.rootScope.context['camelCase'] = scope; } } @@ -718,7 +728,7 @@ class CamelCaseMapComponent { selector: 'parent-expression', template: '
inside {{fromParent()}}
', map: const { - 'from-parent': '&scope.fromParent', + 'from-parent': '&scope.context.fromParent', } ) class ParentExpressionComponent { @@ -778,7 +788,7 @@ class AttachDetachComponent implements NgAttachAware, NgDetachAware, NgShadowRoo attach() => logger('attach:@$attrValue; =>$exprValue'); detach() => logger('detach'); onShadowRoot(shadowRoot) { - scope.$root.shadowRoot = shadowRoot; + scope.rootScope.context['shadowRoot'] = shadowRoot; logger(shadowRoot); } } @@ -789,7 +799,7 @@ class AttachDetachComponent implements NgAttachAware, NgDetachAware, NgShadowRoo ) class MyController { MyController(Scope scope) { - scope.name = 'MyController'; + scope.context['name'] = 'MyController'; } } @@ -822,7 +832,7 @@ class ExprAttrComponent { var exprOnce; ExprAttrComponent(Scope scope) { - scope.$root.exprAttrComponent = this; + scope.rootScope.context['exprAttrComponent'] = this; } } diff --git a/test/core_dom/http_spec.dart b/test/core_dom/http_spec.dart index 54382dbb6..23aa22583 100644 --- a/test/core_dom/http_spec.dart +++ b/test/core_dom/http_spec.dart @@ -51,7 +51,7 @@ main() => describe('http', () { })); afterEach(inject((ExceptionHandler eh, Scope scope) { - scope.$digest(); + scope.apply(); backend.verifyNoOutstandingRequest(); (eh as LoggingExceptionHandler).assertEmpty(); })); @@ -826,7 +826,7 @@ main() => describe('http', () { callback(); }); - //$rootScope.$apply(() { + //$rootScope.apply(() { canceler.resolve(); //}); diff --git a/test/core_dom/ng_mustache_spec.dart b/test/core_dom/ng_mustache_spec.dart index fcd98b5eb..07017c5ee 100644 --- a/test/core_dom/ng_mustache_spec.dart +++ b/test/core_dom/ng_mustache_spec.dart @@ -11,69 +11,65 @@ main() { })); beforeEach(inject((TestBed tb) => _ = tb)); - it('should replace {{}} in text', inject((Compiler $compile, Scope $rootScope, Injector injector, DirectiveMap directives) { + it('should replace {{}} in text', inject((Compiler $compile, Scope rootScope, Injector injector, DirectiveMap directives) { var element = $('
{{name}}!
'); var template = $compile(element, directives); - $rootScope.name = 'OK'; + rootScope.context['name'] = 'OK'; var block = template(injector); element = $(block.elements); - expect(element.text()).toEqual('!'); - $rootScope.$digest(); + rootScope.apply(); expect(element.text()).toEqual('OK!'); })); it('should allow listening on text change events', inject((Logger logger) { _.compile('
{{text}}
'); - _.rootScope.text = 'works'; - _.rootScope.$apply(); + _.rootScope.context['text'] = 'works'; + _.rootScope.apply(); expect(_.rootElement.text).toEqual('works'); - expect(logger).toEqual(['', 'works']); + expect(logger).toEqual(['works']); })); - it('should replace {{}} in attribute', inject((Compiler $compile, Scope $rootScope, Injector injector, DirectiveMap directives) { + it('should replace {{}} in attribute', inject((Compiler $compile, Scope rootScope, Injector injector, DirectiveMap directives) { var element = $('
'); var template = $compile(element, directives); - $rootScope.name = 'OK'; - $rootScope.age = 23; + rootScope.context['name'] = 'OK'; + rootScope.context['age'] = 23; var block = template(injector); element = $(block.elements); - expect(element.attr('some-attr')).toEqual(''); - expect(element.attr('other-attr')).toEqual(''); - $rootScope.$digest(); + rootScope.apply(); expect(element.attr('some-attr')).toEqual('OK'); expect(element.attr('other-attr')).toEqual('23'); })); - it('should allow newlines in attribute', inject((Compiler $compile, Scope $rootScope, Injector injector, DirectiveMap directives) { + it('should allow newlines in attribute', inject((Compiler $compile, RootScope rootScope, Injector injector, DirectiveMap directives) { var element = $('
'); var template = $compile(element, directives); - $rootScope.line1 = 'L1'; - $rootScope.line2 = 'L2'; + rootScope.context['line1'] = 'L1'; + rootScope.context['line2'] = 'L2'; var block = template(injector); element = $(block.elements); - expect(element.attr('multiline-attr')).toEqual(''); - $rootScope.$digest(); + rootScope.apply(); expect(element.attr('multiline-attr')).toEqual('line1: L1\nline2: L2'); })); - it('should handle filters', inject((Compiler $compile, Scope $rootScope, Injector injector, DirectiveMap directives) { + it('should handle filters', inject((Compiler $compile, RootScope rootScope, Injector injector, DirectiveMap directives) { var element = $('
{{"World" | hello}}
'); var template = $compile(element, directives); var block = template(injector); - $rootScope.$digest(); + rootScope.apply(); element = $(block.elements); @@ -91,13 +87,13 @@ main() { expect(element).not.toHaveClass('ng-hide'); - _.rootScope.$apply(() { - _.rootScope['isVisible'] = true; + _.rootScope.apply(() { + _.rootScope.context['isVisible'] = true; }); expect(element).not.toHaveClass('ng-hide'); - _.rootScope.$apply(() { - _.rootScope['isVisible'] = false; + _.rootScope.apply(() { + _.rootScope.context['isVisible'] = false; }); expect(element).toHaveClass('ng-hide'); }); @@ -108,14 +104,14 @@ main() { expect(element).not.toHaveClass('active'); expect(element).not.toHaveClass('ng-hide'); - _.rootScope.$apply(() { - _.rootScope['currentCls'] = 'active'; + _.rootScope.apply(() { + _.rootScope.context['currentCls'] = 'active'; }); expect(element).toHaveClass('active'); expect(element).toHaveClass('ng-hide'); - _.rootScope.$apply(() { - _.rootScope['isVisible'] = true; + _.rootScope.apply(() { + _.rootScope.context['isVisible'] = true; }); expect(element).toHaveClass('active'); expect(element).not.toHaveClass('ng-hide'); diff --git a/test/directive/input_select_spec.dart b/test/directive/input_select_spec.dart index 9947b25a6..7fe66c8c7 100644 --- a/test/directive/input_select_spec.dart +++ b/test/directive/input_select_spec.dart @@ -18,14 +18,14 @@ main() { ''); var r2d2 = {"name":"r2d2"}; var c3p0 = {"name":"c3p0"}; - _.rootScope.robots = [ r2d2, c3p0 ]; - _.rootScope.$digest(); + _.rootScope.context['robots'] = [ r2d2, c3p0 ]; + _.rootScope.apply(); _.selectOption(_.rootElement, 'c3p0'); - expect(_.rootScope.robot).toEqual(c3p0); + expect(_.rootScope.context['robot']).toEqual(c3p0); - _.rootScope.robot = r2d2; - _.rootScope.$digest(); - expect(_.rootScope.robot).toEqual(r2d2); + _.rootScope.context['robot'] = r2d2; + _.rootScope.apply(); + expect(_.rootScope.context['robot']).toEqual(r2d2); expect(_.rootElement).toEqualSelect([['r2d2'], 'c3p0']); }); @@ -36,14 +36,14 @@ main() { ''); var r2d2 = { "name":"r2d2"}; var c3p0 = {"name":"c3p0"}; - _.rootScope.robots = [ r2d2, c3p0 ]; - _.rootScope.$digest(); + _.rootScope.context['robots'] = [ r2d2, c3p0 ]; + _.rootScope.apply(); _.selectOption(_.rootElement, 'c3p0'); - expect(_.rootScope.robot).toEqual([c3p0]); + expect(_.rootScope.context['robot']).toEqual([c3p0]); - _.rootScope.robot = [r2d2]; - _.rootScope.$digest(); - expect(_.rootScope.robot).toEqual([r2d2]); + _.rootScope.context['robot'] = [r2d2]; + _.rootScope.apply(); + expect(_.rootScope.context['robot']).toEqual([r2d2]); expect(_.rootElement).toEqualSelect([['r2d2'], 'c3p0']); }); }); @@ -61,9 +61,9 @@ main() { '' '' ''); - _.rootScope.$apply(() { - _.rootScope['a'] = 'foo'; - _.rootScope['b'] = 'bar'; + _.rootScope.apply(() { + _.rootScope.context['a'] = 'foo'; + _.rootScope.context['b'] = 'bar'; }); expect(_.rootElement.text).toEqual('foobarC'); @@ -77,7 +77,7 @@ main() { '' '' ''); - _.rootScope.$digest(); + _.rootScope.apply(); expect(_.rootElement).toEqualSelect(['not me', ['me!'], 'nah']); }); @@ -88,11 +88,11 @@ main() { '' ''); - _.rootScope['robots'] = ['c3p0', 'r2d2']; - _.rootScope['robot'] = 'r2d2'; - _.rootScope.$apply(); + _.rootScope.context['robots'] = ['c3p0', 'r2d2']; + _.rootScope.context['robot'] = 'r2d2'; + _.rootScope.apply(); - var select = _.rootScope['p'].directive(InputSelectDirective); + var select = _.rootScope.context['p'].directive(InputSelectDirective); expect(_.rootElement).toEqualSelect(['c3p0', ['r2d2']]); _.rootElement.querySelectorAll('option')[0].selected = true; @@ -100,21 +100,21 @@ main() { expect(_.rootElement).toEqualSelect([['c3p0'], 'r2d2']); - expect(_.rootScope['robot']).toEqual('c3p0'); + expect(_.rootScope.context['robot']).toEqual('c3p0'); - _.rootScope.$apply(() { - _.rootScope['robots'].insert(0, 'wallee'); + _.rootScope.apply(() { + _.rootScope.context['robots'].insert(0, 'wallee'); }); expect(_.rootElement).toEqualSelect(['wallee', ['c3p0'], 'r2d2']); - expect(_.rootScope['robot']).toEqual('c3p0'); + expect(_.rootScope.context['robot']).toEqual('c3p0'); - _.rootScope.$apply(() { - _.rootScope['robots'] = ['c3p0+', 'r2d2+']; - _.rootScope['robot'] = 'r2d2+'; + _.rootScope.apply(() { + _.rootScope.context['robots'] = ['c3p0+', 'r2d2+']; + _.rootScope.context['robot'] = 'r2d2+'; }); expect(_.rootElement).toEqualSelect(['c3p0+', ['r2d2+']]); - expect(_.rootScope['robot']).toEqual('r2d2+'); + expect(_.rootScope.context['robot']).toEqual('r2d2+'); }); describe('empty option', () { @@ -125,7 +125,7 @@ main() { '' + '' + ''); - _.rootScope.$digest(); + _.rootScope.apply(); expect(_.rootElement).toEqualSelect([[''], 'x', 'y']); }); @@ -137,61 +137,61 @@ main() { '' + '' + ''); - _.rootScope.$digest(); + _.rootScope.apply(); expect(_.rootElement).toEqualSelect(['x', [''], 'y']); }); it('should set the model to empty string when empty option is selected', () { - _.rootScope['robot'] = 'x'; + _.rootScope.context['robot'] = 'x'; _.compile( ''); - _.rootScope.$digest(); + _.rootScope.apply(); - var select = _.rootScope['p'].directive(InputSelectDirective); + var select = _.rootScope.context['p'].directive(InputSelectDirective); expect(_.rootElement).toEqualSelect(['', ['x'], 'y']); _.selectOption(_.rootElement, '--select--'); expect(_.rootElement).toEqualSelect([[''], 'x', 'y']); - expect(_.rootScope['robot']).toEqual(null); + expect(_.rootScope.context['robot']).toEqual(null); }); describe('interactions with repeated options', () { it('should select empty option when model is undefined', () { - _.rootScope['robots'] = ['c3p0', 'r2d2']; + _.rootScope.context['robots'] = ['c3p0', 'r2d2']; _.compile( ''); - _.rootScope.$digest(); + _.rootScope.apply(); expect(_.rootElement).toEqualSelect([[''], 'c3p0', 'r2d2']); }); it('should set model to empty string when selected', () { - _.rootScope['robots'] = ['c3p0', 'r2d2']; + _.rootScope.context['robots'] = ['c3p0', 'r2d2']; _.compile( ''); - _.rootScope.$digest(); - var select = _.rootScope['p'].directive(InputSelectDirective); + _.rootScope.apply(); + var select = _.rootScope.context['p'].directive(InputSelectDirective); _.selectOption(_.rootElement, 'c3p0'); expect(_.rootElement).toEqualSelect(['', ['c3p0'], 'r2d2']); - expect( _.rootScope['robot']).toEqual('c3p0'); + expect( _.rootScope.context['robot']).toEqual('c3p0'); _.selectOption(_.rootElement, '--select--'); expect(_.rootElement).toEqualSelect([[''], 'c3p0', 'r2d2']); - expect( _.rootScope['robot']).toEqual(null); + expect( _.rootScope.context['robot']).toEqual(null); }); it('should not break if both the select and repeater models change at once', () { @@ -200,16 +200,16 @@ main() { '' + '' + ''); - _.rootScope.$apply(() { - _.rootScope['robots'] = ['c3p0', 'r2d2']; - _.rootScope['robot'] = 'c3p0'; + _.rootScope.apply(() { + _.rootScope.context['robots'] = ['c3p0', 'r2d2']; + _.rootScope.context['robot'] = 'c3p0'; }); expect(_.rootElement).toEqualSelect(['', ['c3p0'], 'r2d2']); - _.rootScope.$apply(() { - _.rootScope['robots'] = ['wallee']; - _.rootScope['robot'] = ''; + _.rootScope.apply(() { + _.rootScope.context['robots'] = ['wallee']; + _.rootScope.context['robot'] = ''; }); expect(_.rootElement).toEqualSelect([[''], 'wallee']); @@ -224,17 +224,17 @@ main() { '' + '' + ''); - _.rootScope.$digest(); + _.rootScope.apply(); expect(_.rootElement).toEqualSelect([['?'], 'c3p0', 'r2d2']); - _.rootScope.$apply(() { - _.rootScope['robot'] = 'r2d2'; + _.rootScope.apply(() { + _.rootScope.context['robot'] = 'r2d2'; }); expect(_.rootElement).toEqualSelect(['c3p0', ['r2d2']]); - _.rootScope.$apply(() { - _.rootScope['robot'] = "wallee"; + _.rootScope.apply(() { + _.rootScope.context['robot'] = "wallee"; }); expect(_.rootElement).toEqualSelect([['?'], 'c3p0', 'r2d2']); }); @@ -247,71 +247,71 @@ main() { '' + '' + ''); - _.rootScope.$digest(); + _.rootScope.apply(); expect(_.rootElement).toEqualSelect([[''], 'c3p0', 'r2d2']); - expect(_.rootScope['robot']).toEqual(null); + expect(_.rootScope.context['robot']).toEqual(null); - _.rootScope.$apply(() { - _.rootScope['robot'] = 'wallee'; + _.rootScope.apply(() { + _.rootScope.context['robot'] = 'wallee'; }); expect(_.rootElement).toEqualSelect([['?'], '', 'c3p0', 'r2d2']); - _.rootScope.$apply(() { - _.rootScope['robot'] = 'r2d2'; + _.rootScope.apply(() { + _.rootScope.context['robot'] = 'r2d2'; }); expect(_.rootElement).toEqualSelect(['', 'c3p0', ['r2d2']]); - _.rootScope.$apply(() { - _.rootScope['robot'] = null; + _.rootScope.apply(() { + _.rootScope.context['robot'] = null; }); expect(_.rootElement).toEqualSelect([[''], 'c3p0', 'r2d2']); }); it("should insert&select temporary unknown option when no options-model match, empty " + "option is present and model is defined", () { - _.rootScope['robot'] = 'wallee'; + _.rootScope.context['robot'] = 'wallee'; _.compile( ''); - _.rootScope.$digest(); + _.rootScope.apply(); expect(_.rootElement).toEqualSelect([['?'], '', 'c3p0', 'r2d2']); - _.rootScope.$apply(() { - _.rootScope['robot'] = 'r2d2'; + _.rootScope.apply(() { + _.rootScope.context['robot'] = 'r2d2'; }); expect(_.rootElement).toEqualSelect(['', 'c3p0', ['r2d2']]); }); describe('interactions with repeated options', () { it('should work with repeated options', () { - _.rootScope['robots'] = []; + _.rootScope.context['robots'] = []; _.compile( ''); - _.rootScope.$apply(() { - _.rootScope['robots'] = []; + _.rootScope.apply(() { + _.rootScope.context['robots'] = []; }); expect(_.rootElement).toEqualSelect([['?']]); - expect(_.rootScope['robot']).toEqual(null); + expect(_.rootScope.context['robot']).toEqual(null); - _.rootScope.$apply(() { - _.rootScope['robot'] = 'r2d2'; + _.rootScope.apply(() { + _.rootScope.context['robot'] = 'r2d2'; }); expect(_.rootElement).toEqualSelect([['?']]); - expect(_.rootScope['robot']).toEqual('r2d2'); + expect(_.rootScope.context['robot']).toEqual('r2d2'); - _.rootScope.$apply(() { - _.rootScope['robots'] = ['c3p0', 'r2d2']; + _.rootScope.apply(() { + _.rootScope.context['robots'] = ['c3p0', 'r2d2']; }); expect(_.rootElement).toEqualSelect(['c3p0', ['r2d2']]); - expect(_.rootScope['robot']).toEqual('r2d2'); + expect(_.rootScope.context['robot']).toEqual('r2d2'); }); it('should work with empty option and repeated options', () { @@ -320,24 +320,24 @@ main() { '' + '' + ''); - _.rootScope.$apply(() { - _.rootScope['robots'] = []; + _.rootScope.apply(() { + _.rootScope.context['robots'] = []; }); expect(_.rootElement).toEqualSelect([['']]); - expect(_.rootScope['robot']).toEqual(null); + expect(_.rootScope.context['robot']).toEqual(null); - _.rootScope.$apply(() { - _.rootScope['robot'] = 'r2d2'; + _.rootScope.apply(() { + _.rootScope.context['robot'] = 'r2d2'; }); expect(_.rootElement).toEqualSelect([['?'], '']); - expect(_.rootScope['robot']).toEqual('r2d2'); + expect(_.rootScope.context['robot']).toEqual('r2d2'); - _.rootScope.$apply(() { - _.rootScope['robots'] = ['c3p0', 'r2d2']; + _.rootScope.apply(() { + _.rootScope.context['robots'] = ['c3p0', 'r2d2']; }); expect(_.rootElement).toEqualSelect(['', 'c3p0', ['r2d2']]); - expect(_.rootScope['robot']).toEqual('r2d2'); + expect(_.rootScope.context['robot']).toEqual('r2d2'); }); it('should insert unknown element when repeater shrinks and selected option is ' + @@ -347,31 +347,31 @@ main() { ''); - _.rootScope.$apply(() { - _.rootScope['robots'] = ['c3p0', 'r2d2']; - _.rootScope['robot'] = 'r2d2'; + _.rootScope.apply(() { + _.rootScope.context['robots'] = ['c3p0', 'r2d2']; + _.rootScope.context['robot'] = 'r2d2'; }); expect(_.rootElement).toEqualSelect(['c3p0', ['r2d2']]); - expect(_.rootScope['robot']).toEqual('r2d2'); + expect(_.rootScope.context['robot']).toEqual('r2d2'); - _.rootScope.$apply(() { - _.rootScope['robots'].remove('r2d2'); + _.rootScope.apply(() { + _.rootScope.context['robots'].remove('r2d2'); }); - expect(_.rootScope['robot']).toEqual('r2d2'); + expect(_.rootScope.context['robot']).toEqual('r2d2'); expect(_.rootElement).toEqualSelect([['?'], 'c3p0']); - _.rootScope.$apply(() { - _.rootScope['robots'].insert(0, 'r2d2'); + _.rootScope.apply(() { + _.rootScope.context['robots'].insert(0, 'r2d2'); }); expect(_.rootElement).toEqualSelect([['r2d2'], 'c3p0']); - expect(_.rootScope['robot']).toEqual('r2d2'); + expect(_.rootScope.context['robot']).toEqual('r2d2'); - _.rootScope.$apply(() { - _.rootScope['robots'].clear(); + _.rootScope.apply(() { + _.rootScope.context['robots'].clear(); }); expect(_.rootElement).toEqualSelect([['?']]); - expect(_.rootScope['robot']).toEqual('r2d2'); + expect(_.rootScope.context['robot']).toEqual('r2d2'); }); }); }); @@ -387,16 +387,16 @@ main() { '' + '' + ''); - _.rootScope.model = 'a'; - _.rootScope.attached = true; - _.rootScope.$apply(); - expect(_.rootElement).toEqualSelect([['a'], 'b']); - _.rootScope.attached = false; - _.rootScope.$apply(); + _.rootScope.context['model'] = 'b'; + _.rootScope.context['attached'] = true; + _.rootScope.apply(); + expect(_.rootElement).toEqualSelect(['a', ['b']]); + _.rootScope.context['attached'] = false; + _.rootScope.apply(); expect(_.rootElement).toEqualSelect([]); - _.rootScope.attached = true; - _.rootScope.$apply(); - expect(_.rootElement).toEqualSelect([['a'], 'b']); + _.rootScope.context['attached'] = true; + _.rootScope.apply(); + expect(_.rootElement).toEqualSelect(['a', ['b']]); }); @@ -410,16 +410,16 @@ main() { '' + '' + ''); - _.rootScope.model = ['a']; - _.rootScope.attached = true; - _.rootScope.$apply(); - expect(_.rootElement).toEqualSelect([['a'], 'b']); - _.rootScope.attached = false; - _.rootScope.$apply(); + _.rootScope.context['model'] = ['b']; + _.rootScope.context['attached'] = true; + _.rootScope.apply(); + expect(_.rootElement).toEqualSelect(['a', ['b']]); + _.rootScope.context['attached'] = false; + _.rootScope.apply(); expect(_.rootElement).toEqualSelect([]); - _.rootScope.attached = true; - _.rootScope.$apply(); - expect(_.rootElement).toEqualSelect([['a'], 'b']); + _.rootScope.context['attached'] = true; + _.rootScope.apply(); + expect(_.rootElement).toEqualSelect(['a', ['b']]); }); }); @@ -433,7 +433,7 @@ main() { compile(html) { _.compile('
' + html + '
'); element = _.rootElement.querySelector('select'); - scope.$apply(); + scope.apply(); } beforeEach(inject((Scope rootScope) { @@ -443,7 +443,7 @@ main() { afterEach(() { - scope.$destroy(); //disables unknown option work during destruction + scope.destroy(); //disables unknown option work during destruction }); @@ -456,9 +456,9 @@ main() { '' + '' + ''); - scope.$apply(() { - scope.a = 'foo'; - scope.b = 'bar'; + scope.apply(() { + scope.context['a'] = 'foo'; + scope.context['b'] = 'bar'; }); expect(element.text).toEqual('foobarC'); @@ -483,12 +483,12 @@ main() { '' + ''); - scope.change = () { - log += 'change:${scope.selection};'; + scope.context['change'] = () { + log += 'change:${scope.context['selection']};'; }; - scope.$apply(() { - scope.selection = 'c'; + scope.apply(() { + scope.context['selection'] = 'c'; }); element.value = 'c'; @@ -504,33 +504,33 @@ main() { '' + ''); - scope.change = () { + scope.context['change'] = () { scope.log += 'change;'; }; - scope.$apply(() { - scope.log = ''; - scope.selection = 'c'; + scope.apply(() { + scope.context['log'] = ''; + scope.context['selection'] = 'c'; }); - expect(scope.form.select.$error.required).toEqual(false);; + expect(scope.context['form'].select.$error.required).toEqual(false);; expect(element).toEqualValid(); expect(element).toEqualPristine(); - scope.$apply(() { - scope.selection = ''; + scope.apply(() { + scope.context['selection'] = ''; }); - expect(scope.form.select.$error.required).toEqual(true);; + expect(scope.context['form'].select.$error.required).toEqual(true);; expect(element).toEqualInvalid(); expect(element).toEqualPristine(); - expect(scope.log).toEqual(''); + expect(scope.context['log']).toEqual(''); element[0].value = 'c'; _.triggerEvent(element, 'change'); expect(element).toEqualValid(); expect(element).toEqualDirty(); - expect(scope.log).toEqual('change;'); + expect(scope.context['log']).toEqual('change;'); }); @@ -581,14 +581,14 @@ main() { '' + ''); - scope.$apply(() { - scope.selection = ['A']; + scope.apply(() { + scope.context['selection'] = ['A']; }); expect(element).toEqualSelect([['A'], 'B']); - scope.$apply(() { - scope.selection.add('B'); + scope.apply(() { + scope.context['selection'].add('B'); }); expect(element).toEqualSelect([['A'], ['B']]); @@ -603,15 +603,15 @@ main() { ''); expect(element).toEqualSelect(['A', 'B']); - expect(scope.selection).toEqual(null); + expect(scope.context['selection']).toEqual(null); - scope.$apply(() { - scope.selection = ['A']; + scope.apply(() { + scope.context['selection'] = ['A']; }); expect(element).toEqualSelect([['A'], 'B']); - scope.$apply(() { - scope.selection.add('B'); + scope.apply(() { + scope.context['selection'].add('B'); }); expect(element).toEqualSelect([['A'], ['B']]); }); @@ -623,16 +623,16 @@ main() { '' + ''); - scope.$apply(() { - scope.selection = []; + scope.apply(() { + scope.context['selection'] = []; }); - expect(scope.form.select.$error.required).toEqual(true);; + expect(scope.context['form'].select.$error.required).toEqual(true);; expect(element).toEqualInvalid(); expect(element).toEqualPristine(); - scope.$apply(() { - scope.selection = ['A']; + scope.apply(() { + scope.context['selection'] = ['A']; }); expect(element).toEqualValid(); @@ -682,9 +682,9 @@ main() { it('should render a list', () { createSingleSelect(); - scope.$apply(() { - scope.values = [{'name': 'A'}, {'name': 'B'}, {'name': 'C'}]; - scope.selected = scope.values[0]; + scope.apply(() { + scope.context['values'] = [{'name': 'A'}, {'name': 'B'}, {'name': 'C'}]; + scope.context['selected'] = scope.context['values'][0]; }); var options = element.querySelectorAll('option'); @@ -695,9 +695,9 @@ main() { it('should render zero as a valid display value', () { createSingleSelect(); - scope.$apply(() { - scope.values = [{'name': '0'}, {'name': '1'}, {'name': '2'}]; - scope.selected = scope.values[0]; + scope.apply(() { + scope.context['values'] = [{'name': '0'}, {'name': '1'}, {'name': '2'}]; + scope.context['selected'] = scope.context['values'][0]; }); var options = element.querySelectorAll('option'); @@ -708,24 +708,24 @@ main() { it('should grow list', () { createSingleSelect(); - scope.$apply(() { - scope.values = []; + scope.apply(() { + scope.context['values'] = []; }); expect(element.querySelectorAll('option').length).toEqual(1); // because we add special empty option expect(element.querySelectorAll('option')[0].text).toEqual(''); expect(element.querySelectorAll('option')[0].value).toEqual('?'); - scope.$apply(() { - scope.values.add({'name':'A'}); - scope.selected = scope.values[0]; + scope.apply(() { + scope.context['values'].add({'name':'A'}); + scope.context['selected'] = scope.context['values'][0]; }); expect(element.querySelectorAll('option').length).toEqual(1); expect(element).toEqualSelect([['A']]); - scope.$apply(() { - scope.values.add({'name':'B'}); + scope.apply(() { + scope.context['values'].add({'name':'B'}); }); expect(element.querySelectorAll('option').length).toEqual(2); @@ -736,30 +736,30 @@ main() { it('should shrink list', () { createSingleSelect(); - scope.$apply(() { - scope.values = [{'name':'A'}, {'name':'B'}, {'name':'C'}]; - scope.selected = scope.values[0]; + scope.apply(() { + scope.context['values'] = [{'name':'A'}, {'name':'B'}, {'name':'C'}]; + scope.context['selected'] = scope.context['values'][0]; }); expect(element.querySelectorAll('option').length).toEqual(3); - scope.$apply(() { - scope.values.removeLast(); + scope.apply(() { + scope.context['values'].removeLast(); }); expect(element.querySelectorAll('option').length).toEqual(2); expect(element).toEqualSelect([['A'], 'B']); - scope.$apply(() { - scope.values.removeLast(); + scope.apply(() { + scope.context['values'].removeLast(); }); expect(element.querySelectorAll('option').length).toEqual(1); expect(element).toEqualSelect([['A']]); - scope.$apply(() { - scope.values.removeLast(); - scope.selected = null; + scope.apply(() { + scope.context['values'].removeLast(); + scope.context['selected'] = null; }); expect(element.querySelectorAll('option').length).toEqual(1); // we add back the special empty option @@ -769,23 +769,23 @@ main() { it('should shrink and then grow list', () { createSingleSelect(); - scope.$apply(() { - scope.values = [{'name':'A'}, {'name':'B'}, {'name':'C'}]; - scope.selected = scope.values[0]; + scope.apply(() { + scope.context['values'] = [{'name':'A'}, {'name':'B'}, {'name':'C'}]; + scope.context['selected'] = scope.context['values'][0]; }); expect(element.querySelectorAll('option').length).toEqual(3); - scope.$apply(() { - scope.values = [{'name': '1'}, {'name': '2'}]; - scope.selected = scope.values[0]; + scope.apply(() { + scope.context['values'] = [{'name': '1'}, {'name': '2'}]; + scope.context['selected'] = scope.context['values'][0]; }); expect(element.querySelectorAll('option').length).toEqual(2); - scope.$apply(() { - scope.values = [{'name': 'A'}, {'name': 'B'}, {'name': 'C'}]; - scope.selected = scope.values[0]; + scope.apply(() { + scope.context['values'] = [{'name': 'A'}, {'name': 'B'}, {'name': 'C'}]; + scope.context['selected'] = scope.context['values'][0]; }); expect(element.querySelectorAll('option').length).toEqual(3); @@ -795,14 +795,14 @@ main() { it('should update list', () { createSingleSelect(); - scope.$apply(() { - scope.values = [{'name': 'A'}, {'name': 'B'}, {'name': 'C'}]; - scope.selected = scope.values[0]; + scope.apply(() { + scope.context['values'] = [{'name': 'A'}, {'name': 'B'}, {'name': 'C'}]; + scope.context['selected'] = scope.context['values'][0]; }); expect(element).toEqualSelect([['A'], 'B', 'C']); - scope.$apply(() { - scope.values = [{'name': 'B'}, {'name': 'C'}, {'name': 'D'}]; - scope.selected = scope.values[0]; + scope.apply(() { + scope.context['values'] = [{'name': 'B'}, {'name': 'C'}, {'name': 'D'}]; + scope.context['selected'] = scope.context['values'][0]; }); var options = element.querySelectorAll('option'); @@ -814,24 +814,24 @@ main() { it('should preserve existing options', () { createSingleSelect(true); - scope.$apply(() { - scope.values = []; + scope.apply(() { + scope.context['values'] = []; }); expect(element.querySelectorAll('option').length).toEqual(1); - scope.$apply(() { - scope.values = [{'name': 'A'}]; - scope.selected = scope.values[0]; + scope.apply(() { + scope.context['values'] = [{'name': 'A'}]; + scope.context['selected'] = scope.context['values'][0]; }); expect(element.querySelectorAll('option').length).toEqual(2); expect(element.querySelectorAll('option')[0].text).toEqual('blank'); expect(element.querySelectorAll('option')[1].text).toEqual('A'); - scope.$apply(() { - scope.values = []; - scope.selected = null; + scope.apply(() { + scope.context['values'] = []; + scope.context['selected'] = null; }); expect(element.querySelectorAll('option').length).toEqual(1); @@ -843,15 +843,15 @@ main() { it('should bind to scope value', () { createSingleSelect(); - scope.$apply(() { - scope.values = [{'name': 'A'}, {'name': 'B'}]; - scope.selected = scope.values[0]; + scope.apply(() { + scope.context['values'] = [{'name': 'A'}, {'name': 'B'}]; + scope.context['selected'] = scope.context['values'][0]; }); expect(element).toEqualSelect([['A'], 'B']); - scope.$apply(() { - scope.selected = scope.values[1]; + scope.apply(() { + scope.context['selected'] = scope.context['values'][1]; }); expect(element).toEqualSelect(['A', ['B']]); @@ -865,13 +865,13 @@ main() { 'ng-options': 'item.name group by item.group for item in values' }); - scope.$apply(() { - scope.values = [{'name': 'A'}, + scope.apply(() { + scope.context['values'] = [{'name': 'A'}, {'name': 'B', group: 'first'}, {'name': 'C', group: 'second'}, {'name': 'D', group: 'first'}, {'name': 'E', group: 'second'}]; - scope.selected = scope.values[3]; + scope.context['selected'] = scope.context['values'][3]; }); expect(element).toEqualSelect(['A', 'B', ['D'], 'C', 'E']); @@ -890,8 +890,8 @@ main() { expect(c.text).toEqual('C'); expect(e.text).toEqual('E'); - scope.$apply(() { - scope.selected = scope.values[0]; + scope.apply(() { + scope.context['selected'] = scope.context['values'][0]; }); expect(element.value).toEqual('0'); @@ -901,15 +901,15 @@ main() { it('should bind to scope value through experession', () { createSelect({'ng-model': 'selected'}, null, null, 'item in values', 'item.name', 'item.id'); - scope.$apply(() { - scope.values = [{'id': 10, 'name': 'A'}, {'id': 20, 'name': 'B'}]; - scope.selected = scope.values[0]['id']; + scope.apply(() { + scope.context['values'] = [{'id': 10, 'name': 'A'}, {'id': 20, 'name': 'B'}]; + scope.context['selected'] = scope.context['values'][0]['id']; }); expect(element).toEqualSelect([['A'], 'B']); - scope.$apply(() { - scope.selected = scope.values[1]['id']; + scope.apply(() { + scope.context['selected'] = scope.context['values'][1]['id']; }); expect(element).toEqualSelect(['A', ['B']]); @@ -919,17 +919,17 @@ main() { it('should insert a blank option if bound to null', () { createSingleSelect(); - scope.$apply(() { - scope.values = [{'name': 'A'}]; - scope.selected = null; + scope.apply(() { + scope.context['values'] = [{'name': 'A'}]; + scope.context['selected'] = null; }); expect(element.querySelectorAll('option').length).toEqual(2); expect(element).toEqualSelect([['?'], 'A']); expect(element.querySelectorAll('option')[0].value).toEqual('?'); - scope.$apply(() { - scope.selected = scope.values[0]; + scope.apply(() { + scope.context['selected'] = scope.context['values'][0]; }); expect(element).toEqualSelect([['A']]); @@ -940,17 +940,17 @@ main() { it('should reuse blank option if bound to null', () { createSingleSelect(true); - scope.$apply(() { - scope.values = [{'name': 'A'}]; - scope.selected = null; + scope.apply(() { + scope.context['values'] = [{'name': 'A'}]; + scope.context['selected'] = null; }); expect(element.querySelectorAll('option').length).toEqual(2); expect(element.value).toEqual(''); expect(element.querySelectorAll('option')[0].value).toEqual(''); - scope.$apply(() { - scope.selected = scope.values[0]; + scope.apply(() { + scope.context['selected'] = scope.context['values'][0]; }); expect(element).toEqualSelect(['', ['A']]); @@ -961,17 +961,17 @@ main() { it('should insert a unknown option if bound to something not in the list', () { createSingleSelect(); - scope.$apply(() { - scope.values = [{'name': 'A'}]; - scope.selected = {}; + scope.apply(() { + scope.context['values'] = [{'name': 'A'}]; + scope.context['selected'] = {}; }); expect(element.querySelectorAll('option').length).toEqual(2); expect(element.value).toEqual('?'); expect(element.querySelectorAll('option')[0].value).toEqual('?'); - scope.$apply(() { - scope.selected = scope.values[0]; + scope.apply(() { + scope.context['selected'] = scope.context['values'][0]; }); expect(element).toEqualSelect([['A']]); @@ -982,9 +982,9 @@ main() { it('should select correct input if previously selected option was "?"', () { createSingleSelect(); - scope.$apply(() { - scope.values = [{'name': 'A'}, {'name': 'B'}]; - scope.selected = {}; + scope.apply(() { + scope.context['values'] = [{'name': 'A'}, {'name': 'B'}]; + scope.context['selected'] = {}; }); expect(element.querySelectorAll('option').length).toEqual(3); @@ -992,7 +992,7 @@ main() { expect(element.querySelectorAll('option')[0].value).toEqual('?'); _.selectOption(element, 'A'); - expect(scope.selected).toBe(scope.values[0]); + expect(scope.context['selected']).toBe(scope.context['values'][0]); expect(element.querySelectorAll('option')[0].selected).toEqual(true); expect(element.querySelectorAll('option')[0].selected).toEqual(true);; expect(element.querySelectorAll('option').length).toEqual(2); @@ -1006,9 +1006,9 @@ main() { var option; createSingleSelect(''); - scope.$apply(() { - scope.blankVal = 'so blank'; - scope.values = [{'name': 'A'}]; + scope.apply(() { + scope.context['blankVal'] = 'so blank'; + scope.context['values'] = [{'name': 'A'}]; }); // check blank option is first and is compiled @@ -1017,8 +1017,8 @@ main() { expect(option.value).toEqual(''); expect(option.text).toEqual('blank is so blank'); - scope.$apply(() { - scope.blankVal = 'not so blank'; + scope.apply(() { + scope.context['blankVal'] = 'not so blank'; }); // check blank option is first and is compiled @@ -1033,9 +1033,9 @@ main() { var option; createSingleSelect(''); - scope.$apply(() { - scope.blankVal = 'so blank'; - scope.values = [{'name': 'A'}]; + scope.apply(() { + scope.context['blankVal'] = 'so blank'; + scope.context['values'] = [{'name': 'A'}]; }); // check blank option is first and is compiled @@ -1050,9 +1050,9 @@ main() { var option; createSingleSelect(''); - scope.$apply(() { - scope.blankVal = 'is blank'; - scope.values = [{'name': 'A'}]; + scope.apply(() { + scope.context['blankVal'] = 'is blank'; + scope.context['values'] = [{'name': 'A'}]; }); // check blank option is first and is compiled @@ -1068,8 +1068,8 @@ main() { createSingleSelect(''); - scope.$apply(() { - scope.blankVal = 'is blank'; + scope.apply(() { + scope.context['blankVal'] = 'is blank'; }); // check blank option is first and is compiled @@ -1081,13 +1081,13 @@ main() { it('should be selected, if it is available and no other option is selected', () { // selectedIndex is used here because $ incorrectly reports element.value - scope.$apply(() { - scope.values = [{'name': 'A'}]; + scope.apply(() { + scope.context['values'] = [{'name': 'A'}]; }); createSingleSelect(true); // ensure the first option (the blank option) is selected expect(element.selectedIndex).toEqual(0); - scope.$digest(); + scope.apply(); // ensure the option has not changed following the digest expect(element.selectedIndex).toEqual(0); }); @@ -1099,16 +1099,16 @@ main() { it('should update model on change', () { createSingleSelect(); - scope.$apply(() { - scope.values = [{'name': 'A'}, {'name': 'B'}]; - scope.selected = scope.values[0]; + scope.apply(() { + scope.context['values'] = [{'name': 'A'}, {'name': 'B'}]; + scope.context['selected'] = scope.context['values'][0]; }); expect(element.querySelectorAll('option')[0].selected).toEqual(true); element.querySelectorAll('option')[1].selected = true; _.triggerEvent(element, 'change'); - expect(scope.selected).toEqual(scope.values[1]); + expect(scope.context['selected']).toEqual(scope.context['values'][1]); }); @@ -1116,32 +1116,32 @@ main() { createSelect({'ng-model': 'selected'}, null, null, 'item in values', 'item.name', 'item.id'); - scope.$apply(() { - scope.values = [{'id': 10, 'name': 'A'}, {'id': 20, 'name': 'B'}]; - scope.selected = scope.values[0]['id']; + scope.apply(() { + scope.context['values'] = [{'id': 10, 'name': 'A'}, {'id': 20, 'name': 'B'}]; + scope.context['selected'] = scope.context['values'][0]['id']; }); expect(element).toEqualSelect([['A'], 'B']); element.querySelectorAll('option')[1].selected = true; _.triggerEvent(element, 'change'); - expect(scope.selected).toEqual(scope.values[1]['id']); + expect(scope.context['selected']).toEqual(scope.context['values'][1]['id']); }); it('should update model to null on change', () { createSingleSelect(true); - scope.$apply(() { - scope.values = [{'name': 'A'}, {'name': 'B'}]; - scope.selected = scope.values[0]; + scope.apply(() { + scope.context['values'] = [{'name': 'A'}, {'name': 'B'}]; + scope.context['selected'] = scope.context['values'][0]; element.value = '0'; }); _.selectOption(element, 'blank'); expect(element).toEqualSelect([[''], 'A', 'B']); - expect(scope.selected).toEqual(null); + expect(scope.context['selected']).toEqual(null); }); }); @@ -1151,25 +1151,25 @@ main() { it('should read multiple selection', () { createMultiSelect(); - scope.$apply(() { - scope.values = [{'name': 'A'}, {'name': 'B'}]; - scope.selected = []; + scope.apply(() { + scope.context['values'] = [{'name': 'A'}, {'name': 'B'}]; + scope.context['selected'] = []; }); expect(element.querySelectorAll('option').length).toEqual(2); expect(element.querySelectorAll('option')[0].selected).toEqual(false);; expect(element.querySelectorAll('option')[1].selected).toEqual(false);; - scope.$apply(() { - scope.selected.add(scope.values[1]); + scope.apply(() { + scope.context['selected'].add(scope.context['values'][1]); }); expect(element.querySelectorAll('option').length).toEqual(2); expect(element.querySelectorAll('option')[0].selected).toEqual(false);; expect(element.querySelectorAll('option')[1].selected).toEqual(true);; - scope.$apply(() { - scope.selected.add(scope.values[0]); + scope.apply(() { + scope.context['selected'].add(scope.context['values'][0]); }); expect(element.querySelectorAll('option').length).toEqual(2); @@ -1181,28 +1181,28 @@ main() { it('should update model on change', () { createMultiSelect(); - scope.$apply(() { - scope.values = [{'name': 'A'}, {'name': 'B'}]; - scope.selected = []; + scope.apply(() { + scope.context['values'] = [{'name': 'A'}, {'name': 'B'}]; + scope.context['selected'] = []; }); element.querySelectorAll('option')[0].selected = true; _.triggerEvent(element, 'change'); - expect(scope.selected).toEqual([scope.values[0]]); + expect(scope.context['selected']).toEqual([scope.context['values'][0]]); }); it('should deselect all options when model is emptied', () { createMultiSelect(); - scope.$apply(() { - scope.values = [{'name': 'A'}, {'name': 'B'}]; - scope.selected = [scope.values[0]]; + scope.apply(() { + scope.context['values'] = [{'name': 'A'}, {'name': 'B'}]; + scope.context['selected'] = [scope.context['values'][0]]; }); expect(element.querySelectorAll('option')[0].selected).toEqual(true); - scope.$apply(() { - scope.selected.removeLast(); + scope.apply(() { + scope.context['selected'].removeLast(); }); expect(element.querySelectorAll('option')[0].selected).toEqual(false); @@ -1220,22 +1220,22 @@ main() { }, true); - scope.$apply(() { - scope.values = [{'name': 'A', 'id': 1}, {'name': 'B', 'id': 2}]; - scope.required = false; + scope.apply(() { + scope.context['values'] = [{'name': 'A', 'id': 1}, {'name': 'B', 'id': 2}]; + scope.context['required'] = false; }); element.value = ''; _.triggerEvent(element, 'change'); expect(element).toEqualValid(); - scope.$apply(() { - scope.required = true; + scope.apply(() { + scope.context['required'] = true; }); expect(element).toEqualInvalid(); - scope.$apply(() { - scope.value = scope.values[0]; + scope.apply(() { + scope.context['value'] = scope.context['values'][0]; }); expect(element).toEqualValid(); @@ -1243,8 +1243,8 @@ main() { _.triggerEvent(element, 'change'); expect(element).toEqualInvalid(); - scope.$apply(() { - scope.required = false; + scope.apply(() { + scope.context['required'] = false; }); expect(element).toEqualValid(); }); @@ -1265,7 +1265,7 @@ main() { }); it('should set value even if self closing HTML', () { - scope.x = 'hello'; + scope.context['x'] = 'hello'; compile(''); expect(element).toEqualSelect([['hello']]); }); @@ -1277,8 +1277,8 @@ main() { '{{foo}}' + ''); - _.rootScope.foo = 'success'; - _.rootScope.$digest(); + _.rootScope.context['foo'] = 'success'; + _.rootScope.apply(); expect(_.rootElement.querySelector('span').text).toEqual('success'); }); }); diff --git a/test/directive/ng_a_spec.dart b/test/directive/ng_a_spec.dart index 082da1448..5a8fd0fb6 100644 --- a/test/directive/ng_a_spec.dart +++ b/test/directive/ng_a_spec.dart @@ -12,18 +12,19 @@ main() { it('should bind click listener when href zero length string', inject((Scope scope) { _.compile(''); _.triggerEvent(_.rootElement, 'click', 'MouseEvent'); - expect(_.rootScope['abc']).toEqual(true); - expect(_.rootScope['event'] is dom.UIEvent).toEqual(true); + expect(_.rootScope.context['abc']).toEqual(true); + expect(_.rootScope.context['event'] is dom.UIEvent).toEqual(true); })); it('should bind click listener when href empty', inject((Scope scope) { _.compile(''); _.triggerEvent(_.rootElement, 'click', 'MouseEvent'); - expect(_.rootScope['abc']).toEqual(true); - expect(_.rootScope['event'] is dom.UIEvent).toEqual(true); + expect(_.rootScope.context['abc']).toEqual(true); + expect(_.rootScope.context['event'] is dom.UIEvent).toEqual(true); })); it('should not bind click listener to non empty href', inject((Scope scope) { + window.location.href = '#something'; _.compile(''); _.triggerEvent(_.rootElement, 'click', 'MouseEvent'); expect(window.location.href.endsWith("#")).toEqual(true); @@ -32,11 +33,12 @@ main() { it('should not cancel click with non-empty interpolated href', inject((Scope scope) { _.compile(''); _.triggerEvent(_.rootElement, 'click', 'MouseEvent'); - expect(_.rootScope['abc']).toEqual(true); - expect(_.rootScope['event'] is dom.UIEvent).toEqual(true); + expect(_.rootScope.context['abc']).toEqual(true); + expect(_.rootScope.context['event'] is dom.UIEvent).toEqual(true); window.location.href = '#'; - _.rootScope['url'] = '#url'; - _.rootScope.$digest(); + _.rootScope.context['url'] = '#url'; + print(_.rootElement.outerHtml); + _.rootScope.apply(); _.triggerEvent(_.rootElement, 'click', 'MouseEvent'); expect(window.location.href.endsWith("#url")).toEqual(true); })); diff --git a/test/directive/ng_bind_html_spec.dart b/test/directive/ng_bind_html_spec.dart index b135657db..791bc7df0 100644 --- a/test/directive/ng_bind_html_spec.dart +++ b/test/directive/ng_bind_html_spec.dart @@ -10,8 +10,8 @@ main() { inject((Scope scope, Injector injector, Compiler compiler, DirectiveMap directives) { var element = $('
'); compiler(element, directives)(injector, element); - scope.htmlVar = 'Google!'; - scope.$digest(); + scope.context['htmlVar'] = 'Google!'; + scope.apply(); // Sanitization removes the href attribute on the tag. expect(element.html()).toEqual('Google!'); })); @@ -28,8 +28,8 @@ main() { inject((Scope scope, Injector injector, Compiler compiler, DirectiveMap directives) { var element = $('
'); compiler(element, directives)(injector, element); - scope.htmlVar = 'Google!'; - scope.$digest(); + scope.context['htmlVar'] = 'Google!'; + scope.apply(); // Sanitation allows href attributes per injected sanitizer. expect(element.html()).toEqual('Google!'); }); diff --git a/test/directive/ng_bind_spec.dart b/test/directive/ng_bind_spec.dart index 3379e6e8f..f08509d7e 100644 --- a/test/directive/ng_bind_spec.dart +++ b/test/directive/ng_bind_spec.dart @@ -11,8 +11,8 @@ main() { it('should set.text', inject((Scope scope, Injector injector, Compiler compiler, DirectiveMap directives) { var element = $('
'); compiler(element, directives)(injector, element); - scope.a = "abc123"; - scope.$digest(); + scope.context['a'] = "abc123"; + scope.apply(); expect(element.text()).toEqual('abc123'); })); @@ -20,18 +20,18 @@ main() { it('should bind to non string values', inject((Scope scope) { var element = _.compile('
'); - scope.$apply(() { - scope['value'] = null; + scope.apply(() { + scope.context['value'] = null; }); expect(element.text).toEqual(''); - scope.$apply(() { - scope['value'] = true; + scope.apply(() { + scope.context['value'] = true; }); expect(element.text).toEqual('true'); - scope.$apply(() { - scope['value'] = 1; + scope.apply(() { + scope.context['value'] = 1; }); expect(element.text).toEqual('1'); })); diff --git a/test/directive/ng_bind_template_spec.dart b/test/directive/ng_bind_template_spec.dart index 4ca832c23..aad7af02d 100644 --- a/test/directive/ng_bind_template_spec.dart +++ b/test/directive/ng_bind_template_spec.dart @@ -11,14 +11,14 @@ main() { it('should bind template', inject((Scope scope, Injector injector, Compiler compiler) { var element = _.compile('
'); - scope.salutation = 'Hello'; - scope.name = 'Heisenberg'; - scope.$digest(); + scope.context['salutation'] = 'Hello'; + scope.context['name'] = 'Heisenberg'; + scope.apply(); expect(element.text).toEqual('Hello Heisenberg!'); - scope.salutation = 'Good-Bye'; - scope.$digest(); + scope.context['salutation'] = 'Good-Bye'; + scope.apply(); expect(element.text).toEqual('Good-Bye Heisenberg!'); })); diff --git a/test/directive/ng_class_spec.dart b/test/directive/ng_class_spec.dart index 75dd72cd3..9d6914a46 100644 --- a/test/directive/ng_class_spec.dart +++ b/test/directive/ng_class_spec.dart @@ -10,19 +10,19 @@ main() { it('should add new and remove old classes dynamically', () { var element = _.compile('
'); - _.rootScope.dynClass = 'A'; - _.rootScope.$digest(); + _.rootScope.context['dynClass'] = 'A'; + _.rootScope.apply(); expect(element.classes.contains('existing')).toBe(true); expect(element.classes.contains('A')).toBe(true); - _.rootScope.dynClass = 'B'; - _.rootScope.$digest(); + _.rootScope.context['dynClass'] = 'B'; + _.rootScope.apply(); expect(element.classes.contains('existing')).toBe(true); expect(element.classes.contains('A')).toBe(false); expect(element.classes.contains('B')).toBe(true); - _.rootScope.dynClass = null; - _.rootScope.$digest(); + _.rootScope.context['dynClass'] = null; + _.rootScope.apply(); expect(element.classes.contains('existing')).toBe(true); expect(element.classes.contains('A')).toBe(false); expect(element.classes.contains('B')).toBe(false); @@ -30,20 +30,20 @@ main() { it('should support adding multiple classes via an array', () { - _.rootScope.a = 'a'; - _.rootScope.b = ''; - _.rootScope.c = null; + _.rootScope.context['a'] = 'a'; + _.rootScope.context['b'] = ''; + _.rootScope.context['c'] = null; var element = _.compile('
'); - _.rootScope.$digest(); + _.rootScope.apply(); expect(element.classes.contains('existing')).toBeTruthy(); expect(element.classes.contains('a')).toBeTruthy(); expect(element.classes.contains('b')).toBeFalsy(); expect(element.classes.contains('c')).toBeFalsy(); expect(element.classes.contains('null')).toBeFalsy(); - _.rootScope.a = null; - _.rootScope.b = 'b'; - _.rootScope.c = 'c'; - _.rootScope.$digest(); + _.rootScope.context['a'] = null; + _.rootScope.context['b'] = 'b'; + _.rootScope.context['c'] = 'c'; + _.rootScope.apply(); expect(element.classes.contains('a')).toBeFalsy(); expect(element.classes.contains('b')).toBeTruthy(); expect(element.classes.contains('c')).toBeTruthy(); @@ -56,16 +56,16 @@ main() { '
' + '
'); - _.rootScope.conditionA = true; - _.rootScope.conditionB = () { return false; }; - _.rootScope.$digest(); + _.rootScope.context['conditionA'] = true; + _.rootScope.context['conditionB'] = () { return false; }; + _.rootScope.apply(); expect(element.classes.contains('existing')).toBeTruthy(); expect(element.classes.contains('A')).toBeTruthy(); expect(element.classes.contains('B')).toBeFalsy(); expect(element.classes.contains('AnotB')).toBeTruthy(); - _.rootScope.conditionB = () { return true; }; - _.rootScope.$digest(); + _.rootScope.context['conditionB'] = () { return true; }; + _.rootScope.apply(); expect(element.classes.contains('existing')).toBeTruthy(); expect(element.classes.contains('A')).toBeTruthy(); expect(element.classes.contains('B')).toBeTruthy(); @@ -76,19 +76,19 @@ main() { it('should remove classes when the referenced object is the same but its property is changed', () { var element = _.compile('
'); - _.rootScope.classes = { 'A': true, 'B': true }; - _.rootScope.$digest(); + _.rootScope.context['classes'] = { 'A': true, 'B': true }; + _.rootScope.apply(); expect(element.classes.contains('A')).toBeTruthy(); expect(element.classes.contains('B')).toBeTruthy(); - _.rootScope.classes['A'] = false; - _.rootScope.$digest(); + _.rootScope.context['classes']['A'] = false; + _.rootScope.apply(); expect(element.classes.contains('A')).toBeFalsy(); expect(element.classes.contains('B')).toBeTruthy(); }); it('should support adding multiple classes via a space delimited string', () { var element = _.compile('
'); - _.rootScope.$digest(); + _.rootScope.apply(); expect(element.classes.contains('existing')).toBeTruthy(); expect(element.classes.contains('A')).toBeTruthy(); expect(element.classes.contains('B')).toBeTruthy(); @@ -97,14 +97,14 @@ main() { it('should preserve class added post compilation with pre-existing classes', () { var element = _.compile('
'); - _.rootScope.dynClass = 'A'; - _.rootScope.$digest(); + _.rootScope.context['dynClass'] = 'A'; + _.rootScope.apply(); expect(element.classes.contains('existing')).toBe(true); // add extra class, change model and eval element.classes.add('newClass'); - _.rootScope.dynClass = 'B'; - _.rootScope.$digest(); + _.rootScope.context['dynClass'] = 'B'; + _.rootScope.apply(); expect(element.classes.contains('existing')).toBe(true); expect(element.classes.contains('B')).toBe(true); @@ -114,14 +114,14 @@ main() { it('should preserve class added post compilation without pre-existing classes"', () { var element = _.compile('
'); - _.rootScope.dynClass = 'A'; - _.rootScope.$digest(); + _.rootScope.context['dynClass'] = 'A'; + _.rootScope.apply(); expect(element.classes.contains('A')).toBe(true); // add extra class, change model and eval element.classes.add('newClass'); - _.rootScope.dynClass = 'B'; - _.rootScope.$digest(); + _.rootScope.context['dynClass'] = 'B'; + _.rootScope.apply(); expect(element.classes.contains('B')).toBe(true); expect(element.classes.contains('newClass')).toBe(true); @@ -130,45 +130,45 @@ main() { it('should preserve other classes with similar name"', () { var element = _.compile('
'); - _.rootScope.dynCls = 'panel'; - _.rootScope.$digest(); - _.rootScope.dynCls = 'foo'; - _.rootScope.$digest(); + _.rootScope.context['dynCls'] = 'panel'; + _.rootScope.apply(); + _.rootScope.context['dynCls'] = 'foo'; + _.rootScope.apply(); expect(element.className).toEqual('ui-panel ui-selected foo'); }); it('should not add duplicate classes', () { var element = _.compile('
'); - _.rootScope.dynCls = 'panel'; - _.rootScope.$digest(); + _.rootScope.context['dynCls'] = 'panel'; + _.rootScope.apply(); expect(element.className).toEqual('panel bar'); }); it('should remove classes even if it was specified via class attribute', () { var element = _.compile('
'); - _.rootScope.dynCls = 'panel'; - _.rootScope.$digest(); - _.rootScope.dynCls = 'window'; - _.rootScope.$digest(); + _.rootScope.context['dynCls'] = 'panel'; + _.rootScope.apply(); + _.rootScope.context['dynCls'] = 'window'; + _.rootScope.apply(); expect(element.className).toEqual('bar window'); }); it('should remove classes even if they were added by another code', () { var element = _.compile('
'); - _.rootScope.dynCls = 'foo'; - _.rootScope.$digest(); + _.rootScope.context['dynCls'] = 'foo'; + _.rootScope.apply(); element.classes.add('foo'); - _.rootScope.dynCls = ''; - _.rootScope.$digest(); + _.rootScope.context['dynCls'] = ''; + _.rootScope.apply(); }); it('should ngClass odd/even', () { var element = _.compile('
    • '); - _.rootScope.$digest(); + _.rootScope.apply(); var e1 = element.nodes[1]; var e2 = element.nodes[2]; expect(e1.classes.contains('existing')).toBeTruthy(); @@ -183,7 +183,7 @@ main() { '
    • {{\$index}}
    • ' + '
        '); - _.rootScope.$digest(); + _.rootScope.apply(); var e1 = element.nodes[1]; var e2 = element.nodes[2]; @@ -200,7 +200,7 @@ main() { '
      • ' + '
          '); - _.rootScope.$apply(); + _.rootScope.apply(); var e1 = element.nodes[1]; var e2 = element.nodes[2]; @@ -223,17 +223,17 @@ main() { it('should reapply ngClass when interpolated class attribute changes', () { var element = _.compile('
          '); - _.rootScope.$apply(() { - _.rootScope.cls = "two"; - _.rootScope.four = true; + _.rootScope.apply(() { + _.rootScope.context['cls'] = "two"; + _.rootScope.context['four'] = true; }); expect(element).toHaveClass('one'); expect(element).toHaveClass('two'); // interpolated expect(element).toHaveClass('three'); expect(element).toHaveClass('four'); - _.rootScope.$apply(() { - _.rootScope.cls = "too"; + _.rootScope.apply(() { + _.rootScope.context['cls'] = "too"; }); expect(element).toHaveClass('one'); @@ -242,8 +242,8 @@ main() { expect(element).toHaveClass('four'); // should still be there expect(element.classes.contains('two')).toBeFalsy(); - _.rootScope.$apply(() { - _.rootScope.cls = "to"; + _.rootScope.apply(() { + _.rootScope.context['cls'] = "to"; }); expect(element).toHaveClass('one'); @@ -256,12 +256,12 @@ main() { it('should not mess up class value due to observing an interpolated class attribute', () { - _.rootScope.foo = true; - _.rootScope.$watch("anything", () { - _.rootScope.foo = false; + _.rootScope.context['foo'] = true; + _.rootScope.watch("anything", (_0, _1) { + _.rootScope.context['foo'] = false; }); var element = _.compile('
          '); - _.rootScope.$digest(); + _.rootScope.apply(); expect(element.classes.contains('foo')).toBe(false); }); @@ -271,11 +271,11 @@ main() { '
        • ' + '
            '); - _.rootScope.items = ['a','b','c']; - _.rootScope.$digest(); + _.rootScope.context['items'] = ['a','b','c']; + _.rootScope.apply(); - _.rootScope.items = ['a','b']; - _.rootScope.$digest(); + _.rootScope.context['items'] = ['a','b']; + _.rootScope.apply(); var e1 = element.nodes[1]; var e2 = element.nodes[2]; @@ -293,11 +293,11 @@ main() { '
          • i
          • ' + '
              '); - _.rootScope.items = ['a','b']; - _.rootScope.$digest(); + _.rootScope.context['items'] = ['a','b']; + _.rootScope.apply(); - _.rootScope.items = ['b','a']; - _.rootScope.$digest(); + _.rootScope.context['items'] = ['b','a']; + _.rootScope.apply(); var e1 = element.nodes[1]; var e2 = element.nodes[2]; diff --git a/test/directive/ng_events_spec.dart b/test/directive/ng_events_spec.dart index 82a526c21..39cdfee35 100644 --- a/test/directive/ng_events_spec.dart +++ b/test/directive/ng_events_spec.dart @@ -16,8 +16,8 @@ void addTest(String name, [String eventType='MouseEvent', String eventName]) { it('should evaluate the expression on $name', inject(() { _.compile(''); _.triggerEvent(_.rootElement, eventName, eventType); - expect(_.rootScope['abc']).toEqual(true); - expect(_.rootScope['event'] is dom.UIEvent).toEqual(true); + expect(_.rootScope.context['abc']).toEqual(true); + expect(_.rootScope.context['event'] is dom.UIEvent).toEqual(true); })); }); } diff --git a/test/directive/ng_form_spec.dart b/test/directive/ng_form_spec.dart index f448266aa..3e16d962f 100644 --- a/test/directive/ng_form_spec.dart +++ b/test/directive/ng_form_spec.dart @@ -4,19 +4,19 @@ import '../_specs.dart'; main() => describe('form', () { - TestBed _; + TestBed _; it('should set the name of the form and attach it to the scope', inject((Scope scope, TestBed _) { var element = $('
              '); - expect(scope['myForm']).toBeNull(); + expect(scope.context['myForm']).toBeNull(); _.compile(element); - scope.$apply(); + scope.apply(); - expect(scope['myForm']).toBeDefined(); + expect(scope.context['myForm']).toBeDefined(); - var form = scope['myForm']; + var form = scope.context['myForm']; expect(form.name).toEqual('myForm'); })); @@ -25,9 +25,9 @@ describe('form', () { var element = $('
              '); _.compile(element); - scope.$apply(); + scope.apply(); - var form = scope['myForm']; + var form = scope.context['myForm']; expect(form.pristine).toEqual(true); expect(form.dirty).toEqual(false); })); @@ -36,9 +36,9 @@ describe('form', () { var element = $('
              '); _.compile(element); - scope.$apply(); + scope.apply(); - var form = scope['myForm']; + var form = scope.context['myForm']; form.dirty = true; expect(form.pristine).toEqual(false); @@ -59,9 +59,9 @@ describe('form', () { var element = $('
              '); _.compile(element); - scope.$apply(); + scope.apply(); - var form = scope['myForm']; + var form = scope.context['myForm']; form.invalid = true; expect(form.valid).toEqual(false); @@ -84,9 +84,9 @@ describe('form', () { ''); _.compile(element); - scope.$apply(); + scope.apply(); - var form = scope['myForm']; + var form = scope.context['myForm']; NgModel one = form['one']; NgModel two = form['two']; NgModel three = form['three']; @@ -114,9 +114,9 @@ describe('form', () { ''); _.compile(element); - scope.$apply(); + scope.apply(); - var form = scope['myForm']; + var form = scope.context['myForm']; NgModel one = form['one']; form.updateControlValidity(one, "validation error", false); @@ -139,9 +139,9 @@ describe('form', () { ''); _.compile(element); - scope.$apply(); + scope.apply(); - var form = scope['myForm']; + var form = scope.context['myForm']; NgModel one = form['one']; NgModel two = form['two']; @@ -170,11 +170,11 @@ describe('form', () { ''); _.compile(element); - scope.$apply(); + scope.apply(); - var form = scope['myForm']; - var fieldset = _.rootScope.f.directive(NgForm); - var model = _.rootScope.m.directive(NgModel); + var form = scope.context['myForm']; + var fieldset = _.rootScope.context['f'].directive(NgForm); + var model = _.rootScope.context['m'].directive(NgModel); model.setValidity("error", false); @@ -211,11 +211,11 @@ describe('form', () { _.compile(element); - scope.mega_model = 'mega'; - scope.fire_model = 'fire'; - scope.$apply(); + scope.context['mega_model'] = 'mega'; + scope.context['fire_model'] = 'fire'; + scope.apply(); - var form = scope['myForm']; + var form = scope.context['myForm']; expect(form['mega_name'].modelValue).toBe('mega'); expect(form['fire_name'].modelValue).toBe('fire'); })); @@ -226,31 +226,31 @@ describe('form', () { ''); _.compile(element); - scope.$apply(); + scope.apply(); - var form = scope['myForm']; + var form = scope.context['myForm']; var control = form['mega_control']; form.removeControl(control); expect(form['mega_control']).toBeNull(); })); it('should remove all controls when the scope is destroyed', inject((Scope scope, TestBed _) { - Scope childScope = scope.$new(); - var element = $('
              ' + Scope childScope = scope.createChild({}); + var element = $('' + ' ' + ' ' + ' ' + '
              '); _.compile(element, scope: childScope); - childScope.$apply(); + childScope.apply(); - var form = childScope['myForm']; + var form = childScope.context['myForm']; expect(form['one']).toBeDefined(); expect(form['two']).toBeDefined(); expect(form['three']).toBeDefined(); - childScope.$destroy(); + childScope.destroy(); expect(form['one']).toBeNull(); expect(form['two']).toBeNull(); @@ -263,7 +263,7 @@ describe('form', () { var element = $('
              '); _.compile(element); - scope.$apply(); + scope.apply(); Event submissionEvent = new Event.eventType('CustomEvent', 'submit'); @@ -282,7 +282,7 @@ describe('form', () { var element = $('
              '); _.compile(element); - scope.$apply(); + scope.apply(); Event submissionEvent = new Event.eventType('CustomEvent', 'submit'); @@ -295,14 +295,14 @@ describe('form', () { var element = $('
              '); _.compile(element); - scope.$apply(); + scope.apply(); - _.rootScope.submitted = false; + _.rootScope.context['submitted'] = false; Event submissionEvent = new Event.eventType('CustomEvent', 'submit'); element[0].dispatchEvent(submissionEvent); - expect(_.rootScope.submitted).toBe(true); + expect(_.rootScope.context['submitted']).toBe(true); })); it('should apply the valid and invalid prefixed submit CSS classes to the element', inject((TestBed _) { @@ -310,8 +310,8 @@ describe('form', () { ' ' + ''); - NgForm form = _.rootScope['superForm']; - Probe probe = _.rootScope.i; + NgForm form = _.rootScope.context['superForm']; + Probe probe = _.rootScope.context['i']; var model = probe.directive(NgModel); expect(form.element.classes.contains('ng-submit-invalid')).toBe(false); @@ -320,12 +320,12 @@ describe('form', () { Event submissionEvent = new Event.eventType('CustomEvent', 'submit'); form.element.dispatchEvent(submissionEvent); - _.rootScope.$apply(); + _.rootScope.apply(); expect(form.element.classes.contains('ng-submit-invalid')).toBe(true); expect(form.element.classes.contains('ng-submit-valid')).toBe(false); - _.rootScope.$apply('myModel = "man"'); + _.rootScope.apply('myModel = "man"'); form.element.dispatchEvent(submissionEvent); expect(form.element.classes.contains('ng-submit-invalid')).toBe(false); @@ -338,27 +338,27 @@ describe('form', () { _.compile('
              ' + ' ' + '
              '); - _.rootScope.$apply('myModel = "animal"'); + _.rootScope.apply('myModel = "animal"'); - NgForm form = _.rootScope['superForm']; + NgForm form = _.rootScope.context['superForm']; - Probe probe = _.rootScope.i; + Probe probe = _.rootScope.context['i']; var model = probe.directive(NgModel); - expect(_.rootScope.myModel).toEqual('animal'); + expect(_.rootScope.context['myModel']).toEqual('animal'); expect(model.modelValue).toEqual('animal'); expect(model.viewValue).toEqual('animal'); - _.rootScope.$apply('myModel = "man"'); + _.rootScope.apply('myModel = "man"'); - expect(_.rootScope.myModel).toEqual('man'); + expect(_.rootScope.context['myModel']).toEqual('man'); expect(model.modelValue).toEqual('man'); expect(model.viewValue).toEqual('man'); form.reset(); - _.rootScope.$apply(); + _.rootScope.apply(); - expect(_.rootScope.myModel).toEqual('animal'); + expect(_.rootScope.context['myModel']).toEqual('animal'); expect(model.modelValue).toEqual('animal'); expect(model.viewValue).toEqual('animal'); })); diff --git a/test/directive/ng_if_spec.dart b/test/directive/ng_if_spec.dart index b49c0d123..2edf2c634 100644 --- a/test/directive/ng_if_spec.dart +++ b/test/directive/ng_if_spec.dart @@ -9,7 +9,7 @@ class ChildController { ChildController(BoundBlockFactory boundBlockFactory, BlockHole blockHole, Scope scope) { - scope.setBy = 'childController'; + scope.context['setBy'] = 'childController'; boundBlockFactory(scope).insertAfter(blockHole); } } @@ -32,19 +32,19 @@ main() { compile = (html, [applyFn]) { element = $(html); compiler(element, _directives)(injector, element); - scope.$apply(applyFn); + scope.apply(applyFn); }; directives = _directives; }); } - they(should, htmlForElements, callback) { + they(should, htmlForElements, callback, [exclusive=false]) { htmlForElements.forEach((html) { var directiveName = html.contains('ng-if') ? 'ng-if' : 'ng-unless'; describe(directiveName, () { beforeEach(configInjector); beforeEach(configState); - it(should, () { + (exclusive ? iit : it)(should, () { callback(html); }); }); @@ -60,16 +60,16 @@ main() { expect(element.contents().length).toEqual(1); expect(element.find('span').html()).toEqual(''); - rootScope.$apply(() { - rootScope['isVisible'] = true; + rootScope.apply(() { + rootScope.context['isVisible'] = true; }); // The span node SHOULD exist in the DOM. expect(element.contents().length).toEqual(2); expect(element.find('span').html()).toEqual('content'); - rootScope.$apply(() { - rootScope['isVisible'] = false; + rootScope.apply(() { + rootScope.context['isVisible'] = false; }); expect(element.find('span').html()).toEqual(''); @@ -93,18 +93,18 @@ main() { ' {{setBy}}'.trim() + ''], (html) { - rootScope['setBy'] = 'topLevel'; + rootScope.context['setBy'] = 'topLevel'; compile(html); expect(element.contents().length).toEqual(2); - rootScope.$apply(() { - rootScope['isVisible'] = true; + rootScope.apply(() { + rootScope.context['isVisible'] = true; }); expect(element.contents().length).toEqual(3); - // The value on the parent scope should be unchanged. - expect(rootScope['setBy']).toEqual('topLevel'); + // The value on the parent scope.context['should'] be unchanged. + expect(rootScope.context['setBy']).toEqual('topLevel'); expect(element.find('#outside').html()).toEqual('topLevel'); - // A child scope must have been created and hold a different value. + // A child scope.context['must'] have been created and hold a different value. expect(element.find('#inside').html()).toEqual('childController'); } ); @@ -124,14 +124,14 @@ main() { '
              '.trim() + ''], (html) { - var values = rootScope['values'] = [1, 2, 3, 4]; + var values = rootScope.context['values'] = [1, 2, 3, 4]; compile(html); expect(element.contents().length).toBe(12); - rootScope.$apply(() { + rootScope.apply(() { values.removeRange(0, 1); }); expect(element.contents().length).toBe(9); - rootScope.$apply(() { + rootScope.apply(() { values.insert(0, 1); }); expect(element.contents().length).toBe(12); @@ -143,17 +143,17 @@ main() { '
              content
              ', '
              content
              '], (html) { - rootScope['isVisible'] = true; + rootScope.context['isVisible'] = true; compile(html); expect(element.contents().length).toEqual(2); element.find('span').removeClass('my-class'); expect(element.find('span').hasClass('my-class')).not.toBe(true); - rootScope.$apply(() { - rootScope['isVisible'] = false; + rootScope.apply(() { + rootScope.context['isVisible'] = false; }); expect(element.contents().length).toEqual(1); - rootScope.$apply(() { - rootScope['isVisible'] = true; + rootScope.apply(() { + rootScope.context['isVisible'] = true; }); // The newly inserted node should be a copy of the compiled state. expect(element.find('span').hasClass('my-class')).toBe(true); @@ -166,8 +166,8 @@ main() { '
              content
              '], (html) { compile(html); - rootScope.$apply(() { - rootScope['isVisible'] = false; + rootScope.apply(() { + rootScope.context['isVisible'] = false; }); expect(element.find('span').html()).toEqual(''); } @@ -181,15 +181,15 @@ main() { compile(html); expect(element.find('span').html()).toEqual(''); - rootScope.$apply(() { - rootScope['isVisible'] = false; + rootScope.apply(() { + rootScope.context['isVisible'] = false; }); expect(element.find('span').html()).toEqual(''); expect(logger.result()).toEqual('ALWAYS'); - rootScope.$apply(() { - rootScope['isVisible'] = true; + rootScope.apply(() { + rootScope.context['isVisible'] = true; }); expect(element.find('span').html()).toEqual('content'); expect(logger.result()).toEqual('ALWAYS; JAMES'); diff --git a/test/directive/ng_include_spec.dart b/test/directive/ng_include_spec.dart index a19210675..625821bdc 100644 --- a/test/directive/ng_include_spec.dart +++ b/test/directive/ng_include_spec.dart @@ -16,8 +16,8 @@ main() { expect(element.innerHtml).toEqual(''); microLeap(); // load the template from cache. - scope.$apply(() { - scope['name'] = 'Vojta'; + scope.applyInZone(() { + scope.context['name'] = 'Vojta'; }); expect(element.text).toEqual('my name is Vojta'); }))); @@ -30,14 +30,14 @@ main() { expect(element.innerHtml).toEqual(''); - scope.$apply(() { - scope['name'] = 'Vojta'; - scope['template'] = 'tpl1.html'; + scope.applyInZone(() { + scope.context['name'] = 'Vojta'; + scope.context['template'] = 'tpl1.html'; }); expect(element.text).toEqual('My name is Vojta'); - scope.$apply(() { - scope['template'] = 'tpl2.html'; + scope.applyInZone(() { + scope.context['template'] = 'tpl2.html'; }); expect(element.text).toEqual('I am Vojta'); }))); diff --git a/test/directive/ng_model_spec.dart b/test/directive/ng_model_spec.dart index 25180c097..27e3771cc 100644 --- a/test/directive/ng_model_spec.dart +++ b/test/directive/ng_model_spec.dart @@ -17,75 +17,77 @@ describe('ng-model', () { describe('type="text" like', () { it('should update input value from model', inject(() { _.compile(''); - _.rootScope.$digest(); + _.rootScope.apply(); expect((_.rootElement as dom.InputElement).value).toEqual(''); - _.rootScope.$apply('model = "misko"'); + _.rootScope.apply('model = "misko"'); expect((_.rootElement as dom.InputElement).value).toEqual('misko'); })); it('should render null as the empty string', inject(() { _.compile(''); - _.rootScope.$digest(); + _.rootScope.apply(); expect((_.rootElement as dom.InputElement).value).toEqual(''); - _.rootScope.$apply('model = null'); + _.rootScope.apply('model = null'); expect((_.rootElement as dom.InputElement).value).toEqual(''); })); it('should update model from the input value', inject(() { _.compile(''); - Probe probe = _.rootScope.p; + Probe probe = _.rootScope.context['p']; var ngModel = probe.directive(NgModel); InputElement inputElement = probe.element; inputElement.value = 'abc'; _.triggerEvent(inputElement, 'change'); - expect(_.rootScope.model).toEqual('abc'); + expect(_.rootScope.context['model']).toEqual('abc'); inputElement.value = 'def'; var input = probe.directive(InputTextLikeDirective); input.processValue(); - expect(_.rootScope.model).toEqual('def'); + expect(_.rootScope.context['model']).toEqual('def'); })); it('should update model from the input value for type=number', inject(() { _.compile(''); - Probe probe = _.rootScope.p; + Probe probe = _.rootScope.context['p']; var ngModel = probe.directive(NgModel); InputElement inputElement = probe.element; inputElement.value = '12'; _.triggerEvent(inputElement, 'change'); - expect(_.rootScope.model).toEqual(12); + expect(_.rootScope.context['model']).toEqual(12); inputElement.value = '14'; var input = probe.directive(InputNumberLikeDirective); input.processValue(); - expect(_.rootScope.model).toEqual(14); + expect(_.rootScope.context['model']).toEqual(14); })); it('should update input type=number to blank when model is null', inject(() { _.compile(''); - Probe probe = _.rootScope.p; + Probe probe = _.rootScope.context['p']; var ngModel = probe.directive(NgModel); InputElement inputElement = probe.element; inputElement.value = '12'; _.triggerEvent(inputElement, 'change'); - expect(_.rootScope.model).toEqual(12); + expect(_.rootScope.context['model']).toEqual(12); - _.rootScope.model = null; - _.rootScope.$apply(); + _.rootScope.context['model'] = null; + _.rootScope.apply(); expect(inputElement.value).toEqual(''); })); - it('should write to input only if value is different', inject((Injector i) { + it('should write to input only if value is different', inject((Injector i, AstParser parser) { var scope = _.rootScope; var element = new dom.InputElement(); - var model = new NgModel(scope, new NodeAttrs(new DivElement()), element, i.createChild([new Module()])); + NodeAttrs nodeAttrs = new NodeAttrs(new DivElement()); + nodeAttrs['ng-model'] = 'model'; + var model = new NgModel(scope, element, i.createChild([new Module()]), new NgNullForm(), parser, nodeAttrs); dom.querySelector('body').append(element); var input = new InputTextLikeDirective(element, model, scope); @@ -140,7 +142,7 @@ describe('ng-model', () { expect(dom.window.getSelection().toString()).toEqual(piX); // When the input is invalid, the model is [double.NAN]: _.triggerEvent(element, 'change'); - expect(_.rootScope[modelFieldName].isNaN).toBeTruthy(); + expect(_.rootScope.context[modelFieldName].isNaN).toBeTruthy(); // Subcase: text representing a valid number (as if the user had erased // the trailing 'x'). @@ -148,7 +150,7 @@ describe('ng-model', () { setNumberInputValue(element, pi.toString()); _.triggerEvent(element, 'change'); expect(element.value).toEqual(pi.toString()); - expect(_.rootScope[modelFieldName]).toEqual(pi); + expect(_.rootScope.context[modelFieldName]).toEqual(pi); })); it('should not reformat user input to equivalent numeric representation', inject((Injector i) { @@ -159,7 +161,7 @@ describe('ng-model', () { var ten = '1e1'; setNumberInputValue(element, ten); _.triggerEvent(element, 'change'); - expect(_.rootScope[modelFieldName]).toEqual(10); + expect(_.rootScope.context[modelFieldName]).toEqual(10); // Ensure that the input text is literally the same element.selectionStart = 0; // Set the selectionEnd to one beyond ten.length in @@ -170,79 +172,79 @@ describe('ng-model', () { it('should update input value from model', inject(() { _.compile(''); - _.rootScope.$digest(); + _.rootScope.apply(); - _.rootScope.$apply('model = 42'); + _.rootScope.apply('model = 42'); expect((_.rootElement as dom.InputElement).value).toEqual('42'); })); it('should update input value from model for range inputs', inject(() { _.compile(''); - _.rootScope.$digest(); + _.rootScope.apply(); - _.rootScope.$apply('model = 42'); + _.rootScope.apply('model = 42'); expect((_.rootElement as dom.InputElement).value).toEqual('42'); })); it('should update model from the input value', inject(() { _.compile(''); - Probe probe = _.rootScope.p; + Probe probe = _.rootScope.context['p']; var ngModel = probe.directive(NgModel); InputElement inputElement = probe.element; inputElement.value = '42'; _.triggerEvent(inputElement, 'change'); - expect(_.rootScope.model).toEqual(42); + expect(_.rootScope.context['model']).toEqual(42); inputElement.value = '43'; var input = probe.directive(InputNumberLikeDirective); input.processValue(); - expect(_.rootScope.model).toEqual(43); + expect(_.rootScope.context['model']).toEqual(43); })); it('should update model to NaN from a blank input value', inject(() { _.compile(''); - Probe probe = _.rootScope.p; + Probe probe = _.rootScope.context['p']; var ngModel = probe.directive(NgModel); InputElement inputElement = probe.element; inputElement.value = ''; _.triggerEvent(inputElement, 'change'); - expect(_.rootScope.model.isNaN).toBeTruthy(); + expect(_.rootScope.context['model'].isNaN).toBeTruthy(); })); it('should update model from the input value for range inputs', inject(() { _.compile(''); - Probe probe = _.rootScope.p; + Probe probe = _.rootScope.context['p']; var ngModel = probe.directive(NgModel); InputElement inputElement = probe.element; inputElement.value = '42'; _.triggerEvent(inputElement, 'change'); - expect(_.rootScope.model).toEqual(42); + expect(_.rootScope.context['model']).toEqual(42); inputElement.value = '43'; var input = probe.directive(InputNumberLikeDirective); input.processValue(); - expect(_.rootScope.model).toEqual(43); + expect(_.rootScope.context['model']).toEqual(43); })); it('should update model to a native default value from a blank range input value', inject(() { _.compile(''); - Probe probe = _.rootScope.p; + Probe probe = _.rootScope.context['p']; var ngModel = probe.directive(NgModel); InputElement inputElement = probe.element; inputElement.value = ''; _.triggerEvent(inputElement, 'change'); - expect(_.rootScope.model).toBeDefined(); + expect(_.rootScope.context['model']).toBeDefined(); })); it('should render null as blank', inject(() { _.compile(''); - _.rootScope.$digest(); + _.rootScope.apply(); - _.rootScope.$apply('model = null'); + _.rootScope.apply('model = null'); expect((_.rootElement as dom.InputElement).value).toEqual(''); })); @@ -251,45 +253,47 @@ describe('ng-model', () { describe('type="password"', () { it('should update input value from model', inject(() { _.compile(''); - _.rootScope.$digest(); + _.rootScope.apply(); expect((_.rootElement as dom.InputElement).value).toEqual(''); - _.rootScope.$apply('model = "misko"'); + _.rootScope.apply('model = "misko"'); expect((_.rootElement as dom.InputElement).value).toEqual('misko'); })); it('should render null as the empty string', inject(() { _.compile(''); - _.rootScope.$digest(); + _.rootScope.apply(); expect((_.rootElement as dom.InputElement).value).toEqual(''); - _.rootScope.$apply('model = null'); + _.rootScope.apply('model = null'); expect((_.rootElement as dom.InputElement).value).toEqual(''); })); it('should update model from the input value', inject(() { _.compile(''); - Probe probe = _.rootScope.p; + Probe probe = _.rootScope.context['p']; var ngModel = probe.directive(NgModel); InputElement inputElement = probe.element; inputElement.value = 'abc'; _.triggerEvent(inputElement, 'change'); - expect(_.rootScope.model).toEqual('abc'); + expect(_.rootScope.context['model']).toEqual('abc'); inputElement.value = 'def'; var input = probe.directive(InputTextLikeDirective); input.processValue(); - expect(_.rootScope.model).toEqual('def'); + expect(_.rootScope.context['model']).toEqual('def'); })); - it('should write to input only if value is different', inject((Injector i) { + it('should write to input only if value is different', inject((Injector i, AstParser parser) { var scope = _.rootScope; var element = new dom.InputElement(); - var model = new NgModel(scope, new NodeAttrs(new DivElement()), element, i.createChild([new Module()])); + NodeAttrs nodeAttrs = new NodeAttrs(new DivElement()); + nodeAttrs['ng-model'] = 'model'; + var model = new NgModel(scope, element, i.createChild([new Module()]), new NgNullForm(), parser, nodeAttrs); dom.querySelector('body').append(element); var input = new InputTextLikeDirective(element, model, scope); @@ -314,44 +318,46 @@ describe('ng-model', () { describe('type="search"', () { it('should update input value from model', inject(() { _.compile(''); - _.rootScope.$digest(); + _.rootScope.apply(); expect((_.rootElement as dom.InputElement).value).toEqual(''); - _.rootScope.$apply('model = "misko"'); + _.rootScope.apply('model = "misko"'); expect((_.rootElement as dom.InputElement).value).toEqual('misko'); })); it('should render null as the empty string', inject(() { _.compile(''); - _.rootScope.$digest(); + _.rootScope.apply(); expect((_.rootElement as dom.InputElement).value).toEqual(''); - _.rootScope.$apply('model = null'); + _.rootScope.apply('model = null'); expect((_.rootElement as dom.InputElement).value).toEqual(''); })); it('should update model from the input value', inject(() { _.compile(''); - Probe probe = _.rootScope.p; + Probe probe = _.rootScope.context['p']; var ngModel = probe.directive(NgModel); InputElement inputElement = probe.element; inputElement.value = 'abc'; _.triggerEvent(inputElement, 'change'); - expect(_.rootScope.model).toEqual('abc'); + expect(_.rootScope.context['model']).toEqual('abc'); inputElement.value = 'def'; var input = probe.directive(InputTextLikeDirective); input.processValue(); - expect(_.rootScope.model).toEqual('def'); + expect(_.rootScope.context['model']).toEqual('def'); })); - it('should write to input only if value is different', inject((Injector i) { + it('should write to input only if value is different', inject((Injector i, AstParser parser) { var scope = _.rootScope; var element = new dom.InputElement(); - var model = new NgModel(scope, new NodeAttrs(new DivElement()), element, i.createChild([new Module()])); + NodeAttrs nodeAttrs = new NodeAttrs(new DivElement()); + nodeAttrs['ng-model'] = 'model'; + var model = new NgModel(scope, element, i.createChild([new Module()]), new NgNullForm(), parser, nodeAttrs); dom.querySelector('body').append(element); var input = new InputTextLikeDirective(element, model, scope); @@ -378,50 +384,52 @@ describe('ng-model', () { describe('no type attribute', () { it('should be set "text" as default value for "type" attribute', inject(() { _.compile(''); - _.rootScope.$digest(); + _.rootScope.apply(); expect((_.rootElement as dom.InputElement).attributes['type']).toEqual('text'); })); it('should update input value from model', inject(() { _.compile(''); - _.rootScope.$digest(); + _.rootScope.apply(); expect((_.rootElement as dom.InputElement).value).toEqual(''); - _.rootScope.$apply('model = "misko"'); + _.rootScope.apply('model = "misko"'); expect((_.rootElement as dom.InputElement).value).toEqual('misko'); })); it('should render null as the empty string', inject(() { _.compile(''); - _.rootScope.$digest(); + _.rootScope.apply(); expect((_.rootElement as dom.InputElement).value).toEqual(''); - _.rootScope.$apply('model = null'); + _.rootScope.apply('model = null'); expect((_.rootElement as dom.InputElement).value).toEqual(''); })); it('should update model from the input value', inject(() { _.compile(''); - Probe probe = _.rootScope.p; + Probe probe = _.rootScope.context['p']; var ngModel = probe.directive(NgModel); InputElement inputElement = probe.element; inputElement.value = 'abc'; _.triggerEvent(inputElement, 'change'); - expect(_.rootScope.model).toEqual('abc'); + expect(_.rootScope.context['model']).toEqual('abc'); inputElement.value = 'def'; var input = probe.directive(InputTextLikeDirective); input.processValue(); - expect(_.rootScope.model).toEqual('def'); + expect(_.rootScope.context['model']).toEqual('def'); })); - it('should write to input only if value is different', inject((Injector i) { + it('should write to input only if value is different', inject((Injector i, AstParser parser) { var scope = _.rootScope; var element = new dom.InputElement(); - var model = new NgModel(scope, new NodeAttrs(new DivElement()), element, i.createChild([new Module()])); + NodeAttrs nodeAttrs = new NodeAttrs(new DivElement()); + nodeAttrs['ng-model'] = 'model'; + var model = new NgModel(scope, element, i.createChild([new Module()]), new NgNullForm(), parser, nodeAttrs); dom.querySelector('body').append(element); var input = new InputTextLikeDirective(element, model, scope); @@ -447,20 +455,20 @@ describe('ng-model', () { it('should update input value from model', inject((Scope scope) { var element = _.compile(''); - scope.$apply(() { - scope['model'] = true; + scope.apply(() { + scope.context['model'] = true; }); expect(element.checked).toBe(true); - scope.$apply(() { - scope['model'] = false; + scope.apply(() { + scope.context['model'] = false; }); expect(element.checked).toBe(false); })); it('should render as dirty when checked', inject((Scope scope) { var element = _.compile(''); - Probe probe = _.rootScope.i; + Probe probe = _.rootScope.context['i']; var model = probe.directive(NgModel); expect(model.pristine).toEqual(true); @@ -476,41 +484,41 @@ describe('ng-model', () { it('should update input value from model using ng-true-value/false', inject((Scope scope) { var element = _.compile(''); - scope.$apply(() { - scope['model'] = 1; + scope.apply(() { + scope.context['model'] = 1; }); expect(element.checked).toBe(true); - scope.$apply(() { - scope['model'] = 0; + scope.apply(() { + scope.context['model'] = 0; }); expect(element.checked).toBe(false); element.checked = true; _.triggerEvent(element, 'change'); - expect(scope['model']).toBe(1); + expect(scope.context['model']).toBe(1); element.checked = false; _.triggerEvent(element, 'change'); - expect(scope['model']).toBe(0); + expect(scope.context['model']).toBe(0); })); it('should allow non boolean values like null, 0, 1', inject((Scope scope) { var element = _.compile(''); - scope.$apply(() { - scope['model'] = 0; + scope.apply(() { + scope.context['model'] = 0; }); expect(element.checked).toBe(false); - scope.$apply(() { - scope['model'] = 1; + scope.apply(() { + scope.context['model'] = 1; }); expect(element.checked).toBe(true); - scope.$apply(() { - scope['model'] = null; + scope.apply(() { + scope.context['model'] = null; }); expect(element.checked).toBe(false); })); @@ -521,49 +529,49 @@ describe('ng-model', () { element.checked = true; _.triggerEvent(element, 'change'); - expect(scope['model']).toBe(true); + expect(scope.context['model']).toBe(true); element.checked = false; _.triggerEvent(element, 'change'); - expect(scope['model']).toBe(false); + expect(scope.context['model']).toBe(false); })); }); describe('textarea', () { it('should update textarea value from model', inject(() { _.compile(''); - _.rootScope.$apply('myModel = "animal"'); + _.rootScope.apply('myModel = "animal"'); - Probe probe = _.rootScope.i; + Probe probe = _.rootScope.context['i']; var model = probe.directive(NgModel); - expect(_.rootScope.myModel).toEqual('animal'); + expect(_.rootScope.context['myModel']).toEqual('animal'); expect(model.modelValue).toEqual('animal'); expect(model.viewValue).toEqual('animal'); - _.rootScope.$apply('myModel = "man"'); + _.rootScope.apply('myModel = "man"'); - expect(_.rootScope.myModel).toEqual('man'); + expect(_.rootScope.context['myModel']).toEqual('man'); expect(model.modelValue).toEqual('man'); expect(model.viewValue).toEqual('man'); model.reset(); - expect(_.rootScope.myModel).toEqual('animal'); + expect(_.rootScope.context['myModel']).toEqual('animal'); expect(model.modelValue).toEqual('animal'); expect(model.viewValue).toEqual('animal'); }); diff --git a/test/directive/ng_model_validators_spec.dart b/test/directive/ng_model_validators_spec.dart index 3709400fd..d42e5fb5c 100644 --- a/test/directive/ng_model_validators_spec.dart +++ b/test/directive/ng_model_validators_spec.dart @@ -9,16 +9,16 @@ describe('ngModel validators', () { beforeEach(inject((TestBed tb) => _ = tb)); describe('required', () { - it('should validate the input field if the required attribute is set', inject((Scope scope) { + it('should validate the input field if the required attribute is set', inject((RootScope scope) { _.compile(''); - Probe probe = _.rootScope.i; + Probe probe = _.rootScope.context['i']; var model = probe.directive(NgModel); model.validate(); expect(model.valid).toEqual(false); expect(model.invalid).toEqual(true); - _.rootScope.val = 'value'; + _.rootScope.context['val'] = 'value'; model.validate(); expect(model.valid).toEqual(true); @@ -26,16 +26,16 @@ describe('ngModel validators', () { })); - it('should validate a number input field if the required attribute is set', inject((Scope scope) { + it('should validate a number input field if the required attribute is set', inject((RootScope scope) { _.compile(''); - Probe probe = _.rootScope.i; + Probe probe = _.rootScope.context['i']; var model = probe.directive(NgModel); model.validate(); expect(model.valid).toEqual(false); expect(model.invalid).toEqual(true); - _.rootScope.val = 5; + _.rootScope.context['val'] = 5; model.validate(); expect(model.valid).toEqual(true); @@ -43,27 +43,27 @@ describe('ngModel validators', () { })); - it('should validate the input field depending on if ng-required is true', inject((Scope scope) { + it('should validate the input field depending on if ng-required is true', inject((RootScope scope) { _.compile(''); - Probe probe = _.rootScope.i; + Probe probe = _.rootScope.context['i']; var model = probe.directive(NgModel); - _.rootScope.$apply(); + _.rootScope.apply(); model.validate(); expect(model.valid).toEqual(true); expect(model.invalid).toEqual(false); - _.rootScope.$apply(() { - _.rootScope['requireMe'] = true; + _.rootScope.apply(() { + _.rootScope.context['requireMe'] = true; }); model.validate(); expect(model.valid).toEqual(false); expect(model.invalid).toEqual(true); - _.rootScope.$apply(() { - _.rootScope['requireMe'] = false; + _.rootScope.apply(() { + _.rootScope.context['requireMe'] = false; }); model.validate(); @@ -73,25 +73,25 @@ describe('ngModel validators', () { }); describe('[type="url"]', () { - it('should validate the input field given a valid or invalid URL', inject((Scope scope) { + it('should validate the input field given a valid or invalid URL', inject((RootScope scope) { _.compile(''); - Probe probe = _.rootScope.i; + Probe probe = _.rootScope.context['i']; var model = probe.directive(NgModel); model.validate(); expect(model.valid).toEqual(true); expect(model.invalid).toEqual(false); - _.rootScope.$apply(() { - _.rootScope['val'] = 'googledotcom'; + _.rootScope.apply(() { + _.rootScope.context['val'] = 'googledotcom'; }); model.validate(); expect(model.valid).toEqual(false); expect(model.invalid).toEqual(true); - _.rootScope.$apply(() { - _.rootScope['val'] = 'http://www.google.com'; + _.rootScope.apply(() { + _.rootScope.context['val'] = 'http://www.google.com'; }); model.validate(); @@ -101,25 +101,25 @@ describe('ngModel validators', () { }); describe('[type="email"]', () { - it('should validate the input field given a valid or invalid email address', inject((Scope scope) { + it('should validate the input field given a valid or invalid email address', inject((RootScope scope) { _.compile(''); - Probe probe = _.rootScope.i; + Probe probe = _.rootScope.context['i']; var model = probe.directive(NgModel); model.validate(); expect(model.valid).toEqual(true); expect(model.invalid).toEqual(false); - _.rootScope.$apply(() { - _.rootScope['val'] = 'matiasatemail.com'; + _.rootScope.apply(() { + _.rootScope.context['val'] = 'matiasatemail.com'; }); model.validate(); expect(model.valid).toEqual(false); expect(model.invalid).toEqual(true); - _.rootScope.$apply(() { - _.rootScope['val'] = 'matias@gmail.com'; + _.rootScope.apply(() { + _.rootScope.context['val'] = 'matias@gmail.com'; }); model.validate(); @@ -129,17 +129,17 @@ describe('ngModel validators', () { }); describe('[type="number"]', () { - it('should validate the input field given a valid or invalid number', inject((Scope scope) { + it('should validate the input field given a valid or invalid number', inject((RootScope scope) { _.compile(''); - Probe probe = _.rootScope.i; + Probe probe = _.rootScope.context['i']; var model = probe.directive(NgModel); model.validate(); expect(model.valid).toEqual(true); expect(model.invalid).toEqual(false); - _.rootScope.$apply(() { - _.rootScope['val'] = '11'; + _.rootScope.apply(() { + _.rootScope.context['val'] = '11'; }); model.validate(); @@ -147,16 +147,16 @@ describe('ngModel validators', () { expect(model.invalid).toEqual(false); - _.rootScope.$apply(() { - _.rootScope['val'] = 10; + _.rootScope.apply(() { + _.rootScope.context['val'] = 10; }); model.validate(); expect(model.valid).toEqual(true); expect(model.invalid).toEqual(false); - _.rootScope.$apply(() { - _.rootScope['val'] = 'twelve'; + _.rootScope.apply(() { + _.rootScope.context['val'] = 'twelve'; }); model.validate(); @@ -166,36 +166,36 @@ describe('ngModel validators', () { }); describe('pattern', () { - it('should validate the input field if a ng-pattern attribute is provided', inject((Scope scope) { + it('should validate the input field if a ng-pattern attribute is provided', inject((RootScope scope) { _.compile(''); - Probe probe = _.rootScope.i; + Probe probe = _.rootScope.context['i']; var model = probe.directive(NgModel); model.validate(); expect(model.valid).toEqual(true); expect(model.invalid).toEqual(false); - _.rootScope.$apply(() { - _.rootScope['val'] = "abc"; - _.rootScope['myPattern'] = "[a-z]+"; + _.rootScope.apply(() { + _.rootScope.context['val'] = "abc"; + _.rootScope.context['myPattern'] = "[a-z]+"; }); model.validate(); expect(model.valid).toEqual(true); expect(model.invalid).toEqual(false); - _.rootScope.$apply(() { - _.rootScope['val'] = "abc"; - _.rootScope['myPattern'] = "[0-9]+"; + _.rootScope.apply(() { + _.rootScope.context['val'] = "abc"; + _.rootScope.context['myPattern'] = "[0-9]+"; }); model.validate(); expect(model.valid).toEqual(false); expect(model.invalid).toEqual(true); - _.rootScope.$apply(() { - _.rootScope['val'] = "123"; - _.rootScope['myPattern'] = "123"; + _.rootScope.apply(() { + _.rootScope.context['val'] = "123"; + _.rootScope.context['myPattern'] = "123"; }); model.validate(); @@ -203,33 +203,33 @@ describe('ngModel validators', () { expect(model.invalid).toEqual(false); })); - it('should validate the input field if a pattern attribute is provided', inject((Scope scope) { + it('should validate the input field if a pattern attribute is provided', inject((RootScope scope) { _.compile(''); - Probe probe = _.rootScope.i; + Probe probe = _.rootScope.context['i']; var model = probe.directive(NgModel); model.validate(); expect(model.valid).toEqual(true); expect(model.invalid).toEqual(false); - _.rootScope.$apply(() { - _.rootScope['val'] = "abc"; + _.rootScope.apply(() { + _.rootScope.context['val'] = "abc"; }); model.validate(); expect(model.valid).toEqual(false); expect(model.invalid).toEqual(true); - _.rootScope.$apply(() { - _.rootScope['val'] = "012345"; + _.rootScope.apply(() { + _.rootScope.context['val'] = "012345"; }); model.validate(); expect(model.valid).toEqual(true); expect(model.invalid).toEqual(false); - _.rootScope.$apply(() { - _.rootScope['val'] = "6789"; + _.rootScope.apply(() { + _.rootScope.context['val'] = "6789"; }); model.validate(); @@ -239,25 +239,25 @@ describe('ngModel validators', () { }); describe('minlength', () { - it('should validate the input field if a minlength attribute is provided', inject((Scope scope) { + it('should validate the input field if a minlength attribute is provided', inject((RootScope scope) { _.compile(''); - Probe probe = _.rootScope.i; + Probe probe = _.rootScope.context['i']; var model = probe.directive(NgModel); model.validate(); expect(model.valid).toEqual(true); expect(model.invalid).toEqual(false); - _.rootScope.$apply(() { - _.rootScope['val'] = "abc"; + _.rootScope.apply(() { + _.rootScope.context['val'] = "abc"; }); model.validate(); expect(model.valid).toEqual(false); expect(model.invalid).toEqual(true); - _.rootScope.$apply(() { - _.rootScope['val'] = "abcdef"; + _.rootScope.apply(() { + _.rootScope.context['val'] = "abcdef"; }); model.validate(); @@ -265,27 +265,27 @@ describe('ngModel validators', () { expect(model.invalid).toEqual(false); })); - it('should validate the input field if a ng-minlength attribute is provided', inject((Scope scope) { + it('should validate the input field if a ng-minlength attribute is provided', inject((RootScope scope) { _.compile(''); - Probe probe = _.rootScope.i; + Probe probe = _.rootScope.context['i']; var model = probe.directive(NgModel); model.validate(); expect(model.valid).toEqual(true); expect(model.invalid).toEqual(false); - _.rootScope.$apply(() { - _.rootScope['val'] = "abcdef"; - _.rootScope['len'] = 3; + _.rootScope.apply(() { + _.rootScope.context['val'] = "abcdef"; + _.rootScope.context['len'] = 3; }); model.validate(); expect(model.valid).toEqual(true); expect(model.invalid).toEqual(false); - _.rootScope.$apply(() { - _.rootScope['val'] = "abc"; - _.rootScope['len'] = 5; + _.rootScope.apply(() { + _.rootScope.context['val'] = "abc"; + _.rootScope.context['len'] = 5; }); model.validate(); @@ -295,25 +295,25 @@ describe('ngModel validators', () { }); describe('maxlength', () { - it('should validate the input field if a maxlength attribute is provided', inject((Scope scope) { + it('should validate the input field if a maxlength attribute is provided', inject((RootScope scope) { _.compile(''); - Probe probe = _.rootScope.i; + Probe probe = _.rootScope.context['i']; var model = probe.directive(NgModel); model.validate(); expect(model.valid).toEqual(true); expect(model.invalid).toEqual(false); - _.rootScope.$apply(() { - _.rootScope['val'] = "abcdef"; + _.rootScope.apply(() { + _.rootScope.context['val'] = "abcdef"; }); model.validate(); expect(model.valid).toEqual(false); expect(model.invalid).toEqual(true); - _.rootScope.$apply(() { - _.rootScope['val'] = "abc"; + _.rootScope.apply(() { + _.rootScope.context['val'] = "abc"; }); model.validate(); @@ -321,27 +321,27 @@ describe('ngModel validators', () { expect(model.invalid).toEqual(false); })); - it('should validate the input field if a ng-maxlength attribute is provided', inject((Scope scope) { + it('should validate the input field if a ng-maxlength attribute is provided', inject((RootScope scope) { _.compile(''); - Probe probe = _.rootScope.i; + Probe probe = _.rootScope.context['i']; var model = probe.directive(NgModel); model.validate(); expect(model.valid).toEqual(true); expect(model.invalid).toEqual(false); - _.rootScope.$apply(() { - _.rootScope['val'] = "abcdef"; - _.rootScope['len'] = 6; + _.rootScope.apply(() { + _.rootScope.context['val'] = "abcdef"; + _.rootScope.context['len'] = 6; }); model.validate(); expect(model.valid).toEqual(true); expect(model.invalid).toEqual(false); - _.rootScope.$apply(() { - _.rootScope['val'] = "abc"; - _.rootScope['len'] = 1; + _.rootScope.apply(() { + _.rootScope.context['val'] = "abc"; + _.rootScope.context['len'] = 1; }); model.validate(); diff --git a/test/directive/ng_non_bindable_spec.dart b/test/directive/ng_non_bindable_spec.dart index 1d48ac2c3..867716c9d 100644 --- a/test/directive/ng_non_bindable_spec.dart +++ b/test/directive/ng_non_bindable_spec.dart @@ -20,9 +20,9 @@ main() { ' ' + ''); compiler(element, directives)(injector, element); - scope.a = "one"; - scope.b = "two"; - scope.$digest(); + scope.context['a'] = "one"; + scope.context['b'] = "two"; + scope.apply(); // Bindings not contained by ng-non-bindable should resolve. expect(element.find("#s1").text().trim()).toEqual('one'); expect(element.find("#s2").text().trim()).toEqual('two'); diff --git a/test/directive/ng_pluralize_spec.dart b/test/directive/ng_pluralize_spec.dart index d340b61d5..951e1c80a 100644 --- a/test/directive/ng_pluralize_spec.dart +++ b/test/directive/ng_pluralize_spec.dart @@ -34,95 +34,95 @@ main() { })); it('should show single/plural strings', () { - _.rootScope.email = '0'; - _.rootScope.$digest(); + _.rootScope.context['email'] = '0'; + _.rootScope.apply(); expect(element.text).toEqual('You have no new email'); expect(elementAlt.text).toEqual('You have no new email'); - _.rootScope.email = '0'; - _.rootScope.$digest(); + _.rootScope.context['email'] = '0'; + _.rootScope.apply(); expect(element.text).toEqual('You have no new email'); expect(elementAlt.text).toEqual('You have no new email'); - _.rootScope.email = 1; - _.rootScope.$digest(); + _.rootScope.context['email'] = 1; + _.rootScope.apply(); expect(element.text).toEqual('You have one new email'); expect(elementAlt.text).toEqual('You have one new email'); - _.rootScope.email = 0.01; - _.rootScope.$digest(); + _.rootScope.context['email'] = 0.01; + _.rootScope.apply(); expect(element.text).toEqual('You have 0.01 new emails'); expect(elementAlt.text).toEqual('You have 0.01 new emails'); - _.rootScope.email = '0.1'; - _.rootScope.$digest(); + _.rootScope.context['email'] = '0.1'; + _.rootScope.apply(); expect(element.text).toEqual('You have 0.1 new emails'); expect(elementAlt.text).toEqual('You have 0.1 new emails'); - _.rootScope.email = 2; - _.rootScope.$digest(); + _.rootScope.context['email'] = 2; + _.rootScope.apply(); expect(element.text).toEqual('You have 2 new emails'); expect(elementAlt.text).toEqual('You have 2 new emails'); - _.rootScope.email = -0.1; - _.rootScope.$digest(); + _.rootScope.context['email'] = -0.1; + _.rootScope.apply(); expect(element.text).toEqual('You have -0.1 new emails'); expect(elementAlt.text).toEqual('You have -0.1 new emails'); - _.rootScope.email = '-0.01'; - _.rootScope.$digest(); + _.rootScope.context['email'] = '-0.01'; + _.rootScope.apply(); expect(element.text).toEqual('You have -0.01 new emails'); expect(elementAlt.text).toEqual('You have -0.01 new emails'); - _.rootScope.email = -2; - _.rootScope.$digest(); + _.rootScope.context['email'] = -2; + _.rootScope.apply(); expect(element.text).toEqual('You have -2 new emails'); expect(elementAlt.text).toEqual('You have -2 new emails'); - _.rootScope.email = -1; - _.rootScope.$digest(); + _.rootScope.context['email'] = -1; + _.rootScope.apply(); expect(element.text).toEqual('You have negative email. Whohoo!'); expect(elementAlt.text).toEqual('You have negative email. Whohoo!'); }); it('should show single/plural strings with mal-formed inputs', () { - _.rootScope.email = ''; - _.rootScope.$digest(); + _.rootScope.context['email'] = ''; + _.rootScope.apply(); expect(element.text).toEqual(''); expect(elementAlt.text).toEqual(''); - _.rootScope.email = null; - _.rootScope.$digest(); + _.rootScope.context['email'] = null; + _.rootScope.apply(); expect(element.text).toEqual(''); expect(elementAlt.text).toEqual(''); - _.rootScope.email = 'a3'; - _.rootScope.$digest(); + _.rootScope.context['email'] = 'a3'; + _.rootScope.apply(); expect(element.text).toEqual(''); expect(elementAlt.text).toEqual(''); - _.rootScope.email = '011'; - _.rootScope.$digest(); + _.rootScope.context['email'] = '011'; + _.rootScope.apply(); expect(element.text).toEqual('You have 11 new emails'); expect(elementAlt.text).toEqual('You have 11 new emails'); - _.rootScope.email = '-011'; - _.rootScope.$digest(); + _.rootScope.context['email'] = '-011'; + _.rootScope.apply(); expect(element.text).toEqual('You have -11 new emails'); expect(elementAlt.text).toEqual('You have -11 new emails'); - _.rootScope.email = '1fff'; - _.rootScope.$digest(); + _.rootScope.context['email'] = '1fff'; + _.rootScope.apply(); expect(element.text).toEqual(''); expect(elementAlt.text).toEqual(''); - _.rootScope.email = '0aa22'; - _.rootScope.$digest(); + _.rootScope.context['email'] = '0aa22'; + _.rootScope.apply(); expect(element.text).toEqual(''); expect(elementAlt.text).toEqual(''); - _.rootScope.email = '000001'; - _.rootScope.$digest(); + _.rootScope.context['email'] = '000001'; + _.rootScope.apply(); expect(element.text).toEqual('You have one new email'); expect(elementAlt.text).toEqual('You have one new email'); }); @@ -136,8 +136,8 @@ main() { "'one': 'Some text'," + "'other': 'Some text'}\">" + ''); - _.rootScope.email = '0'; - _.rootScope.$digest(); + _.rootScope.context['email'] = '0'; + _.rootScope.apply(); expect(element.text).toEqual(''); }))); }); @@ -160,31 +160,31 @@ main() { "when-one='\${p1}, \${p2} and one other person are viewing.'" + "when-other='\${p1}, \${p2} and {} other people are viewing.'>" + ""); - _.rootScope.p1 = 'Igor'; - _.rootScope.p2 = 'Misko'; + _.rootScope.context['p1'] = 'Igor'; + _.rootScope.context['p2'] = 'Misko'; - _.rootScope.viewCount = 0; - _.rootScope.$digest(); + _.rootScope.context['viewCount'] = 0; + _.rootScope.apply(); expect(element.text).toEqual('Nobody is viewing.'); expect(elementAlt.text).toEqual('Nobody is viewing.'); - _.rootScope.viewCount = 1; - _.rootScope.$digest(); + _.rootScope.context['viewCount'] = 1; + _.rootScope.apply(); expect(element.text).toEqual('Igor is viewing.'); expect(elementAlt.text).toEqual('Igor is viewing.'); - _.rootScope.viewCount = 2; - _.rootScope.$digest(); + _.rootScope.context['viewCount'] = 2; + _.rootScope.apply(); expect(element.text).toEqual('Igor and Misko are viewing.'); expect(elementAlt.text).toEqual('Igor and Misko are viewing.'); - _.rootScope.viewCount = 3; - _.rootScope.$digest(); + _.rootScope.context['viewCount'] = 3; + _.rootScope.apply(); expect(element.text).toEqual('Igor, Misko and one other person are viewing.'); expect(elementAlt.text).toEqual('Igor, Misko and one other person are viewing.'); - _.rootScope.viewCount = 4; - _.rootScope.$digest(); + _.rootScope.context['viewCount'] = 4; + _.rootScope.apply(); expect(element.text).toEqual('Igor, Misko and 2 other people are viewing.'); expect(elementAlt.text).toEqual('Igor, Misko and 2 other people are viewing.'); }))); diff --git a/test/directive/ng_repeat_spec.dart b/test/directive/ng_repeat_spec.dart index 46f947110..eb0fa376c 100644 --- a/test/directive/ng_repeat_spec.dart +++ b/test/directive/ng_repeat_spec.dart @@ -22,8 +22,8 @@ main() { var element = $('
              {{item}}
              '); BlockFactory blockFactory = compiler(element, directives); Block block = blockFactory(injector, element); - scope.items = ['a', 'b']; - scope.$apply(); + scope.context['items'] = ['a', 'b']; + scope.apply(); expect(element.text()).toEqual('ab'); })); @@ -33,8 +33,8 @@ main() { var element = $('
              {{item}}
              '); BlockFactory blockFactory = compiler(element, directives); Block block = blockFactory(injector, element); - scope.items = ['a', 'b'].map((i) => i); // makes an iterable - scope.$apply(); + scope.context['items'] = ['a', 'b'].map((i) => i); // makes an iterable + scope.apply(); expect(element.text()).toEqual('ab'); })); @@ -46,21 +46,21 @@ main() { '
            '); // INIT - scope.items = [{"name": 'misko'}, {"name":'shyam'}]; - scope.$digest(); + scope.context['items'] = [{"name": 'misko'}, {"name":'shyam'}]; + scope.apply(); expect(element.find('li').length).toEqual(2); expect(element.text()).toEqual('misko;shyam;'); // GROW - scope.items.add({"name": 'adam'}); - scope.$digest(); + scope.context['items'].add({"name": 'adam'}); + scope.apply(); expect(element.find('li').length).toEqual(3); expect(element.text()).toEqual('misko;shyam;adam;'); // SHRINK - scope.items.removeLast(); - scope.items.removeAt(0); - scope.$digest(); + scope.context['items'].removeLast(); + scope.context['items'].removeAt(0); + scope.apply(); expect(element.find('li').length).toEqual(1); expect(element.text()).toEqual('shyam;'); }); @@ -73,7 +73,7 @@ main() { '
          • {{item.name}};
          • ' + '
          ' + ''); - scope.$digest(); + scope.apply(); expect(element.find('ul').length).toEqual(1); expect(element.find('li').length).toEqual(0); }); @@ -85,13 +85,13 @@ main() { '
            ' + '
          • {{item.name}};
          • ' + '
          '); - scope.items = [{"id": 'misko'}, {"id": 'igor'}]; - scope.$digest(); + scope.context['items'] = [{"id": 'misko'}, {"id": 'igor'}]; + scope.apply(); var li0 = element.find('li')[0]; var li1 = element.find('li')[1]; - scope.items.add(scope.items.removeAt(0)); - scope.$digest(); + scope.context['items'].add(scope.context['items'].removeAt(0)); + scope.apply(); expect(element.find('li')[0]).toBe(li1); expect(element.find('li')[1]).toBe(li0); }); @@ -102,13 +102,13 @@ main() { '
            ' + r'
          • {{item.name}};
          • ' + '
          '); - scope.items = [{"name": 'misko'}, {"name": 'igor'}]; - scope.$digest(); + scope.context['items'] = [{"name": 'misko'}, {"name": 'igor'}]; + scope.apply(); var li0 = element.find('li')[0]; var li1 = element.find('li')[1]; - scope.items.add(scope.items.removeAt(0)); - scope.$digest(); + scope.context['items'].add(scope.context['items'].removeAt(0)); + scope.apply(); expect(element.find('li')[0]).toBe(li1); expect(element.find('li')[1]).toBe(li0); }); @@ -121,75 +121,75 @@ main() { r'
        '); // INIT - scope.items = [true, true, true]; - scope.$digest(); + scope.context['items'] = [true, true, true]; + scope.apply(); expect(element.find('li').length).toEqual(3); expect(element.text()).toEqual('true;true;true;'); - scope.items = [false, true, true]; - scope.$digest(); + scope.context['items'] = [false, true, true]; + scope.apply(); expect(element.find('li').length).toEqual(3); expect(element.text()).toEqual('false;true;true;'); - scope.items = [false, true, false]; - scope.$digest(); + scope.context['items'] = [false, true, false]; + scope.apply(); expect(element.find('li').length).toEqual(3); expect(element.text()).toEqual('false;true;false;'); - scope.items = [true]; - scope.$digest(); + scope.context['items'] = [true]; + scope.apply(); expect(element.find('li').length).toEqual(1); expect(element.text()).toEqual('true;'); - scope.items = [true, true, false]; - scope.$digest(); + scope.context['items'] = [true, true, false]; + scope.apply(); expect(element.find('li').length).toEqual(3); expect(element.text()).toEqual('true;true;false;'); - scope.items = [true, false, false]; - scope.$digest(); + scope.context['items'] = [true, false, false]; + scope.apply(); expect(element.find('li').length).toEqual(3); expect(element.text()).toEqual('true;false;false;'); // string - scope.items = ['a', 'a', 'a']; - scope.$digest(); + scope.context['items'] = ['a', 'a', 'a']; + scope.apply(); expect(element.find('li').length).toEqual(3); expect(element.text()).toEqual('a;a;a;'); - scope.items = ['ab', 'a', 'a']; - scope.$digest(); + scope.context['items'] = ['ab', 'a', 'a']; + scope.apply(); expect(element.find('li').length).toEqual(3); expect(element.text()).toEqual('ab;a;a;'); - scope.items = ['test']; - scope.$digest(); + scope.context['items'] = ['test']; + scope.apply(); expect(element.find('li').length).toEqual(1); expect(element.text()).toEqual('test;'); - scope.items = ['same', 'value']; - scope.$digest(); + scope.context['items'] = ['same', 'value']; + scope.apply(); expect(element.find('li').length).toEqual(2); expect(element.text()).toEqual('same;value;'); // number - scope.items = [12, 12, 12]; - scope.$digest(); + scope.context['items'] = [12, 12, 12]; + scope.apply(); expect(element.find('li').length).toEqual(3); expect(element.text()).toEqual('12;12;12;'); - scope.items = [53, 12, 27]; - scope.$digest(); + scope.context['items'] = [53, 12, 27]; + scope.apply(); expect(element.find('li').length).toEqual(3); expect(element.text()).toEqual('53;12;27;'); - scope.items = [89]; - scope.$digest(); + scope.context['items'] = [89]; + scope.apply(); expect(element.find('li').length).toEqual(1); expect(element.text()).toEqual('89;'); - scope.items = [89, 23]; - scope.$digest(); + scope.context['items'] = [89, 23]; + scope.apply(); expect(element.find('li').length).toEqual(2); expect(element.text()).toEqual('89;23;'); }); @@ -219,8 +219,8 @@ main() { '
          ' + '
        • {{item}}:{{\$index}}|
        • ' + '
        '); - scope.items = ['misko', 'shyam', 'frodo']; - scope.$digest(); + scope.context['items'] = ['misko', 'shyam', 'frodo']; + scope.apply(); expect(element.text()).toEqual('misko:0|shyam:1|frodo:2|'); }); @@ -231,26 +231,26 @@ main() { '
          ' + '
        • {{item}}:{{\$first}}-{{\$middle}}-{{\$last}}|
        • ' + '
        '); - scope.items = ['misko', 'shyam', 'doug']; - scope.$digest(); + scope.context['items'] = ['misko', 'shyam', 'doug']; + scope.apply(); expect(element.text()). toEqual('misko:true-false-false|shyam:false-true-false|doug:false-false-true|'); - scope.items.add('frodo'); - scope.$digest(); + scope.context['items'].add('frodo'); + scope.apply(); expect(element.text()). toEqual('misko:true-false-false|' + 'shyam:false-true-false|' + 'doug:false-true-false|' + 'frodo:false-false-true|'); - scope.items.removeLast(); - scope.items.removeLast(); - scope.$digest(); + scope.context['items'].removeLast(); + scope.context['items'].removeLast(); + scope.apply(); expect(element.text()).toEqual('misko:true-false-false|shyam:false-false-true|'); - scope.items.removeLast(); - scope.$digest(); + scope.context['items'].removeLast(); + scope.apply(); expect(element.text()).toEqual('misko:true-false-true|'); }); @@ -259,21 +259,21 @@ main() { '
          ' + '
        • {{item}}:{{\$odd}}-{{\$even}}|
        • ' + '
        '); - scope.items = ['misko', 'shyam', 'doug']; - scope.$digest(); + scope.context['items'] = ['misko', 'shyam', 'doug']; + scope.apply(); expect(element.text()).toEqual('misko:false-true|shyam:true-false|doug:false-true|'); - scope.items.add('frodo'); - scope.$digest(); + scope.context['items'].add('frodo'); + scope.apply(); expect(element.text()).toEqual('misko:false-true|shyam:true-false|doug:false-true|frodo:true-false|'); - scope.items.removeLast(); - scope.items.removeLast(); - scope.$digest(); + scope.context['items'].removeLast(); + scope.context['items'].removeLast(); + scope.apply(); expect(element.text()).toEqual('misko:false-true|shyam:true-false|'); - scope.items.removeLast(); - scope.$digest(); + scope.context['items'].removeLast(); + scope.apply(); expect(element.text()).toEqual('misko:false-true|'); }); @@ -284,8 +284,8 @@ main() { '
        {{group}}|
        X' + '' + '
      '); - scope.groups = [['a', 'b'], ['c','d']]; - scope.$digest(); + scope.context['groups'] = [['a', 'b'], ['c','d']]; + scope.apply(); expect(element.text()).toEqual('a|b|Xc|d|X'); }); @@ -304,15 +304,15 @@ main() { c = {}; d = {}; - scope.items = [a, b, c]; - scope.$digest(); + scope.context['items'] = [a, b, c]; + scope.apply(); lis = element.find('li'); }); it(r'should preserve the order of elements', () { - scope.items = [a, c, d]; - scope.$digest(); + scope.context['items'] = [a, c, d]; + scope.apply(); var newElements = element.find('li'); expect(newElements[0]).toEqual(lis[0]); expect(newElements[1]).toEqual(lis[2]); @@ -321,52 +321,52 @@ main() { it(r'should throw error on adding existing duplicates and recover', () { - scope.items = [a, a, a]; + scope.context['items'] = [a, a, a]; expect(() { - scope.$digest(); + scope.apply(); }).toThrow("[NgErr50] ngRepeat error! Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: item in items, Duplicate key: {}"); // recover - scope.items = [a]; - scope.$digest(); + scope.context['items'] = [a]; + scope.apply(); var newElements = element.find('li'); expect(newElements.length).toEqual(1); expect(newElements[0]).toEqual(lis[0]); - scope.items = []; - scope.$digest(); + scope.context['items'] = []; + scope.apply(); newElements = element.find('li'); expect(newElements.length).toEqual(0); }); it(r'should throw error on new duplicates and recover', () { - scope.items = [d, d, d]; + scope.context['items'] = [d, d, d]; expect(() { - scope.$digest(); + scope.apply(); }).toThrow("[NgErr50] ngRepeat error! Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: item in items, Duplicate key: {}"); // recover - scope.items = [a]; - scope.$digest(); + scope.context['items'] = [a]; + scope.apply(); var newElements = element.find('li'); expect(newElements.length).toEqual(1); expect(newElements[0]).toEqual(lis[0]); - scope.items = []; - scope.$digest(); + scope.context['items'] = []; + scope.apply(); newElements = element.find('li'); expect(newElements.length).toEqual(0); }); it(r'should reverse items when the collection is reversed', () { - scope.items = [a, b, c]; - scope.$digest(); + scope.context['items'] = [a, b, c]; + scope.apply(); lis = element.find('li'); - scope.items = [c, b, a]; - scope.$digest(); + scope.context['items'] = [c, b, a]; + scope.apply(); var newElements = element.find('li'); expect(newElements.length).toEqual(3); expect(newElements[0]).toEqual(lis[2]); @@ -379,13 +379,13 @@ main() { // rebuilding repeater from scratch can be expensive, we should try to avoid it even for // model that is composed of primitives. - scope.items = ['hello', 'cau', 'ahoj']; - scope.$digest(); + scope.context['items'] = ['hello', 'cau', 'ahoj']; + scope.apply(); lis = element.find('li'); lis[2].id = 'yes'; - scope.items = ['ahoj', 'hello', 'cau']; - scope.$digest(); + scope.context['items'] = ['ahoj', 'hello', 'cau']; + scope.apply(); var newLis = element.find('li'); expect(newLis.length).toEqual(3); expect(newLis[0]).toEqual(lis[2]); @@ -394,32 +394,5 @@ main() { }); }); - describe('shallow', () { - TestBed _; - beforeEach(inject((TestBed tb) => _ = tb)); - - it('should x', () { - _.compile('
      • {{i.name}}
      '); - _.rootScope.items = [{'name': 'a'}, {'name':'b'}]; - _.rootScope.$digest(); - expect(_.rootElement.text).toEqual('ab'); - - // Should not see this. - _.rootScope.items[0]['name'] = 'x'; - _.rootScope.$digest(); - expect(_.rootElement.text).toEqual('ab'); - - // We see additions but not changse - _.rootScope.items.add({'name': 'C'}); - _.rootScope.$digest(); - expect(_.rootElement.text).toEqual('abC'); - - - // Cloning list does a full update - _.rootScope.items = new List.from(_.rootScope.items); - _.rootScope.$digest(); - expect(_.rootElement.text).toEqual('xbC'); - }); - }); }); } diff --git a/test/directive/ng_show_hide_spec.dart b/test/directive/ng_show_hide_spec.dart index f3e778463..c0c01d66f 100644 --- a/test/directive/ng_show_hide_spec.dart +++ b/test/directive/ng_show_hide_spec.dart @@ -13,13 +13,13 @@ main() { expect(_.rootElement).not.toHaveClass('ng-hide'); - _.rootScope.$apply(() { - _.rootScope['isHidden'] = true; + _.rootScope.apply(() { + _.rootScope.context['isHidden'] = true; }); expect(_.rootElement).toHaveClass('ng-hide'); - _.rootScope.$apply(() { - _.rootScope['isHidden'] = false; + _.rootScope.apply(() { + _.rootScope.context['isHidden'] = false; }); expect(_.rootElement).not.toHaveClass('ng-hide'); }); @@ -34,13 +34,13 @@ main() { expect(_.rootElement).not.toHaveClass('ng-hide'); - _.rootScope.$apply(() { - _.rootScope['isShown'] = true; + _.rootScope.apply(() { + _.rootScope.context['isShown'] = true; }); expect(_.rootElement).not.toHaveClass('ng-hide'); - _.rootScope.$apply(() { - _.rootScope['isShown'] = false; + _.rootScope.apply(() { + _.rootScope.context['isShown'] = false; }); expect(_.rootElement).toHaveClass('ng-hide'); }); diff --git a/test/directive/ng_src_boolean_spec.dart b/test/directive/ng_src_boolean_spec.dart index f09bd8e16..e9a8a1795 100644 --- a/test/directive/ng_src_boolean_spec.dart +++ b/test/directive/ng_src_boolean_spec.dart @@ -11,66 +11,66 @@ main() { it('should properly evaluate 0 as false', inject(() { _.compile(''); - _.rootScope.isDisabled = 0; - _.rootScope.$digest(); + _.rootScope.context['isDisabled'] = 0; + _.rootScope.apply(); expect(_.rootElement.attributes['disabled']).toBeFalsy(); - _.rootScope.isDisabled = 1; - _.rootScope.$digest(); + _.rootScope.context['isDisabled'] = 1; + _.rootScope.apply(); expect(_.rootElement.attributes['disabled']).toBeTruthy(); })); it('should bind disabled', inject(() { _.compile(''); - _.rootScope.isDisabled = false; - _.rootScope.$digest(); + _.rootScope.context['isDisabled'] = false; + _.rootScope.apply(); expect(_.rootElement.attributes['disabled']).toBeFalsy(); - _.rootScope.isDisabled = true; - _.rootScope.$digest(); + _.rootScope.context['isDisabled'] = true; + _.rootScope.apply(); expect(_.rootElement.attributes['disabled']).toBeTruthy(); })); it('should bind checked', inject(() { _.compile(''); - _.rootScope.isChecked = false; - _.rootScope.$digest(); + _.rootScope.context['isChecked'] = false; + _.rootScope.apply(); expect(_.rootElement.attributes['checked']).toBeFalsy(); - _.rootScope.isChecked=true; - _.rootScope.$digest(); + _.rootScope.context['isChecked']=true; + _.rootScope.apply(); expect(_.rootElement.attributes['checked']).toBeTruthy(); })); it('should bind selected', inject(() { _.compile(''); - _.rootScope.isSelected=false; - _.rootScope.$digest(); + _.rootScope.context['isSelected']=false; + _.rootScope.apply(); expect((_.rootElement.childNodes[1] as dom.OptionElement).selected).toBeFalsy(); - _.rootScope.isSelected=true; - _.rootScope.$digest(); + _.rootScope.context['isSelected']=true; + _.rootScope.apply(); expect((_.rootElement.childNodes[1] as dom.OptionElement).selected).toBeTruthy(); })); it('should bind readonly', inject(() { _.compile(''); - _.rootScope.isReadonly=false; - _.rootScope.$digest(); + _.rootScope.context['isReadonly']=false; + _.rootScope.apply(); expect(_.rootElement.attributes['readOnly']).toBeFalsy(); - _.rootScope.isReadonly=true; - _.rootScope.$digest(); + _.rootScope.context['isReadonly']=true; + _.rootScope.apply(); expect(_.rootElement.attributes['readOnly']).toBeTruthy(); })); it('should bind open', inject(() { _.compile('
      '); - _.rootScope.isOpen=false; - _.rootScope.$digest(); + _.rootScope.context['isOpen']=false; + _.rootScope.apply(); expect(_.rootElement.attributes['open']).toBeFalsy(); - _.rootScope.isOpen=true; - _.rootScope.$digest(); + _.rootScope.context['isOpen']=true; + _.rootScope.apply(); expect(_.rootElement.attributes['open']).toBeTruthy(); })); @@ -78,11 +78,11 @@ main() { describe('multiple', () { it('should NOT bind to multiple via ngMultiple', inject(() { _.compile(''); - _.rootScope.isMultiple=false; - _.rootScope.$digest(); + _.rootScope.context['isMultiple']=false; + _.rootScope.apply(); expect(_.rootElement.attributes['multiple']).toBeFalsy(); - _.rootScope.isMultiple='multiple'; - _.rootScope.$digest(); + _.rootScope.context['isMultiple']='multiple'; + _.rootScope.apply(); expect(_.rootElement.attributes['multiple']).toBeFalsy(); // ignore })); }); @@ -97,11 +97,11 @@ main() { inject(() { _.compile('
      '); - _.rootScope.$digest(); + _.rootScope.apply(); expect(_.rootElement.attributes['src']).toEqual(''); - _.rootScope.$apply(() { - _.rootScope.id = '/somewhere/here'; + _.rootScope.apply(() { + _.rootScope.context['id'] = '/somewhere/here'; }); expect(_.rootElement.attributes['src']).toEqual('/somewhere/here'); })); @@ -110,11 +110,11 @@ main() { xit('should interpolate the expression and bind to src with a trusted value', inject(($sce) { _.compile('
      '); - _.rootScope.$digest(); + _.rootScope.apply(); expect(_.rootElement.attributes['src']).toEqual(null); - _.rootScope.$apply(() { - _.rootScope.id = $sce.trustAsResourceUrl('http://somewhere'); + _.rootScope.apply(() { + _.rootScope.context['id'] = $sce.trustAsResourceUrl('http://somewhere'); }); expect(_.rootElement.attributes['src']).toEqual('http://somewhere'); })); @@ -131,10 +131,10 @@ main() { it('should interpolate a multi-part expression for regular attributes', inject(() { _.compile('
      '); - _.rootScope.$digest(); + _.rootScope.apply(); expect(_.rootElement.attributes['foo']).toEqual('some/'); - _.rootScope.$apply(() { - _.rootScope.id = 1; + _.rootScope.apply(() { + _.rootScope.context['id'] = 1; }); expect(_.rootElement.attributes['foo']).toEqual('some/1'); })); @@ -143,8 +143,8 @@ main() { xit('should NOT interpolate a wrongly typed expression', inject(($sce) { expect(() { _.compile('
      '); - _.rootScope.$apply(() { - _.rootScope.id = $sce.trustAsUrl('http://somewhere'); + _.rootScope.apply(() { + _.rootScope.context['id'] = $sce.trustAsUrl('http://somewhere'); }); _.rootElement.attributes['src']; }).toThrow("Can't interpolate: {{id}}\nError: [\$sce:insecurl] Blocked " + @@ -161,11 +161,11 @@ main() { it('should interpolate the expression and bind to srcset', inject(() { _.compile('
      '); - _.rootScope.$digest(); + _.rootScope.apply(); expect(_.rootElement.attributes['srcset']).toEqual('some/ 2x'); - _.rootScope.$apply(() { - _.rootScope.id = 1; + _.rootScope.apply(() { + _.rootScope.context['id'] = 1; }); expect(_.rootElement.attributes['srcset']).toEqual('some/1 2x'); })); @@ -178,11 +178,11 @@ main() { it('should interpolate the expression and bind to href', inject(() { _.compile('
      '); - _.rootScope.$digest(); + _.rootScope.apply(); expect(_.rootElement.attributes['href']).toEqual('some/'); - _.rootScope.$apply(() { - _.rootScope.id = 1; + _.rootScope.apply(() { + _.rootScope.context['id'] = 1; }); expect(_.rootElement.attributes['href']).toEqual('some/1'); })); @@ -190,9 +190,9 @@ main() { it('should bind href and merge with other attrs', inject(() { _.compile(''); - _.rootScope.url = 'http://server'; - _.rootScope.rel = 'REL'; - _.rootScope.$digest(); + _.rootScope.context['url'] = 'http://server'; + _.rootScope.context['rel'] = 'REL'; + _.rootScope.apply(); expect(_.rootElement.attributes['href']).toEqual('http://server'); expect(_.rootElement.attributes['rel']).toEqual('REL'); })); @@ -200,7 +200,7 @@ main() { it('should bind href even if no interpolation', inject(() { _.compile(''); - _.rootScope.$digest(); + _.rootScope.apply(); expect(_.rootElement.attributes['href']).toEqual('http://server'); })); }); @@ -211,11 +211,11 @@ main() { it('should interpolate the expression and bind to *', inject(() { _.compile('
      '); - _.rootScope.$digest(); + _.rootScope.apply(); expect(_.rootElement.attributes['foo']).toEqual('some/'); - _.rootScope.$apply(() { - _.rootScope.id = 1; + _.rootScope.apply(() { + _.rootScope.context['id'] = 1; }); expect(_.rootElement.attributes['foo']).toEqual('some/1'); })); @@ -223,17 +223,17 @@ main() { it('should bind * and merge with other attrs', inject(() { _.compile('
      '); - _.rootScope.bar = 'foo'; - _.rootScope.bar2 = 'foo2'; - _.rootScope.bam = 'boom'; - _.rootScope.$digest(); + _.rootScope.context['bar'] = 'foo'; + _.rootScope.context['bar2'] = 'foo2'; + _.rootScope.context['bam'] = 'boom'; + _.rootScope.apply(); expect(_.rootElement.attributes['bar']).toEqual('foo'); expect(_.rootElement.attributes['bar2']).toEqual('foo2'); expect(_.rootElement.attributes['bam']).toEqual('boom'); - _.rootScope.bar = 'FOO'; - _.rootScope.bar2 = 'FOO2'; - _.rootScope.bam = 'BOOM'; - _.rootScope.$digest(); + _.rootScope.context['bar'] = 'FOO'; + _.rootScope.context['bar2'] = 'FOO2'; + _.rootScope.context['bam'] = 'BOOM'; + _.rootScope.apply(); expect(_.rootElement.attributes['bar']).toEqual('FOO'); expect(_.rootElement.attributes['bar2']).toEqual('FOO2'); expect(_.rootElement.attributes['bam']).toEqual('BOOM'); @@ -242,7 +242,7 @@ main() { it('should bind * even if no interpolation', inject(() { _.compile(''); - _.rootScope.$digest(); + _.rootScope.apply(); expect(_.rootElement.attributes['quack']).toEqual('vanilla'); })); }); diff --git a/test/directive/ng_style_spec.dart b/test/directive/ng_style_spec.dart index 35dafe964..8406a14cc 100644 --- a/test/directive/ng_style_spec.dart +++ b/test/directive/ng_style_spec.dart @@ -10,14 +10,14 @@ main() => describe('NgStyle', () { it('should set', () { dom.Element element = _.compile('
      '); - _.rootScope.$digest(); + _.rootScope.apply(); expect(element.style.height).toEqual('40px'); }); it('should silently ignore undefined style', () { dom.Element element = _.compile('
      '); - _.rootScope.$digest(); + _.rootScope.apply(); expect(element.classes.contains('ng-exception')).toBeFalsy(); }); @@ -35,8 +35,8 @@ main() => describe('NgStyle', () { document.body.append(element[0]); _.compile(element); scope = _.rootScope; - scope['styleObj'] = {'margin-top': '44px'}; - scope.$apply(); + scope.context['styleObj'] = {'margin-top': '44px'}; + scope.apply(); element.css(postCompStyle, postCompVal); })); @@ -54,7 +54,7 @@ main() => describe('NgStyle', () { it(r'should not mess up stuff after $apply with no model changes', () { element.css('padding-top', '33px'); - scope.$apply(); + scope.apply(); expect(element.css(preCompStyle)).toEqual(preCompVal); expect(element.css('margin-top')).toEqual('44px'); expect(element.css(postCompStyle)).toEqual(postCompVal); @@ -63,8 +63,8 @@ main() => describe('NgStyle', () { it(r'should not mess up stuff after $apply with non-colliding model changes', () { - scope['styleObj'] = {'padding-top': '99px'}; - scope.$apply(); + scope.context['styleObj'] = {'padding-top': '99px'}; + scope.apply(); expect(element.css(preCompStyle)).toEqual(preCompVal); expect(element.css('margin-top')).not.toEqual('44px'); expect(element.css('padding-top')).toEqual('99px'); @@ -73,12 +73,12 @@ main() => describe('NgStyle', () { it(r'should overwrite original styles after a colliding model change', () { - scope['styleObj'] = {'height': '99px', 'width': '88px'}; - scope.$apply(); + scope.context['styleObj'] = {'height': '99px', 'width': '88px'}; + scope.apply(); expect(element.css(preCompStyle)).toEqual('88px'); expect(element.css(postCompStyle)).toEqual('99px'); - scope['styleObj'] = {}; - scope.$apply(); + scope.context['styleObj'] = {}; + scope.apply(); expect(element.css(preCompStyle)).not.toEqual('88px'); expect(element.css(postCompStyle)).not.toEqual('99px'); }); diff --git a/test/directive/ng_switch_spec.dart b/test/directive/ng_switch_spec.dart index f43b273ef..55f0c70ff 100644 --- a/test/directive/ng_switch_spec.dart +++ b/test/directive/ng_switch_spec.dart @@ -16,20 +16,20 @@ main() => describe('ngSwitch', () { '
      '); expect(element.innerHtml).toEqual( ''); - _.rootScope.select = 1; - _.rootScope.$apply(); + _.rootScope.context['select'] = 1; + _.rootScope.apply(); expect(element.text).toEqual('first:'); - _.rootScope.name="shyam"; - _.rootScope.$apply(); + _.rootScope.context['name'] = "shyam"; + _.rootScope.apply(); expect(element.text).toEqual('first:shyam'); - _.rootScope.select = 2; - _.rootScope.$apply(); + _.rootScope.context['select'] = 2; + _.rootScope.apply(); expect(element.text).toEqual('second:shyam'); - _.rootScope.name = 'misko'; - _.rootScope.$apply(); + _.rootScope.context['name'] = 'misko'; + _.rootScope.apply(); expect(element.text).toEqual('second:misko'); - _.rootScope.select = true; - _.rootScope.$apply(); + _.rootScope.context['select'] = true; + _.rootScope.apply(); expect(element.text).toEqual('true:misko'); })); @@ -46,20 +46,20 @@ main() => describe('ngSwitch', () { '' '' ''); - _.rootScope.select = 1; - _.rootScope.$apply(); + _.rootScope.context['select'] = 1; + _.rootScope.apply(); expect(element.text).toEqual('first:, first too:'); - _.rootScope.name="shyam"; - _.rootScope.$apply(); + _.rootScope.context['name'] = "shyam"; + _.rootScope.apply(); expect(element.text).toEqual('first:shyam, first too:shyam'); - _.rootScope.select = 2; - _.rootScope.$apply(); + _.rootScope.context['select'] = 2; + _.rootScope.apply(); expect(element.text).toEqual('second:shyam'); - _.rootScope.name = 'misko'; - _.rootScope.$apply(); + _.rootScope.context['name'] = 'misko'; + _.rootScope.apply(); expect(element.text).toEqual('second:misko'); - _.rootScope.select = true; - _.rootScope.$apply(); + _.rootScope.context['select'] = true; + _.rootScope.apply(); expect(element.text).toEqual('true:misko'); })); @@ -70,10 +70,10 @@ main() => describe('ngSwitch', () { '
      one
      ' + '
      other
      ' + ''); - _.rootScope.$apply(); + _.rootScope.apply(); expect(element.text).toEqual('other'); - _.rootScope.select = 1; - _.rootScope.$apply(); + _.rootScope.context['select'] = 1; + _.rootScope.apply(); expect(element.text).toEqual('one'); })); @@ -85,10 +85,10 @@ main() => describe('ngSwitch', () { '
    • other
    • ' + '
    • , other too
    • ' + '
    '); - _.rootScope.$apply(); + _.rootScope.apply(); expect(element.text).toEqual('other, other too'); - _.rootScope.select = 1; - _.rootScope.$apply(); + _.rootScope.context['select'] = 1; + _.rootScope.apply(); expect(element.text).toEqual('one'); })); @@ -103,10 +103,10 @@ main() => describe('ngSwitch', () { '
  • other,
  • ' + '
  • other too
  • ' + '
'); - _.rootScope.$apply(); + _.rootScope.apply(); expect(element.text).toEqual('always other, other too '); - _.rootScope.select = 1; - _.rootScope.$apply(); + _.rootScope.context['select'] = 1; + _.rootScope.apply(); expect(element.text).toEqual('always one '); })); @@ -126,10 +126,10 @@ main() => describe('ngSwitch', () { '
  • 7
  • ' + '
  • 8
  • ' + ''); - _.rootScope.$apply(); + _.rootScope.apply(); expect(element.text).toEqual('135678'); - _.rootScope.select = 1; - _.rootScope.$apply(); + _.rootScope.context['select'] = 1; + _.rootScope.apply(); expect(element.text).toEqual('12368'); })); @@ -147,10 +147,10 @@ main() => describe('ngSwitch', () { '
  • 6
  • ' + '
  • 7
  • ' + ''); - _.rootScope.$apply(); + _.rootScope.apply(); expect(element.text).toEqual('3567'); - _.rootScope.select = 1; - _.rootScope.$apply(); + _.rootScope.context['select'] = 1; + _.rootScope.apply(); expect(element.text).toEqual('236'); })); @@ -160,9 +160,9 @@ main() => describe('ngSwitch', () { '
    ' + '
    {{name}}
    ' + '
    '); - _.rootScope.url = 'a'; - _.rootScope.$apply(); - expect(_.rootScope.name).toEqual('works'); + _.rootScope.context['url'] = 'a'; + _.rootScope.apply(); + expect(_.rootScope.context['name']).toEqual('works'); expect(element.text).toEqual('works'); })); @@ -172,30 +172,30 @@ main() => describe('ngSwitch', () { '
    ' + '
    {{name}}
    ' + '
    '); - _.rootScope.$apply(); + _.rootScope.apply(); - var getChildScope = () => _.rootScope.probe == null ? - null : _.rootScope.probe.scope; + var getChildScope = () => _.rootScope.context['probe'] == null ? + null : _.rootScope.context['probe'].scope; expect(getChildScope()).toBeNull(); - _.rootScope.url = 'a'; - _.rootScope.name = 'works'; - _.rootScope.$apply(); + _.rootScope.context['url'] = 'a'; + _.rootScope.context['name'] = 'works'; + _.rootScope.apply(); var child1 = getChildScope(); expect(child1).toBeNotNull(); expect(element.text).toEqual('works'); var destroyListener = jasmine.createSpy('watch listener'); - var listenerRemove = child1.$on('\$destroy', destroyListener); + var watcher = child1.on(ScopeEvent.DESTROY).listen(destroyListener); - _.rootScope.url = 'x'; - _.rootScope.$apply(); + _.rootScope.context['url'] = 'x'; + _.rootScope.apply(); expect(getChildScope()).toBeNull(); expect(destroyListener).toHaveBeenCalledOnce(); - listenerRemove(); + watcher.cancel(); - _.rootScope.url = 'a'; - _.rootScope.$apply(); + _.rootScope.context['url'] = 'a'; + _.rootScope.apply(); var child2 = getChildScope(); expect(child2).toBeDefined(); expect(child2).not.toBe(child1); diff --git a/test/filter/json_spec.dart b/test/filter/json_spec.dart index 20b3fd8fd..15ae9e42b 100644 --- a/test/filter/json_spec.dart +++ b/test/filter/json_spec.dart @@ -3,8 +3,8 @@ library json_spec; import '../_specs.dart'; main() => describe('json', () { - it('should convert primitives, array, map to json', inject((Scope scope) { - scope.foo = [{"string":'foo', "number": 123, "bool": false}]; - expect(scope.$eval('foo | json')).toEqual('[{"string":"foo","number":123,"bool":false}]'); + it('should convert primitives, array, map to json', inject((Scope scope, Parser parser, FilterMap filters) { + scope.context['foo'] = [{"string":'foo', "number": 123, "bool": false}]; + expect(parser('foo | json').eval(scope.context, filters)).toEqual('[{"string":"foo","number":123,"bool":false}]'); })); }); diff --git a/test/filter/limit_to_spec.dart b/test/filter/limit_to_spec.dart index 9ce832fe7..4652e6a0b 100644 --- a/test/filter/limit_to_spec.dart +++ b/test/filter/limit_to_spec.dart @@ -4,51 +4,51 @@ import '../_specs.dart'; main() { describe('orderBy filter', () { - beforeEach(() => inject((Scope scope) { - scope['list'] = 'abcdefgh'.split(''); - scope['string'] = 'tuvwxyz'; + beforeEach(() => inject((Scope scope, Parser parse, FilterMap filters) { + scope.context['list'] = 'abcdefgh'.split(''); + scope.context['string'] = 'tuvwxyz'; })); - it('should return the first X items when X is positive', inject((Scope scope) { - scope['limit'] = 3; - expect(scope.$eval('list | limitTo: 3')).toEqual(['a', 'b', 'c']); - expect(scope.$eval('list | limitTo: limit')).toEqual(['a', 'b', 'c']); - expect(scope.$eval('string | limitTo: 3')).toEqual('tuv'); - expect(scope.$eval('string | limitTo: limit')).toEqual('tuv'); + it('should return the first X items when X is positive', inject((Scope scope, Parser parse, FilterMap filters) { + scope.context['limit'] = 3; + expect(parse('list | limitTo: 3').eval(scope.context, filters)).toEqual(['a', 'b', 'c']); + expect(parse('list | limitTo: limit').eval(scope.context, filters)).toEqual(['a', 'b', 'c']); + expect(parse('string | limitTo: 3').eval(scope.context, filters)).toEqual('tuv'); + expect(parse('string | limitTo: limit').eval(scope.context, filters)).toEqual('tuv'); })); - it('should return the last X items when X is negative', inject((Scope scope) { - scope['limit'] = 3; - expect(scope.$eval('list | limitTo: -3')).toEqual(['f', 'g', 'h']); - expect(scope.$eval('list | limitTo: -limit')).toEqual(['f', 'g', 'h']); - expect(scope.$eval('string | limitTo: -3')).toEqual('xyz'); - expect(scope.$eval('string | limitTo: -limit')).toEqual('xyz'); + it('should return the last X items when X is negative', inject((Scope scope, Parser parse, FilterMap filters) { + scope.context['limit'] = 3; + expect(parse('list | limitTo: -3').eval(scope.context, filters)).toEqual(['f', 'g', 'h']); + expect(parse('list | limitTo: -limit').eval(scope.context, filters)).toEqual(['f', 'g', 'h']); + expect(parse('string | limitTo: -3').eval(scope.context, filters)).toEqual('xyz'); + expect(parse('string | limitTo: -limit').eval(scope.context, filters)).toEqual('xyz'); })); it('should return an null when limiting null list', - inject((Scope scope) { - expect(scope.$eval('null | limitTo: 1')).toEqual(null); - expect(scope.$eval('thisIsNull | limitTo: 1')).toEqual(null); + inject((Scope scope, Parser parse, FilterMap filters) { + expect(parse('null | limitTo: 1').eval(scope.context, filters)).toEqual(null); + expect(parse('thisIsNull | limitTo: 1').eval(scope.context, filters)).toEqual(null); })); it('should return an empty array when X cannot be parsed', - inject((Scope scope) { - expect(scope.$eval('list | limitTo: bogus')).toEqual([]); - expect(scope.$eval('string | limitTo: null')).toEqual([]); - expect(scope.$eval('string | limitTo: thisIsNull')).toEqual([]); + inject((Scope scope, Parser parse, FilterMap filters) { + expect(parse('list | limitTo: bogus').eval(scope.context, filters)).toEqual([]); + expect(parse('string | limitTo: null').eval(scope.context, filters)).toEqual([]); + expect(parse('string | limitTo: thisIsNull').eval(scope.context, filters)).toEqual([]); })); it('should return a copy of input array if X is exceeds array length', - inject((Scope scope) { - expect(scope.$eval('list | limitTo: 20')).toEqual(scope['list']); - expect(scope.$eval('list | limitTo: -20')).toEqual(scope['list']); - expect(scope.$eval('list | limitTo: 20')).not.toBe(scope['list']); + inject((Scope scope, Parser parse, FilterMap filters) { + expect(parse('list | limitTo: 20').eval(scope.context, filters)).toEqual(scope.context['list']); + expect(parse('list | limitTo: -20').eval(scope.context, filters)).toEqual(scope.context['list']); + expect(parse('list | limitTo: 20').eval(scope.context, filters)).not.toBe(scope.context['list']); })); it('should return the entire string if X exceeds input length', - inject((Scope scope) { - expect(scope.$eval('string | limitTo: 20')).toEqual(scope['string']); - expect(scope.$eval('string | limitTo: -20')).toEqual(scope['string']); + inject((Scope scope, Parser parse, FilterMap filters) { + expect(parse('string | limitTo: 20').eval(scope.context, filters)).toEqual(scope.context['string']); + expect(parse('string | limitTo: -20').eval(scope.context, filters)).toEqual(scope.context['string']); })); }); diff --git a/test/filter/lowercase_spec.dart b/test/filter/lowercase_spec.dart index cb9efc6e7..575deb7ce 100644 --- a/test/filter/lowercase_spec.dart +++ b/test/filter/lowercase_spec.dart @@ -3,8 +3,8 @@ library lowercase_spec; import '../_specs.dart'; main() => describe('lowercase', () { - it('should convert string to lowercase', inject((Scope scope) { - expect(scope.$eval('null | lowercase')).toEqual(null); - expect(scope.$eval('"FOO" | lowercase')).toEqual('foo'); + it('should convert string to lowercase', inject((Parser parse, FilterMap filters) { + expect(parse('null | lowercase').eval(null, filters)).toEqual(null); + expect(parse('"FOO" | lowercase').eval(null, filters)).toEqual('foo'); })); }); diff --git a/test/filter/order_by_spec.dart b/test/filter/order_by_spec.dart index 694767e74..80f4a34c9 100644 --- a/test/filter/order_by_spec.dart +++ b/test/filter/order_by_spec.dart @@ -20,15 +20,15 @@ main() { Jeffrey_Archer = {'firstName': 'Jeffrey', 'lastName': 'Archer'}, Isaac___Asimov = new Name(firstName: 'Isaac', lastName: 'Asimov'), Oscar___Wilde = {'firstName': 'Oscar', 'lastName': 'Wilde'}; - beforeEach(() => inject((Scope scope) { - scope['authors'] = [ + beforeEach(() => inject((Scope scope, Parser parse, FilterMap filters) { + scope.context['authors'] = [ Emily___Bronte, Mark____Twain, Jeffrey_Archer, Isaac___Asimov, Oscar___Wilde, ]; - scope['items'] = [ + scope.context['items'] = [ {'a': 10, 'b': 10}, {'a': 10, 'b': 20}, {'a': 20, 'b': 10}, @@ -36,32 +36,32 @@ main() { ]; })); - it('should pass through null list when input list is null', inject((Scope scope) { + it('should pass through null list when input list is null', inject((Scope scope, Parser parse, FilterMap filters) { var list = null; - expect(scope.$eval('list | orderBy:"foo"')).toBe(null); + expect(parse('list | orderBy:"foo"').eval(scope.context, filters)).toBe(null); })); - it('should pass through argument when expression is null', inject((Scope scope) { - var list = scope['list'] = [1, 3, 2]; - expect(scope.$eval('list | orderBy:thisIsNull')).toBe(list); + it('should pass through argument when expression is null', inject((Scope scope, Parser parse, FilterMap filters) { + var list = scope.context['list'] = [1, 3, 2]; + expect(parse('list | orderBy:thisIsNull').eval(scope.context, filters)).toBe(list); })); - it('should sort with "empty" expression using default comparator', inject((Scope scope) { - scope['list'] = [1, 3, 2]; - expect(scope.$eval('list | orderBy:""')).toEqual([1, 2, 3]); - expect(scope.$eval('list | orderBy:"+"')).toEqual([1, 2, 3]); - expect(scope.$eval('list | orderBy:"-"')).toEqual([3, 2, 1]); + it('should sort with "empty" expression using default comparator', inject((Scope scope, Parser parse, FilterMap filters) { + scope.context['list'] = [1, 3, 2]; + expect(parse('list | orderBy:""').eval(scope.context, filters)).toEqual([1, 2, 3]); + expect(parse('list | orderBy:"+"').eval(scope.context, filters)).toEqual([1, 2, 3]); + expect(parse('list | orderBy:"-"').eval(scope.context, filters)).toEqual([3, 2, 1]); })); - it('should sort by expression', inject((Scope scope) { - expect(scope.$eval('authors | orderBy:"firstName"')).toEqual([ + it('should sort by expression', inject((Scope scope, Parser parse, FilterMap filters) { + expect(parse('authors | orderBy:"firstName"').eval(scope.context, filters)).toEqual([ Emily___Bronte, Isaac___Asimov, Jeffrey_Archer, Mark____Twain, Oscar___Wilde, ]); - expect(scope.$eval('authors | orderBy:"lastName"')).toEqual([ + expect(parse('authors | orderBy:"lastName"').eval(scope.context, filters)).toEqual([ Jeffrey_Archer, Isaac___Asimov, Emily___Bronte, @@ -69,8 +69,8 @@ main() { Oscar___Wilde, ]); - scope['sortKey'] = 'firstName'; - expect(scope.$eval('authors | orderBy:sortKey')).toEqual([ + scope.context['sortKey'] = 'firstName'; + expect(parse('authors | orderBy:sortKey').eval(scope.context, filters)).toEqual([ Emily___Bronte, Isaac___Asimov, Jeffrey_Archer, @@ -80,8 +80,8 @@ main() { })); - it('should reverse order when passed the additional descending param', inject((Scope scope) { - expect(scope.$eval('authors | orderBy:"lastName":true')).toEqual([ + it('should reverse order when passed the additional descending param', inject((Scope scope, Parser parse, FilterMap filters) { + expect(parse('authors | orderBy:"lastName":true').eval(scope.context, filters)).toEqual([ Oscar___Wilde, Mark____Twain, Emily___Bronte, @@ -90,8 +90,8 @@ main() { ]); })); - it('should reverse order when expression is prefixed with "-"', inject((Scope scope) { - expect(scope.$eval('authors | orderBy:"-lastName"')).toEqual([ + it('should reverse order when expression is prefixed with "-"', inject((Scope scope, Parser parse, FilterMap filters) { + expect(parse('authors | orderBy:"-lastName"').eval(scope.context, filters)).toEqual([ Oscar___Wilde, Mark____Twain, Emily___Bronte, @@ -101,8 +101,8 @@ main() { })); it('should NOT reverse order when BOTH expression is prefixed with "-" AND additional parameter also asks reversal', - inject((Scope scope) { - expect(scope.$eval('authors | orderBy:"-lastName":true')).toEqual([ + inject((Scope scope, Parser parse, FilterMap filters) { + expect(parse('authors | orderBy:"-lastName":true').eval(scope.context, filters)).toEqual([ Jeffrey_Archer, Isaac___Asimov, Emily___Bronte, @@ -112,22 +112,22 @@ main() { })); it('should allow a "+" prefix on the expression that should be a nop (ascending order)', - inject((Scope scope) { - expect(scope.$eval('authors | orderBy:"+lastName"')).toEqual([ + inject((Scope scope, Parser parse, FilterMap filters) { + expect(parse('authors | orderBy:"+lastName"').eval(scope.context, filters)).toEqual([ Jeffrey_Archer, Isaac___Asimov, Emily___Bronte, Mark____Twain, Oscar___Wilde, ]); - expect(scope.$eval('authors | orderBy:"+lastName":false')).toEqual([ + expect(parse('authors | orderBy:"+lastName":false').eval(scope.context, filters)).toEqual([ Jeffrey_Archer, Isaac___Asimov, Emily___Bronte, Mark____Twain, Oscar___Wilde, ]); - expect(scope.$eval('authors | orderBy:"+lastName":true')).toEqual([ + expect(parse('authors | orderBy:"+lastName":true').eval(scope.context, filters)).toEqual([ Oscar___Wilde, Mark____Twain, Emily___Bronte, @@ -137,26 +137,26 @@ main() { })); it('should support an array of expressions', - inject((Scope scope) { - expect(scope.$eval('items | orderBy:["-a", "-b"]')).toEqual([ + inject((Scope scope, Parser parse, FilterMap filters) { + expect(parse('items | orderBy:["-a", "-b"]').eval(scope.context, filters)).toEqual([ {'a': 20, 'b': 20}, {'a': 20, 'b': 10}, {'a': 10, 'b': 20}, {'a': 10, 'b': 10}, ]); - expect(scope.$eval('items | orderBy:["-b", "-a"]')).toEqual([ + expect(parse('items | orderBy:["-b", "-a"]').eval(scope.context, filters)).toEqual([ {'a': 20, 'b': 20}, {'a': 10, 'b': 20}, {'a': 20, 'b': 10}, {'a': 10, 'b': 10}, ]); - expect(scope.$eval('items | orderBy:["a", "-b"]')).toEqual([ + expect(parse('items | orderBy:["a", "-b"]').eval(scope.context, filters)).toEqual([ {'a': 10, 'b': 20}, {'a': 10, 'b': 10}, {'a': 20, 'b': 20}, {'a': 20, 'b': 10}, ]); - expect(scope.$eval('items | orderBy:["a", "-b"]:true')).toEqual([ + expect(parse('items | orderBy:["a", "-b"]:true').eval(scope.context, filters)).toEqual([ {'a': 20, 'b': 10}, {'a': 20, 'b': 20}, {'a': 10, 'b': 10}, @@ -165,16 +165,16 @@ main() { })); it('should support function expressions', - inject((Scope scope) { - scope['func'] = (e) => -(e['a'] + e['b']); - expect(scope.$eval('items | orderBy:[func, "a", "-b"]')).toEqual([ + inject((Scope scope, Parser parse, FilterMap filters) { + scope.context['func'] = (e) => -(e['a'] + e['b']); + expect(parse('items | orderBy:[func, "a", "-b"]').eval(scope.context, filters)).toEqual([ {'a': 20, 'b': 20}, {'a': 10, 'b': 20}, {'a': 20, 'b': 10}, {'a': 10, 'b': 10}, ]); - scope['func'] = (e) => (e is Name) ? e.lastName : e['lastName']; - expect(scope.$eval('authors | orderBy:func')).toEqual([ + scope.context['func'] = (e) => (e is Name) ? e.lastName : e['lastName']; + expect(parse('authors | orderBy:func').eval(scope.context, filters)).toEqual([ Jeffrey_Archer, Isaac___Asimov, Emily___Bronte, diff --git a/test/filter/uppercase_spec.dart b/test/filter/uppercase_spec.dart index be81f6e5c..803b35605 100644 --- a/test/filter/uppercase_spec.dart +++ b/test/filter/uppercase_spec.dart @@ -3,8 +3,8 @@ library uppercase_spec; import '../_specs.dart'; main() => describe('uppercase', () { - it('should convert string to uppercase', inject((Scope scope) { - expect(scope.$eval('null | uppercase')).toEqual(null); - expect(scope.$eval('"foo" | uppercase')).toEqual('FOO'); + it('should convert string to uppercase', inject((Parser parse, FilterMap filters) { + expect(parse('null | uppercase').eval(null, filters)).toEqual(null); + expect(parse('"foo" | uppercase').eval(null, filters)).toEqual('FOO'); })); }); diff --git a/test/mock/test_bed_spec.dart b/test/mock/test_bed_spec.dart index 755ce6eef..19380a111 100644 --- a/test/mock/test_bed_spec.dart +++ b/test/mock/test_bed_spec.dart @@ -17,15 +17,15 @@ describe('test bed', () { it('should allow for a scope-based compile', () { inject((Scope scope) { - Scope childScope = scope.$new(); + Scope childScope = scope.createChild({}); var element = $('
    '); _.compile(element, scope: childScope); - Probe probe = _.rootScope.i; + Probe probe = _.rootScope.context['i']; var directiveInst = probe.directive(MyTestBedDirective); - childScope.$destroy(); + childScope.destroy(); expect(directiveInst.destroyed).toBe(true); }); @@ -38,7 +38,7 @@ class MyTestBedDirective { bool destroyed = false; MyTestBedDirective(Scope scope) { - scope.$on(r'$destroy', () { + scope.on(ScopeEvent.DESTROY).listen((_) { destroyed = true; }); } diff --git a/test/routing/ng_bind_route_spec.dart b/test/routing/ng_bind_route_spec.dart index 827b99f96..7d21b15f9 100644 --- a/test/routing/ng_bind_route_spec.dart +++ b/test/routing/ng_bind_route_spec.dart @@ -20,14 +20,14 @@ main() { it('should inject null RouteProvider when no ng-bind-route', async(() { Element root = _.compile('
    '); - expect(_.rootScope['routeProbe'].injector.get(RouteProvider)).toBeNull(); + expect(_.rootScope.context['routeProbe'].injector.get(RouteProvider)).toBeNull(); })); it('should inject RouteProvider with correct flat route', async(() { Element root = _.compile( '
    '); - expect(_.rootScope['routeProbe'].injector.get(RouteProvider).routeName) + expect(_.rootScope.context['routeProbe'].injector.get(RouteProvider).routeName) .toEqual('library'); })); @@ -39,7 +39,7 @@ main() { '
    ' ' ' ''); - expect(_.rootScope['routeProbe'].injector.get(RouteProvider).route.name) + expect(_.rootScope.context['routeProbe'].injector.get(RouteProvider).route.name) .toEqual('all'); })); diff --git a/test/routing/ng_view_spec.dart b/test/routing/ng_view_spec.dart index 22c2304b6..0ace24542 100644 --- a/test/routing/ng_view_spec.dart +++ b/test/routing/ng_view_spec.dart @@ -54,7 +54,7 @@ main() { Element root = _.compile(''); expect(root.text).toEqual(''); - _.rootScope.$digest(); + _.rootScope.apply(); microLeap(); expect(root.text).toEqual('Foo'); })); diff --git a/test/routing/routing_spec.dart b/test/routing/routing_spec.dart index be2b6aba9..ee1350d3d 100644 --- a/test/routing/routing_spec.dart +++ b/test/routing/routing_spec.dart @@ -319,7 +319,7 @@ main() { router.route('/foo'); microLeap(); - _.rootScope.$digest(); + _.rootScope.apply(); expect(root.text).toEqual('Hello, World!'); })); @@ -345,7 +345,7 @@ main() { router.route('/foo'); microLeap(); - _.rootScope.$digest(); + _.rootScope.apply(); expect(root.text).toEqual('Hello, World!'); }));