From 31ec4127d1ec4c3ea132406d702e9182bba6a0dd Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Wed, 29 Jan 2014 16:47:34 -0800 Subject: [PATCH 01/35] chore(perf): Get watch_group_perf running on Dart2js DARTIUM: ======== fieldRead: => 58,240 ops/sec (17 us) stdev(0.0028) fieldReadGetter: => 1,480,972 ops/sec (1 us) stdev(0.08649) mapRead: => 1,720,752 ops/sec (1 us) stdev(0.05972) obj.method?(): => 93,362 ops/sec (11 us) stdev(0.00549) obj.method?(obj): => 79,881 ops/sec (13 us) stdev(0.00301) add?(a, a): => 1,342,560 ops/sec (1 us) stdev(0.10907) change-detect List[1000](RunTime): 61.251990689697415 us. DART2JS: ======== fieldRead: => 372,422 ops/sec (3 us) stdev(0.0109) fieldReadGetter: => 1,038,660 ops/sec (1 us) stdev(0.19283) mapRead: => 721,103 ops/sec (1 us) stdev(0.04434) obj.method?(): => 172,910 ops/sec (6 us) stdev(0.00801) obj.method?(obj): => 161,329 ops/sec (6 us) stdev(0.00598) add?(a, a): => 2,432,982 ops/sec (0 us) stdev(0.17624) change-detect List[1000](RunTime): 145.12734924896597 us. --- perf/watch_group_perf.dart | 110 +++++++++++++++++++++++-------------- 1 file changed, 70 insertions(+), 40 deletions(-) diff --git a/perf/watch_group_perf.dart b/perf/watch_group_perf.dart index b2b9ee086..d0e9142af 100644 --- a/perf/watch_group_perf.dart +++ b/perf/watch_group_perf.dart @@ -5,6 +5,14 @@ import 'package:angular/change_detection/dirty_checking_change_detector.dart'; import 'package:angular/change_detection/watch_group.dart'; import 'package:benchmark_harness/benchmark_harness.dart'; +@MirrorsUsed( + targets: const [ + 'angular.perf.watch_group' + ], + override: '*' +) +import 'dart:mirrors' show MirrorsUsed; + var _reactionFn = (_, __, ___) => null; var _getterCache = new GetterCache({}); main() { @@ -164,26 +172,26 @@ _methodInvoke1() { context.a = new _Obj(); var watchGrp = new RootWatchGroup( new DirtyCheckingChangeDetector(_getterCache), context) - ..watch(_method('a', 'methodA', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodB', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodC', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodD', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodE', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodF', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodG', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodH', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodI', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodJ', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodK', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodL', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodM', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodN', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodO', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodP', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodQ', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodR', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodS', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodT', [_parse('a')]), _reactionFn); + ..watch(_method('a', 'methodA1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodB1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodC1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodD1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodE1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodF1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodG1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodH1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodI1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodJ1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodK1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodL1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodM1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodN1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodO1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodP1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodQ1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodR1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodS1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodT1', [_parse('a')]), _reactionFn); print('Watch: ${watchGrp.fieldCost}; eval: ${watchGrp.evalCost}'); time('obj.method?(obj)', () => watchGrp.detectChanges()); @@ -260,24 +268,46 @@ class _Obj { var s = 19; var t = 20; - methodA([arg0]) => a; - methodB([arg0]) => b; - methodC([arg0]) => c; - methodD([arg0]) => d; - methodE([arg0]) => e; - methodF([arg0]) => f; - methodG([arg0]) => g; - methodH([arg0]) => h; - methodI([arg0]) => i; - methodJ([arg0]) => j; - methodK([arg0]) => k; - methodL([arg0]) => l; - methodM([arg0]) => m; - methodN([arg0]) => n; - methodO([arg0]) => o; - methodP([arg0]) => p; - methodQ([arg0]) => q; - methodR([arg0]) => r; - methodS([arg0]) => s; - methodT([arg0]) => t; + methodA1(arg0) => a; + methodB1(arg0) => b; + methodC1(arg0) => c; + methodD1(arg0) => d; + methodE1(arg0) => e; + methodF1(arg0) => f; + methodG1(arg0) => g; + methodH1(arg0) => h; + methodI1(arg0) => i; + methodJ1(arg0) => j; + methodK1(arg0) => k; + methodL1(arg0) => l; + methodM1(arg0) => m; + methodN1(arg0) => n; + methodO1(arg0) => o; + methodP1(arg0) => p; + methodQ1(arg0) => q; + methodR1(arg0) => r; + methodS1(arg0) => s; + methodT1(arg0) => t; + + methodA() => a; + methodB() => b; + methodC() => c; + methodD() => d; + methodE() => e; + methodF() => f; + methodG() => g; + methodH() => h; + methodI() => i; + methodJ() => j; + methodK() => k; + methodL() => l; + methodM() => m; + methodN() => n; + methodO() => o; + methodP() => p; + methodQ() => q; + methodR() => r; + methodS() => s; + methodT() => t; + } From fc4f0557406c71326c24e8b549ce34cfa50152c1 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Fri, 24 Jan 2014 23:55:39 -0800 Subject: [PATCH 02/35] feat(scope2): Basic implementation of Scope v2 --- lib/change_detection/ast.dart | 10 +- lib/change_detection/change_detection.dart | 4 +- .../dirty_checking_change_detector.dart | 18 +- lib/change_detection/watch_group.dart | 27 +- lib/core/scope2.dart | 775 ++++++++++++ lib/mock/test_injection.dart | 6 + perf/watch_group_perf.dart | 2 +- test/change_detection/watch_group_spec.dart | 58 +- test/core/scope2_spec.dart | 1065 +++++++++++++++++ 9 files changed, 1920 insertions(+), 45 deletions(-) create mode 100644 lib/core/scope2.dart create mode 100644 test/core/scope2_spec.dart diff --git a/lib/change_detection/ast.dart b/lib/change_detection/ast.dart index d6ba9c5c3..1f8d1bf65 100644 --- a/lib/change_detection/ast.dart +++ b/lib/change_detection/ast.dart @@ -9,7 +9,11 @@ part of angular.watch_group; abstract class AST { static final String _CONTEXT = '#'; final String expression; - AST(this.expression) { assert(expression!=null); } + AST(expression) + : expression = expression.startsWith('#.') ? expression.substring(2) : expression + { + assert(expression!=null); + } WatchRecord<_Handler> setupWatch(WatchGroup watchGroup); toString() => expression; } @@ -34,7 +38,7 @@ class ConstantAST extends AST { final constant; ConstantAST(dynamic constant): - super('$constant'), + super(constant is String ? '"$constant"' : '$constant'), constant = constant; WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) @@ -51,7 +55,7 @@ class FieldReadAST extends AST { final String name; FieldReadAST(lhs, name) - : super(lhs.expression == AST._CONTEXT ? name : '$lhs.$name'), + : super('$lhs.$name'), lhs = lhs, name = name; diff --git a/lib/change_detection/change_detection.dart b/lib/change_detection/change_detection.dart index e7237b5b3..1c41631a0 100644 --- a/lib/change_detection/change_detection.dart +++ b/lib/change_detection/change_detection.dart @@ -1,5 +1,7 @@ library change_detection; +typedef EvalExceptionHandler(error, stack); + /** * An interface for [ChangeDetectorGroup] groups related watches together. It * guarentees that within the group all watches will be reported in the order in @@ -53,7 +55,7 @@ abstract class ChangeDetector extends ChangeDetectorGroup { * linked list of [ChangeRecord]s. The [ChangeRecord]s are to be returned in * the same order as they were registered. */ - ChangeRecord collectChanges(); + ChangeRecord collectChanges([EvalExceptionHandler exceptionHandler]); } abstract class Record { diff --git a/lib/change_detection/dirty_checking_change_detector.dart b/lib/change_detection/dirty_checking_change_detector.dart index 1d32b7fae..6a144acff 100644 --- a/lib/change_detection/dirty_checking_change_detector.dart +++ b/lib/change_detection/dirty_checking_change_detector.dart @@ -216,17 +216,25 @@ class DirtyCheckingChangeDetector extends DirtyCheckingChangeDetectorGroup implements ChangeDetector { DirtyCheckingChangeDetector(GetterCache getterCache): super(null, getterCache); - DirtyCheckingRecord collectChanges() { + DirtyCheckingRecord collectChanges([EvalExceptionHandler exceptionHandler]) { DirtyCheckingRecord changeHead = null; DirtyCheckingRecord changeTail = null; DirtyCheckingRecord current = _head; // current index while (current != null) { - if (current.check() != null) { - if (changeHead == null) { - changeHead = changeTail = current; + try { + if (current.check() != null) { + if (changeHead == null) { + changeHead = changeTail = current; + } else { + changeTail = changeTail.nextChange = current; + } + } + } catch (e, s) { + if (exceptionHandler == null) { + rethrow; } else { - changeTail = changeTail.nextChange = current; + exceptionHandler(e, s); } } current = current._nextWatch; diff --git a/lib/change_detection/watch_group.dart b/lib/change_detection/watch_group.dart index 923cb1b2a..196c930b3 100644 --- a/lib/change_detection/watch_group.dart +++ b/lib/change_detection/watch_group.dart @@ -7,7 +7,8 @@ part 'linked_list.dart'; part 'ast.dart'; part 'prototype_map.dart'; -typedef ReactionFn(value, previousValue, object); +typedef ReactionFn(value, previousValue); +typedef ChangeLog(expression); /** * Extend this class if you wish to pretend to be a function, but you don't know @@ -333,11 +334,14 @@ class RootWatchGroup extends WatchGroup { * Each step is called in sequence. ([ReactionFn]s are not called until all previous steps are * completed). */ - int detectChanges() { + int detectChanges({ExceptionHandler exceptionHandler, ChangeLog changeLog}) { // Process the ChangeRecords from the change detector ChangeRecord<_Handler> changeRecord = - (_changeDetector as ChangeDetector<_Handler>).collectChanges(); + (_changeDetector as ChangeDetector<_Handler>).collectChanges(exceptionHandler); while (changeRecord != null) { + if (changeLog != null) { + changeLog(changeRecord.handler.expression); + } changeRecord.handler.onChange(changeRecord); changeRecord = changeRecord.nextChange; } @@ -346,7 +350,14 @@ class RootWatchGroup extends WatchGroup { // Process our own function evaluations _EvalWatchRecord evalRecord = _evalWatchHead; while (evalRecord != null) { - evalRecord.check(); + try { + var change = evalRecord.check(); + if (change != null && changeLog != null) { + changeLog(evalRecord.handler.expression); + } + } catch (e, s) { + if (exceptionHandler == null) rethrow; else exceptionHandler(e, s); + } evalRecord = evalRecord._nextEvalWatch; } @@ -356,7 +367,11 @@ class RootWatchGroup extends WatchGroup { Watch dirtyWatch = _dirtyWatchHead; while(dirtyWatch != null) { count++; - dirtyWatch.invoke(); + try { + dirtyWatch.invoke(); + } catch (e, s) { + if (exceptionHandler == null) rethrow; else exceptionHandler(e, s); + } dirtyWatch = dirtyWatch._nextDirtyWatch; } _dirtyWatchHead = _dirtyWatchTail = null; @@ -400,7 +415,7 @@ class Watch { invoke() { _dirty = false; - reactionFn(_record.currentValue, _record.previousValue, _record.object); + reactionFn(_record.currentValue, _record.previousValue); } remove() { diff --git a/lib/core/scope2.dart b/lib/core/scope2.dart new file mode 100644 index 000000000..8943f3b8b --- /dev/null +++ b/lib/core/scope2.dart @@ -0,0 +1,775 @@ +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/mock/test_injection.dart b/lib/mock/test_injection.dart index ae1542d34..eb45e1815 100644 --- a/lib/mock/test_injection.dart +++ b/lib/mock/test_injection.dart @@ -5,6 +5,7 @@ _SpecInjector _currentSpecInjector = null; class _SpecInjector { DynamicInjector moduleInjector; DynamicInjector injector; + dynamic injectiorCreateLocation; List modules = []; List initFns = []; @@ -23,6 +24,9 @@ class _SpecInjector { } module(fnOrModule, [declarationStack]) { + if (injectiorCreateLocation != null) { + throw "Injector already created at:\n$injectiorCreateLocation"; + } try { if (fnOrModule is Function) { var initFn = moduleInjector.invoke(fnOrModule); @@ -42,6 +46,7 @@ class _SpecInjector { inject(Function fn, [declarationStack]) { try { if (injector == null) { + injectiorCreateLocation = declarationStack; injector = new DynamicInjector(modules: modules); // Implicit injection is disabled. initFns.forEach((fn) { injector.invoke(fn); @@ -55,6 +60,7 @@ class _SpecInjector { reset() { injector = null; + injectiorCreateLocation = null; } } diff --git a/perf/watch_group_perf.dart b/perf/watch_group_perf.dart index d0e9142af..a5923c6bd 100644 --- a/perf/watch_group_perf.dart +++ b/perf/watch_group_perf.dart @@ -13,7 +13,7 @@ import 'package:benchmark_harness/benchmark_harness.dart'; ) import 'dart:mirrors' show MirrorsUsed; -var _reactionFn = (_, __, ___) => null; +var _reactionFn = (_, __) => null; var _getterCache = new GetterCache({}); main() { _fieldRead(); diff --git a/test/change_detection/watch_group_spec.dart b/test/change_detection/watch_group_spec.dart index 2608addc2..e36cdc25b 100644 --- a/test/change_detection/watch_group_spec.dart +++ b/test/change_detection/watch_group_spec.dart @@ -40,7 +40,7 @@ main() => describe('WatchGroup', () { // should fire on initial adding expect(watchGrp.fieldCost).toEqual(0); - var watch = watchGrp.watch(parse('a'), (v, p, o) => logger(v)); + var watch = watchGrp.watch(parse('a'), (v, p) => logger(v)); expect(watch.expression).toEqual('a'); expect(watchGrp.fieldCost).toEqual(1); watchGrp.detectChanges(); @@ -69,7 +69,7 @@ main() => describe('WatchGroup', () { // should fire on initial adding expect(watchGrp.fieldCost).toEqual(0); expect(changeDetector.count).toEqual(0); - var watch = watchGrp.watch(parse('a.b'), (v, p, o) => logger(v)); + var watch = watchGrp.watch(parse('a.b'), (v, p) => logger(v)); expect(watch.expression).toEqual('a.b'); expect(watchGrp.fieldCost).toEqual(2); expect(changeDetector.count).toEqual(2); @@ -111,9 +111,9 @@ main() => describe('WatchGroup', () { // should fire on initial adding expect(watchGrp.fieldCost).toEqual(0); - var watch = watchGrp.watch(parse('user'), (v, p, o) => logger(v)); - var watchFirst = watchGrp.watch(parse('user.first'), (v, p, o) => logger(v)); - var watchLast = watchGrp.watch(parse('user.last'), (v, p, o) => logger(v)); + var watch = watchGrp.watch(parse('user'), (v, p) => logger(v)); + var watchFirst = watchGrp.watch(parse('user.first'), (v, p) => logger(v)); + var watchLast = watchGrp.watch(parse('user.last'), (v, p) => logger(v)); expect(watchGrp.fieldCost).toEqual(3); watchGrp.detectChanges(); @@ -143,7 +143,7 @@ main() => describe('WatchGroup', () { FunctionApply fn = new LoggingFunctionApply(logger); var watch = watchGrp.watch( new PureFunctionAST('add', fn, [parse('a.val')]), - (v, p, o) => logger(v) + (v, p) => logger(v) ); // a; a.val; b; b.val; @@ -165,7 +165,7 @@ main() => describe('WatchGroup', () { (a, b) { logger('+'); return a+b; }, [parse('a.val'), parse('b.val')] ), - (v, p, o) => logger(v) + (v, p) => logger(v) ); // a; a.val; b; b.val; @@ -213,7 +213,7 @@ main() => describe('WatchGroup', () { (b, c) { logger('$b+$c'); return b + c; }, [a_plus_b, parse('c.val')]); - var watch = watchGrp.watch(a_plus_b_plus_c, (v, p, o) => logger(v)); + var watch = watchGrp.watch(a_plus_b_plus_c, (v, p) => logger(v)); // a; a.val; b; b.val; c; c.val; expect(watchGrp.fieldCost).toEqual(6); @@ -275,7 +275,7 @@ main() => describe('WatchGroup', () { var watch = watchGrp.watch( new MethodAST(parse('obj'), 'methodA', [parse('arg0')]), - (v, p, o) => logger(v) + (v, p) => logger(v) ); // obj, arg0; @@ -319,7 +319,7 @@ main() => describe('WatchGroup', () { var watch = watchGrp.watch( new MethodAST(parse('obj'), 'methodA', [parse('arg0')]), - (v, p, o) => logger(v) + (v, p) => logger(v) ); // obj, arg0; @@ -366,7 +366,7 @@ main() => describe('WatchGroup', () { // obj.methodA(arg0) var ast = new MethodAST(parse('obj'), 'methodA', [parse('arg0')]); ast = new MethodAST(ast, 'methodA', [parse('arg1')]); - var watch = watchGrp.watch(ast, (v, p, o) => logger(v)); + var watch = watchGrp.watch(ast, (v, p) => logger(v)); // obj, arg0, arg1; expect(watchGrp.fieldCost).toEqual(3); @@ -406,7 +406,7 @@ main() => describe('WatchGroup', () { it('should read connstant', () { // should fire on initial adding expect(watchGrp.fieldCost).toEqual(0); - var watch = watchGrp.watch(new ConstantAST(123), (v, p, o) => logger(v)); + var watch = watchGrp.watch(new ConstantAST(123), (v, p) => logger(v)); expect(watch.expression).toEqual('123'); expect(watchGrp.fieldCost).toEqual(0); watchGrp.detectChanges(); @@ -419,7 +419,7 @@ main() => describe('WatchGroup', () { it('should wrap iterable in ObservableList', () { context['list'] = []; - var watch = watchGrp.watch(new CollectionAST(parse('list')), (v, p, o) => logger(v)); + var watch = watchGrp.watch(new CollectionAST(parse('list')), (v, p) => logger(v)); expect(watchGrp.fieldCost).toEqual(1); expect(watchGrp.collectionCost).toEqual(1); @@ -453,15 +453,15 @@ main() => describe('WatchGroup', () { describe('child group', () { it('should remove all field watches in group and group\'s children', () { - watchGrp.watch(parse('a'), (v, p, o) => logger('0a')); + watchGrp.watch(parse('a'), (v, p) => logger('0a')); var child1a = watchGrp.newGroup(new PrototypeMap(context)); var child1b = watchGrp.newGroup(new PrototypeMap(context)); var child2 = child1a.newGroup(new PrototypeMap(context)); - child1a.watch(parse('a'), (v, p, o) => logger('1a')); - child1b.watch(parse('a'), (v, p, o) => logger('1b')); - watchGrp.watch(parse('a'), (v, p, o) => logger('0A')); - child1a.watch(parse('a'), (v, p, o) => logger('1A')); - child2.watch(parse('a'), (v, p, o) => logger('2A')); + child1a.watch(parse('a'), (v, p) => logger('1a')); + child1b.watch(parse('a'), (v, p) => logger('1b')); + watchGrp.watch(parse('a'), (v, p) => logger('0A')); + child1a.watch(parse('a'), (v, p) => logger('1A')); + child2.watch(parse('a'), (v, p) => logger('2A')); // flush initial reaction functions expect(watchGrp.detectChanges()).toEqual(6); @@ -487,21 +487,21 @@ main() => describe('WatchGroup', () { it('should remove all method watches in group and group\'s children', () { context['my'] = new MyClass(logger); AST countMethod = new MethodAST(parse('my'), 'count', []); - watchGrp.watch(countMethod, (v, p, o) => logger('0a')); + watchGrp.watch(countMethod, (v, p) => logger('0a')); expectOrder(['0a']); var child1a = watchGrp.newGroup(new PrototypeMap(context)); var child1b = watchGrp.newGroup(new PrototypeMap(context)); var child2 = child1a.newGroup(new PrototypeMap(context)); - child1a.watch(countMethod, (v, p, o) => logger('1a')); + child1a.watch(countMethod, (v, p) => logger('1a')); expectOrder(['0a', '1a']); - child1b.watch(countMethod, (v, p, o) => logger('1b')); + child1b.watch(countMethod, (v, p) => logger('1b')); expectOrder(['0a', '1a', '1b']); - watchGrp.watch(countMethod, (v, p, o) => logger('0A')); + watchGrp.watch(countMethod, (v, p) => logger('0A')); expectOrder(['0a', '0A', '1a', '1b']); - child1a.watch(countMethod, (v, p, o) => logger('1A')); + child1a.watch(countMethod, (v, p) => logger('1A')); expectOrder(['0a', '0A', '1a', '1A', '1b']); - child2.watch(countMethod, (v, p, o) => logger('2A')); + child2.watch(countMethod, (v, p) => logger('2A')); expectOrder(['0a', '0A', '1a', '1A', '2A', '1b']); // flush initial reaction functions @@ -516,9 +516,9 @@ main() => describe('WatchGroup', () { it('should add watches within its own group', () { context['my'] = new MyClass(logger); AST countMethod = new MethodAST(parse('my'), 'count', []); - var ra = watchGrp.watch(countMethod, (v, p, o) => logger('a')); + var ra = watchGrp.watch(countMethod, (v, p) => logger('a')); var child = watchGrp.newGroup(new PrototypeMap(context)); - var cb = child.watch(countMethod, (v, p, o) => logger('b')); + var cb = child.watch(countMethod, (v, p) => logger('b')); expectOrder(['a', 'b']); expectOrder(['a', 'b']); @@ -530,8 +530,8 @@ main() => describe('WatchGroup', () { expectOrder([]); // TODO: add them back in wrong order, assert events in right order - cb = child.watch(countMethod, (v, p, o) => logger('b')); - ra = watchGrp.watch(countMethod, (v, p, o) => logger('a'));; + cb = child.watch(countMethod, (v, p) => logger('b')); + ra = watchGrp.watch(countMethod, (v, p) => logger('a'));; expectOrder(['a', 'b']); }); }); diff --git a/test/core/scope2_spec.dart b/test/core/scope2_spec.dart new file mode 100644 index 000000000..bd4dbe4f0 --- /dev/null +++ b/test/core/scope2_spec.dart @@ -0,0 +1,1065 @@ +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(); + } +} From e61bc6343e8a8ae00a15aceea281ec6318ca8505 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Mon, 3 Feb 2014 16:08:53 -0800 Subject: [PATCH 03/35] 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 | 1476 +++++------- 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 | 78 +- 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_events.dart | 2 +- lib/directive/ng_form.dart | 2 +- lib/directive/ng_if.dart | 20 +- lib/directive/ng_include.dart | 4 +- lib/directive/ng_model.dart | 32 +- lib/directive/ng_pluralize.dart | 4 +- lib/directive/ng_repeat.dart | 82 +- 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 | 1 + 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 | 1988 +++++++---------- test/core/templateurl_spec.dart | 8 +- test/core_dom/block_spec.dart | 4 +- test/core_dom/compiler_spec.dart | 238 +- 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 | 70 +- test/directive/ng_if_spec.dart | 56 +- test/directive/ng_include_spec.dart | 14 +- test/directive/ng_model_spec.dart | 186 +- test/directive/ng_model_validators_spec.dart | 158 +- 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 | 8 +- 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 +- 76 files changed, 3751 insertions(+), 5504 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 814d096a7..c3b82199e 100644 --- a/lib/core/interpolate.dart +++ b/lib/core/interpolate.dart @@ -8,12 +8,13 @@ int _endSymbolLength = _endSymbol.length; 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 6d1d004c6..8943f3b8b 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). */ - 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,700 @@ 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; +//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 direct parent scope that created this scope (this can also be the $rootScope) - */ - final Scope $parent; + Map _scope; + Map _locals; - /** - * The auto-incremented ID of the scope - */ - String $id; + ScopeLocals(this._scope, this._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; - } + operator []=(String name, value) => _scope[name] = value; + operator [](String name) => (_locals.containsKey(name) ? _locals : _scope)[name]; - 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(); - } + 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); +} - 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; +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(); } - _set$Properties(); } - _autoDigestOnTurnDone() { - if ($root._skipAutoDigest) { - $root._skipAutoDigest = false; - } else { - $digest(); + 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(); } } - _identical(a, b) => - identical(a, b) || - (a is String && b is String && a == b) || - (a is num && b is num && a.isNaN && b.isNaN); - containsKey(String name) { - for (var scope = this; scope != null; scope = scope.$parent) { - if (scope._properties.containsKey(name)) { - return true; - } else if(scope._isolate) { - break; - } - } - return false; - } + 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); - 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; + 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; } - 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); - } - } - } + 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; - /** - * 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); + watchGroup.remove(); + observeGroup.remove(); + _Streams.destroy(this); - /** - * *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; + _parentScope = null; + broadcast(ScopeEvent.DESTROY); + } +} - /** - * 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(); - // 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); - } +class RootScope extends Scope { + static final STATE_APPLY = 'apply'; + static final STATE_DIGEST = 'digest'; + static final STATE_FLUSH = 'digest'; - /** - * 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](); - } - }; - } + final ExceptionHandler _exceptionHandler; + final Parser _parser; + final ScopeDigestTTL _ttl; + final ExpressionVisitor visitor = new ExpressionVisitor(); // TODO(misko): delete me + final NgZone _zone; - /** - * 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); - } + _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(); }; + } - Function removeMissingKeys = (k) => oldValue.remove(k); - - var $watchCollectionWatch; + RootScope get rootScope => this; - 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++; + 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; } - return changeDetected; - }; - } else { - $watchCollectionWatch = (_) { - newValue = objGetter(this, _filters); - if (newValue is! Map && newValue is! List) { - 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++; - } - - 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(Function 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(Function 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(Function 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(this.fn); +} +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 8856e37ca..36891b94b 100644 --- a/lib/core_dom/block_factory.dart +++ b/lib/core_dom/block_factory.dart @@ -119,7 +119,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) { @@ -131,7 +131,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) { @@ -140,7 +141,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)); } }); } @@ -193,23 +195,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); } 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 { @@ -302,7 +306,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 012023c00..eeed40cd7 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 081bbfa92..aaf7bb9f7 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,68 +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) { - attrs.observe(attrName, (value) => dstPathFn.assign(dst, value)); + mappingFn = (NodeAttrs attrs, Scope scope, Object controller) { + attrs.observe(attrName, (value) => dstPathFn.assign(controller, value)); }; break; case '<=>': - mappingFn = (NodeAttrs attrs, Scope scope, Object dst) { + mappingFn = (NodeAttrs attrs, Scope scope, Object controller) { if (attrs[attrName] == null) return; - Expression attrExprFn = _parser(attrs[attrName]); - var shadowValue = null; - scope.$watch( - () => attrExprFn.eval(scope), - (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( + expression, + (inboundValue, _) { + if (!blockInbound) { + blockOutbound = true; + scope.rootScope.runAsync(() => blockOutbound = false); + return dstPathFn.assign(controller, inboundValue); + } + } + ); + if (expressionFn.isAssignable) { + scope.watch( + _astParser(dstExpression, context: controller), + (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) { + mappingFn = (NodeAttrs attrs, Scope scope, Object controller) { if (attrs[attrName] == null) return; Expression attrExprFn = _parser(attrs[attrName]); var shadowValue = null; - scope.$watch( - () => attrExprFn.eval(scope), - (v) => dstPathFn.assign(dst, shadowValue = v), - attrs[attrName]); + scope.watch(attrs[attrName], + (v, _) => dstPathFn.assign(controller, shadowValue = v)); }; break; case '=>!': - mappingFn = (NodeAttrs attrs, Scope scope, Object dst) { + mappingFn = (NodeAttrs attrs, Scope scope, Object controller) { if (attrs[attrName] == null) return; Expression attrExprFn = _parser(attrs[attrName]); - var stopWatching; - stopWatching = scope.$watch( - () => attrExprFn.eval(scope), - (value) { - if (dstPathFn.assign(dst, value) != null) { - stopWatching(); + var watch; + watch = scope.watch( + attrs[attrName], + (value, _) { + if (dstPathFn.assign(controller, value) != null) { + watch.remove(); } - }, - attrs[attrName]); + }); }; break; case '&': mappingFn = (NodeAttrs attrs, Scope scope, Object dst) { - 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 4127fec40..621174959 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); + }); }); } } @@ -204,8 +216,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 0e6004392..5bb302a84 100644 --- a/lib/directive/module.dart +++ b/lib/directive/module.dart @@ -8,6 +8,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_events.dart b/lib/directive/ng_events.dart index 402d6422e..08e9f516c 100644 --- a/lib/directive/ng_events.dart +++ b/lib/directive/ng_events.dart @@ -152,7 +152,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 a9651e4f4..06622e6cd 100644 --- a/lib/directive/ng_form.dart +++ b/lib/directive/ng_form.dart @@ -62,7 +62,7 @@ class NgForm extends NgControl implements NgDetachAware, Map { get name => _name; set name(name) { _name = name; - _scope[name] = this; + _scope.context[name] = this; } /** 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 2eb46f672..ebd97d477 100644 --- a/lib/directive/ng_model.dart +++ b/lib/directive/ng_model.dart @@ -15,6 +15,7 @@ class NgModel extends NgControl { final NgForm _form; final dom.Element _element; final Scope _scope; + final AstParser _parser; BoundGetter getter = ([_]) => null; BoundSetter setter = (_, [__]) => null; @@ -25,14 +26,14 @@ class NgModel extends NgControl { final List<_NgModelValidator> _validators = new List<_NgModelValidator>(); final Map errors = new Map(); - Function _removeWatch = () => null; + Watch _removeWatch; bool _watchCollection; Function render = (value) => null; - NgModel(this._scope, NodeAttrs attrs, [dom.Element this._element, - NgForm this._form]) { - _exp = 'ng-model=${attrs["ng-model"]}'; + NgModel(this._scope, NodeAttrs attrs, dom.Element this._element, + NgForm this._form, this._parser) { + _exp = attrs["ng-model"]; watchCollection = false; _form.addControl(this); @@ -48,18 +49,27 @@ class NgModel extends NgControl { _form.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) => render(value), _exp); - } else { - _removeWatch = _scope.$watch((s) => getter(), (value) => render(value), _exp); + _removeWatch = _scope.watch( + _parser(_exp, collection: true), + (changeRecord, _) { + var value = changeRecord is CollectionChangeRecord ? changeRecord.iterable: changeRecord; + _scope.rootScope.domWrite(() => render(value)); + }); + } else if (_exp != null) { + _removeWatch = _scope.watch(_exp, (value, _) { + _scope.rootScope.domWrite(() => render(value)); + }); } } + // TODO(misko): getters/setters need to go. We need AST here. @NgCallback('ng-model') set model(BoundExpression boundExpression) { getter = boundExpression; @@ -169,7 +179,7 @@ class InputCheckboxDirective { inputElement.checked = value == null ? false : toBool(value); }; inputElement.onChange.listen((value) { - scope.$apply(() => ngModel.viewValue = inputElement.checked); + scope.apply(() => ngModel.viewValue = inputElement.checked); }); } } @@ -226,7 +236,7 @@ class InputTextLikeDirective { var value = typedValue; if (value != ngModel.viewValue) { ngModel.dirty = true; - scope.$apply(() => ngModel.viewValue = value); + scope.apply(() => ngModel.viewValue = value); } ngModel.validate(); } @@ -294,7 +304,7 @@ class InputRadioDirective { }; radioButtonElement.onClick.listen((_) { if (radioButtonElement.checked) { - scope.$apply(() => ngModel.viewValue = radioButtonElement.value); + scope.apply(() => ngModel.viewValue = radioButtonElement.value); } }); } diff --git a/lib/directive/ng_pluralize.dart b/lib/directive/ng_pluralize.dart index 1978ca922..375bd661c 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); 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 9fb37fc9d..e5535d1ea 100644 --- a/lib/directive/ng_repeat.dart +++ b/lib/directive/ng_repeat.dart @@ -83,9 +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 _shalow => false; + AstParser astParser) + : super(blockHole, boundBlockFactory, scope, parser, astParser); } /** @@ -110,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 NgShalowRepeatDirective extends AbstractNgRepeatDirective { NgShalowRepeatDirective(BlockHole blockHole, BoundBlockFactory boundBlockFactory, + Scope scope, Parser parser, - Scope scope) - : super(blockHole, boundBlockFactory, parser, scope); - get _shalow => true; + AstParser astParser) + : super(blockHole, boundBlockFactory, scope, parser, astParser) + { + print('DEPRICATED: [ng-shallow-repeat] use [ng-repeat]'); + } } abstract class AbstractNgRepeatDirective { @@ -125,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; @@ -134,23 +140,22 @@ abstract class AbstractNgRepeatDirective { String _listExpr; Map _rows = new Map(); Function _trackByIdFn = (key, value, index) => value; - Function _removeWatch = () => null; + Watch _watch = null; Iterable _lastCollection; - AbstractNgRepeatDirective(this._blockHole, this._boundBlockFactory, this._parser, this._scope); - - get _shalow; + 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) { @@ -159,7 +164,7 @@ abstract class AbstractNgRepeatDirective { trackByLocals[_valueIdentifier] = value; trackByLocals[r'$index'] = index; trackByLocals[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); @@ -173,8 +178,13 @@ abstract class AbstractNgRepeatDirective { if (_valueIdentifier == null) _valueIdentifier = match.group(1); _keyIdentifier = match.group(2); - _removeWatch = _scope.$watchCollection(_listExpr, _onCollectionChange, - value, _shalow); + _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) { @@ -213,24 +223,19 @@ 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); @@ -242,6 +247,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 { @@ -255,22 +261,18 @@ abstract class AbstractNgRepeatDirective { previousNode = row.endNode; } else { // new item which we don't know about - childScope = _scope.$new(lazy:_shalow); + childScope = _scope.createChild(childContext = new PrototypeMap(_scope.context)); } - if (!identical(childScope[_valueIdentifier], value)) { - childScope[_valueIdentifier] = value; - childScope.$dirty(); - } - childScope[r'$index'] = index; - childScope[r'$first'] = (index == 0); - childScope[r'$last'] = (index == (collection.length - 1)); - childScope[r'$middle'] = !(childScope.$first || childScope.$last); - childScope[r'$odd'] = index & 1 == 1; - childScope[r'$even'] = index & 1 == 0; - if (arrayChange && _shalow) { - childScope.$dirty(); + if (!identical(childScope.context[_valueIdentifier], value)) { + childContext[_valueIdentifier] = value; } + childContext[r'$index'] = index; + var first = childContext[r'$first'] = (index == 0); + var last = childContext[r'$last'] = (index == (length - 1)); + childContext[r'$middle'] = !first && !last; + childContext[r'$odd'] = index & 1 == 1; + childContext[r'$even'] = index & 1 == 0; if (row.startNode == null) { _rows[row.id] = row; 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 04a20246f..2bfacd7b9 100644 --- a/lib/introspection.dart +++ b/lib/introspection.dart @@ -98,10 +98,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 46e20b467..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.join(', ')); + 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..f2366bad2 100644 --- a/lib/mock/test_bed.dart +++ b/lib/mock/test_bed.dart @@ -84,5 +84,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 74a65a81b..3bcfefe81 100644 --- a/lib/routing/ng_view.dart +++ b/lib/routing/ng_view.dart @@ -66,14 +66,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); if (routeProvider != null) { _route = routeProvider.route.newHandle(); @@ -117,7 +122,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)])); @@ -132,7 +137,7 @@ class NgViewDirective implements NgDetachAware, RouteProvider { } _previousBlock.remove(); - _previousScope.$destroy(); + _previousScope.destroy(); _previousBlock = null; _previousScope = null; diff --git a/lib/utils.dart b/lib/utils.dart index dc0be1ca8..8306e71dd 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 e3efcd781..1ee86ade8 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 4f284f0c9..6182c440f 100644 --- a/test/core/scope_spec.dart +++ b/test/core/scope_spec.dart @@ -1,1365 +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'); + it(r'should have preventDefault method and defaultPrevented property', inject((RootScope rootScope) { + var event = grandChild.emit(r'myEvent'); + expect(event.defaultPrevented).toBe(false); - 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"]' - ']'); - })); + 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); - }; + describe('broadcast', () { + describe(r'event propagation', () { + var log, child1, child2, child3, grandChild11, grandChild21, grandChild22, grandChild23, + greatGrandChild211; - $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('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'); - })); + rootScope.digest(); + log = ''; + })); - it(r'should remove first', inject((Scope $rootScope) { - first.$destroy(); - $rootScope.$digest(); - expect(log).toEqual('23'); - })); + it(r'should ignore remove on root', inject((RootScope rootScope) { + rootScope.destroy(); + rootScope.digest(); + expect(log).toEqual('123'); + })); - it(r'should remove middle', inject((Scope $rootScope) { - middle.$destroy(); - $rootScope.$digest(); - expect(log).toEqual('13'); - })); + it(r'should remove first', inject((RootScope rootScope) { + first.destroy(); + rootScope.digest(); + expect(log).toEqual('destroy:first;23'); + })); - it(r'should remove last', inject((Scope $rootScope) { - last.$destroy(); - $rootScope.$digest(); - expect(log).toEqual('12'); - })); - + it(r'should remove middle', inject((RootScope rootScope) { + middle.destroy(); + rootScope.digest(); + expect(log).toEqual('13'); + })); - 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')); - - 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); + })); + beforeEach(inject((RootScope rootScope) { + rootScope.context['log'] = () { log += 'digest;'; return null; }; + log = ''; + rootScope.observe('log()', (v, o) => null); + rootScope.digest(); + log = ''; })); - it('should not trigger if nothing change', inject((Scope $rootScope) { - $rootScope.$digest(); - expect(log).toEqual(['null']); - - $rootScope.$digest(); - expect(log).toEqual(['null']); + 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('should allow deregistration', inject((Scope $rootScope) { - $rootScope.obj = []; - $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); + })); - $rootScope.obj.add('a'); - deregister(); + 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.$digest(); - expect(log).toEqual(['[]']); - })); + rootScope.watch('logger("watch")', (n, v) => null); + rootScope.observe('logger("flush")', (n, v) => null); + // clear watches + rootScope.digest(); + log.clear(); - describe('array', () { - it('should trigger when property changes into array', () { - $rootScope.obj = 'test'; - $rootScope.$digest(); - expect(log).toEqual(['"test"']); + rootScope.flush(); + expect(log).toEqual(['flush', /*assertion*/ 'watch', 'flush']); - $rootScope.obj = []; - $rootScope.$digest(); - expect(log).toEqual(['"test"', '[]']); - }); + 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: '); + })); + }); + }); - it('should not trigger change when object in collection changes', () { - $rootScope.obj = [{}]; - $rootScope.$digest(); - expect(log).toEqual(['[{}]']); - $rootScope.obj[0]['name'] = 'foo'; - $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'); + })); + 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'}); - it('should watch array properties', () { - $rootScope.obj = []; - $rootScope.$digest(); - expect(log).toEqual(['[]']); + scopeLocal['a'] = 'aW'; + scopeLocal['b'] = 'bW'; + scopeLocal['c'] = 'cW'; - $rootScope.obj.add('a'); - $rootScope.$digest(); - expect(log).toEqual(['[]', '["a"]']); + expect(scope.context['a']).toEqual('aW'); + expect(scope.context['b']).toEqual('bW'); + expect(scope.context['c']).toEqual('cW'); - $rootScope.obj[0] = 'b'; - $rootScope.$digest(); - expect(log).toEqual(['[]', '["a"]', '["b"]']); + expect(scopeLocal['a']).toEqual('A'); + expect(scopeLocal['b']).toEqual('B'); + expect(scopeLocal['c']).toEqual('cW'); + })); + }); - $rootScope.obj.add([]); - $rootScope.obj.add({}); - log = []; - $rootScope.$digest(); - expect(log).toEqual(['["b",[],{}]']); - var temp = $rootScope.obj[1]; - $rootScope.obj[1] = $rootScope.obj[2]; - $rootScope.obj[2] = temp; - $rootScope.$digest(); - expect(log).toEqual([ '["b",[],{}]', '["b",{},[]]' ]); + describe(r'watch/digest', () { + it(r'should watch and fire on simple property change', inject((RootScope rootScope) { + var log; - $rootScope.obj.removeAt(0); - log = []; - $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]); + })); - it('should watch iterable properties', () { - $rootScope.obj = _toJsonableIterable([]); - $rootScope.$digest(); - expect(log).toEqual(['[]']); + it(r'should watch and fire on expression change', inject((RootScope rootScope) { + var log; - $rootScope.obj = _toJsonableIterable(['a']); - $rootScope.$digest(); - expect(log).toEqual(['[]', '["a"]']); + 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(['b']); - $rootScope.$digest(); - expect(log).toEqual(['[]', '["a"]', '["b"]']); - $rootScope.obj = _toJsonableIterable(['b', [], {}]); - log = []; - $rootScope.$digest(); - expect(log).toEqual(['["b",[],{}]']); + 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 6cc6b60bf..2bcb8fb62 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'); })); }); @@ -191,13 +188,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); @@ -205,12 +201,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(() => @@ -230,7 +226,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)); @@ -244,44 +240,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'
'); @@ -289,30 +290,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(); @@ -323,35 +324,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(); @@ -375,12 +376,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); @@ -420,7 +421,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); @@ -437,25 +438,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'); }))); @@ -487,7 +496,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(); @@ -495,12 +504,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'); }))); }); @@ -508,10 +517,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'); })); }); @@ -559,8 +568,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'); @@ -604,11 +613,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'; } } @@ -616,16 +625,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'; } } @@ -650,7 +660,7 @@ class IoControllerComponent { var onOptional; IoControllerComponent(Scope scope) { this.scope = scope; - scope.$root.ioComponent = this; + scope.rootScope.context['ioComponent'] = this; } } @@ -673,7 +683,7 @@ class UnpublishedIoControllerComponent { var onOptional; UnpublishedIoControllerComponent(Scope scope) { this.scope = scope; - scope.$root.ioComponent = this; + scope.rootScope.context['ioComponent'] = this; } } @@ -692,13 +702,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; } } @@ -706,7 +716,7 @@ class CamelCaseMapComponent { selector: 'parent-expression', template: '
inside {{fromParent()}}
', map: const { - 'from-parent': '&scope.fromParent', + 'from-parent': '&scope.context.fromParent', } ) class ParentExpressionComponent { @@ -766,7 +776,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); } } @@ -777,7 +787,7 @@ class AttachDetachComponent implements NgAttachAware, NgDetachAware, NgShadowRoo ) class MyController { MyController(Scope scope) { - scope.name = 'MyController'; + scope.context['name'] = 'MyController'; } } diff --git a/test/core_dom/http_spec.dart b/test/core_dom/http_spec.dart index 7812d5d37..4b198f46b 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 0686d8550..9b0a59ae1 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-show'); - _.rootScope.$apply(() { - _.rootScope['isVisible'] = true; + _.rootScope.apply(() { + _.rootScope.context['isVisible'] = true; }); expect(element).toHaveClass('ng-show'); - _.rootScope.$apply(() { - _.rootScope['isVisible'] = false; + _.rootScope.apply(() { + _.rootScope.context['isVisible'] = false; }); expect(element).not.toHaveClass('ng-show'); }); @@ -108,14 +104,14 @@ main() { expect(element).not.toHaveClass('active'); expect(element).not.toHaveClass('ng-show'); - _.rootScope.$apply(() { - _.rootScope['currentCls'] = 'active'; + _.rootScope.apply(() { + _.rootScope.context['currentCls'] = 'active'; }); expect(element).toHaveClass('active'); expect(element).not.toHaveClass('ng-show'); - _.rootScope.$apply(() { - _.rootScope['isVisible'] = true; + _.rootScope.apply(() { + _.rootScope.context['isVisible'] = true; }); expect(element).toHaveClass('active'); expect(element).toHaveClass('ng-show'); diff --git a/test/directive/input_select_spec.dart b/test/directive/input_select_spec.dart index b69a91eef..4f5dd6af4 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 f8d8da0b7..6e8da58dc 100644 --- a/test/directive/ng_form_spec.dart +++ b/test/directive/ng_form_spec.dart @@ -11,14 +11,14 @@ describe('form', () { it('should set the name of the form and attach it to the scope', inject((Scope scope) { 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'); })); @@ -27,9 +27,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); })); @@ -38,9 +38,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); @@ -61,9 +61,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); @@ -86,9 +86,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']; @@ -116,9 +116,9 @@ describe('form', () { ''); _.compile(element); - scope.$apply(); + scope.apply(); - var form = scope['myForm']; + var form = scope.context['myForm']; NgModel one = form['one']; form.setValidity(one, "validation error", false); @@ -141,9 +141,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']; @@ -172,11 +172,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); @@ -213,11 +213,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'); })); @@ -228,16 +228,16 @@ 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) { - Scope childScope = scope.$new(); + Scope childScope = scope.createChild({}); var element = $('
              ' + ' ' + ' ' + @@ -245,14 +245,14 @@ describe('form', () { '
              '); _.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(); @@ -265,7 +265,7 @@ describe('form', () { var element = $('
              '); _.compile(element); - scope.$apply(); + scope.apply(); Event submissionEvent = new Event.eventType('CustomEvent', 'submit'); @@ -284,7 +284,7 @@ describe('form', () { var element = $('
              '); _.compile(element); - scope.$apply(); + scope.apply(); Event submissionEvent = new Event.eventType('CustomEvent', 'submit'); @@ -297,14 +297,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); })); }); }); 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 ff1bd27e9..e8e2f39a8 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(InputTextLikeDirective); 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(() { var scope = _.rootScope; var element = new dom.InputElement(); - var model = new NgModel(scope, new NodeAttrs(new DivElement()), element, new NgNullForm()); + NodeAttrs nodeAttrs = new NodeAttrs(new DivElement()); + nodeAttrs['ng-model'] = 'model'; + var model = new NgModel(scope, nodeAttrs, element, new NgNullForm(), null); dom.querySelector('body').append(element); var input = new InputTextLikeDirective(element, model, scope); @@ -112,45 +114,45 @@ 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(() { var scope = _.rootScope; var element = new dom.InputElement(); - var model = new NgModel(scope, new NodeAttrs(new DivElement()), element, new NgNullForm()); + var model = new NgModel(scope, new NodeAttrs(new DivElement()), element, new NgNullForm(), null); dom.querySelector('body').append(element); var input = new InputTextLikeDirective(element, model, scope); @@ -175,44 +177,44 @@ 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(() { var scope = _.rootScope; var element = new dom.InputElement(); - var model = new NgModel(scope, new NodeAttrs(new DivElement()), element, new NgNullForm()); + var model = new NgModel(scope, new NodeAttrs(new DivElement()), element, new NgNullForm(), null); dom.querySelector('body').append(element); var input = new InputTextLikeDirective(element, model, scope); @@ -239,50 +241,50 @@ 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(() { var scope = _.rootScope; var element = new dom.InputElement(); - var model = new NgModel(scope, new NodeAttrs(new DivElement()), element, new NgNullForm()); + var model = new NgModel(scope, new NodeAttrs(new DivElement()), element, new NgNullForm(), null); dom.querySelector('body').append(element); var input = new InputTextLikeDirective(element, model, scope); @@ -308,13 +310,13 @@ 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); })); @@ -323,18 +325,18 @@ describe('ng-model', () { 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); })); @@ -345,49 +347,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(' _ = 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,25 +43,25 @@ 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); 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(); @@ -71,25 +71,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(); @@ -99,25 +99,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(); @@ -127,17 +127,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(); @@ -145,16 +145,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(); @@ -164,36 +164,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(); @@ -201,33 +201,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(); @@ -237,25 +237,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(); @@ -263,27 +263,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(); @@ -293,25 +293,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(); @@ -319,27 +319,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 88d492b69..ebb10f691 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 c37c3b0c1..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('shalow', () { - 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 2cdd0db51..1ae8be80b 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'); }); 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!'); })); From a92efe97947b09cd2379a859c83327aaac9002c6 Mon Sep 17 00:00:00 2001 From: Victor Date: Mon, 10 Feb 2014 21:40:11 +0100 Subject: [PATCH 04/35] fix(change detection): Fix for comparing string by value --- .../dirty_checking_change_detector.dart | 8 ++++---- .../dirty_checking_change_detector_spec.dart | 10 ++++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/change_detection/dirty_checking_change_detector.dart b/lib/change_detection/dirty_checking_change_detector.dart index 6f67b6154..1b431c9bb 100644 --- a/lib/change_detection/dirty_checking_change_detector.dart +++ b/lib/change_detection/dirty_checking_change_detector.dart @@ -488,7 +488,7 @@ class _MapChangeRecord implements MapChangeRecord { _truncate(lastOldSeqRecord, oldSeqRecord); return isDirty; } - + void _reset() { var record = _changesHead; while (record != null) { @@ -577,7 +577,7 @@ class _MapChangeRecord implements MapChangeRecord { _removalsTail = record; } } - + void _removeFromSeq(KeyValueRecord prev, KeyValueRecord record) { KeyValueRecord next = record._nextKeyValue; if (prev == null) _mapHead = next; else prev._nextKeyValue = next; @@ -678,7 +678,7 @@ class _CollectionChangeRecord implements CollectionChangeRecord { } void forEachMove(void f(MovedItem change)) { - ItemRecord record = _changesHead; + ItemRecord record = _movesHead; while(record != null) { f(record); record = record._nextMovedRec; @@ -782,7 +782,7 @@ class _CollectionChangeRecord implements CollectionChangeRecord { mismatch(ItemRecord record, dynamic item, int index) { // Guard against bogus String changes if (record != null && item is String && record.item is String && - record == item) { + record.item == item) { // this is false change in strings we need to recover, and pretend it is // the same. We save the value so that next time identity will pass return record..item = item; diff --git a/test/change_detection/dirty_checking_change_detector_spec.dart b/test/change_detection/dirty_checking_change_detector_spec.dart index d6da96cac..72a398a46 100644 --- a/test/change_detection/dirty_checking_change_detector_spec.dart +++ b/test/change_detection/dirty_checking_change_detector_spec.dart @@ -242,6 +242,16 @@ main() => describe('DirtyCheckingChangeDetector', () { removals: [])); }); + it('should test string by value rather than by reference', () { + var list = ['a', 'boo']; + var record = detector.watch(list, null, 'handler'); + detector.collectChanges(); + + list[1] = 'b' + 'oo'; + + expect(detector.collectChanges()).toEqual(null); + }); + it('should remove and add same item', () { var list = ['a', 'b', 'c']; var record = detector.watch(list, null, 'handler'); From 03755a8a7db3f30ba866d0d41ea3b00fe0af7f0b Mon Sep 17 00:00:00 2001 From: Victor Date: Thu, 30 Jan 2014 13:44:12 +0100 Subject: [PATCH 05/35] refactor: minor code cleanup / fixes --- lib/change_detection/ast.dart | 40 +-- lib/change_detection/change_detection.dart | 3 +- .../dirty_checking_change_detector.dart | 74 +++--- lib/change_detection/linked_list.dart | 23 +- lib/change_detection/watch_group.dart | 55 ++-- lib/core/scope.dart | 204 ++++++++------- lib/introspection.dart | 47 ++-- perf/watch_group_perf.dart | 240 +++++++++--------- 8 files changed, 363 insertions(+), 323 deletions(-) diff --git a/lib/change_detection/ast.dart b/lib/change_detection/ast.dart index e6b9d970b..088884389 100644 --- a/lib/change_detection/ast.dart +++ b/lib/change_detection/ast.dart @@ -10,7 +10,9 @@ abstract class AST { static final String _CONTEXT = '#'; final String expression; AST(expression) - : expression = expression.startsWith('#.') ? expression.substring(2) : expression + : expression = expression.startsWith('#.') + ? expression.substring(2) + : expression { assert(expression!=null); } @@ -25,8 +27,8 @@ abstract class AST { */ class ContextReferenceAST extends AST { ContextReferenceAST(): super(AST._CONTEXT); - WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) - => new _ConstantWatchRecord(watchGroup, expression, watchGroup.context); + WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) => + new _ConstantWatchRecord(watchGroup, expression, watchGroup.context); } /** @@ -37,14 +39,14 @@ class ContextReferenceAST extends AST { class ConstantAST extends AST { final constant; - ConstantAST(dynamic constant, [String expression]): - super(expression == null - ? (constant is String ? '"$constant"' : '$constant') - : expression), - constant = constant; + ConstantAST(constant, [String expression]) + : constant = constant, + super(expression == null + ? constant is String ? '"$constant"' : '$constant' + : expression); - WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) - => new _ConstantWatchRecord(watchGroup, expression, constant); + WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) => + new _ConstantWatchRecord(watchGroup, expression, constant); } /** @@ -62,8 +64,7 @@ class FieldReadAST extends AST { name = name; WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) => - watchGroup.addFieldWatch(lhs, name, expression); - + watchGroup.addFieldWatch(lhs, name, expression); } /** @@ -83,7 +84,7 @@ class PureFunctionAST extends AST { name = name; WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) => - watchGroup.addFunctionWatch(fn, argsAST, expression); + watchGroup.addFunctionWatch(fn, argsAST, expression); } /** @@ -103,7 +104,7 @@ class MethodAST extends AST { argsAST = argsAST; WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) => - watchGroup.addMethodWatch(lhsAST, name, argsAST, expression); + watchGroup.addMethodWatch(lhsAST, name, argsAST, expression); } @@ -113,9 +114,8 @@ class CollectionAST extends AST { : super('#collection($valueAST)'), valueAST = valueAST; - WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) { - return watchGroup.addCollectionWatch(valueAST); - } + WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) => + watchGroup.addCollectionWatch(valueAST); } _argList(List items) => items.join(', '); @@ -130,9 +130,9 @@ class _ConstantWatchRecord extends WatchRecord<_Handler> { final currentValue; final _Handler handler; - _ConstantWatchRecord(WatchGroup watchGroup, String expression, dynamic currentValue): - currentValue = currentValue, - handler = new _ConstantHandler(watchGroup, expression, currentValue); + _ConstantWatchRecord(WatchGroup watchGroup, String expression, currentValue) + : currentValue = currentValue, + handler = new _ConstantHandler(watchGroup, expression, currentValue); ChangeRecord<_Handler> check() => null; void remove() => null; diff --git a/lib/change_detection/change_detection.dart b/lib/change_detection/change_detection.dart index 6acc152fd..34052671b 100644 --- a/lib/change_detection/change_detection.dart +++ b/lib/change_detection/change_detection.dart @@ -4,7 +4,7 @@ typedef EvalExceptionHandler(error, stack); /** * An interface for [ChangeDetectorGroup] groups related watches together. It - * guarentees that within the group all watches will be reported in the order in + * guarantees that within the group all watches will be reported in the order in * which they were registered. It also provides an efficient way of removing the * watch group. */ @@ -27,7 +27,6 @@ abstract class ChangeDetectorGroup { */ WatchRecord watch(Object object, String field, H handler); - /** Use to remove all watches in the group in an efficient manner. */ void remove(); diff --git a/lib/change_detection/dirty_checking_change_detector.dart b/lib/change_detection/dirty_checking_change_detector.dart index 1b431c9bb..a7c13ebcc 100644 --- a/lib/change_detection/dirty_checking_change_detector.dart +++ b/lib/change_detection/dirty_checking_change_detector.dart @@ -7,7 +7,7 @@ import 'package:angular/change_detection/change_detection.dart'; typedef FieldGetter(object); class GetterCache { - Map _map; + final Map _map; GetterCache(this._map); @@ -146,7 +146,7 @@ class DirtyCheckingChangeDetectorGroup implements ChangeDetectorGroup { } } - _recordAdd(DirtyCheckingRecord record) { + DirtyCheckingRecord _recordAdd(DirtyCheckingRecord record) { DirtyCheckingRecord previous = _tail; DirtyCheckingRecord next = previous == null ? null : previous._nextWatch; @@ -163,7 +163,7 @@ class DirtyCheckingChangeDetectorGroup implements ChangeDetectorGroup { return record; } - _recordRemove(DirtyCheckingRecord record) { + void _recordRemove(DirtyCheckingRecord record) { DirtyCheckingRecord previous = record._prevWatch; DirtyCheckingRecord next = record._nextWatch; @@ -182,7 +182,7 @@ class DirtyCheckingChangeDetectorGroup implements ChangeDetectorGroup { } } - toString() { + String toString() { var lines = []; if (_parent == null) { var allRecords = []; @@ -243,7 +243,7 @@ class DirtyCheckingChangeDetector extends DirtyCheckingChangeDetectorGroup return changeHead; } - remove() { + void remove() { throw new StateError('Root ChangeDetector can not be removed'); } } @@ -307,7 +307,7 @@ class DirtyCheckingRecord implements ChangeRecord, WatchRecord { * reflection. If [Map] then it sets up map accessor. */ set object(obj) { - this._object = obj; + _object = obj; if (obj == null) { _mode = _MODE_IDENTITY_; } else if (field == null) { @@ -645,9 +645,9 @@ class KeyValueRecord implements KeyValue, AddedKeyValue, Remov RemovedKeyValue get nextRemovedKeyValue => _nextRemovedKeyValue; ChangedKeyValue get nextChangedKeyValue => _nextChangedKeyValue; - toString() { - return _previousValue == _currentValue ? key : '$key[$_previousValue -> $_currentValue]'; - } + String toString() => _previousValue == _currentValue + ? key + : '$key[$_previousValue -> $_currentValue]'; } @@ -741,7 +741,7 @@ class _CollectionChangeRecord implements CollectionChangeRecord { * previousKey to currentKey, and clear all of the queues (additions, moves, * removals). */ - _reset() { + void _reset() { ItemRecord record; record = _additionsHead; @@ -779,7 +779,7 @@ class _CollectionChangeRecord implements CollectionChangeRecord { * - [item] is the current item in the collection * - [index] is the position of the item in the collection */ - mismatch(ItemRecord record, dynamic item, int index) { + ItemRecord mismatch(ItemRecord record, dynamic item, int index) { // Guard against bogus String changes if (record != null && item is String && record.item is String && record.item == item) { @@ -791,7 +791,8 @@ class _CollectionChangeRecord implements CollectionChangeRecord { // 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. + // Remove the record from the collection since we know it does not match the + // item. if (record != null) _collection_remove(record); // Attempt to see if we have seen the item before. record = _items.get(item, index); @@ -802,7 +803,8 @@ class _CollectionChangeRecord implements CollectionChangeRecord { // Never seen it, check evicted list. record = _removedItems.get(item); if (record != null) { - // It is an item which we have earlier evict it, reinsert it back into the list. + // It is an item which we have earlier evict it, reinsert it back into + // the list. _collection_reinsertAfter(record, prev, index); } else { // It is a new item add it. @@ -838,7 +840,8 @@ class _CollectionChangeRecord implements CollectionChangeRecord { * position. This is incorrect, since a better way to think of it is as insert * of 'b' rather then switch 'a' with 'b' and then add 'a' at the end. */ - verifyReinsertion(ItemRecord record, dynamic item, int index) { + ItemRecord verifyReinsertion(ItemRecord record, dynamic item, + int index) { ItemRecord reinsertRecord = _removedItems.get(item); if (reinsertRecord != null) { record = _collection_reinsertAfter(reinsertRecord, record._prevRec, index); @@ -864,7 +867,8 @@ class _CollectionChangeRecord implements CollectionChangeRecord { _removedItems.clear(); } - ItemRecord _collection_reinsertAfter(ItemRecord record, ItemRecord insertPrev, int index) { + ItemRecord _collection_reinsertAfter(ItemRecord record, ItemRecord insertPrev, + int index) { _removedItems.remove(record); var prev = record._prevRemovedRec; var next = record._nextRemovedRec; @@ -888,14 +892,16 @@ class _CollectionChangeRecord implements CollectionChangeRecord { return record; } - ItemRecord _collection_moveAfter(ItemRecord record, ItemRecord prev, int index) { + ItemRecord _collection_moveAfter(ItemRecord record, ItemRecord prev, + int index) { _collection_unlink(record); _collection_insertAfter(record, prev, index); _moves_add(record); return record; } - ItemRecord _collection_addAfter(ItemRecord record, ItemRecord prev, int index) { + ItemRecord _collection_addAfter(ItemRecord record, ItemRecord prev, + int index) { _collection_insertAfter(record, prev, index); if (_additionsTail == null) { @@ -909,7 +915,8 @@ class _CollectionChangeRecord implements CollectionChangeRecord { return record; } - ItemRecord _collection_insertAfter(ItemRecord record, ItemRecord prev, int index) { + ItemRecord _collection_insertAfter(ItemRecord record, ItemRecord prev, + int index) { assert(record != prev); assert(record._nextRec == null); assert(record._prevRec == null); @@ -990,7 +997,7 @@ class _CollectionChangeRecord implements CollectionChangeRecord { return record; } - toString() { + String toString() { ItemRecord record; var list = []; @@ -1030,7 +1037,8 @@ removals: ${removals.join(", ")}' } } -class ItemRecord implements CollectionItem, AddedItem, MovedItem, RemovedItem { +class ItemRecord implements CollectionItem, AddedItem, + MovedItem, RemovedItem { K previousKey = null; K currentKey = null; V item = _INITIAL_; @@ -1055,7 +1063,7 @@ class ItemRecord implements CollectionItem, AddedItem, MovedIt class _DuplicateItemRecordList { ItemRecord head, tail; - add(ItemRecord record, ItemRecord beforeRecord) { + void add(ItemRecord record, ItemRecord beforeRecord) { assert(record._prevDupRec == null); assert(record._nextDupRec == null); assert(beforeRecord == null ? true : beforeRecord.item == record.item); @@ -1079,11 +1087,11 @@ class _DuplicateItemRecordList { } } - ItemRecord get(dynamic key, int hideIndex) { + ItemRecord get(key, int hideIndex) { ItemRecord record = head; while(record != null) { - if (hideIndex == null ? true : hideIndex < record.currentKey && - identical(record.item, key)) { + if (hideIndex == null || + hideIndex < record.currentKey && identical(record.item, key)) { return record; } record = record._nextDupRec; @@ -1123,7 +1131,8 @@ class _DuplicateItemRecordList { } /** - * This is a custom map which supports duplicate [ItemRecord] values for each key. + * This is a custom map which supports duplicate [ItemRecord] values for each + * key. */ class DuplicateMap { final Map map = @@ -1132,8 +1141,8 @@ class DuplicateMap { void put(ItemRecord record, [ItemRecord beforeRecord = null]) { assert(record._nextDupRec == null); assert(record._prevDupRec == null); - map.putIfAbsent(record.item, () => - new _DuplicateItemRecordList()).add(record, beforeRecord); + map.putIfAbsent(record.item, () => new _DuplicateItemRecordList()) + .add(record, beforeRecord); } /** @@ -1145,20 +1154,17 @@ class DuplicateMap { * then asking if we have any more `a`s needs to return the last `a` not the * first or second. */ - ItemRecord get(dynamic key, [int hideIndex]) { + ItemRecord get(key, [int hideIndex]) { _DuplicateItemRecordList recordList = map[key]; - ItemRecord item = recordList == null ? null : recordList.get(key, hideIndex); - return item; + return recordList == null ? null : recordList.get(key, hideIndex); } ItemRecord remove(ItemRecord record) { _DuplicateItemRecordList recordList = map[record.item]; assert(recordList != null); - if (recordList.remove(record)) { - map.remove(record.item); - } + if (recordList.remove(record)) map.remove(record.item); return record; } - clear() => map.clear(); + void clear() => map.clear(); } diff --git a/lib/change_detection/linked_list.dart b/lib/change_detection/linked_list.dart index 6c7f883ef..1c31c5657 100644 --- a/lib/change_detection/linked_list.dart +++ b/lib/change_detection/linked_list.dart @@ -21,9 +21,9 @@ class _LinkedList { return item; } - static _isEmpty(_Handler list) => list._head == null; + static bool _isEmpty(_Handler list) => list._head == null; - static _remove(_Handler list, _Handler item) { + static void _remove(_Handler list, _Handler item) { var previous = item._previous; var next = item._next; @@ -48,9 +48,9 @@ class _ArgHandlerList { return item; } - static _isEmpty(_InvokeHandler list) => list._argHandlerHead == null; + static bool _isEmpty(_InvokeHandler list) => list._argHandlerHead == null; - static _remove(_InvokeHandler list, _ArgHandler item) { + static void _remove(_InvokeHandler list, _ArgHandler item) { var previous = item._previousArgHandler; var next = item._nextArgHandler; @@ -75,9 +75,9 @@ class _WatchList { return item; } - static _isEmpty(_Handler list) => list._watchHead == null; + static bool _isEmpty(_Handler list) => list._watchHead == null; - static _remove(_Handler list, Watch item) { + static void _remove(_Handler list, Watch item) { var previous = item._previousWatch; var next = item._nextWatch; @@ -106,13 +106,12 @@ abstract class _EvalWatchList { if (prev != null) prev._nextEvalWatch = item; if (next != null) next._previousEvalWatch = item; - list._evalWatchTail = item; - return item; + return list._evalWatchTail = item; } - static _isEmpty(_EvalWatchList list) => list._evalWatchHead == null; + static bool _isEmpty(_EvalWatchList list) => list._evalWatchHead == null; - static _remove(_EvalWatchList list, _EvalWatchRecord item) { + static void _remove(_EvalWatchList list, _EvalWatchRecord item) { assert(item.watchGrp == list); var prev = item._previousEvalWatch; var next = item._nextEvalWatch; @@ -149,9 +148,9 @@ class _WatchGroupList { return item; } - static _isEmpty(_WatchGroupList list) => list._watchGroupHead == null; + static bool _isEmpty(_WatchGroupList list) => list._watchGroupHead == null; - static _remove(_WatchGroupList list, WatchGroup item) { + static void _remove(_WatchGroupList list, WatchGroup item) { var previous = item._previousWatchGroup; var next = item._nextWatchGroup; diff --git a/lib/change_detection/watch_group.dart b/lib/change_detection/watch_group.dart index 3783bcf60..1388b7e7d 100644 --- a/lib/change_detection/watch_group.dart +++ b/lib/change_detection/watch_group.dart @@ -49,6 +49,7 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { /// STATS: Number of field watchers which are in use. int _fieldCost = 0; int _collectionCost = 0; + int _evalCost = 0; /// STATS: Number of field watchers which are in use including child [WatchGroup]s. int get fieldCost => _fieldCost; @@ -76,7 +77,6 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { /// STATS: Number of invocation watchers (closures/methods) which are in use. int get evalCost => _evalCost; - int _evalCost = 0; /// STATS: Number of invocation watchers which are in use including child [WatchGroup]s. int get totalEvalCost { @@ -154,10 +154,11 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { var watchRecord = _changeDetector.watch(null, null, collectionHandler); _collectionCost++; collectionHandler.watchRecord = watchRecord; - WatchRecord<_Handler> astWR = _cache.putIfAbsent(ast.expression, () => ast.setupWatch(this)); + WatchRecord<_Handler> astWR = _cache.putIfAbsent(ast.expression, + () => ast.setupWatch(this)); - // We set a field forwarding handler on LHS. This will allow the change objects to propagate - // to the current WatchRecord. + // We set a field forwarding handler on LHS. This will allow the change + // objects to propagate to the current WatchRecord. astWR.handler.addForwardHandler(collectionHandler); // propagate the value from the LHS to here @@ -223,8 +224,7 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { } WatchGroup get _childWatchGroupTail { - WatchGroup tail = this; - WatchGroup nextTail; + WatchGroup tail = this, nextTail; while ((nextTail = tail._watchGroupTail) != null) { tail = nextTail; } @@ -321,7 +321,7 @@ class RootWatchGroup extends WatchGroup { RootWatchGroup(ChangeDetector changeDetector, Object context): super._root(changeDetector, context); - get _rootGroup => this; + WatchGroup get _rootGroup => this; /** * Detect changes and process the [ReactionFn]s. @@ -331,8 +331,8 @@ class RootWatchGroup extends WatchGroup { * 2) process function/closure/method changes * 3) call an [ReactionFn]s * - * Each step is called in sequence. ([ReactionFn]s are not called until all previous steps are - * completed). + * Each step is called in sequence. ([ReactionFn]s are not called until all + * previous steps are completed). */ int detectChanges({EvalExceptionHandler exceptionHandler, ChangeLog changeLog}) { // Process the ChangeRecords from the change detector @@ -414,12 +414,12 @@ class Watch { get expression => _record.handler.expression; - invoke() { + void invoke() { _dirty = false; reactionFn(_record.currentValue, _record.previousValue); } - remove() { + void remove() { if (_deleted) throw new StateError('Already deleted!'); _deleted = true; var handler = _record.handler; @@ -491,11 +491,11 @@ abstract class _Handler implements _LinkedList, _LinkedListItem, _WatchList { } } - _releaseWatch() { + void _releaseWatch() { watchRecord.remove(); watchGrp._fieldCost--; } - acceptValue(dynamic object) => null; + acceptValue(object) => null; void onChange(ChangeRecord<_Handler> record) { assert(_next != this); // verify we are not detached @@ -531,7 +531,7 @@ class _FieldHandler extends _Handler { * This function forwards the watched object to the next [_Handler] * synchronously. */ - acceptValue(dynamic object) { + void acceptValue(object) { watchRecord.object = object; var changeRecord = watchRecord.check(); if (changeRecord != null) onChange(changeRecord); @@ -539,16 +539,18 @@ class _FieldHandler extends _Handler { } class _CollectionHandler extends _Handler { - _CollectionHandler(watchGrp, expression): super(watchGrp, expression); + _CollectionHandler(WatchGroup watchGrp, String expression) + : super(watchGrp, expression); /** * This function forwards the watched object to the next [_Handler] synchronously. */ - acceptValue(dynamic object) { + void acceptValue(object) { watchRecord.object = object; var changeRecord = watchRecord.check(); if (changeRecord != null) onChange(changeRecord); } - _releaseWatch() { + + void _releaseWatch() { watchRecord.remove(); watchGrp._collectionCost--; } @@ -566,7 +568,7 @@ class _ArgHandler extends _Handler { _ArgHandler(WatchGroup watchGrp, this.watchRecord, int index) : super(watchGrp, 'arg[$index]'), index = index; - acceptValue(dynamic object) { + void acceptValue(object) { watchRecord.dirtyArgs = true; watchRecord.args[index] = object; } @@ -575,19 +577,22 @@ class _ArgHandler extends _Handler { class _InvokeHandler extends _Handler implements _ArgHandlerList { _ArgHandler _argHandlerHead, _argHandlerTail; - _InvokeHandler(watchGrp, expression): super(watchGrp, expression); + _InvokeHandler(WatchGroup watchGrp, String expression) + : super(watchGrp, expression); - acceptValue(dynamic object) { - return watchRecord.object = object; + void acceptValue(object) { + watchRecord.object = object; } - onChange(ChangeRecord<_Handler> record) { + void onChange(ChangeRecord<_Handler> record) { super.onChange(record); } - _releaseWatch() => (watchRecord as _EvalWatchRecord).remove(); + void _releaseWatch() { + (watchRecord as _EvalWatchRecord).remove(); + } - release() { + void release() { super.release(); _ArgHandler current = _argHandlerHead; while(current != null) { @@ -725,7 +730,7 @@ class _EvalWatchRecord implements WatchRecord<_Handler>, ChangeRecord<_Handler> get nextChange => null; - remove() { + void remove() { assert(mode != _MODE_DELETED_); assert((mode = _MODE_DELETED_) == _MODE_DELETED_); // Mark as deleted. watchGrp._evalCost--; diff --git a/lib/core/scope.dart b/lib/core/scope.dart index 8943f3b8b..b72c8af1d 100644 --- a/lib/core/scope.dart +++ b/lib/core/scope.dart @@ -5,16 +5,16 @@ NOT_IMPLEMENTED() { } typedef EvalFunction0(); -typedef EvalFunction1(dynamic context); +typedef EvalFunction1(context); /** - * Injected into the listener function within [Scope.on] to provide event-specific - * details to the scope listener. + * 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; + final data; /** * The name of the intercepted scope event. @@ -51,14 +51,19 @@ class ScopeEvent { ScopeEvent(this.name, this.targetScope, this.data); /** - * Prevents the intercepted event from propagating further to successive scopes. + * Prevents the intercepted event from propagating further to successive + * scopes. */ - stopPropagation () => _propagationStopped = true; + void stopPropagation () { + _propagationStopped = true; + } /** * Sets the defaultPrevented flag to true. */ - preventDefault() => _defaultPrevented = true; + void preventDefault() { + _defaultPrevented = true; + } } /** @@ -73,12 +78,13 @@ class ScopeEvent { class ScopeDigestTTL { final num ttl; ScopeDigestTTL(): ttl = 5; - ScopeDigestTTL.value(num this.ttl); + ScopeDigestTTL.value(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); + static wrapper(scope, Map locals) => + new ScopeLocals(scope, locals); Map _scope; Map _locals; @@ -104,7 +110,7 @@ class ScopeLocals implements Map { } class Scope { - final dynamic context; + final context; final RootScope rootScope; Scope _parentScope; Scope get parentScope => _parentScope; @@ -118,12 +124,11 @@ class Scope { _Streams _streams; int _nextChildIndex = 0; - Scope(Object this.context, this.rootScope, this._parentScope, - this._depth, this._index, - this.watchGroup, this.observeGroup); + 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. + // A better way to do this is to remove the parser from the scope. Watch watchSet(List exprs, Function reactionFn) { var expr = '{{${exprs.join('}}?{{')}}}'; List items = exprs.map(rootScope._parse).toList(); @@ -161,8 +166,8 @@ class Scope { } } - dynamic applyInZone([expression, Map locals]) - => rootScope._zone.run(() => apply(expression, locals)); + dynamic applyInZone([expression, Map locals]) => + rootScope._zone.run(() => apply(expression, locals)); dynamic apply([expression, Map locals]) { rootScope._transitionState(null, RootScope.STATE_APPLY); @@ -177,17 +182,18 @@ class Scope { } } - 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); + 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 child = new Scope(childContext, rootScope, this, + _depth + 1, _nextChildIndex++, + watchGroup.newGroup(childContext), + observeGroup.newGroup(childContext)); var next = null; var prev = _childTail; child._next = next; @@ -232,9 +238,8 @@ class RootScope extends Scope { String _state; - RootScope(Object context, Parser this._parser, GetterCache cacheGetter, - FilterMap filterMap, ExceptionHandler this._exceptionHandler, - ScopeDigestTTL this._ttl, this._zone) + RootScope(Object context, this._parser, GetterCache cacheGetter, + FilterMap filterMap, this._exceptionHandler, this._ttl, this._zone) : super(context, null, null, 0, 0, new RootWatchGroup(new DirtyCheckingChangeDetector(cacheGetter), context), new RootWatchGroup(new DirtyCheckingChangeDetector(cacheGetter), context)) @@ -250,10 +255,10 @@ class RootScope extends Scope { void digest() { _transitionState(null, STATE_DIGEST); try { - RootWatchGroup rootWatchGroup = (watchGroup as RootWatchGroup); + var rootWatchGroup = (watchGroup as RootWatchGroup); int digestTTL = _ttl.ttl; - const int logCount = 3; + const int LOG_COUNT = 3; List log; List digestLog; var count; @@ -267,10 +272,10 @@ class RootScope extends Scope { digestTTL--; count = rootWatchGroup.detectChanges( - exceptionHandler: _exceptionHandler, - changeLog: changeLog); + exceptionHandler: _exceptionHandler, + changeLog: changeLog); - if (digestTTL <= logCount) { + if (digestTTL <= LOG_COUNT) { if (changeLog == null) { log = []; digestLog = []; @@ -282,7 +287,7 @@ class RootScope extends Scope { } if (digestTTL == 0) { throw 'Model did not stabilize in ${_ttl.ttl} digests. ' + - 'Last $logCount iterations:\n${log.join('\n')}'; + 'Last $LOG_COUNT iterations:\n${log.join('\n')}'; } } while (count > 0); } finally { @@ -292,7 +297,7 @@ class RootScope extends Scope { void flush() { _transitionState(null, STATE_FLUSH); - RootWatchGroup observeGroup = this.observeGroup as RootWatchGroup; + var observeGroup = this.observeGroup as RootWatchGroup; bool runObservers = true; try { do { @@ -358,13 +363,11 @@ class RootScope extends Scope { } - AST _parse(expression) => visitor.visit(_parser.call(expression)); + 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."; - } + if (_state != from) throw "$_state already in progress can not enter $to."; _state = to; } } @@ -426,8 +429,7 @@ class _Streams { _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); + Queue queue = new Queue()..addFirst(scopeStreams._scope); while(queue.isNotEmpty) { scope = queue.removeFirst(); scopeStreams = scope._streams; @@ -450,7 +452,9 @@ class _Streams { return event; } - static ScopeStream on(Scope scope, ExceptionHandler _exceptionHandler, String name) { + 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. @@ -469,8 +473,7 @@ class _Streams { 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) { + while (scope != null && scope._streams == toBeDeletedStreams) { scope._streams = null; scope = scope._parentScope; } @@ -485,7 +488,8 @@ class _Streams { assert(scope._streams == this); assert(scope._streams._scope == scope); assert(_exceptionHandler != null); - return _streams.putIfAbsent(name, () => new ScopeStream(this, _exceptionHandler, name)); + return _streams.putIfAbsent(name, () => + new ScopeStream(this, _exceptionHandler, name)); } void _addCount(String name, int amount) { @@ -522,15 +526,13 @@ class ScopeStream extends async.Stream { { Function onError, void onDone(), bool cancelOnError }) { - if (subscriptions.isEmpty) { - _streams._addCount(_name, 1); - } - ScopeStreamSubscription subscription = new ScopeStreamSubscription(this, onData); + if (subscriptions.isEmpty) _streams._addCount(_name, 1); + var subscription = new ScopeStreamSubscription(this, onData); subscriptions.add(subscription); return subscription; } - _fire(ScopeEvent event) { + void _fire(ScopeEvent event) { for(ScopeStreamSubscription subscription in subscriptions) { try { subscription._onData(event); @@ -540,16 +542,13 @@ class ScopeStream extends async.Stream { } } - _remove(ScopeStreamSubscription subscription) { + void _remove(ScopeStreamSubscription subscription) { assert(subscription._scopeStream == this); if (subscriptions.remove(subscription)) { - if (subscriptions.isEmpty) { - _streams._addCount(_name, -1); - } + if (subscriptions.isEmpty) _streams._addCount(_name, -1); } else { throw new StateError('AlreadyCanceled'); } - return null; } } @@ -558,7 +557,8 @@ class ScopeStreamSubscription implements async.StreamSubscription { final Function _onData; ScopeStreamSubscription(this._scopeStream, this._onData); - async.Future cancel() => _scopeStream._remove(this); + // TODO(vbe) should return a Future + cancel() => _scopeStream._remove(this); void onData(void handleData(ScopeEvent data)) => NOT_IMPLEMENTED(); void onError(Function handleError) => NOT_IMPLEMENTED(); @@ -572,7 +572,7 @@ class ScopeStreamSubscription implements async.StreamSubscription { class _FunctionChain { final Function fn; _FunctionChain _next; - + _FunctionChain(this.fn); } @@ -621,58 +621,88 @@ class ExpressionVisitor implements Visitor { AST visitCollection(Expression exp) => new CollectionAST(visit(exp)); AST _mapToAst(Expression expression) => visit(expression); - List _toAst(List expressions) => expressions.map(_mapToAst).toList(); + 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) { + void visitCallScope(CallScope exp) { + ast = new MethodAST(contextRef, exp.name, _toAst(exp.arguments)); + } + void 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)]); + } + void visitPrefix(Prefix exp) { + ast = new PureFunctionAST(exp.operation, + _operationToFunction(exp.operation), + [visit(exp.expression)]); + } + void visitConditional(Conditional exp) { + ast = new PureFunctionAST('?:', _operation_ternary, + [visit(exp.condition), visit(exp.yes), + visit(exp.no)]); + } + void visitAccessKeyed(AccessKeyed exp) { + ast = new PureFunctionAST('[]', _operation_bracket, + [visit(exp.object), visit(exp.key)]); + } + void visitLiteralPrimitive(LiteralPrimitive exp) { + ast = new ConstantAST(exp.value); + } + void visitLiteralString(LiteralString exp) { + ast = new ConstantAST(exp.value); + } + void visitLiteralArray(LiteralArray exp) { List items = _toAst(exp.elements); ast = new PureFunctionAST('[${items.join(', ')}]', new ArrayFn(), items); } - visitLiteralObject(LiteralObject exp) { + void visitLiteralObject(LiteralObject exp) { List keys = exp.keys; List values = _toAst(exp.values); assert(keys.length == values.length); - List kv = []; + var 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) { + void 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); + 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(';'); + void visitCallFunction(CallFunction exp) { + _notSupported("function's returing functions"); + } + void visitAssign(Assign exp) { + _notSupported('assignement'); + } + void visitLiteral(Literal exp) { + _notSupported('literal'); + } + void visitExpression(Expression exp) { + _notSupported('?'); + } + void visitChain(Chain exp) { + _notSupported(';'); + } - _notSupported(String name) { + void _notSupported(String name) { throw new StateError("Can not watch expression containing '$name'."); } } @@ -712,7 +742,7 @@ _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_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. diff --git a/lib/introspection.dart b/lib/introspection.dart index 2bfacd7b9..9af282267 100644 --- a/lib/introspection.dart +++ b/lib/introspection.dart @@ -1,17 +1,18 @@ part of angular; /** - * A global write only variable which keeps track of objects attached to the elements. - * This is usefull for debugging AngularDart application from the browser's REPL. + * A global write only variable which keeps track of objects attached to the + * elements. This is useful for debugging AngularDart application from the + * browser's REPL. */ Expando _elementExpando = new Expando('element'); /** * Return the closest [ElementProbe] object for a given [Element]. * - * NOTE: This global method is here to make it easier to debug Angular application from - * the browser's REPL, unit or end-to-end tests. The function is not intended to - * be called from Angular application. + * NOTE: This global method is here to make it easier to debug Angular + * application from the browser's REPL, unit or end-to-end tests. The + * function is not intended to be called from Angular application. */ ElementProbe ngProbe(dom.Node node) { while(node != null) { @@ -26,9 +27,9 @@ ElementProbe ngProbe(dom.Node node) { /** * Return the [Injector] associated with a current [Element]. * - * **NOTE**: This global method is here to make it easier to debug Angular application from - * the browser's REPL, unit or end-to-end tests. The function is not intended to be called - * from Angular application. + * **NOTE**: This global method is here to make it easier to debug Angular + * application from the browser's REPL, unit or end-to-end tests. The function + * is not intended to be called from Angular application. */ Injector ngInjector(dom.Node node) => ngProbe(node).injector; @@ -36,14 +37,15 @@ Injector ngInjector(dom.Node node) => ngProbe(node).injector; /** * Return the [Scope] associated with a current [Element]. * - * **NOTE**: This global method is here to make it easier to debug Angular application from - * the browser's REPL, unit or end-to-end tests. The function is not intended to be called - * from Angular application. + * **NOTE**: This global method is here to make it easier to debug Angular + * application from the browser's REPL, unit or end-to-end tests. The function + * is not intended to be called from Angular application. */ Scope ngScope(dom.Node node) => ngProbe(node).scope; -List ngQuery(dom.Node element, String selector, [String containsText]) { +List ngQuery(dom.Node element, String selector, + [String containsText]) { var list = []; var children = [element]; if ((element is dom.Element) && element.shadowRoot != null) { @@ -64,9 +66,9 @@ List ngQuery(dom.Node element, String selector, [String containsTex /** * Return a List of directive controllers associated with a current [Element]. * - * **NOTE**: This global method is here to make it easier to debug Angular application from - * the browser's REPL, unit or end-to-end tests. The function is not intended to be called - * from Angular application. + * **NOTE**: This global method is here to make it easier to debug Angular + * application from the browser's REPL, unit or end-to-end tests. The function + * is not intended to be called from Angular application. */ List ngDirectives(dom.Node node) { ElementProbe probe = _elementExpando[node]; @@ -74,11 +76,12 @@ List ngDirectives(dom.Node node) { } _publishToJavaScript() { - js.context['ngProbe'] = (dom.Node node) => _jsProbe(ngProbe(node)); - js.context['ngInjector'] = (dom.Node node) => _jsInjector(ngInjector(node)); - js.context['ngScope'] = (dom.Node node) => _jsScope(ngScope(node)); - js.context['ngQuery'] = (dom.Node node, String selector, [String containsText]) => - new js.JsArray.from(ngQuery(node, selector, containsText)); + js.context + ..['ngProbe'] = (dom.Node node) => _jsProbe(ngProbe(node)) + ..['ngInjector'] = (dom.Node node) => _jsInjector(ngInjector(node)) + ..['ngScope'] = (dom.Node node) => _jsScope(ngScope(node)) + ..['ngQuery'] = (dom.Node node, String selector, [String containsText]) => + new js.JsArray.from(ngQuery(node, selector, containsText)); } js.JsObject _jsProbe(ElementProbe probe) { @@ -91,9 +94,7 @@ js.JsObject _jsProbe(ElementProbe probe) { } js.JsObject _jsInjector(Injector injector) { - return new js.JsObject.jsify({ - "get": injector.get - })..['_dart_'] = injector; + return new js.JsObject.jsify({"get": injector.get})..['_dart_'] = injector; } js.JsObject _jsScope(Scope scope) { diff --git a/perf/watch_group_perf.dart b/perf/watch_group_perf.dart index a5923c6bd..bfc2ab6a5 100644 --- a/perf/watch_group_perf.dart +++ b/perf/watch_group_perf.dart @@ -43,26 +43,26 @@ class _CollectionCheck extends BenchmarkBase { _fieldRead() { var watchGrp = new RootWatchGroup( new DirtyCheckingChangeDetector(_getterCache), new _Obj()) - ..watch(_parse('a'), _reactionFn) - ..watch(_parse('b'), _reactionFn) - ..watch(_parse('c'), _reactionFn) - ..watch(_parse('d'), _reactionFn) - ..watch(_parse('e'), _reactionFn) - ..watch(_parse('f'), _reactionFn) - ..watch(_parse('g'), _reactionFn) - ..watch(_parse('h'), _reactionFn) - ..watch(_parse('i'), _reactionFn) - ..watch(_parse('j'), _reactionFn) - ..watch(_parse('k'), _reactionFn) - ..watch(_parse('l'), _reactionFn) - ..watch(_parse('m'), _reactionFn) - ..watch(_parse('n'), _reactionFn) - ..watch(_parse('o'), _reactionFn) - ..watch(_parse('p'), _reactionFn) - ..watch(_parse('q'), _reactionFn) - ..watch(_parse('r'), _reactionFn) - ..watch(_parse('s'), _reactionFn) - ..watch(_parse('t'), _reactionFn); + ..watch(_parse('a'), _reactionFn) + ..watch(_parse('b'), _reactionFn) + ..watch(_parse('c'), _reactionFn) + ..watch(_parse('d'), _reactionFn) + ..watch(_parse('e'), _reactionFn) + ..watch(_parse('f'), _reactionFn) + ..watch(_parse('g'), _reactionFn) + ..watch(_parse('h'), _reactionFn) + ..watch(_parse('i'), _reactionFn) + ..watch(_parse('j'), _reactionFn) + ..watch(_parse('k'), _reactionFn) + ..watch(_parse('l'), _reactionFn) + ..watch(_parse('m'), _reactionFn) + ..watch(_parse('n'), _reactionFn) + ..watch(_parse('o'), _reactionFn) + ..watch(_parse('p'), _reactionFn) + ..watch(_parse('q'), _reactionFn) + ..watch(_parse('r'), _reactionFn) + ..watch(_parse('s'), _reactionFn) + ..watch(_parse('t'), _reactionFn); print('Watch: ${watchGrp.fieldCost}; eval: ${watchGrp.evalCost}'); @@ -78,26 +78,26 @@ _fieldReadGetter() { }); var watchGrp= new RootWatchGroup( new DirtyCheckingChangeDetector(getterCache), new _Obj()) - ..watch(_parse('a'), _reactionFn) - ..watch(_parse('b'), _reactionFn) - ..watch(_parse('c'), _reactionFn) - ..watch(_parse('d'), _reactionFn) - ..watch(_parse('e'), _reactionFn) - ..watch(_parse('f'), _reactionFn) - ..watch(_parse('g'), _reactionFn) - ..watch(_parse('h'), _reactionFn) - ..watch(_parse('i'), _reactionFn) - ..watch(_parse('j'), _reactionFn) - ..watch(_parse('k'), _reactionFn) - ..watch(_parse('l'), _reactionFn) - ..watch(_parse('m'), _reactionFn) - ..watch(_parse('n'), _reactionFn) - ..watch(_parse('o'), _reactionFn) - ..watch(_parse('p'), _reactionFn) - ..watch(_parse('q'), _reactionFn) - ..watch(_parse('r'), _reactionFn) - ..watch(_parse('s'), _reactionFn) - ..watch(_parse('t'), _reactionFn); + ..watch(_parse('a'), _reactionFn) + ..watch(_parse('b'), _reactionFn) + ..watch(_parse('c'), _reactionFn) + ..watch(_parse('d'), _reactionFn) + ..watch(_parse('e'), _reactionFn) + ..watch(_parse('f'), _reactionFn) + ..watch(_parse('g'), _reactionFn) + ..watch(_parse('h'), _reactionFn) + ..watch(_parse('i'), _reactionFn) + ..watch(_parse('j'), _reactionFn) + ..watch(_parse('k'), _reactionFn) + ..watch(_parse('l'), _reactionFn) + ..watch(_parse('m'), _reactionFn) + ..watch(_parse('n'), _reactionFn) + ..watch(_parse('o'), _reactionFn) + ..watch(_parse('p'), _reactionFn) + ..watch(_parse('q'), _reactionFn) + ..watch(_parse('r'), _reactionFn) + ..watch(_parse('s'), _reactionFn) + ..watch(_parse('t'), _reactionFn); print('Watch: ${watchGrp.fieldCost}; eval: ${watchGrp.evalCost}'); @@ -112,26 +112,26 @@ _mapRead() { 'p': 0, 'q': 1, 'r': 2, 's': 3, 't': 4}; var watchGrp = new RootWatchGroup( new DirtyCheckingChangeDetector(_getterCache), map) - ..watch(_parse('a'), _reactionFn) - ..watch(_parse('b'), _reactionFn) - ..watch(_parse('c'), _reactionFn) - ..watch(_parse('d'), _reactionFn) - ..watch(_parse('e'), _reactionFn) - ..watch(_parse('f'), _reactionFn) - ..watch(_parse('g'), _reactionFn) - ..watch(_parse('h'), _reactionFn) - ..watch(_parse('i'), _reactionFn) - ..watch(_parse('j'), _reactionFn) - ..watch(_parse('k'), _reactionFn) - ..watch(_parse('l'), _reactionFn) - ..watch(_parse('m'), _reactionFn) - ..watch(_parse('n'), _reactionFn) - ..watch(_parse('o'), _reactionFn) - ..watch(_parse('p'), _reactionFn) - ..watch(_parse('q'), _reactionFn) - ..watch(_parse('r'), _reactionFn) - ..watch(_parse('s'), _reactionFn) - ..watch(_parse('t'), _reactionFn); + ..watch(_parse('a'), _reactionFn) + ..watch(_parse('b'), _reactionFn) + ..watch(_parse('c'), _reactionFn) + ..watch(_parse('d'), _reactionFn) + ..watch(_parse('e'), _reactionFn) + ..watch(_parse('f'), _reactionFn) + ..watch(_parse('g'), _reactionFn) + ..watch(_parse('h'), _reactionFn) + ..watch(_parse('i'), _reactionFn) + ..watch(_parse('j'), _reactionFn) + ..watch(_parse('k'), _reactionFn) + ..watch(_parse('l'), _reactionFn) + ..watch(_parse('m'), _reactionFn) + ..watch(_parse('n'), _reactionFn) + ..watch(_parse('o'), _reactionFn) + ..watch(_parse('p'), _reactionFn) + ..watch(_parse('q'), _reactionFn) + ..watch(_parse('r'), _reactionFn) + ..watch(_parse('s'), _reactionFn) + ..watch(_parse('t'), _reactionFn); print('Watch: ${watchGrp.fieldCost}; eval: ${watchGrp.evalCost}'); time('mapRead', () => watchGrp.detectChanges()); @@ -142,26 +142,26 @@ _methodInvoke0() { context.a = new _Obj(); var watchGrp = new RootWatchGroup( new DirtyCheckingChangeDetector(_getterCache), context) - ..watch(_method('a', 'methodA'), _reactionFn) - ..watch(_method('a', 'methodB'), _reactionFn) - ..watch(_method('a', 'methodC'), _reactionFn) - ..watch(_method('a', 'methodD'), _reactionFn) - ..watch(_method('a', 'methodE'), _reactionFn) - ..watch(_method('a', 'methodF'), _reactionFn) - ..watch(_method('a', 'methodG'), _reactionFn) - ..watch(_method('a', 'methodH'), _reactionFn) - ..watch(_method('a', 'methodI'), _reactionFn) - ..watch(_method('a', 'methodJ'), _reactionFn) - ..watch(_method('a', 'methodK'), _reactionFn) - ..watch(_method('a', 'methodL'), _reactionFn) - ..watch(_method('a', 'methodM'), _reactionFn) - ..watch(_method('a', 'methodN'), _reactionFn) - ..watch(_method('a', 'methodO'), _reactionFn) - ..watch(_method('a', 'methodP'), _reactionFn) - ..watch(_method('a', 'methodQ'), _reactionFn) - ..watch(_method('a', 'methodR'), _reactionFn) - ..watch(_method('a', 'methodS'), _reactionFn) - ..watch(_method('a', 'methodT'), _reactionFn); + ..watch(_method('a', 'methodA'), _reactionFn) + ..watch(_method('a', 'methodB'), _reactionFn) + ..watch(_method('a', 'methodC'), _reactionFn) + ..watch(_method('a', 'methodD'), _reactionFn) + ..watch(_method('a', 'methodE'), _reactionFn) + ..watch(_method('a', 'methodF'), _reactionFn) + ..watch(_method('a', 'methodG'), _reactionFn) + ..watch(_method('a', 'methodH'), _reactionFn) + ..watch(_method('a', 'methodI'), _reactionFn) + ..watch(_method('a', 'methodJ'), _reactionFn) + ..watch(_method('a', 'methodK'), _reactionFn) + ..watch(_method('a', 'methodL'), _reactionFn) + ..watch(_method('a', 'methodM'), _reactionFn) + ..watch(_method('a', 'methodN'), _reactionFn) + ..watch(_method('a', 'methodO'), _reactionFn) + ..watch(_method('a', 'methodP'), _reactionFn) + ..watch(_method('a', 'methodQ'), _reactionFn) + ..watch(_method('a', 'methodR'), _reactionFn) + ..watch(_method('a', 'methodS'), _reactionFn) + ..watch(_method('a', 'methodT'), _reactionFn); print('Watch: ${watchGrp.fieldCost}; eval: ${watchGrp.evalCost}'); time('obj.method?()', () => watchGrp.detectChanges()); @@ -172,26 +172,26 @@ _methodInvoke1() { context.a = new _Obj(); var watchGrp = new RootWatchGroup( new DirtyCheckingChangeDetector(_getterCache), context) - ..watch(_method('a', 'methodA1', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodB1', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodC1', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodD1', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodE1', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodF1', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodG1', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodH1', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodI1', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodJ1', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodK1', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodL1', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodM1', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodN1', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodO1', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodP1', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodQ1', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodR1', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodS1', [_parse('a')]), _reactionFn) - ..watch(_method('a', 'methodT1', [_parse('a')]), _reactionFn); + ..watch(_method('a', 'methodA1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodB1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodC1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodD1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodE1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodF1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodG1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodH1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodI1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodJ1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodK1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodL1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodM1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodN1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodO1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodP1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodQ1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodR1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodS1', [_parse('a')]), _reactionFn) + ..watch(_method('a', 'methodT1', [_parse('a')]), _reactionFn); print('Watch: ${watchGrp.fieldCost}; eval: ${watchGrp.evalCost}'); time('obj.method?(obj)', () => watchGrp.detectChanges()); @@ -201,26 +201,26 @@ _function2() { var context = new _Obj(); var watchGrp = new RootWatchGroup( new DirtyCheckingChangeDetector(_getterCache), context) - ..watch(_add(0, _parse('a'), _parse('a')), _reactionFn) - ..watch(_add(1, _parse('a'), _parse('a')), _reactionFn) - ..watch(_add(2, _parse('a'), _parse('a')), _reactionFn) - ..watch(_add(3, _parse('a'), _parse('a')), _reactionFn) - ..watch(_add(4, _parse('a'), _parse('a')), _reactionFn) - ..watch(_add(5, _parse('a'), _parse('a')), _reactionFn) - ..watch(_add(6, _parse('a'), _parse('a')), _reactionFn) - ..watch(_add(7, _parse('a'), _parse('a')), _reactionFn) - ..watch(_add(8, _parse('a'), _parse('a')), _reactionFn) - ..watch(_add(9, _parse('a'), _parse('a')), _reactionFn) - ..watch(_add(10, _parse('a'), _parse('a')), _reactionFn) - ..watch(_add(11, _parse('a'), _parse('a')), _reactionFn) - ..watch(_add(12, _parse('a'), _parse('a')), _reactionFn) - ..watch(_add(13, _parse('a'), _parse('a')), _reactionFn) - ..watch(_add(14, _parse('a'), _parse('a')), _reactionFn) - ..watch(_add(15, _parse('a'), _parse('a')), _reactionFn) - ..watch(_add(16, _parse('a'), _parse('a')), _reactionFn) - ..watch(_add(17, _parse('a'), _parse('a')), _reactionFn) - ..watch(_add(18, _parse('a'), _parse('a')), _reactionFn) - ..watch(_add(19, _parse('a'), _parse('a')), _reactionFn); + ..watch(_add(0, _parse('a'), _parse('a')), _reactionFn) + ..watch(_add(1, _parse('a'), _parse('a')), _reactionFn) + ..watch(_add(2, _parse('a'), _parse('a')), _reactionFn) + ..watch(_add(3, _parse('a'), _parse('a')), _reactionFn) + ..watch(_add(4, _parse('a'), _parse('a')), _reactionFn) + ..watch(_add(5, _parse('a'), _parse('a')), _reactionFn) + ..watch(_add(6, _parse('a'), _parse('a')), _reactionFn) + ..watch(_add(7, _parse('a'), _parse('a')), _reactionFn) + ..watch(_add(8, _parse('a'), _parse('a')), _reactionFn) + ..watch(_add(9, _parse('a'), _parse('a')), _reactionFn) + ..watch(_add(10, _parse('a'), _parse('a')), _reactionFn) + ..watch(_add(11, _parse('a'), _parse('a')), _reactionFn) + ..watch(_add(12, _parse('a'), _parse('a')), _reactionFn) + ..watch(_add(13, _parse('a'), _parse('a')), _reactionFn) + ..watch(_add(14, _parse('a'), _parse('a')), _reactionFn) + ..watch(_add(15, _parse('a'), _parse('a')), _reactionFn) + ..watch(_add(16, _parse('a'), _parse('a')), _reactionFn) + ..watch(_add(17, _parse('a'), _parse('a')), _reactionFn) + ..watch(_add(18, _parse('a'), _parse('a')), _reactionFn) + ..watch(_add(19, _parse('a'), _parse('a')), _reactionFn); print('Watch: ${watchGrp.fieldCost}; eval: ${watchGrp.evalCost}'); time('add?(a, a)', () => watchGrp.detectChanges()); From 67f1cd5f46d241998c806077331c2a21c46b43bd Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Tue, 11 Feb 2014 11:21:43 -0800 Subject: [PATCH 06/35] chore(scope): remove watchSet API --- lib/core/scope.dart | 9 --------- lib/directive/ng_pluralize.dart | 7 +++++-- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/lib/core/scope.dart b/lib/core/scope.dart index b72c8af1d..52c340555 100644 --- a/lib/core/scope.dart +++ b/lib/core/scope.dart @@ -127,15 +127,6 @@ class Scope { 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 parser 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); diff --git a/lib/directive/ng_pluralize.dart b/lib/directive/ng_pluralize.dart index 375bd661c..da224595b 100644 --- a/lib/directive/ng_pluralize.dart +++ b/lib/directive/ng_pluralize.dart @@ -92,13 +92,14 @@ class NgPluralizeDirective { final dom.Element element; final Scope scope; final Interpolate interpolate; + final AstParser parser; int offset; Map discreteRules = new Map(); Map categoryRules = new Map(); static final RegExp IS_WHEN = new RegExp(r'^when-(minus-)?.'); NgPluralizeDirective(this.scope, this.element, this.interpolate, - NodeAttrs attributes) { + NodeAttrs attributes, this.parser) { Map whens = attributes['when'] == null ? {} : scope.eval(attributes['when']); @@ -156,6 +157,8 @@ class NgPluralizeDirective { var interpolation = interpolate(expression); interpolation.setter = (text) => element.text = text; interpolation.setter(expression); - scope.watchSet(interpolation.expressions, interpolation.call); + List items = interpolation.expressions.map((exp) => parser(exp)).toList(); + AST ast = new PureFunctionAST(expression, new ArrayFn(), items); + scope.watch(ast, interpolation.call); } } From b10d4010c710b260e42d9ce80f694fdb1a166b33 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Tue, 11 Feb 2014 13:04:41 -0800 Subject: [PATCH 07/35] feat(scope): Allow expressions on non-scope context --- lib/core/scope.dart | 21 ++++++++++++--------- lib/core_dom/compiler.dart | 8 ++++---- lib/directive/input_select.dart | 2 +- test/core/scope_spec.dart | 10 ++++++++++ 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/lib/core/scope.dart b/lib/core/scope.dart index 52c340555..90360ad41 100644 --- a/lib/core/scope.dart +++ b/lib/core/scope.dart @@ -127,17 +127,19 @@ class Scope { Scope(Object this.context, this.rootScope, this._parentScope, this._depth, this._index, this.watchGroup, this.observeGroup); - Watch watch(expression, ReactionFn reactionFn) { - // Todo(misko): remove the parser from here. It should only take AST. + Watch watch(expression, ReactionFn reactionFn, {context, FilterMap filters}) { assert(expression != null); - AST ast = expression is AST ? expression : rootScope._parse(expression); + AST ast = expression is AST + ? expression + : rootScope._astParser(expression, context: context, filters: filters); return watchGroup.watch(ast, reactionFn); } - Watch observe(expression, ReactionFn reactionFn) { - // Todo(misko): remove the parser from here. It should only take AST. + Watch observe(expression, ReactionFn reactionFn, {context, FilterMap filters}) { assert(expression != null); - AST ast = expression is AST ? expression : rootScope._parse(expression); + AST ast = expression is AST + ? expression + : rootScope._astParser(expression, context: context, filters: filters); return observeGroup.watch(ast, reactionFn); } @@ -218,6 +220,7 @@ class RootScope extends Scope { static final STATE_FLUSH = 'digest'; final ExceptionHandler _exceptionHandler; + final AstParser _astParser; final Parser _parser; final ScopeDigestTTL _ttl; final ExpressionVisitor visitor = new ExpressionVisitor(); // TODO(misko): delete me @@ -229,8 +232,9 @@ class RootScope extends Scope { String _state; - RootScope(Object context, this._parser, GetterCache cacheGetter, - FilterMap filterMap, this._exceptionHandler, this._ttl, this._zone) + RootScope(Object context, this._astParser, this._parser, + GetterCache cacheGetter, FilterMap filterMap, + this._exceptionHandler, this._ttl, this._zone) : super(context, null, null, 0, 0, new RootWatchGroup(new DirtyCheckingChangeDetector(cacheGetter), context), new RootWatchGroup(new DirtyCheckingChangeDetector(cacheGetter), context)) @@ -354,7 +358,6 @@ class RootScope extends Scope { } - AST _parse(expression) => visitor.visit(_parser.call(expression)); void destroy() {} void _transitionState(String from, String to) { diff --git a/lib/core_dom/compiler.dart b/lib/core_dom/compiler.dart index aaf7bb9f7..ff99e4be6 100644 --- a/lib/core_dom/compiler.dart +++ b/lib/core_dom/compiler.dart @@ -4,10 +4,9 @@ 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._astParser, this._expando); + Compiler(this._perf, this._parser, this._expando); _compileBlock(NodeCursor domCursor, NodeCursor templateCursor, List useExistingDirectiveRefs, @@ -171,14 +170,15 @@ class Compiler { ); if (expressionFn.isAssignable) { scope.watch( - _astParser(dstExpression, context: controller), + dstExpression, (outboundValue, _) { if(!blockOutbound) { blockInbound = true; scope.rootScope.runAsync(() => blockInbound = false); expressionFn.assign(scope.context, outboundValue); } - } + }, + context: controller ); } }; diff --git a/lib/directive/input_select.dart b/lib/directive/input_select.dart index 621174959..d86f40041 100644 --- a/lib/directive/input_select.dart +++ b/lib/directive/input_select.dart @@ -63,7 +63,7 @@ class InputSelectDirective implements NgAttachAware { _selectElement.onChange.listen((event) => _mode.onViewChange(event)); _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 + // because the modelChange reads from the DOM. We should be able to render // without DOM changes. _scope.rootScope.domRead(() { _scope.rootScope.domWrite(() => _mode.onModelChange(value)); diff --git a/test/core/scope_spec.dart b/test/core/scope_spec.dart index 6182c440f..deb64470c 100644 --- a/test/core/scope_spec.dart +++ b/test/core/scope_spec.dart @@ -755,6 +755,16 @@ main() => describe('scope', () { })); + it('should watch/observe on objects other then contex', inject((RootScope rootScope) { + var log = ''; + var map = {'a': 'A', 'b': 'B'}; + rootScope.watch('a', (a, b) => log += a, context: map); + rootScope.watch('b', (a, b) => log += a, context: map); + rootScope.apply(); + expect(log).toEqual('AB'); + })); + + it(r'should watch and fire on expression change', inject((RootScope rootScope) { var log; From 05ab37889a9eb27add1f1c5f8e9b26c69948f044 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Tue, 11 Feb 2014 13:04:59 -0800 Subject: [PATCH 08/35] chore(demo): update to work with new scope --- demo/bouncing_balls/bouncy_balls.dart | 14 ++--- demo/bouncing_balls/pubspec.lock | 38 ++++++------- demo/helloworld/pubspec.lock | 77 +++++++++++++++++++++++++++ demo/todo/pubspec.lock | 8 +-- 4 files changed, 105 insertions(+), 32 deletions(-) create mode 100644 demo/helloworld/pubspec.lock diff --git a/demo/bouncing_balls/bouncy_balls.dart b/demo/bouncing_balls/bouncy_balls.dart index 4fefe5d27..7495c8eee 100644 --- a/demo/bouncing_balls/bouncy_balls.dart +++ b/demo/bouncing_balls/bouncy_balls.dart @@ -34,6 +34,7 @@ class BounceController { var run = true; var fps = 0; var digestTime = 0; + var currentDigestTime = 0; var balls = []; var zone; var scope; @@ -71,9 +72,10 @@ class BounceController { timeDigest() { var start = window.performance.now(); - scope.runAsync(() { - digestTime = (window.performance.now() - start).round(); - }, outsideDigest: true); + digestTime = currentDigestTime; + scope.rootScope.domRead(() { + currentDigestTime = (window.performance.now() - start).round(); + }); } tick() { @@ -109,10 +111,8 @@ class BallPositionDirective { set position(BallModel model) { element.style.backgroundColor = model.color; - scope.watch(() { - element.style.left = '${model.x + 10}px'; - element.style.top = '${model.y + 10}px'; - }); + scope.observe('x', (x, _) => element.style.left = '${x + 10}px', context:model); + scope.observe('y', (y, _) => element.style.top = '${y + 10}px', context:model); } } diff --git a/demo/bouncing_balls/pubspec.lock b/demo/bouncing_balls/pubspec.lock index 989a95424..8fbecc02c 100644 --- a/demo/bouncing_balls/pubspec.lock +++ b/demo/bouncing_balls/pubspec.lock @@ -5,16 +5,12 @@ packages: description: analyzer source: hosted version: "0.10.5" - analyzer_experimental: - description: analyzer_experimental - source: hosted - version: "0.8.6" angular: description: - path: "/usr/local/google/gits/angular-dart/demo/bouncing_balls/../.." + path: "../.." relative: true source: path - version: "0.9.3" + version: "0.9.7" args: description: args source: hosted @@ -22,31 +18,27 @@ packages: browser: description: browser source: hosted - version: "0.8.7" + version: "0.9.1" collection: description: collection source: hosted - version: "0.9.0" + version: "0.9.1" di: description: di source: hosted - version: "0.0.24" + version: "0.0.32" html5lib: description: html5lib source: hosted - version: "0.8.7" + version: "0.9.1" intl: description: intl source: hosted - version: "0.8.7" + version: "0.9.1" logging: description: logging source: hosted version: "0.9.1+1" - meta: - description: meta - source: hosted - version: "0.8.7" path: description: path source: hosted @@ -58,24 +50,28 @@ packages: route_hierarchical: description: route_hierarchical source: hosted - version: "0.4.7" + version: "0.4.14" + shadow_dom: + description: shadow_dom + source: hosted + version: "0.9.1" source_maps: description: source_maps source: hosted - version: "0.8.7" + version: "0.9.0" stack_trace: description: stack_trace source: hosted - version: "0.8.7" + version: "0.9.1" unittest: description: unittest source: hosted - version: "0.8.7" + version: "0.10.0" unmodifiable_collection: description: unmodifiable_collection source: hosted - version: "0.9.2" + version: "0.9.2+1" utf: description: utf source: hosted - version: "0.8.7" + version: "0.9.0" diff --git a/demo/helloworld/pubspec.lock b/demo/helloworld/pubspec.lock new file mode 100644 index 000000000..8fbecc02c --- /dev/null +++ b/demo/helloworld/pubspec.lock @@ -0,0 +1,77 @@ +# Generated by pub +# See http://pub.dartlang.org/doc/glossary.html#lockfile +packages: + analyzer: + description: analyzer + source: hosted + version: "0.10.5" + angular: + description: + path: "../.." + relative: true + source: path + version: "0.9.7" + args: + description: args + source: hosted + version: "0.9.0" + browser: + description: browser + source: hosted + version: "0.9.1" + collection: + description: collection + source: hosted + version: "0.9.1" + di: + description: di + source: hosted + version: "0.0.32" + html5lib: + description: html5lib + source: hosted + version: "0.9.1" + intl: + description: intl + source: hosted + version: "0.9.1" + logging: + description: logging + source: hosted + version: "0.9.1+1" + path: + description: path + source: hosted + version: "1.0.0" + perf_api: + description: perf_api + source: hosted + version: "0.0.8" + route_hierarchical: + description: route_hierarchical + source: hosted + version: "0.4.14" + shadow_dom: + description: shadow_dom + source: hosted + version: "0.9.1" + source_maps: + description: source_maps + source: hosted + version: "0.9.0" + stack_trace: + description: stack_trace + source: hosted + version: "0.9.1" + unittest: + description: unittest + source: hosted + version: "0.10.0" + unmodifiable_collection: + description: unmodifiable_collection + source: hosted + version: "0.9.2+1" + utf: + description: utf + source: hosted + version: "0.9.0" diff --git a/demo/todo/pubspec.lock b/demo/todo/pubspec.lock index dca084f5b..8fbecc02c 100644 --- a/demo/todo/pubspec.lock +++ b/demo/todo/pubspec.lock @@ -10,7 +10,7 @@ packages: path: "../.." relative: true source: path - version: "0.9.4" + version: "0.9.7" args: description: args source: hosted @@ -50,7 +50,7 @@ packages: route_hierarchical: description: route_hierarchical source: hosted - version: "0.4.10" + version: "0.4.14" shadow_dom: description: shadow_dom source: hosted @@ -66,11 +66,11 @@ packages: unittest: description: unittest source: hosted - version: "0.9.3" + version: "0.10.0" unmodifiable_collection: description: unmodifiable_collection source: hosted - version: "0.9.2" + version: "0.9.2+1" utf: description: utf source: hosted From c099b445c71f2981101f311539a920fe352bc3c2 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Wed, 12 Feb 2014 13:10:33 -0800 Subject: [PATCH 09/35] fix(scope): allow sending emit/broadcast when no on() --- lib/core/scope.dart | 3 ++- test/core/scope_spec.dart | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/core/scope.dart b/lib/core/scope.dart index 90360ad41..0bdb89f82 100644 --- a/lib/core/scope.dart +++ b/lib/core/scope.dart @@ -406,7 +406,8 @@ class _Streams { ScopeEvent event = new ScopeEvent(name, scope, data); Scope scopeCursor = scope; while(scopeCursor != null) { - if (scopeCursor._streams._scope == scopeCursor) { + if (scopeCursor._streams !=null && + scopeCursor._streams._scope == scopeCursor) { ScopeStream stream = scopeCursor._streams._streams[name]; if (stream != null) { event._currentScope = scopeCursor; diff --git a/test/core/scope_spec.dart b/test/core/scope_spec.dart index deb64470c..5543ddb2d 100644 --- a/test/core/scope_spec.dart +++ b/test/core/scope_spec.dart @@ -216,6 +216,12 @@ main() => describe('scope', () { describe(r'events', () { describe('on', () { + it('should allow emit/broadcast when no listeners', inject((RootScope scope) { + scope.emit('foo'); + scope.broadcast('foo'); + })); + + it(r'should add listener for both emit and broadcast events', inject((RootScope rootScope) { var log = '', child = rootScope.createChild(); From 20ce7c6e3b70d2ca9159ca43972e2b456a85d2ba Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Wed, 12 Feb 2014 14:49:36 -0800 Subject: [PATCH 10/35] feat(zone): Allow escaping of auto-digest mechanism. Closes #557 --- lib/core/zone.dart | 26 +++++++++++++++++++++++--- test/core/zone_spec.dart | 17 +++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/lib/core/zone.dart b/lib/core/zone.dart index 3c443beaf..600b775aa 100644 --- a/lib/core/zone.dart +++ b/lib/core/zone.dart @@ -29,8 +29,13 @@ class LongStackTrace { * A better zone API which implements onTurnDone. */ class NgZone { - NgZone() { - _zone = async.Zone.current.fork(specification: new async.ZoneSpecification( + final async.Zone _outerZone; + async.Zone _zone; + + NgZone() + : _outerZone = async.Zone.current + { + _zone = _outerZone.fork(specification: new async.ZoneSpecification( run: _onRun, runUnary: _onRunUnary, scheduleMicrotask: _onScheduleMicrotask, @@ -38,7 +43,6 @@ class NgZone { )); } - async.Zone _zone; List _asyncQueue = []; bool _errorThrownFromOnRun = false; @@ -142,6 +146,22 @@ class NgZone { */ run(body()) => _zone.run(body); + /** + * Allows one to escape the auto-digest mechanism of Angular. + * + * myFunction(NgZone zone, Element element) { + * element.onClick.listen(() { + * // auto-digest will run after element click. + * }); + * zone.runOutsideAngular(() { + * element.onMouseMove.listen(() { + * // auto-digest will NOT run after mouse move + * }); + * }); + * } + */ + runOutsideAngular(body()) => _outerZone.run(body); + assertInTurn() { assert(_runningInTurn > 0 || _inFinishTurn); } diff --git a/test/core/zone_spec.dart b/test/core/zone_spec.dart index 1725d837b..a50b1c9a0 100644 --- a/test/core/zone_spec.dart +++ b/test/core/zone_spec.dart @@ -46,6 +46,23 @@ main() => describe('zone', () { }))); + it('should allow executing code outside the zone', inject(() { + var zone = new NgZone(); + var outerZone = Zone.current; + var ngZone; + var outsideZone; + zone.run(() { + ngZone = Zone.current; + zone.runOutsideAngular(() { + outsideZone = Zone.current; + }); + }); + + expect(outsideZone).toEqual(outerZone); + expect(ngZone.parent).toEqual((outerZone)); + })); + + it('should rethrow exceptions from the onTurnDone and call onError when the zone is sync', () { zone.onTurnDone = () { throw ["fromOnTurnDone"]; From cf9191246abc3fca287b2658925859b811281185 Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 12 Feb 2014 11:45:50 +0100 Subject: [PATCH 11/35] chore(style): code cleanup --- demo/bouncing_balls/bouncy_balls.dart | 42 +++-- demo/bouncing_balls/index.html | 4 +- demo/helloworld/helloworld.dart | 3 +- demo/todo/web/index.html | 43 +++--- demo/todo/web/main.dart | 6 +- demo/todo/web/todo.dart | 34 ++--- lib/change_detection/ast.dart | 14 +- lib/change_detection/change_detection.dart | 50 +++--- .../dirty_checking_change_detector.dart | 43 ++++-- lib/change_detection/prototype_map.dart | 39 +++-- lib/change_detection/watch_group.dart | 47 +++--- lib/core/scope.dart | 143 ++++++++++-------- lib/directive/ng_pluralize.dart | 12 +- .../dirty_checking_change_detector_spec.dart | 102 ++++++------- test/change_detection/watch_group_spec.dart | 2 +- 15 files changed, 308 insertions(+), 276 deletions(-) diff --git a/demo/bouncing_balls/bouncy_balls.dart b/demo/bouncing_balls/bouncy_balls.dart index 7495c8eee..3e98042a9 100644 --- a/demo/bouncing_balls/bouncy_balls.dart +++ b/demo/bouncing_balls/bouncy_balls.dart @@ -17,7 +17,7 @@ class BallModel { static _color() { var color = '#'; - for(var i=0; i < 6; i++) { + for(var i = 0; i < 6; i++) { color += (16 * random.nextDouble()).floor().toRadixString(16); } return color; @@ -27,8 +27,7 @@ class BallModel { @NgController( selector: '[bounce-controller]', - publishAs: 'bounce' -) + publishAs: 'bounce') class BounceController { var lastTime = window.performance.now(); var run = true; @@ -36,29 +35,29 @@ class BounceController { var digestTime = 0; var currentDigestTime = 0; var balls = []; - var zone; - var scope; + final NgZone zone; + final Scope scope; var ballClassName = 'ball'; - BounceController(NgZone this.zone, Scope this.scope) { + BounceController(this.zone, this.scope) { changeCount(100); tick(); } - toggleCSS() { + void toggleCSS() { ballClassName = ballClassName == '' ? 'ball' : ''; } - playPause() { + void playPause() { run = !run; if (run) requestAnimationFrame(tick); } - requestAnimationFrame(fn) { + void requestAnimationFrame(fn) { window.requestAnimationFrame((_) => zone.run(fn)); } - changeCount(count) { + void changeCount(count) { while(count > 0) { balls.add(new BallModel()); count--; @@ -70,7 +69,7 @@ class BounceController { tick(); } - timeDigest() { + void timeDigest() { var start = window.performance.now(); digestTime = currentDigestTime; scope.rootScope.domRead(() { @@ -78,9 +77,9 @@ class BounceController { }); } - tick() { - var now = window.performance.now(), - delay = now - lastTime; + void tick() { + var now = window.performance.now(); + var delay = now - lastTime; fps = (1000/delay).round(); for(var i=0, ii=balls.length; iposition' - } -) + "ballPosition": '=>position'}) class BallPositionDirective { - Element element; - Scope scope; - BallPositionDirective(Element this.element, Scope this.scope); + final Element element; + final Scope scope; + BallPositionDirective(this.element, this.scope); set position(BallModel model) { element.style.backgroundColor = model.color; - scope.observe('x', (x, _) => element.style.left = '${x + 10}px', context:model); - scope.observe('y', (y, _) => element.style.top = '${y + 10}px', context:model); + scope + ..observe('x', (x, _) => element.style.left = '${x + 10}px', context: model) + ..observe('y', (y, _) => element.style.top = '${y + 10}px', context: model); } } diff --git a/demo/bouncing_balls/index.html b/demo/bouncing_balls/index.html index 267371c28..568d6336d 100644 --- a/demo/bouncing_balls/index.html +++ b/demo/bouncing_balls/index.html @@ -45,7 +45,9 @@
    -
    +
    +
    +
    {{bounce.fps}} fps. ({{bounce.balls.length}} balls) [{{(1000/bounce.fps).round()}} ms]
    diff --git a/demo/helloworld/helloworld.dart b/demo/helloworld/helloworld.dart index 50e5d2c57..338e33bb6 100644 --- a/demo/helloworld/helloworld.dart +++ b/demo/helloworld/helloworld.dart @@ -10,8 +10,7 @@ import 'dart:mirrors'; @NgController( selector: '[hello-world-controller]', - publishAs: 'ctrl' -) + publishAs: 'ctrl') class HelloWorldController { String name = "world"; } diff --git a/demo/todo/web/index.html b/demo/todo/web/index.html index a3010d815..7cea710c6 100644 --- a/demo/todo/web/index.html +++ b/demo/todo/web/index.html @@ -8,33 +8,32 @@ -
    Wait, Dart is loading this awesome app...
    +
    Wait, Dart is loading this awesome app...
    -
    -

    Things To Do ;-)

    +
    +

    Things To Do ;-)

    -
    - - -
    - -

    Remaining {{todo.remaining()}} of {{todo.items.length}} items.

    +
    + + +
    +

    Remaining {{todo.remaining()}} of {{todo.items.length}} items.

    -
      -
    • - -
    • -
    -
    - - - -
    +
      +
    • + +
    • +
    -
    +
    + + + +
    +
    diff --git a/demo/todo/web/main.dart b/demo/todo/web/main.dart index 5c67d8084..02741b947 100644 --- a/demo/todo/web/main.dart +++ b/demo/todo/web/main.dart @@ -15,8 +15,8 @@ main() { print(window.location.search); var module = new Module() - ..type(TodoController) - ..type(PlaybackHttpBackendConfig); + ..type(TodoController) + ..type(PlaybackHttpBackendConfig); // If these is a query in the URL, use the server-backed // TodoController. Otherwise, use the stored-data controller. @@ -39,5 +39,5 @@ main() { module.type(HttpBackend, implementedBy: PlaybackHttpBackend); } - ngBootstrap(module:module); + ngBootstrap(module: module); } diff --git a/demo/todo/web/todo.dart b/demo/todo/web/todo.dart index d287be25c..289500a82 100644 --- a/demo/todo/web/todo.dart +++ b/demo/todo/web/todo.dart @@ -7,13 +7,13 @@ class Item { String text; bool done; - Item([String this.text = '', bool this.done = false]); + Item([this.text = '', this.done = false]); bool get isEmpty => text.isEmpty; - clone() => new Item(text, done); + Item clone() => new Item(text, done); - clear() { + void clear() { text = ''; done = false; } @@ -50,11 +50,10 @@ class HttpServerController implements ServerController { @NgController( - selector: '[todo-controller]', - publishAs: 'todo' -) + selector: '[todo-controller]', + publishAs: 'todo') class TodoController { - List items; + var items = []; Item newItem; TodoController(ServerController serverController) { @@ -69,33 +68,24 @@ class TodoController { } // workaround for https://github.com/angular/angular.dart/issues/37 - dynamic operator [](String key) { - if (key == 'newItem') { - return newItem; - } - return null; - } + dynamic operator [](String key) => key == 'newItem' ? newItem : null; - add() { + void add() { if (newItem.isEmpty) return; items.add(newItem.clone()); newItem.clear(); } - markAllDone() { + void markAllDone() { items.forEach((item) => item.done = true); } - archiveDone() { + void archiveDone() { items.removeWhere((item) => item.done); } - String classFor(Item item) { - return item.done ? 'done' : ''; - } + String classFor(Item item) => item.done ? 'done' : ''; - int remaining() { - return items.where((item) => !item.done).length; - } + int remaining() => items.fold(0, (count, item) => count += item.done ? 0 : 1); } diff --git a/lib/change_detection/ast.dart b/lib/change_detection/ast.dart index 088884389..66c55fba3 100644 --- a/lib/change_detection/ast.dart +++ b/lib/change_detection/ast.dart @@ -17,7 +17,7 @@ abstract class AST { assert(expression!=null); } WatchRecord<_Handler> setupWatch(WatchGroup watchGroup); - toString() => expression; + String toString() => expression; } /** @@ -59,9 +59,9 @@ class FieldReadAST extends AST { final String name; FieldReadAST(lhs, name) - : super('$lhs.$name'), - lhs = lhs, - name = name; + : lhs = lhs, + name = name, + super('$lhs.$name'); WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) => watchGroup.addFieldWatch(lhs, name, expression); @@ -111,14 +111,14 @@ class MethodAST extends AST { class CollectionAST extends AST { final AST valueAST; CollectionAST(valueAST) - : super('#collection($valueAST)'), - valueAST = valueAST; + : valueAST = valueAST, + super('#collection($valueAST)'); WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) => watchGroup.addCollectionWatch(valueAST); } -_argList(List items) => items.join(', '); +String _argList(List items) => items.join(', '); /** * The name is a bit oxymoron, but it is essentially the NullObject pattern. diff --git a/lib/change_detection/change_detection.dart b/lib/change_detection/change_detection.dart index 34052671b..80c4a0369 100644 --- a/lib/change_detection/change_detection.dart +++ b/lib/change_detection/change_detection.dart @@ -13,8 +13,8 @@ abstract class ChangeDetectorGroup { * Watch a specific [field] on an [object]. * * If the [field] is: - * - _name_ - Name of the field to watch. (If the [object] is a Map then - * treat it as a key.) + * - _name_ - Name of the property to watch. (If the [object] is a Map then + * treat the name as a key.) * - _[]_ - Watch all items in an array. * - _{}_ - Watch all items in a Map. * - _._ - Watch the actual object identity. @@ -51,8 +51,8 @@ abstract class ChangeDetectorGroup { abstract class ChangeDetector extends ChangeDetectorGroup { /** * This method does the work of collecting the changes and returns them as a - * linked list of [ChangeRecord]s. The [ChangeRecord]s are to be returned in - * the same order as they were registered. + * linked list of [ChangeRecord]s. The [ChangeRecord]s are returned in the + * same order as they were registered. */ ChangeRecord collectChanges([EvalExceptionHandler exceptionHandler]); } @@ -93,7 +93,7 @@ abstract class WatchRecord extends Record { /** * Check to see if the field on the object has changed. Returns [null] if no - * change, or a [ChangeRecord] if the change has been detected. + * change, or a [ChangeRecord] if a change has been detected. */ ChangeRecord check(); @@ -111,11 +111,11 @@ abstract class ChangeRecord extends Record { } /** - * 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. + * If the [ChangeDetector] is watching a [Map] then the [currentValue] of + * [Record] will contain an instance of this object. A [MapChangeRecord] + * contains the changes to the map since the last execution. The changes are + * reported as a list of [MapKeyValue]s which contain the key as well as its + * current and previous value. */ abstract class MapChangeRecord { /// The underlying iterable object @@ -168,11 +168,11 @@ abstract class ChangedKeyValue extends MapKeyValue { /** - * 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 - * and previous position in the list as well as the item. + * If the [ChangeDetector] is watching an [Iterable] then the [currentValue] of + * [Record] will contain this object. The [CollectionChangeRecord] contains the + * changes to the collection since the last execution. The changes are reported + * as a list of [CollectionChangeItem]s which contain the item as well as its + * current and previous position in the list. */ abstract class CollectionChangeRecord { /** The underlying iterable object */ @@ -193,8 +193,8 @@ abstract class CollectionChangeRecord { } /** - * Each item in collection is wrapped in [CollectionChangeItem], which can track - * the [item]s [currentKey] and [previousKey] location. + * Each changed item in the collection is wrapped in a [CollectionChangeItem], + * which tracks the [item]s [currentKey] and [previousKey] location. */ abstract class CollectionChangeItem { // TODO(misko): change to since K is int. /** Previous item location in the list or [null] if addition. */ @@ -208,32 +208,32 @@ abstract class CollectionChangeItem { // TODO(misko): change to } /** - * Used to create a linked list of collection items. - * These items are always in the iteration order of the collection. + * Used to create a linked list of collection items. These items are always in + * the iteration order of the collection. */ abstract class CollectionItem extends CollectionChangeItem { CollectionItem get nextCollectionItem; } /** - * A linked list of new items added to the collection. - * These items are always in the iteration order of the collection. + * A linked list of new items added to the collection. These items are always in + * the iteration order of the collection. */ abstract class AddedItem extends CollectionChangeItem { AddedItem get nextAddedItem; } /** - * A linked list of moved items in to the collection. - * These items are always in the iteration order of the collection. + * A linked list of items moved in the collection. These items are always in + * the iteration order of the collection. */ abstract class MovedItem extends CollectionChangeItem { MovedItem get nextMovedItem; } /** - * A linked list of removed items in to the collection. - * These items are always in the iteration order of the collection. + * A linked list of items removed from the collection. These items are always + * in the iteration order of the collection. */ abstract class RemovedItem extends CollectionChangeItem { RemovedItem get nextRemovedItem; diff --git a/lib/change_detection/dirty_checking_change_detector.dart b/lib/change_detection/dirty_checking_change_detector.dart index a7c13ebcc..8e812fd99 100644 --- a/lib/change_detection/dirty_checking_change_detector.dart +++ b/lib/change_detection/dirty_checking_change_detector.dart @@ -557,11 +557,10 @@ class _MapChangeRecord implements MapChangeRecord { } } - bool _isInRemovals(KeyValueRecord record) { - return record == _removalsHead || - record._nextRemovedKeyValue != null || - record._prevRemovedKeyValue != null; - } + bool _isInRemovals(KeyValueRecord record) => + record == _removalsHead || + record._nextRemovedKeyValue != null || + record._prevRemovedKeyValue != null; void _addToRemovals(KeyValueRecord record) { assert(record._nextKeyValue == null); @@ -580,7 +579,11 @@ class _MapChangeRecord implements MapChangeRecord { void _removeFromSeq(KeyValueRecord prev, KeyValueRecord record) { KeyValueRecord next = record._nextKeyValue; - if (prev == null) _mapHead = next; else prev._nextKeyValue = next; + if (prev == null) { + _mapHead = next; + } else { + prev._nextKeyValue = next; + } assert((() { record._nextKeyValue = null; return true; @@ -594,8 +597,16 @@ class _MapChangeRecord implements MapChangeRecord { 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; + if (prev == null) { + _removalsHead = next; + } else { + prev._nextRemovedKeyValue = next; + } + if (next == null) { + _removalsTail = prev; + } else { + next._prevRemovedKeyValue = prev; + } record._prevRemovedKeyValue = record._nextRemovedKeyValue = null; } @@ -627,7 +638,8 @@ class _MapChangeRecord implements MapChangeRecord { } } -class KeyValueRecord implements KeyValue, AddedKeyValue, RemovedKeyValue, ChangedKeyValue { +class KeyValueRecord implements KeyValue, AddedKeyValue, + RemovedKeyValue, ChangedKeyValue { final K key; V _previousValue, _currentValue; @@ -706,7 +718,7 @@ class _CollectionChangeRecord implements CollectionChangeRecord { return false; } else if (collection is List) { List list = collection; - for(int index = 0, length = list.length; index < length; index++) { + for(int index = 0; index < list.length; index++) { var item = list[index]; if (record == null || !identical(item, record.item)) { record = mismatch(record, item, index); @@ -779,7 +791,7 @@ class _CollectionChangeRecord implements CollectionChangeRecord { * - [item] is the current item in the collection * - [index] is the position of the item in the collection */ - ItemRecord mismatch(ItemRecord record, dynamic item, int index) { + ItemRecord mismatch(ItemRecord record, item, int index) { // Guard against bogus String changes if (record != null && item is String && record.item is String && record.item == item) { @@ -1081,7 +1093,11 @@ class _DuplicateItemRecordList { var next = beforeRecord; record._prevDupRec = prev; record._nextDupRec = next; - if (prev == null) head = record; else prev._nextDupRec = record; + if (prev == null) { + head = record; + } else { + prev._nextDupRec = record; + } next._prevDupRec = record; } } @@ -1135,8 +1151,7 @@ class _DuplicateItemRecordList { * key. */ class DuplicateMap { - final Map map = - new Map(); + final map = {}; void put(ItemRecord record, [ItemRecord beforeRecord = null]) { assert(record._nextDupRec == null); diff --git a/lib/change_detection/prototype_map.dart b/lib/change_detection/prototype_map.dart index 5c0b7f298..130444184 100644 --- a/lib/change_detection/prototype_map.dart +++ b/lib/change_detection/prototype_map.dart @@ -3,22 +3,35 @@ part of angular.watch_group; class PrototypeMap implements Map { final Map prototype; final Map self = new Map(); + PrototypeMap(this.prototype); - operator []=(name, value) => self[name] = value; - operator [](name) => self.containsKey(name) ? self[name] : prototype[name]; + void operator []=(name, value) { + self[name] = value; + } + V operator [](name) => self.containsKey(name) ? self[name] : prototype[name]; - get isEmpty => self.isEmpty && prototype.isEmpty; - get isNotEmpty => self.isNotEmpty || prototype.isNotEmpty; - get keys => self.keys; - get values => self.values; - get length => self.length; + bool get isEmpty => self.isEmpty && prototype.isEmpty; + bool get isNotEmpty => self.isNotEmpty || prototype.isNotEmpty; + // todo(vbe) include prototype keys ? + Iterable get keys => self.keys; + // todo(vbe) include prototype values ? + Iterable get values => self.values; + int get length => self.length; - forEach(fn) => self.forEach(fn); - remove(key) => self.remove(key); + void forEach(fn) { + // todo(vbe) include prototype ? + self.forEach(fn); + } + V remove(key) => self.remove(key); clear() => self.clear; - containsKey(key) => self.containsKey(key); - containsValue(key) => self.containsValue(key); - addAll(map) => self.addAll(map); - putIfAbsent(key, fn) => self.putIfAbsent(key, fn); + // todo(vbe) include prototype ? + bool containsKey(key) => self.containsKey(key); + // todo(vbe) include prototype ? + bool containsValue(key) => self.containsValue(key); + void addAll(map) { + self.addAll(map); + } + // todo(vbe) include prototype ? + V putIfAbsent(key, fn) => self.putIfAbsent(key, fn); } diff --git a/lib/change_detection/watch_group.dart b/lib/change_detection/watch_group.dart index 1388b7e7d..ed6274813 100644 --- a/lib/change_detection/watch_group.dart +++ b/lib/change_detection/watch_group.dart @@ -96,8 +96,8 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { _nextWatchGroup; final WatchGroup _parentWatchGroup; - WatchGroup._child(_parentWatchGroup, this._changeDetector, - this.context, this._cache, this._rootGroup) + WatchGroup._child(_parentWatchGroup, this._changeDetector, this.context, + this._cache, this._rootGroup) : _parentWatchGroup = _parentWatchGroup, id = '${_parentWatchGroup.id}.${_parentWatchGroup._nextChildId++}' { @@ -224,7 +224,7 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { } WatchGroup get _childWatchGroupTail { - WatchGroup tail = this, nextTail; + var tail = this, nextTail; while ((nextTail = tail._watchGroupTail) != null) { tail = nextTail; } @@ -245,7 +245,7 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { this, _changeDetector.newGroup(), context == null ? this.context : context, - context == null ? this._cache: new Map>(), + context == null ? this._cache: >{}, _rootGroup == null ? this : _rootGroup); _WatchGroupList._add(this, childGroup); var marker = childGroup._marker; @@ -321,7 +321,7 @@ class RootWatchGroup extends WatchGroup { RootWatchGroup(ChangeDetector changeDetector, Object context): super._root(changeDetector, context); - WatchGroup get _rootGroup => this; + RootWatchGroup get _rootGroup => this; /** * Detect changes and process the [ReactionFn]s. @@ -334,14 +334,14 @@ class RootWatchGroup extends WatchGroup { * Each step is called in sequence. ([ReactionFn]s are not called until all * previous steps are completed). */ - int detectChanges({EvalExceptionHandler 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); + (_changeDetector as ChangeDetector<_Handler>) + .collectChanges(exceptionHandler); while (changeRecord != null) { - if (changeLog != null) { - changeLog(changeRecord.handler.expression); - } + if (changeLog != null) changeLog(changeRecord.handler.expression); changeRecord.handler.onChange(changeRecord); changeRecord = changeRecord.nextChange; } @@ -369,7 +369,6 @@ class RootWatchGroup extends WatchGroup { count++; try { dirtyWatch.invoke(); - } catch (e, s) { if (exceptionHandler == null) rethrow; else exceptionHandler(e, s); } @@ -566,7 +565,8 @@ class _ArgHandler extends _Handler { _releaseWatch() => null; _ArgHandler(WatchGroup watchGrp, this.watchRecord, int index) - : super(watchGrp, 'arg[$index]'), index = index; + : index = index, + super(watchGrp, 'arg[$index]'); void acceptValue(object) { watchRecord.dirtyArgs = true; @@ -628,11 +628,14 @@ class _EvalWatchRecord implements WatchRecord<_Handler>, ChangeRecord<_Handler> _EvalWatchRecord(this.watchGrp, this.handler, this.fn, name, int arity) : args = new List(arity), name = name, - symbol = name == null ? null : new Symbol(name) - { - if (fn is FunctionApply) mode = _MODE_FUNCTION_APPLY_; - else if (fn is Function) mode = _MODE_FUNCTION_; - else mode = _MODE_NULL_; + symbol = name == null ? null : new Symbol(name) { + if (fn is FunctionApply) { + mode = _MODE_FUNCTION_APPLY_; + } else if (fn is Function) { + mode = _MODE_FUNCTION_; + } else { + mode = _MODE_NULL_; + } } _EvalWatchRecord.marker() @@ -673,11 +676,9 @@ class _EvalWatchRecord implements WatchRecord<_Handler>, ChangeRecord<_Handler> mode = _MODE_MAP_CLOSURE_; } else { _instanceMirror = reflect(value); - if(_hasMethod(_instanceMirror.type, symbol)) { - mode = _MODE_METHOD_; - } else { - mode = _MODE_FIELD_CLOSURE_; - } + mode = _hasMethod(_instanceMirror.type, symbol) + ? _MODE_METHOD_ + : _MODE_FIELD_CLOSURE_; } } } @@ -737,7 +738,7 @@ class _EvalWatchRecord implements WatchRecord<_Handler>, ChangeRecord<_Handler> _EvalWatchList._remove(watchGrp, this); } - toString() { + String toString() { if (mode == _MODE_MARKER_) return 'MARKER[$currentValue]'; return '${watchGrp.id}:${handler.expression}'; } diff --git a/lib/core/scope.dart b/lib/core/scope.dart index 0bdb89f82..55417b63d 100644 --- a/lib/core/scope.dart +++ b/lib/core/scope.dart @@ -71,12 +71,12 @@ class ScopeEvent { * 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. + * triggers watch A. If the system does not stabilize in TTL iterations then + * the digest is stopped and an exception is thrown. */ @NgInjectableService() class ScopeDigestTTL { - final num ttl; + final int ttl; ScopeDigestTTL(): ttl = 5; ScopeDigestTTL.value(this.ttl); } @@ -91,22 +91,31 @@ class ScopeLocals implements Map { 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); + void operator []=(String name, value) { + _scope[name] = value; + } + dynamic operator [](String name) => + (_locals.containsKey(name) ? _locals : _scope)[name]; + + bool get isEmpty => _scope.isEmpty && _locals.isEmpty; + bool get isNotEmpty => _scope.isNotEmpty || _locals.isNotEmpty; + List get keys => _scope.keys; + List get values => _scope.values; + int get length => _scope.length; + + void forEach(fn) { + _scope.forEach(fn); + } + dynamic remove(key) => _scope.remove(key); + void clear() { + _scope.clear; + } + bool containsKey(key) => _scope.containsKey(key); + bool containsValue(key) => _scope.containsValue(key); + void addAll(map) { + _scope.addAll(map); + } + dynamic putIfAbsent(key, fn) => _scope.putIfAbsent(key, fn); } class Scope { @@ -150,12 +159,10 @@ class Scope { 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) { + } else { assert(locals == null); - return expression(); + if (expression is EvalFunction1) return expression(context); + if (expression is EvalFunction0) return expression(); } } @@ -169,9 +176,10 @@ class Scope { } catch (e, s) { rootScope._exceptionHandler(e, s); } finally { - rootScope._transitionState(RootScope.STATE_APPLY, null); - rootScope.digest(); - rootScope.flush(); + rootScope + .._transitionState(RootScope.STATE_APPLY, null) + ..digest() + ..flush(); } } @@ -197,10 +205,18 @@ class Scope { } 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; + var prev = _prev; + var next = _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; @@ -260,15 +276,17 @@ class RootScope extends Scope { ChangeLog changeLog; do { while(_runAsyncHead != null) { - try { _runAsyncHead.fn(); } - catch (e, s) { _exceptionHandler(e, s); } + try { + _runAsyncHead.fn(); + } catch (e, s) { + _exceptionHandler(e, s); + } _runAsyncHead = _runAsyncHead._next; } digestTTL--; count = rootWatchGroup.detectChanges( - exceptionHandler: _exceptionHandler, - changeLog: changeLog); + exceptionHandler: _exceptionHandler, changeLog: changeLog); if (digestTTL <= LOG_COUNT) { if (changeLog == null) { @@ -281,7 +299,7 @@ class RootScope extends Scope { } } if (digestTTL == 0) { - throw 'Model did not stabilize in ${_ttl.ttl} digests. ' + + throw 'Model did not stabilize in ${_ttl.ttl} digests. ' 'Last $LOG_COUNT iterations:\n${log.join('\n')}'; } } while (count > 0); @@ -297,8 +315,11 @@ class RootScope extends Scope { try { do { while(_domWriteHead != null) { - try { _domWriteHead.fn(); } - catch (e, s) { _exceptionHandler(e, s); } + try { + _domWriteHead.fn(); + } catch (e, s) { + _exceptionHandler(e, s); + } _domWriteHead = _domWriteHead._next; } if (runObservers) { @@ -306,8 +327,11 @@ class RootScope extends Scope { observeGroup.detectChanges(exceptionHandler:_exceptionHandler); } while(_domReadHead != null) { - try { _domReadHead.fn(); } - catch (e, s) { _exceptionHandler(e, s); } + try { + _domReadHead.fn(); + } catch (e, s) { + _exceptionHandler(e, s); + } _domReadHead = _domReadHead._next; } } while (_domWriteHead != null || _domReadHead != null); @@ -357,7 +381,6 @@ class RootScope extends Scope { } } - void destroy() {} void _transitionState(String from, String to) { @@ -378,7 +401,7 @@ class RootScope extends Scope { * 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 + * Scope with [_ScopeStreams] but who's [_scope] does not match the scope * is only inherited * * Only [Scope] with [_ScopeStreams] who's [_scope] matches the [Scope] @@ -393,20 +416,20 @@ class _Streams { /// Scope we belong to. final Scope _scope; /// [Stream]s for [_scope] only - final Map _streams = new Map(); + final _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; + var event = new ScopeEvent(name, scope, data); + var scopeCursor = scope; while(scopeCursor != null) { - if (scopeCursor._streams !=null && + if (scopeCursor._streams != null && scopeCursor._streams._scope == scopeCursor) { ScopeStream stream = scopeCursor._streams._streams[name]; if (stream != null) { @@ -422,10 +445,10 @@ class _Streams { static ScopeEvent broadcast(Scope scope, String name, data) { _Streams scopeStreams = scope._streams; - ScopeEvent event = new ScopeEvent(name, scope, data); + var event = new ScopeEvent(name, scope, data); if (scopeStreams != null && scopeStreams._typeCounts.containsKey(name)) { - Queue queue = new Queue()..addFirst(scopeStreams._scope); - while(queue.isNotEmpty) { + var queue = new Queue()..addFirst(scopeStreams._scope); + while (queue.isNotEmpty) { scope = queue.removeFirst(); scopeStreams = scope._streams; assert(scopeStreams._scope == scope); @@ -437,9 +460,7 @@ class _Streams { var childScope = scope._childTail; while(childScope != null) { scopeStreams = childScope._streams; - if (scopeStreams != null) { - queue.addFirst(scopeStreams._scope); - } + if (scopeStreams != null) queue.addFirst(scopeStreams._scope); childScope = childScope._prev; } } @@ -490,7 +511,7 @@ class _Streams { void _addCount(String name, int amount) { // decrement the counters on all parent scopes _Streams lastStreams = null; - Scope scope = _scope; + var scope = _scope; while (scope != null) { if (lastStreams != scope._streams) { // we have a transition, need to decrement it @@ -513,7 +534,7 @@ class ScopeStream extends async.Stream { final ExceptionHandler _exceptionHandler; final _Streams _streams; final String _name; - final List subscriptions = []; + final subscriptions = []; ScopeStream(this._streams, this._exceptionHandler, this._name); @@ -702,7 +723,7 @@ class ExpressionVisitor implements Visitor { } } -_operationToFunction(String operation) { +Function _operationToFunction(String operation) { switch(operation) { case '!' : return _operation_negate; case '+' : return _operation_add; @@ -759,12 +780,8 @@ class MapFn extends FunctionApply { 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; + return new Map.fromIterables(keys, values); } } @@ -791,8 +808,8 @@ class _FilterWrapper extends FunctionApply { 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. + // 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/directive/ng_pluralize.dart b/lib/directive/ng_pluralize.dart index da224595b..2bb3b4c0a 100644 --- a/lib/directive/ng_pluralize.dart +++ b/lib/directive/ng_pluralize.dart @@ -94,15 +94,15 @@ class NgPluralizeDirective { final Interpolate interpolate; final AstParser parser; int offset; - Map discreteRules = new Map(); - Map categoryRules = new Map(); + var discreteRules = {}; + var categoryRules = {}; static final RegExp IS_WHEN = new RegExp(r'^when-(minus-)?.'); NgPluralizeDirective(this.scope, this.element, this.interpolate, NodeAttrs attributes, this.parser) { - Map whens = attributes['when'] == null ? - {} : - scope.eval(attributes['when']); + Map whens = attributes['when'] == null + ? {} + : scope.eval(attributes['when']); offset = attributes['offset'] == null ? 0 : int.parse(attributes['offset']); element.attributes.keys.where((k) => IS_WHEN.hasMatch(k)).forEach((k) { @@ -157,7 +157,7 @@ class NgPluralizeDirective { var interpolation = interpolate(expression); interpolation.setter = (text) => element.text = text; interpolation.setter(expression); - List items = interpolation.expressions.map((exp) => parser(exp)).toList(); + var items = interpolation.expressions.map((exp) => parser(exp)).toList(); AST ast = new PureFunctionAST(expression, new ArrayFn(), items); scope.watch(ast, interpolation.call); } diff --git a/test/change_detection/dirty_checking_change_detector_spec.dart b/test/change_detection/dirty_checking_change_detector_spec.dart index 72a398a46..4c6509be0 100644 --- a/test/change_detection/dirty_checking_change_detector_spec.dart +++ b/test/change_detection/dirty_checking_change_detector_spec.dart @@ -26,14 +26,15 @@ main() => describe('DirtyCheckingChangeDetector', () { var user = new _User('', ''); var change; - detector.watch(user, 'first', null); - detector.watch(user, 'last', null); - detector.collectChanges(); // throw away first set + detector + ..watch(user, 'first', null) + ..watch(user, 'last', null) + ..collectChanges(); // throw away first set change = detector.collectChanges(); expect(change).toEqual(null); - user.first = 'misko'; - user.last = 'hevery'; + user..first = 'misko' + ..last = 'hevery'; change = detector.collectChanges(); expect(change.currentValue).toEqual('misko'); @@ -59,8 +60,7 @@ main() => describe('DirtyCheckingChangeDetector', () { it('should ignore NaN != NaN', () { var user = new _User(); user.age = double.NAN; - detector.watch(user, 'age', null); - detector.collectChanges(); // throw away first set + detector..watch(user, 'age', null)..collectChanges(); // throw away first set var changes = detector.collectChanges(); expect(changes).toEqual(null); @@ -121,7 +121,8 @@ main() => describe('DirtyCheckingChangeDetector', () { child2.watch(obj,'a', '2A'); obj['a'] = 1; - expect(detector.collectChanges(), toEqualChanges(['0a', '0A', '1a', '1A', '2A', '1b'])); + expect(detector.collectChanges(), + toEqualChanges(['0a', '0A', '1a', '1A', '2A', '1b'])); obj['a'] = 2; child1a.remove(); // should also remove child2 @@ -397,8 +398,7 @@ main() => describe('DirtyCheckingChangeDetector', () { it('should do basic operations', () { var k1 = 'a'; - var r1 = new ItemRecord(k1); - r1.currentKey = 1; + var r1 = new ItemRecord(k1)..currentKey = 1; map.put(r1); expect(map.get(k1, 2)).toEqual(null); expect(map.get(k1, 1)).toEqual(null); @@ -409,12 +409,9 @@ main() => describe('DirtyCheckingChangeDetector', () { it('should do basic operations on duplicate keys', () { var k1 = 'a'; - var r1 = new ItemRecord(k1); - var r2 = new ItemRecord(k1); - r1.currentKey = 1; - r2.currentKey = 2; - map.put(r1); - map.put(r2); + var r1 = new ItemRecord(k1)..currentKey = 1; + var r2 = new ItemRecord(k1)..currentKey = 2; + map..put(r1)..put(r2); expect(map.get(k1, 0)).toEqual(r1); expect(map.get(k1, 1)).toEqual(r2); expect(map.get(k1, 2)).toEqual(null); @@ -439,7 +436,7 @@ Matcher toEqualCollectionRecord({collection, additions, moves, removals}) => moves:moves, removals:removals); Matcher toEqualMapRecord({map, additions, changes, removals}) => new MapRecordMatcher(map:map, additions:additions, - changes:changes, removals:removals); + changes:changes, removals:removals); Matcher toEqualChanges(List changes) => new ChangeMatcher(changes); class ChangeMatcher extends Matcher { @@ -447,9 +444,11 @@ class ChangeMatcher extends Matcher { ChangeMatcher(this.expected); - Description describe(Description description) => description..add(expected.toString()); + Description describe(Description description) => + description..add(expected.toString()); - Description describeMismatch(changes, Description mismatchDescription, Map matchState, bool verbose) { + Description describeMismatch(changes, Description mismatchDescription, + Map matchState, bool verbose) { List list = []; while(changes != null) { list.add(changes.handler); @@ -469,14 +468,16 @@ class ChangeMatcher extends Matcher { } class CollectionRecordMatcher extends Matcher { - List collection; - List additions; - List moves; - List removals; + final List collection; + final List additions; + final List moves; + final List removals; - CollectionRecordMatcher({this.collection, this.additions, this.moves, this.removals}); + CollectionRecordMatcher({this.collection, this.additions, this.moves, + this.removals}); - Description describeMismatch(changes, Description mismatchDescription, Map matchState, bool verbose) { + Description describeMismatch(changes, Description mismatchDescription, + Map matchState, bool verbose) { List diffs = matchState['diffs']; return mismatchDescription..add(diffs.join('\n')); } @@ -496,16 +497,14 @@ class CollectionRecordMatcher extends Matcher { } bool matches(CollectionChangeRecord changeRecord, Map matchState) { - List diffs = matchState['diffs'] = []; - var equals = true; - equals = equals && checkCollection(changeRecord, diffs); - equals = equals && checkAdditions(changeRecord, diffs); - equals = equals && checkMoves(changeRecord, diffs); - equals = equals && checkRemovals(changeRecord, diffs); - return equals; + var diffs = matchState['diffs'] = []; + return checkCollection(changeRecord, diffs) && + checkAdditions(changeRecord, diffs) && + checkMoves(changeRecord, diffs) && + checkRemovals(changeRecord, diffs); } - checkCollection(CollectionChangeRecord changeRecord, List diffs) { + bool checkCollection(CollectionChangeRecord changeRecord, List diffs) { var equals = true; if (collection != null) { CollectionItem collectionItem = changeRecord.collectionHead; @@ -529,7 +528,7 @@ class CollectionRecordMatcher extends Matcher { return equals; } - checkAdditions(CollectionChangeRecord changeRecord, List diffs) { + bool checkAdditions(CollectionChangeRecord changeRecord, List diffs) { var equals = true; if (additions != null) { AddedItem addedItem = changeRecord.additionsHead; @@ -553,7 +552,7 @@ class CollectionRecordMatcher extends Matcher { return equals; } - checkMoves(CollectionChangeRecord changeRecord, List diffs) { + bool checkMoves(CollectionChangeRecord changeRecord, List diffs) { var equals = true; if (moves != null) { MovedItem movedItem = changeRecord.movesHead; @@ -577,7 +576,7 @@ class CollectionRecordMatcher extends Matcher { return equals; } - checkRemovals(CollectionChangeRecord changeRecord, List diffs) { + bool checkRemovals(CollectionChangeRecord changeRecord, List diffs) { var equals = true; if (removals != null) { RemovedItem removedItem = changeRecord.removalsHead; @@ -603,14 +602,15 @@ class CollectionRecordMatcher extends Matcher { } class MapRecordMatcher extends Matcher { - List map; - List additions; - List changes; - List removals; + final List map; + final List additions; + final List changes; + final List removals; MapRecordMatcher({this.map, this.additions, this.changes, this.removals}); - Description describeMismatch(changes, Description mismatchDescription, Map matchState, bool verbose) { + Description describeMismatch(changes, Description mismatchDescription, + Map matchState, bool verbose) { List diffs = matchState['diffs']; return mismatchDescription..add(diffs.join('\n')); } @@ -630,16 +630,14 @@ class MapRecordMatcher extends Matcher { } 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; + var diffs = matchState['diffs'] = []; + return checkMap(changeRecord, diffs) && + checkAdditions(changeRecord, diffs) && + checkChanges(changeRecord, diffs) && + checkRemovals(changeRecord, diffs); } - checkMap(MapChangeRecord changeRecord, List diffs) { + bool checkMap(MapChangeRecord changeRecord, List diffs) { var equals = true; if (map != null) { KeyValue mapKeyValue = changeRecord.mapHead; @@ -663,7 +661,7 @@ class MapRecordMatcher extends Matcher { return equals; } - checkAdditions(MapChangeRecord changeRecord, List diffs) { + bool checkAdditions(MapChangeRecord changeRecord, List diffs) { var equals = true; if (additions != null) { AddedKeyValue addedKeyValue = changeRecord.additionsHead; @@ -687,7 +685,7 @@ class MapRecordMatcher extends Matcher { return equals; } - checkChanges(MapChangeRecord changeRecord, List diffs) { + bool checkChanges(MapChangeRecord changeRecord, List diffs) { var equals = true; if (changes != null) { ChangedKeyValue movedKeyValue = changeRecord.changesHead; @@ -711,7 +709,7 @@ class MapRecordMatcher extends Matcher { return equals; } - checkRemovals(MapChangeRecord changeRecord, List diffs) { + bool checkRemovals(MapChangeRecord changeRecord, List diffs) { var equals = true; if (removals != null) { RemovedKeyValue removedKeyValue = changeRecord.removalsHead; diff --git a/test/change_detection/watch_group_spec.dart b/test/change_detection/watch_group_spec.dart index cbe9dfd26..597b9ee77 100644 --- a/test/change_detection/watch_group_spec.dart +++ b/test/change_detection/watch_group_spec.dart @@ -620,7 +620,7 @@ class MyClass { count() => _count++; - toString() => 'MyClass'; + String toString() => 'MyClass'; } class LoggingFunctionApply extends FunctionApply { From 34851bfa096ec5eb0c9d6e46f39f0a7f40053438 Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 12 Feb 2014 15:29:57 +0100 Subject: [PATCH 12/35] fix(balls): ball number can not go below 0 --- demo/bouncing_balls/bouncy_balls.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/bouncing_balls/bouncy_balls.dart b/demo/bouncing_balls/bouncy_balls.dart index 3e98042a9..45741b7ec 100644 --- a/demo/bouncing_balls/bouncy_balls.dart +++ b/demo/bouncing_balls/bouncy_balls.dart @@ -62,7 +62,7 @@ class BounceController { balls.add(new BallModel()); count--; } - while(count < 0) { + while(count < 0 && balls.isNotEmpty) { balls.removeAt(0); count++; } From b5eaeb570eb2dc28a370c57dc18f9cd6b12a1fc5 Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 12 Feb 2014 15:54:57 +0100 Subject: [PATCH 13/35] refactor(collection items): Collection items indexes are int --- lib/change_detection/change_detection.dart | 38 +++++------ .../dirty_checking_change_detector.dart | 68 +++++++++---------- .../dirty_checking_change_detector_spec.dart | 6 +- 3 files changed, 56 insertions(+), 56 deletions(-) diff --git a/lib/change_detection/change_detection.dart b/lib/change_detection/change_detection.dart index 80c4a0369..2eb49fe2e 100644 --- a/lib/change_detection/change_detection.dart +++ b/lib/change_detection/change_detection.dart @@ -174,34 +174,34 @@ abstract class ChangedKeyValue extends MapKeyValue { * as a list of [CollectionChangeItem]s which contain the item as well as its * current and previous position in the list. */ -abstract class CollectionChangeRecord { +abstract class CollectionChangeRecord { /** The underlying iterable object */ Iterable get iterable; /** A list of [CollectionItem]s which are in the iteration order. */ - CollectionItem get collectionHead; + CollectionItem get collectionHead; /** A list of new [AddedItem]s. */ - AddedItem get additionsHead; + AddedItem get additionsHead; /** A list of [MovedItem]s. */ - MovedItem get movesHead; + MovedItem get movesHead; /** A list of [RemovedItem]s. */ - RemovedItem get removalsHead; + RemovedItem get removalsHead; - void forEachAddition(void f(AddedItem addition)); - void forEachMove(void f(MovedItem move)); - void forEachRemoval(void f(RemovedItem removal)); + void forEachAddition(void f(AddedItem addition)); + void forEachMove(void f(MovedItem move)); + void forEachRemoval(void f(RemovedItem removal)); } /** * Each changed item in the collection is wrapped in a [CollectionChangeItem], * which tracks the [item]s [currentKey] and [previousKey] location. */ -abstract class CollectionChangeItem { // TODO(misko): change to since K is int. +abstract class CollectionChangeItem { /** Previous item location in the list or [null] if addition. */ - K get previousKey; // TODO(misko): rename to previousIndex + int get previousIndex; /** Current item location in the list or [null] if removal. */ - K get currentKey; // TODO(misko): rename to CurrentIndex + int get currentIndex; /** The item. */ V get item; @@ -211,30 +211,30 @@ abstract class CollectionChangeItem { // TODO(misko): change to * Used to create a linked list of collection items. These items are always in * the iteration order of the collection. */ -abstract class CollectionItem extends CollectionChangeItem { - CollectionItem get nextCollectionItem; +abstract class CollectionItem extends CollectionChangeItem { + CollectionItem get nextCollectionItem; } /** * A linked list of new items added to the collection. These items are always in * the iteration order of the collection. */ -abstract class AddedItem extends CollectionChangeItem { - AddedItem get nextAddedItem; +abstract class AddedItem extends CollectionChangeItem { + AddedItem get nextAddedItem; } /** * A linked list of items moved in the collection. These items are always in * the iteration order of the collection. */ -abstract class MovedItem extends CollectionChangeItem { - MovedItem get nextMovedItem; +abstract class MovedItem extends CollectionChangeItem { + MovedItem get nextMovedItem; } /** * A linked list of items removed from the collection. These items are always * in the iteration order of the collection. */ -abstract class RemovedItem extends CollectionChangeItem { - RemovedItem get nextRemovedItem; +abstract class RemovedItem extends CollectionChangeItem { + RemovedItem get nextRemovedItem; } diff --git a/lib/change_detection/dirty_checking_change_detector.dart b/lib/change_detection/dirty_checking_change_detector.dart index 8e812fd99..24a568341 100644 --- a/lib/change_detection/dirty_checking_change_detector.dart +++ b/lib/change_detection/dirty_checking_change_detector.dart @@ -663,7 +663,7 @@ class KeyValueRecord implements KeyValue, AddedKeyValue, } -class _CollectionChangeRecord implements CollectionChangeRecord { +class _CollectionChangeRecord implements CollectionChangeRecord { Iterable _iterable; /** Used to keep track of items during moves. */ DuplicateMap _items = new DuplicateMap(); @@ -671,17 +671,17 @@ class _CollectionChangeRecord implements CollectionChangeRecord { /** Used to keep track of removed items. */ DuplicateMap _removedItems = new DuplicateMap(); - ItemRecord _collectionHead, _collectionTail; - ItemRecord _additionsHead, _additionsTail; - ItemRecord _movesHead, _movesTail; - ItemRecord _removalsHead, _removalsTail; + ItemRecord _collectionHead, _collectionTail; + ItemRecord _additionsHead, _additionsTail; + ItemRecord _movesHead, _movesTail; + ItemRecord _removalsHead, _removalsTail; - CollectionChangeItem get collectionHead => _collectionHead; - CollectionChangeItem get additionsHead => _additionsHead; - CollectionChangeItem get movesHead => _movesHead; - CollectionChangeItem get removalsHead => _removalsHead; + CollectionChangeItem get collectionHead => _collectionHead; + CollectionChangeItem get additionsHead => _additionsHead; + CollectionChangeItem get movesHead => _movesHead; + CollectionChangeItem get removalsHead => _removalsHead; - void forEachAddition(void f(AddedItem addition)){ + void forEachAddition(void f(AddedItem addition)){ ItemRecord record = _additionsHead; while(record != null) { f(record); @@ -689,7 +689,7 @@ class _CollectionChangeRecord implements CollectionChangeRecord { } } - void forEachMove(void f(MovedItem change)) { + void forEachMove(void f(MovedItem change)) { ItemRecord record = _movesHead; while(record != null) { f(record); @@ -697,7 +697,7 @@ class _CollectionChangeRecord implements CollectionChangeRecord { } } - void forEachRemoval(void f(RemovedItem removal)){ + void forEachRemoval(void f(RemovedItem removal)){ ItemRecord record = _removalsHead; while(record != null) { f(record); @@ -758,14 +758,14 @@ class _CollectionChangeRecord implements CollectionChangeRecord { record = _additionsHead; while(record != null) { - record.previousKey = record.currentKey; + record.previousIndex = record.currentIndex; record = record._nextAddedRec; } _additionsHead = _additionsTail = null; record = _movesHead; while(record != null) { - record.previousKey = record.currentKey; + record.previousIndex = record.currentIndex; var nextRecord = record._nextMovedRec; assert((record._nextMovedRec = null) == null); record = nextRecord; @@ -857,8 +857,8 @@ class _CollectionChangeRecord implements CollectionChangeRecord { ItemRecord reinsertRecord = _removedItems.get(item); if (reinsertRecord != null) { record = _collection_reinsertAfter(reinsertRecord, record._prevRec, index); - } else if (record.currentKey != index) { - record.currentKey = index; + } else if (record.currentIndex != index) { + record.currentIndex = index; _moves_add(record); } return record; @@ -950,7 +950,7 @@ class _CollectionChangeRecord implements CollectionChangeRecord { } _items.put(record); - record.currentKey = index; + record.currentIndex = index; return record; } @@ -994,7 +994,7 @@ class _CollectionChangeRecord implements CollectionChangeRecord { } ItemRecord _removals_add(ItemRecord record) { - record.currentKey = null; + record.currentIndex = null; _removedItems.put(record); if (_removalsTail == null) { @@ -1049,27 +1049,27 @@ removals: ${removals.join(", ")}' } } -class ItemRecord implements CollectionItem, AddedItem, - MovedItem, RemovedItem { - K previousKey = null; - K currentKey = null; +class ItemRecord implements CollectionItem, AddedItem, MovedItem, + RemovedItem { + int previousIndex = null; + int currentIndex = null; V item = _INITIAL_; - ItemRecord _prevRec, _nextRec; - ItemRecord _prevDupRec, _nextDupRec; - ItemRecord _prevRemovedRec, _nextRemovedRec; - ItemRecord _nextAddedRec, _nextMovedRec; + ItemRecord _prevRec, _nextRec; + ItemRecord _prevDupRec, _nextDupRec; + ItemRecord _prevRemovedRec, _nextRemovedRec; + ItemRecord _nextAddedRec, _nextMovedRec; - CollectionItem get nextCollectionItem => _nextRec; - RemovedItem get nextRemovedItem => _nextRemovedRec; - AddedItem get nextAddedItem => _nextAddedRec; - MovedItem get nextMovedItem => _nextMovedRec; + CollectionItem get nextCollectionItem => _nextRec; + RemovedItem get nextRemovedItem => _nextRemovedRec; + AddedItem get nextAddedItem => _nextAddedRec; + MovedItem get nextMovedItem => _nextMovedRec; ItemRecord(this.item); - toString() => previousKey == currentKey ? - '$item' : - '$item[$previousKey -> $currentKey]'; + String toString() => previousIndex == currentIndex + ? '$item' + : '$item[$previousIndex -> $currentIndex]'; } class _DuplicateItemRecordList { @@ -1107,7 +1107,7 @@ class _DuplicateItemRecordList { ItemRecord record = head; while(record != null) { if (hideIndex == null || - hideIndex < record.currentKey && identical(record.item, key)) { + hideIndex < record.currentIndex && identical(record.item, key)) { return record; } record = record._nextDupRec; diff --git a/test/change_detection/dirty_checking_change_detector_spec.dart b/test/change_detection/dirty_checking_change_detector_spec.dart index 4c6509be0..3e7b5fb8a 100644 --- a/test/change_detection/dirty_checking_change_detector_spec.dart +++ b/test/change_detection/dirty_checking_change_detector_spec.dart @@ -398,7 +398,7 @@ main() => describe('DirtyCheckingChangeDetector', () { it('should do basic operations', () { var k1 = 'a'; - var r1 = new ItemRecord(k1)..currentKey = 1; + var r1 = new ItemRecord(k1)..currentIndex = 1; map.put(r1); expect(map.get(k1, 2)).toEqual(null); expect(map.get(k1, 1)).toEqual(null); @@ -409,8 +409,8 @@ main() => describe('DirtyCheckingChangeDetector', () { it('should do basic operations on duplicate keys', () { var k1 = 'a'; - var r1 = new ItemRecord(k1)..currentKey = 1; - var r2 = new ItemRecord(k1)..currentKey = 2; + var r1 = new ItemRecord(k1)..currentIndex = 1; + var r2 = new ItemRecord(k1)..currentIndex = 2; map..put(r1)..put(r2); expect(map.get(k1, 0)).toEqual(r1); expect(map.get(k1, 1)).toEqual(r2); From 7188fddf515f8fef8af2195b9a52f083e87e309a Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 12 Feb 2014 16:42:23 +0100 Subject: [PATCH 14/35] test(change detector): re-add a previously failing test --- .../dirty_checking_change_detector_spec.dart | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/test/change_detection/dirty_checking_change_detector_spec.dart b/test/change_detection/dirty_checking_change_detector_spec.dart index 3e7b5fb8a..367a2fd96 100644 --- a/test/change_detection/dirty_checking_change_detector_spec.dart +++ b/test/change_detection/dirty_checking_change_detector_spec.dart @@ -294,12 +294,11 @@ main() => describe('DirtyCheckingChangeDetector', () { detector.collectChanges(); list.insert(0, 'b'); expect(list).toEqual(['b', 'a', 'a', 'b', 'b']); - // todo(vbe) There is something wrong when running this test w/ karma -// expect(detector.collectChanges().currentValue, toEqualCollectionRecord( -// collection: ['b[2 -> 0]', 'a[0 -> 1]', 'a[1 -> 2]', 'b', 'b[null -> 4]'], -// additions: ['b[null -> 4]'], -// moves: ['b[2 -> 0]', 'a[0 -> 1]', 'a[1 -> 2]'], -// removals: [])); + expect(detector.collectChanges().currentValue, toEqualCollectionRecord( + collection: ['b[2 -> 0]', 'a[0 -> 1]', 'a[1 -> 2]', 'b', 'b[null -> 4]'], + additions: ['b[null -> 4]'], + moves: ['b[2 -> 0]', 'a[0 -> 1]', 'a[1 -> 2]'], + removals: [])); }); it('should support UnmodifiableListView', () { From 39a2f29ff8d5929097734f417de3e90420b207b4 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Thu, 13 Feb 2014 14:13:47 -0800 Subject: [PATCH 15/35] docs(Scope): adding documentation --- lib/core/scope.dart | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/lib/core/scope.dart b/lib/core/scope.dart index 55417b63d..4a071831b 100644 --- a/lib/core/scope.dart +++ b/lib/core/scope.dart @@ -14,6 +14,10 @@ typedef EvalFunction1(context); class ScopeEvent { static final String DESTROY = 'ng-destroy'; + /** + * Data attached to the event. This would be the optional parameter + * from [Scope.emit] and [Scope.broadcast]. + */ final data; /** @@ -27,7 +31,10 @@ class ScopeEvent { final Scope targetScope; /** - * The destination scope that intercepted the event. + * The destination scope that intercepted the event. As + * the event traverses the scope hierarchy the the event instance + * stays the same, but the [currentScope] reflects the scope + * of the current listener which is firing. */ Scope get currentScope => _currentScope; Scope _currentScope; @@ -118,12 +125,35 @@ class ScopeLocals implements Map { dynamic putIfAbsent(key, fn) => _scope.putIfAbsent(key, fn); } +/** + * [Scope] is represents a collection of [watch]es [observe]ers, and [context] + * for the watchers, observers and [eval]uations. Scopes structure loosely + * mimics the DOM structure. Scopes and [Block]s are bound to each other. + * As scopes are created and destroyed by [BlockFactory] they are responsible + * for change detection, change processing and memory management. + */ class Scope { + + /** + * The default execution context for [watch]es [observe]ers, and [eval]uation. + */ final context; + + /** + * The [RootScope] of the application. + */ final RootScope rootScope; + Scope _parentScope; + + /** + * The parent [Scope]. + */ Scope get parentScope => _parentScope; + // TODO(misko): WatchGroup should be private. + // Instead we should expose performance stats about the watches + // such as # of watches, checks/1ms, field checks, function checks, etc final WatchGroup watchGroup; final WatchGroup observeGroup; final int _depth; @@ -136,6 +166,12 @@ class Scope { Scope(Object this.context, this.rootScope, this._parentScope, this._depth, this._index, this.watchGroup, this.observeGroup); + /** + * A [watch] sets up a watch in the [digest] phase of the [apply] cycle. + * + * Use [watch] if the reaction function can cause updates to model. In your + * controller code you will most likely use [watch]. + */ Watch watch(expression, ReactionFn reactionFn, {context, FilterMap filters}) { assert(expression != null); AST ast = expression is AST From 01efda9e0aa2428026cc3961e2aec6257aa4c29e Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Thu, 13 Feb 2014 14:14:08 -0800 Subject: [PATCH 16/35] chore(scope): clean up style / remove $ --- test/core/scope_spec.dart | 54 +++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/test/core/scope_spec.dart b/test/core/scope_spec.dart index 5543ddb2d..8ae8d5218 100644 --- a/test/core/scope_spec.dart +++ b/test/core/scope_spec.dart @@ -184,6 +184,7 @@ main() => describe('scope', () { })); }); + describe('properties', () { describe('root', () { it('should point to itself', inject((RootScope rootScope) { @@ -213,6 +214,7 @@ main() => describe('scope', () { }); }); + describe(r'events', () { describe('on', () { @@ -482,7 +484,8 @@ main() => describe('scope', () { }); }); - describe(r'$destroy', () { + + describe(r'destroy', () { var first = null, middle = null, last = null, log = null; beforeEach(inject((RootScope rootScope) { @@ -531,7 +534,7 @@ main() => describe('scope', () { })); - it(r'should broadcast the $destroy event', inject((RootScope rootScope) { + 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')); @@ -540,7 +543,8 @@ main() => describe('scope', () { expect(log).toEqual(['first', 'first-child']); })); }); - + + describe('digest lifecycle', () { it(r'should apply expression with full lifecycle', inject((RootScope rootScope) { var log = ''; @@ -554,16 +558,16 @@ main() => describe('scope', () { it(r'should catch exceptions', () { module((Module module) => module.type(ExceptionHandler, implementedBy: LoggingExceptionHandler)); inject((RootScope rootScope, ExceptionHandler e) { - LoggingExceptionHandler $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(); + expect(exceptionHandler.errors[0].error).toEqual('MyError'); + exceptionHandler.errors.removeAt(0); + exceptionHandler.assertEmpty(); }); }); @@ -584,11 +588,11 @@ main() => describe('scope', () { it(r'should execute and return value and update', inject( (RootScope rootScope, ExceptionHandler e) { - LoggingExceptionHandler $exceptionHandler = e; + LoggingExceptionHandler exceptionHandler = e; rootScope.context['name'] = 'abc'; expect(rootScope.apply((context) => context['name'])).toEqual('abc'); expect(log).toEqual('digest;digest;'); - $exceptionHandler.assertEmpty(); + exceptionHandler.assertEmpty(); })); @@ -599,11 +603,11 @@ main() => describe('scope', () { it(r'should catch exception and update', inject((RootScope rootScope, ExceptionHandler e) { - LoggingExceptionHandler $exceptionHandler = e; + LoggingExceptionHandler exceptionHandler = e; var error = 'MyError'; rootScope.apply(() { throw error; }); expect(log).toEqual('digest;digest;'); - expect($exceptionHandler.errors[0].error).toEqual(error); + expect(exceptionHandler.errors[0].error).toEqual(error); })); }); @@ -613,8 +617,8 @@ main() => describe('scope', () { expect(() => rootScope.apply(() { throw error; })).toThrow(error); })); }); - - + + describe('flush lifecycle', () { it(r'should apply expression with full lifecycle', inject((RootScope rootScope) { var log = ''; @@ -637,16 +641,16 @@ main() => describe('scope', () { it(r'should catch exceptions', () { module((Module module) => module.type(ExceptionHandler, implementedBy: LoggingExceptionHandler)); inject((RootScope rootScope, ExceptionHandler e) { - LoggingExceptionHandler $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(); + expect(exceptionHandler.errors[0].error).toEqual('MyError'); + exceptionHandler.errors.removeAt(0); + exceptionHandler.assertEmpty(); }); }); @@ -667,11 +671,11 @@ main() => describe('scope', () { it(r'should execute and return value and update', inject( (RootScope rootScope, ExceptionHandler e) { - LoggingExceptionHandler $exceptionHandler = e; + LoggingExceptionHandler exceptionHandler = e; rootScope.context['name'] = 'abc'; expect(rootScope.apply((context) => context['name'])).toEqual('abc'); expect(log).toEqual('digest;digest;'); - $exceptionHandler.assertEmpty(); + exceptionHandler.assertEmpty(); })); it(r'should execute and return value and update', inject((RootScope rootScope) { @@ -680,11 +684,11 @@ main() => describe('scope', () { })); it(r'should catch exception and update', inject((RootScope rootScope, ExceptionHandler e) { - LoggingExceptionHandler $exceptionHandler = e; + LoggingExceptionHandler exceptionHandler = e; var error = 'MyError'; rootScope.apply(() { throw error; }); expect(log).toEqual('digest;digest;'); - expect($exceptionHandler.errors[0].error).toEqual(error); + expect(exceptionHandler.errors[0].error).toEqual(error); })); it(r'should throw assertion when model changes in flush', inject((RootScope rootScope, Logger log) { @@ -793,12 +797,12 @@ main() => describe('scope', () { module.type(ExceptionHandler, implementedBy: LoggingExceptionHandler); }); inject((RootScope rootScope, ExceptionHandler e) { - LoggingExceptionHandler $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'); + expect(exceptionHandler.errors.length).toEqual(1); + expect(exceptionHandler.errors[0].error).toEqual('abc'); }); }); @@ -990,7 +994,7 @@ main() => describe('scope', () { expect(log).toEqual('parent.async;parent.digest;'); })); - it(r'should cause a $digest rerun', inject((RootScope rootScope) { + 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 From 8b49b6accbecbb9bb07e61fbc7a5efe09b01a906 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Thu, 13 Feb 2014 16:42:48 -0800 Subject: [PATCH 17/35] refactor(NgAttachAware): wait until all bindings fire. This changes the behavior in that it will wait for one time bindings to resolve before calling attach(); --- lib/core_dom/block_factory.dart | 27 ++++++++++++++++++++++++--- lib/core_dom/common.dart | 2 +- lib/core_dom/compiler.dart | 28 +++++++++++++++++++--------- test/core_dom/compiler_spec.dart | 24 ++++++++++++++++++++---- 4 files changed, 64 insertions(+), 17 deletions(-) diff --git a/lib/core_dom/block_factory.dart b/lib/core_dom/block_factory.dart index 36891b94b..23ae4242a 100644 --- a/lib/core_dom/block_factory.dart +++ b/lib/core_dom/block_factory.dart @@ -200,16 +200,37 @@ class BlockFactory { shadowScope.context[(ref.annotation as NgComponent).publishAs] = controller; } if (nodeAttrs == null) nodeAttrs = new _AnchorAttrs(ref); + var attachDelayStatus = controller is NgAttachAware ? [false] : null; + checkAttachReady() { + if (attachDelayStatus.reduce((a, b) => a && b)) { + attachDelayStatus = null; + controller.attach(); + } + } for(var map in ref.mappings) { - map(nodeAttrs, scope, controller); + var notify; + if (attachDelayStatus != null) { + var index = attachDelayStatus.length; + attachDelayStatus.add(false); + notify = () { + if (attachDelayStatus != null) { + attachDelayStatus[index] = true; + checkAttachReady(); + } + }; + } else { + notify = () => null; + } + map(nodeAttrs, scope, controller, notify); } - if (controller is NgAttachAware) { + if (attachDelayStatus != null) { Watch watch; watch = scope.watch( '1', // Cheat a bit. (_, __) { watch.remove(); - controller.attach(); + attachDelayStatus[0] = true; + checkAttachReady(); }); } if (controller is NgDetachAware) { diff --git a/lib/core_dom/common.dart b/lib/core_dom/common.dart index eeed40cd7..623e59b0d 100644 --- a/lib/core_dom/common.dart +++ b/lib/core_dom/common.dart @@ -4,7 +4,7 @@ List cloneElements(elements) { return elements.map((el) => el.clone(true)).toList(); } -typedef ApplyMapping(NodeAttrs attrs, Scope scope, Object dst); +typedef ApplyMapping(NodeAttrs attrs, Scope scope, Object dst, Function notify); class DirectiveRef { final dom.Node element; diff --git a/lib/core_dom/compiler.dart b/lib/core_dom/compiler.dart index ff99e4be6..5fadefc48 100644 --- a/lib/core_dom/compiler.dart +++ b/lib/core_dom/compiler.dart @@ -147,12 +147,15 @@ class Compiler { ApplyMapping mappingFn; switch (mode) { case '@': - mappingFn = (NodeAttrs attrs, Scope scope, Object controller) { - attrs.observe(attrName, (value) => dstPathFn.assign(controller, value)); + mappingFn = (NodeAttrs attrs, Scope scope, Object controller, Function notify) { + attrs.observe(attrName, (value) { + dstPathFn.assign(controller, value); + notify(); + }); }; break; case '<=>': - mappingFn = (NodeAttrs attrs, Scope scope, Object controller) { + mappingFn = (NodeAttrs attrs, Scope scope, Object controller, Function notify) { if (attrs[attrName] == null) return; String expression = attrs[attrName]; Expression expressionFn = _parser(expression); @@ -164,7 +167,9 @@ class Compiler { if (!blockInbound) { blockOutbound = true; scope.rootScope.runAsync(() => blockOutbound = false); - return dstPathFn.assign(controller, inboundValue); + var value = dstPathFn.assign(controller, inboundValue); + notify(); + return value; } } ); @@ -176,6 +181,7 @@ class Compiler { blockInbound = true; scope.rootScope.runAsync(() => blockInbound = false); expressionFn.assign(scope.context, outboundValue); + notify(); } }, context: controller @@ -184,16 +190,18 @@ class Compiler { }; break; case '=>': - mappingFn = (NodeAttrs attrs, Scope scope, Object controller) { + mappingFn = (NodeAttrs attrs, Scope scope, Object controller, Function notify) { if (attrs[attrName] == null) return; Expression attrExprFn = _parser(attrs[attrName]); var shadowValue = null; - scope.watch(attrs[attrName], - (v, _) => dstPathFn.assign(controller, shadowValue = v)); + scope.watch(attrs[attrName], (v, _) { + dstPathFn.assign(controller, shadowValue = v); + notify(); + }); }; break; case '=>!': - mappingFn = (NodeAttrs attrs, Scope scope, Object controller) { + mappingFn = (NodeAttrs attrs, Scope scope, Object controller, Function notify) { if (attrs[attrName] == null) return; Expression attrExprFn = _parser(attrs[attrName]); var watch; @@ -202,13 +210,15 @@ class Compiler { (value, _) { if (dstPathFn.assign(controller, value) != null) { watch.remove(); + notify(); } }); }; break; case '&': - mappingFn = (NodeAttrs attrs, Scope scope, Object dst) { + mappingFn = (NodeAttrs attrs, Scope scope, Object dst, Function notify) { dstPathFn.assign(dst, _parser(attrs[attrName]).bind(scope.context, ScopeLocals.wrapper)); + notify(); }; break; } diff --git a/test/core_dom/compiler_spec.dart b/test/core_dom/compiler_spec.dart index 2bcb8fb62..6e1eea192 100644 --- a/test/core_dom/compiler_spec.dart +++ b/test/core_dom/compiler_spec.dart @@ -441,14 +441,28 @@ main() => describe('dte.compiler', () { var scope = rootScope.createChild({}); scope.context['isReady'] = 'ready'; scope.context['logger'] = logger; - var element = $('{{logger("inner")}}'); + scope.context['once'] = null; + var element = $('{{logger("inner")}}'); $compile(element, directives)(injector.createChild([new Module()..value(Scope, scope)]), element); expect(logger).toEqual(['new']); expect(logger).toEqual(['new']); rootScope.apply(); - var expected = ['new', 'attach:@ready; =>ready', 'inner']; + var expected = ['new', '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(); + + scope.context['once'] = '123'; + rootScope.apply(); + expected = ['attach:@ready; =>ready; =>!123', 'inner']; assert((() { // there is an assertion in flush which double checks that // flushes do not change model. This assertion creates one @@ -759,7 +773,8 @@ class LogComponent { templateUrl: 'some/template.url', map: const { 'attr-value': '@attrValue', - 'expr-value': '<=>exprValue' + 'expr-value': '<=>exprValue', + 'once-value': '=>!onceValue' } ) class AttachDetachComponent implements NgAttachAware, NgDetachAware, NgShadowRootAware { @@ -767,13 +782,14 @@ class AttachDetachComponent implements NgAttachAware, NgDetachAware, NgShadowRoo Scope scope; String attrValue = 'too early'; String exprValue = 'too early'; + String onceValue = 'too early'; AttachDetachComponent(Logger this.logger, TemplateLoader templateLoader, Scope this.scope) { logger('new'); templateLoader.template.then((_) => logger('templateLoaded')); } - attach() => logger('attach:@$attrValue; =>$exprValue'); + attach() => logger('attach:@$attrValue; =>$exprValue; =>!$onceValue'); detach() => logger('detach'); onShadowRoot(shadowRoot) { scope.rootScope.context['shadowRoot'] = shadowRoot; From acb8a57e36ffd2c6e24fea7efbb5153e766e6f9b Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Thu, 13 Feb 2014 22:00:33 -0800 Subject: [PATCH 18/35] feat(scope): Experimental: Watch once, watch not null expressions This is an experimental feature, and may not get into v1.0. Depend on with caution. --- lib/core/scope.dart | 42 ++++++++++++++++++++++++++++++--------- test/core/scope_spec.dart | 28 ++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/lib/core/scope.dart b/lib/core/scope.dart index 4a071831b..45cf201d9 100644 --- a/lib/core/scope.dart +++ b/lib/core/scope.dart @@ -173,19 +173,43 @@ class Scope { * controller code you will most likely use [watch]. */ Watch watch(expression, ReactionFn reactionFn, {context, FilterMap filters}) { - assert(expression != null); - AST ast = expression is AST - ? expression - : rootScope._astParser(expression, context: context, filters: filters); - return watchGroup.watch(ast, reactionFn); + return _watch(watchGroup, expression, reactionFn, context, filters); } Watch observe(expression, ReactionFn reactionFn, {context, FilterMap filters}) { + return _watch(observeGroup, expression, reactionFn, context, filters); + } + + Watch _watch(WatchGroup group, expression, ReactionFn reactionFn, + context, FilterMap filters) { assert(expression != null); - AST ast = expression is AST - ? expression - : rootScope._astParser(expression, context: context, filters: filters); - return observeGroup.watch(ast, reactionFn); + AST ast; + Watch watch; + ReactionFn fn = reactionFn; + if (expression is AST) { + ast = expression; + } else if (expression is String) { + if (expression.startsWith('::')) { + expression = expression.substring(2); + fn = (value, last) { + if (value != null) { + watch.remove(); + return reactionFn(value, last); + } + }; + } else if (expression.startsWith(':')) { + expression = expression.substring(1); + fn = (value, last) { + if (value != null) { + return reactionFn(value, last); + } + }; + } + ast = rootScope._astParser(expression, context: context, filters: filters); + } else { + throw 'expressions must be String or AST got $expression.'; + } + return watch = group.watch(ast, fn); } dynamic eval(expression, [Map locals]) { diff --git a/test/core/scope_spec.dart b/test/core/scope_spec.dart index 8ae8d5218..3e58a4521 100644 --- a/test/core/scope_spec.dart +++ b/test/core/scope_spec.dart @@ -984,6 +984,34 @@ main() => describe('scope', () { })); }); + + describe('special binding modes', () { + it('should bind one time', inject((RootScope rootScope, Logger log) { + rootScope.watch('foo', (v, _) => log('foo:$v')); + rootScope.watch(':foo', (v, _) => log(':foo:$v')); + rootScope.watch('::foo', (v, _) => log('::foo:$v')); + + rootScope.apply(); + expect(log).toEqual(['foo:null']); + log.clear(); + + rootScope.context['foo'] = true; + rootScope.apply(); + expect(log).toEqual(['foo:true', ':foo:true', '::foo:true']); + log.clear(); + + rootScope.context['foo'] = 123; + rootScope.apply(); + expect(log).toEqual(['foo:123', ':foo:123']); + log.clear(); + + rootScope.context['foo'] = null; + rootScope.apply(); + expect(log).toEqual(['foo:null']); + log.clear(); + })); + }); + describe('runAsync', () { it(r'should run callback before watch', inject((RootScope rootScope) { From 30f63223aa3e0b1a5274f8b50e533e35298064df Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Fri, 14 Feb 2014 09:38:18 -0800 Subject: [PATCH 19/35] fix(scope): improve error msg on unstable model --- lib/change_detection/watch_group.dart | 10 +++++++--- lib/core/scope.dart | 8 +++++--- lib/core_dom/ng_mustache.dart | 4 ++-- test/core/scope_spec.dart | 8 ++++---- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/lib/change_detection/watch_group.dart b/lib/change_detection/watch_group.dart index ed6274813..53ce391ea 100644 --- a/lib/change_detection/watch_group.dart +++ b/lib/change_detection/watch_group.dart @@ -8,7 +8,7 @@ part 'ast.dart'; part 'prototype_map.dart'; typedef ReactionFn(value, previousValue); -typedef ChangeLog(expression); +typedef ChangeLog(String expression, current, previous); /** * Extend this class if you wish to pretend to be a function, but you don't know @@ -341,7 +341,9 @@ class RootWatchGroup extends WatchGroup { (_changeDetector as ChangeDetector<_Handler>) .collectChanges(exceptionHandler); while (changeRecord != null) { - if (changeLog != null) changeLog(changeRecord.handler.expression); + if (changeLog != null) changeLog(changeRecord.handler.expression, + changeRecord.currentValue, + changeRecord.previousValue); changeRecord.handler.onChange(changeRecord); changeRecord = changeRecord.nextChange; } @@ -353,7 +355,9 @@ class RootWatchGroup extends WatchGroup { try { var change = evalRecord.check(); if (change != null && changeLog != null) { - changeLog(evalRecord.handler.expression); + changeLog(evalRecord.handler.expression, + evalRecord.currentValue, + evalRecord.previousValue); } } catch (e, s) { if (exceptionHandler == null) rethrow; else exceptionHandler(e, s); diff --git a/lib/core/scope.dart b/lib/core/scope.dart index 45cf201d9..4b931fec4 100644 --- a/lib/core/scope.dart +++ b/lib/core/scope.dart @@ -352,7 +352,7 @@ class RootScope extends Scope { if (changeLog == null) { log = []; digestLog = []; - changeLog = (value) => digestLog.add(value); + changeLog = (e, c, p) => digestLog.add('$e: $c <= $p'); } else { log.add(digestLog.join(', ')); digestLog.clear(); @@ -398,8 +398,10 @@ class RootScope extends Scope { assert((() { var watchLog = []; var observeLog = []; - (watchGroup as RootWatchGroup).detectChanges(changeLog: watchLog.add); - (observeGroup as RootWatchGroup).detectChanges(changeLog: observeLog.add); + (watchGroup as RootWatchGroup).detectChanges( + changeLog: (s, c, p) => watchLog.add('$s: $c <= $p')); + (observeGroup as RootWatchGroup).detectChanges( + changeLog: (s, c, p) => watchLog.add('$s: $c <= $p')); if (watchLog.isNotEmpty || observeLog.isNotEmpty) { throw 'Observer reaction functions should not change model. \n' 'These watch changes were detected: ${watchLog.join('; ')}\n' diff --git a/lib/core_dom/ng_mustache.dart b/lib/core_dom/ng_mustache.dart index 315e0bcda..d86545ba2 100644 --- a/lib/core_dom/ng_mustache.dart +++ b/lib/core_dom/ng_mustache.dart @@ -19,7 +19,7 @@ class NgTextMustacheDirective { List items = interpolation.expressions.map((exp) { return parser(exp, filters:filters); }).toList(); - AST ast = new PureFunctionAST(markup, new ArrayFn(), items); + AST ast = new PureFunctionAST('[[$markup]]', new ArrayFn(), items); scope.observe(ast, interpolation.call); } @@ -51,7 +51,7 @@ class NgAttrMustacheDirective { List items = interpolation.expressions.map((exp) { return parser(exp, filters:filters); }).toList(); - AST ast = new PureFunctionAST(markup, new ArrayFn(), items); + 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 diff --git a/test/core/scope_spec.dart b/test/core/scope_spec.dart index 3e58a4521..3d917bc9e 100644 --- a/test/core/scope_spec.dart +++ b/test/core/scope_spec.dart @@ -708,7 +708,7 @@ main() => describe('scope', () { retValue = 2; expect(rootScope.flush). toThrow('Observer reaction functions should not change model. \n' - 'These watch changes were detected: logger("watch")\n' + 'These watch changes were detected: logger("watch"): 2 <= 1\n' 'These observe changes were detected: '); })); }); @@ -949,9 +949,9 @@ main() => describe('scope', () { rootScope.digest(); }).toThrow('Model did not stabilize in 5 digests. ' 'Last 3 iterations:\n' - 'a, b\n' - 'a, b\n' - 'a, b'); + 'a: 2 <= 1, b: 2 <= 1\n' + 'a: 3 <= 2, b: 3 <= 2\n' + 'a: 4 <= 3, b: 4 <= 3'); })); From 0f066cf44adc2360b8e2c18634b17f856ba65952 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Fri, 14 Feb 2014 09:42:18 -0800 Subject: [PATCH 20/35] fix(binding): call attach when attribute is not specified --- lib/core_dom/compiler.dart | 6 +++--- test/core_dom/compiler_spec.dart | 8 ++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/core_dom/compiler.dart b/lib/core_dom/compiler.dart index 5fadefc48..063f31420 100644 --- a/lib/core_dom/compiler.dart +++ b/lib/core_dom/compiler.dart @@ -156,7 +156,7 @@ class Compiler { break; case '<=>': mappingFn = (NodeAttrs attrs, Scope scope, Object controller, Function notify) { - if (attrs[attrName] == null) return; + if (attrs[attrName] == null) return notify(); String expression = attrs[attrName]; Expression expressionFn = _parser(expression); var blockOutbound = false; @@ -191,7 +191,7 @@ class Compiler { break; case '=>': mappingFn = (NodeAttrs attrs, Scope scope, Object controller, Function notify) { - if (attrs[attrName] == null) return; + if (attrs[attrName] == null) return notify(); Expression attrExprFn = _parser(attrs[attrName]); var shadowValue = null; scope.watch(attrs[attrName], (v, _) { @@ -202,7 +202,7 @@ class Compiler { break; case '=>!': mappingFn = (NodeAttrs attrs, Scope scope, Object controller, Function notify) { - if (attrs[attrName] == null) return; + if (attrs[attrName] == null) return notify(); Expression attrExprFn = _parser(attrs[attrName]); var watch; watch = scope.watch( diff --git a/test/core_dom/compiler_spec.dart b/test/core_dom/compiler_spec.dart index 6e1eea192..975bce2f0 100644 --- a/test/core_dom/compiler_spec.dart +++ b/test/core_dom/compiler_spec.dart @@ -436,7 +436,7 @@ main() => describe('dte.compiler', () { ..value(MockHttpBackend, backend); })); - it('should fire onTemplate method', async(inject((Logger logger, MockHttpBackend backend) { + iit('should fire onTemplate method', async(inject((Logger logger, MockHttpBackend backend) { backend.whenGET('some/template.url').respond('
    WORKED
    '); var scope = rootScope.createChild({}); scope.context['isReady'] = 'ready'; @@ -774,7 +774,10 @@ class LogComponent { map: const { 'attr-value': '@attrValue', 'expr-value': '<=>exprValue', - 'once-value': '=>!onceValue' + 'once-value': '=>!onceValue', + 'optional-one': '=>optional', + 'optional-two': '<=>optional', + 'optional-once': '=>!optional', } ) class AttachDetachComponent implements NgAttachAware, NgDetachAware, NgShadowRootAware { @@ -783,6 +786,7 @@ class AttachDetachComponent implements NgAttachAware, NgDetachAware, NgShadowRoo String attrValue = 'too early'; String exprValue = 'too early'; String onceValue = 'too early'; + String optional; AttachDetachComponent(Logger this.logger, TemplateLoader templateLoader, Scope this.scope) { logger('new'); From f9e5a3db3a5fca2b98c2eff2d69da79a0db2112b Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Fri, 14 Feb 2014 09:44:24 -0800 Subject: [PATCH 21/35] chore(style): formating --- test/core/scope_spec.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/test/core/scope_spec.dart b/test/core/scope_spec.dart index 3d917bc9e..e01a28605 100644 --- a/test/core/scope_spec.dart +++ b/test/core/scope_spec.dart @@ -1046,7 +1046,6 @@ main() => describe('scope', () { }); - describe('domRead/domWrite', () { it(r'should run writes before reads', () { module((Module module) { From b4c037353f86fb0c4a878cca407828816c45412b Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Fri, 14 Feb 2014 10:06:37 -0800 Subject: [PATCH 22/35] fixup! fix(binding): call attach when attribute is not specified --- test/core_dom/compiler_spec.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/core_dom/compiler_spec.dart b/test/core_dom/compiler_spec.dart index 975bce2f0..f0cf90a45 100644 --- a/test/core_dom/compiler_spec.dart +++ b/test/core_dom/compiler_spec.dart @@ -436,7 +436,7 @@ main() => describe('dte.compiler', () { ..value(MockHttpBackend, backend); })); - iit('should fire onTemplate method', async(inject((Logger logger, MockHttpBackend backend) { + it('should fire onTemplate method', async(inject((Logger logger, MockHttpBackend backend) { backend.whenGET('some/template.url').respond('
    WORKED
    '); var scope = rootScope.createChild({}); scope.context['isReady'] = 'ready'; From 1eecde47ed85514b51241259c929153ce378a45a Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Fri, 14 Feb 2014 10:12:14 -0800 Subject: [PATCH 23/35] fix(scope): createChild now requires context --- lib/core/scope.dart | 3 +-- lib/core_dom/common.dart | 6 ++++-- test/core/scope_spec.dart | 12 ++++++------ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/core/scope.dart b/lib/core/scope.dart index 4b931fec4..128f2bf84 100644 --- a/lib/core/scope.dart +++ b/lib/core/scope.dart @@ -249,8 +249,7 @@ class Scope { ScopeStream on(String name) => _Streams.on(this, rootScope._exceptionHandler, name); - Scope createChild([Object childContext]) { - if (childContext == null) childContext = context; + Scope createChild(Object childContext) { var child = new Scope(childContext, rootScope, this, _depth + 1, _nextChildIndex++, watchGroup.newGroup(childContext), diff --git a/lib/core_dom/common.dart b/lib/core_dom/common.dart index 623e59b0d..ec04d830e 100644 --- a/lib/core_dom/common.dart +++ b/lib/core_dom/common.dart @@ -28,8 +28,10 @@ class DirectiveRef { * services from the provided modules. */ Injector forceNewDirectivesAndFilters(Injector injector, List modules) { - modules.add(new Module() - ..factory(Scope, (i) => i.parent.get(Scope).createChild())); + modules.add(new Module()..factory(Scope, (i) { + var scope = i.parent.get(Scope); + return scope.createChild(new PrototypeMap(scope.context)); + })); return injector.createChild(modules, forceNewInstances: [DirectiveMap, FilterMap]); } diff --git a/test/core/scope_spec.dart b/test/core/scope_spec.dart index e01a28605..a6f65bed9 100644 --- a/test/core/scope_spec.dart +++ b/test/core/scope_spec.dart @@ -192,9 +192,9 @@ main() => describe('scope', () { })); it('children should point to root', inject((RootScope rootScope) { - var child = rootScope.createChild(); + var child = rootScope.createChild(new PrototypeMap(rootScope.context)); expect(child.rootScope).toEqual(rootScope); - expect(child.createChild().rootScope).toEqual(rootScope); + expect(child.createChild(new PrototypeMap(rootScope.context)).rootScope).toEqual(rootScope); })); }); @@ -206,10 +206,10 @@ main() => describe('scope', () { it('should point to parent', inject((RootScope rootScope) { - var child = rootScope.createChild(); + var child = rootScope.createChild(new PrototypeMap(rootScope.context)); expect(rootScope.parentScope).toEqual(null); expect(child.parentScope).toEqual(rootScope); - expect(child.createChild().parentScope).toEqual(child); + expect(child.createChild(new PrototypeMap(rootScope.context)).parentScope).toEqual(child); })); }); }); @@ -226,7 +226,7 @@ main() => describe('scope', () { it(r'should add listener for both emit and broadcast events', inject((RootScope rootScope) { var log = '', - child = rootScope.createChild(); + child = rootScope.createChild(new PrototypeMap(rootScope.context)); eventFn(event) { expect(event).not.toEqual(null); @@ -246,7 +246,7 @@ main() => describe('scope', () { it(r'should return a function that deregisters the listener', inject((RootScope rootScope) { var log = ''; - var child = rootScope.createChild(); + var child = rootScope.createChild(new PrototypeMap(rootScope.context)); var subscription; eventFn(e) { From 6913755d460d2f298ea66cb3f0d34fb6790f61e0 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Fri, 14 Feb 2014 10:20:41 -0800 Subject: [PATCH 24/35] chore(mustach): revert incorect watch/observ optimization --- lib/core_dom/ng_mustache.dart | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/core_dom/ng_mustache.dart b/lib/core_dom/ng_mustache.dart index d86545ba2..45387322c 100644 --- a/lib/core_dom/ng_mustache.dart +++ b/lib/core_dom/ng_mustache.dart @@ -59,12 +59,8 @@ class NgAttrMustacheDirective { 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); - }); + // TODO(misko): figure out how to get most of these on observe rather then watch. + scope.watch(ast, interpolation.call); } } From 21b41dd5029cd162c7676942d9e99d6fd029b904 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Fri, 14 Feb 2014 14:13:55 -0800 Subject: [PATCH 25/35] fix(watch_group): prevent removed watches from firing --- lib/change_detection/watch_group.dart | 1 + test/change_detection/watch_group_spec.dart | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/lib/change_detection/watch_group.dart b/lib/change_detection/watch_group.dart index 53ce391ea..13ca937d9 100644 --- a/lib/change_detection/watch_group.dart +++ b/lib/change_detection/watch_group.dart @@ -418,6 +418,7 @@ class Watch { get expression => _record.handler.expression; void invoke() { + if (_deleted || !_dirty) return; _dirty = false; reactionFn(_record.currentValue, _record.previousValue); } diff --git a/test/change_detection/watch_group_spec.dart b/test/change_detection/watch_group_spec.dart index 597b9ee77..6648fb79a 100644 --- a/test/change_detection/watch_group_spec.dart +++ b/test/change_detection/watch_group_spec.dart @@ -34,6 +34,20 @@ main() => describe('WatchGroup', () { logger = _logger; })); + describe('watch lifecycle', () { + it('should prevent reaction fn on removed', () { + context['a'] = 'hello'; + var watch ; + watchGrp.watch(parse('a'), (v, p) { + logger('removed'); + watch.remove(); + }); + watch = watchGrp.watch(parse('a'), (v, p) => logger(v)); + watchGrp.detectChanges(); + expect(logger).toEqual(['removed']); + }); + }); + describe('property chaining', () { it('should read property', () { context['a'] = 'hello'; From f0d9d6c5f45fbd04a89c23ef1a910999f2cf703a Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Fri, 14 Feb 2014 14:47:05 -0800 Subject: [PATCH 26/35] fix(scope): skip scopes whithot event on broadcast --- lib/core/scope.dart | 14 +++++++++----- test/core/scope_spec.dart | 13 +++++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/core/scope.dart b/lib/core/scope.dart index 128f2bf84..fe4f9c656 100644 --- a/lib/core/scope.dart +++ b/lib/core/scope.dart @@ -513,15 +513,19 @@ class _Streams { 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); + if(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); + if (scopeStreams != null && + scopeStreams._typeCounts.containsKey(name)) { + queue.addFirst(scopeStreams._scope); + } childScope = childScope._prev; } } diff --git a/test/core/scope_spec.dart b/test/core/scope_spec.dart index a6f65bed9..f1f72b294 100644 --- a/test/core/scope_spec.dart +++ b/test/core/scope_spec.dart @@ -448,6 +448,19 @@ main() => describe('scope', () { expect(result.name).toBe('some'); expect(result.targetScope).toBe(child1); })); + + + it('should skip scopes which dont have given event', + inject((RootScope rootScope, Logger log) { + var child1 = rootScope.createChild('A'); + rootScope.createChild('A1'); + rootScope.createChild('A2'); + rootScope.createChild('A3'); + var child2 = rootScope.createChild('B'); + child2.on('event').listen((e) => log(e.data)); + rootScope.broadcast('event', 'OK'); + expect(log).toEqual(['OK']); + })); }); From 097b298bbbad974a8e3152bfdad671f3e8fcd6a2 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Fri, 14 Feb 2014 15:12:51 -0800 Subject: [PATCH 27/35] chore(scope): rename observe to watch(readOnly:true) --- demo/bouncing_balls/bouncy_balls.dart | 4 ++-- lib/core/scope.dart | 34 ++++++++++----------------- lib/core_dom/ng_mustache.dart | 2 +- lib/directive/ng_class.dart | 9 +++---- test/core/scope_spec.dart | 12 +++++----- test/core_dom/compiler_spec.dart | 15 ++++++------ 6 files changed, 35 insertions(+), 41 deletions(-) diff --git a/demo/bouncing_balls/bouncy_balls.dart b/demo/bouncing_balls/bouncy_balls.dart index 45741b7ec..621fc5c80 100644 --- a/demo/bouncing_balls/bouncy_balls.dart +++ b/demo/bouncing_balls/bouncy_balls.dart @@ -109,8 +109,8 @@ class BallPositionDirective { set position(BallModel model) { element.style.backgroundColor = model.color; scope - ..observe('x', (x, _) => element.style.left = '${x + 10}px', context: model) - ..observe('y', (y, _) => element.style.top = '${y + 10}px', context: model); + ..watch('x', (x, _) => element.style.left = '${x + 10}px', context: model, readOnly: true) + ..watch('y', (y, _) => element.style.top = '${y + 10}px', context: model, readOnly: true); } } diff --git a/lib/core/scope.dart b/lib/core/scope.dart index fe4f9c656..4e4a8e038 100644 --- a/lib/core/scope.dart +++ b/lib/core/scope.dart @@ -154,8 +154,8 @@ class Scope { // TODO(misko): WatchGroup should be private. // Instead we should expose performance stats about the watches // such as # of watches, checks/1ms, field checks, function checks, etc - final WatchGroup watchGroup; - final WatchGroup observeGroup; + final WatchGroup _readWriteGroup; + final WatchGroup _readOnlyGroup; final int _depth; final int _index; @@ -164,7 +164,7 @@ class Scope { int _nextChildIndex = 0; Scope(Object this.context, this.rootScope, this._parentScope, this._depth, - this._index, this.watchGroup, this.observeGroup); + this._index, this._readWriteGroup, this._readOnlyGroup); /** * A [watch] sets up a watch in the [digest] phase of the [apply] cycle. @@ -172,16 +172,8 @@ class Scope { * Use [watch] if the reaction function can cause updates to model. In your * controller code you will most likely use [watch]. */ - Watch watch(expression, ReactionFn reactionFn, {context, FilterMap filters}) { - return _watch(watchGroup, expression, reactionFn, context, filters); - } - - Watch observe(expression, ReactionFn reactionFn, {context, FilterMap filters}) { - return _watch(observeGroup, expression, reactionFn, context, filters); - } - - Watch _watch(WatchGroup group, expression, ReactionFn reactionFn, - context, FilterMap filters) { + Watch watch(expression, ReactionFn reactionFn, + {context, FilterMap filters, bool readOnly: false}) { assert(expression != null); AST ast; Watch watch; @@ -209,7 +201,7 @@ class Scope { } else { throw 'expressions must be String or AST got $expression.'; } - return watch = group.watch(ast, fn); + return watch = (readOnly ? _readOnlyGroup : _readWriteGroup).watch(ast, fn); } dynamic eval(expression, [Map locals]) { @@ -252,8 +244,8 @@ class Scope { Scope createChild(Object childContext) { var child = new Scope(childContext, rootScope, this, _depth + 1, _nextChildIndex++, - watchGroup.newGroup(childContext), - observeGroup.newGroup(childContext)); + _readWriteGroup.newGroup(childContext), + _readOnlyGroup.newGroup(childContext)); var next = null; var prev = _childTail; child._next = next; @@ -279,8 +271,8 @@ class Scope { this._next = this._prev = null; - watchGroup.remove(); - observeGroup.remove(); + _readWriteGroup.remove(); + _readOnlyGroup.remove(); _Streams.destroy(this); _parentScope = null; @@ -325,7 +317,7 @@ class RootScope extends Scope { void digest() { _transitionState(null, STATE_DIGEST); try { - var rootWatchGroup = (watchGroup as RootWatchGroup); + var rootWatchGroup = (_readWriteGroup as RootWatchGroup); int digestTTL = _ttl.ttl; const int LOG_COUNT = 3; @@ -369,7 +361,7 @@ class RootScope extends Scope { void flush() { _transitionState(null, STATE_FLUSH); - var observeGroup = this.observeGroup as RootWatchGroup; + var observeGroup = this._readOnlyGroup as RootWatchGroup; bool runObservers = true; try { do { @@ -397,7 +389,7 @@ class RootScope extends Scope { assert((() { var watchLog = []; var observeLog = []; - (watchGroup as RootWatchGroup).detectChanges( + (_readWriteGroup as RootWatchGroup).detectChanges( changeLog: (s, c, p) => watchLog.add('$s: $c <= $p')); (observeGroup as RootWatchGroup).detectChanges( changeLog: (s, c, p) => watchLog.add('$s: $c <= $p')); diff --git a/lib/core_dom/ng_mustache.dart b/lib/core_dom/ng_mustache.dart index 45387322c..40a770761 100644 --- a/lib/core_dom/ng_mustache.dart +++ b/lib/core_dom/ng_mustache.dart @@ -20,7 +20,7 @@ class NgTextMustacheDirective { return parser(exp, filters:filters); }).toList(); AST ast = new PureFunctionAST('[[$markup]]', new ArrayFn(), items); - scope.observe(ast, interpolation.call); + scope.watch(ast, interpolation.call, readOnly: listener == null); } } diff --git a/lib/directive/ng_class.dart b/lib/directive/ng_class.dart index 74c216182..6c77f7302 100644 --- a/lib/directive/ng_class.dart +++ b/lib/directive/ng_class.dart @@ -164,15 +164,16 @@ abstract class _NgClassBase { set valueExpression(currentExpression) { // this should be called only once, so we don't worry about cleaning up // watcher registrations. - scope.observe( + scope.watch( _parser(currentExpression, collection: true), (current, _) { currentSet = _flatten(current); _handleChange(scope.context[r'$index']); - } + }, + readOnly: true ); if (mode != null) { - scope.observe(_parser(r'$index'), (index, oldIndex) { + scope.watch(_parser(r'$index'), (index, oldIndex) { var mod = index % 2; if (oldIndex == null || mod != oldIndex % 2) { if (mod == mode) { @@ -181,7 +182,7 @@ abstract class _NgClassBase { element.classes.removeAll(previousSet); } } - }); + }, readOnly: true); } } diff --git a/test/core/scope_spec.dart b/test/core/scope_spec.dart index f1f72b294..5255fa8be 100644 --- a/test/core/scope_spec.dart +++ b/test/core/scope_spec.dart @@ -636,7 +636,7 @@ main() => describe('scope', () { 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'; }); + rootScope.watch('a', (a, _) { log += '1'; }, readOnly: true); child.apply('parent.a = 0'); expect(log).toEqual('1'); })); @@ -645,7 +645,7 @@ main() => describe('scope', () { 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'; }); + rootScope.watch('a', (a, _) { log += '1'; }, readOnly: true); child.apply('parent.a = 0'); expect(log).toEqual('1'); })); @@ -657,7 +657,7 @@ main() => describe('scope', () { LoggingExceptionHandler exceptionHandler = e; var log = []; var child = rootScope.createChild({}); - rootScope.observe('a', (a, _) => log.add('1')); + rootScope.watch('a', (a, _) => log.add('1'), readOnly: true); rootScope.context['a'] = 0; child.apply(() { throw 'MyError'; }); expect(log.join(',')).toEqual('1'); @@ -676,7 +676,7 @@ main() => describe('scope', () { beforeEach(inject((RootScope rootScope) { rootScope.context['log'] = () { log += 'digest;'; return null; }; log = ''; - rootScope.observe('log()', (v, o) => null); + rootScope.watch('log()', (v, o) => null, readOnly: true); rootScope.digest(); log = ''; })); @@ -709,7 +709,7 @@ main() => describe('scope', () { rootScope.context['logger'] = (name) { log(name); return retValue; }; rootScope.watch('logger("watch")', (n, v) => null); - rootScope.observe('logger("flush")', (n, v) => null); + rootScope.watch('logger("flush")', (n, v) => null, readOnly: true); // clear watches rootScope.digest(); @@ -1077,7 +1077,7 @@ main() => describe('scope', () { rootScope.domWrite(() => logger('write3')); throw 'read1'; }); - rootScope.observe('value', (_, __) => logger('observe')); + rootScope.watch('value', (_, __) => logger('observe'), readOnly: true); rootScope.flush(); expect(logger).toEqual(['write1', 'write2', 'observe', 'read1', 'read2', 'write3']); expect(exceptionHandler.errors.length).toEqual(2); diff --git a/test/core_dom/compiler_spec.dart b/test/core_dom/compiler_spec.dart index f0cf90a45..f9f5c2ae9 100644 --- a/test/core_dom/compiler_spec.dart +++ b/test/core_dom/compiler_spec.dart @@ -254,17 +254,18 @@ main() => describe('dte.compiler', () { expect(rootScope.context['done']).toEqual(true); }))); - it('should should not create any watchers if no attributes are specified', async(inject((Profiler perf) { + xit('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).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); + // Re-enable once we can publish these numbers + //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(() { From 04911c1d66d43443b40ee62c3453baedbb8f4442 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Fri, 14 Feb 2014 15:16:49 -0800 Subject: [PATCH 28/35] revert(TextChangeListener): remove TextChangeListener support --- lib/core_dom/block_factory.dart | 3 +-- lib/core_dom/directive.dart | 7 ------- lib/core_dom/module.dart | 1 - lib/core_dom/ng_mustache.dart | 8 ++------ lib/directive/input_select.dart | 9 +-------- test/core_dom/ng_mustache_spec.dart | 21 --------------------- 6 files changed, 4 insertions(+), 45 deletions(-) diff --git a/lib/core_dom/block_factory.dart b/lib/core_dom/block_factory.dart index 23ae4242a..3515334ef 100644 --- a/lib/core_dom/block_factory.dart +++ b/lib/core_dom/block_factory.dart @@ -131,8 +131,7 @@ class BlockFactory { nodeModule.factory(NgTextMustacheDirective, (Injector injector) { return new NgTextMustacheDirective( node, ref.value, injector.get(Interpolate), injector.get(Scope), - injector.get(TextChangeListener), injector.get(AstParser), - injector.get(FilterMap)); + injector.get(AstParser), injector.get(FilterMap)); }); } else if (ref.type == NgAttrMustacheDirective) { if (nodesAttrsDirectives == null) { diff --git a/lib/core_dom/directive.dart b/lib/core_dom/directive.dart index 647f954ea..a1bf873b7 100644 --- a/lib/core_dom/directive.dart +++ b/lib/core_dom/directive.dart @@ -5,13 +5,6 @@ part of angular.core.dom; */ typedef AttributeChanged(String newValue); -/** - * Callback function used to notify of text changes. - */ -abstract class TextChangeListener{ - call(String text); -} - /** * NodeAttrs is a facade for element attributes. The facade is responsible * for normalizing attribute names as well as allowing access to the diff --git a/lib/core_dom/module.dart b/lib/core_dom/module.dart index 71d66adc3..6fe343371 100644 --- a/lib/core_dom/module.dart +++ b/lib/core_dom/module.dart @@ -30,7 +30,6 @@ class NgCoreDomModule extends Module { NgCoreDomModule() { value(dom.Window, dom.window); - value(TextChangeListener, null); factory(TemplateCache, (_) => new TemplateCache(capacity: 0)); type(dom.NodeTreeSanitizer, implementedBy: NullTreeSanitizer); diff --git a/lib/core_dom/ng_mustache.dart b/lib/core_dom/ng_mustache.dart index 40a770761..cf7229b17 100644 --- a/lib/core_dom/ng_mustache.dart +++ b/lib/core_dom/ng_mustache.dart @@ -7,20 +7,16 @@ class NgTextMustacheDirective { String markup, Interpolate interpolate, Scope scope, - TextChangeListener listener, AstParser parser, FilterMap filters) { Interpolation interpolation = interpolate(markup); - interpolation.setter = (text) { - element.text = text; - if (listener != null) listener.call(text); - }; + interpolation.setter = (text) => element.text = text; List items = interpolation.expressions.map((exp) { return parser(exp, filters:filters); }).toList(); AST ast = new PureFunctionAST('[[$markup]]', new ArrayFn(), items); - scope.watch(ast, interpolation.call, readOnly: listener == null); + scope.watch(ast, interpolation.call, readOnly: true); } } diff --git a/lib/directive/input_select.dart b/lib/directive/input_select.dart index d86f40041..67e5e2e41 100644 --- a/lib/directive/input_select.dart +++ b/lib/directive/input_select.dart @@ -98,9 +98,8 @@ class InputSelectDirective implements NgAttachAware { */ @NgDirective( selector: 'option', - publishTypes: const [TextChangeListener], map: const {'ng-value': '&ngValue'}) -class OptionValueDirective implements TextChangeListener, NgAttachAware, +class OptionValueDirective implements NgAttachAware, NgDetachAware { final InputSelectDirective _inputSelectDirective; final NodeAttrs _attrs; @@ -119,12 +118,6 @@ class OptionValueDirective implements TextChangeListener, NgAttachAware, } } - call(String text) { - if (_inputSelectDirective != null) { - _inputSelectDirective.dirty(); - } - } - detach() { if (_inputSelectDirective != null) { _inputSelectDirective.dirty(); diff --git a/test/core_dom/ng_mustache_spec.dart b/test/core_dom/ng_mustache_spec.dart index 9b0a59ae1..eaf881f5e 100644 --- a/test/core_dom/ng_mustache_spec.dart +++ b/test/core_dom/ng_mustache_spec.dart @@ -6,7 +6,6 @@ main() { describe('ng-mustache', () { TestBed _; beforeEach(module((Module module) { - module.type(_ListenerDirective); module.type(_HelloFilter); })); beforeEach(inject((TestBed tb) => _ = tb)); @@ -25,15 +24,6 @@ main() { })); - it('should allow listening on text change events', inject((Logger logger) { - _.compile('
    {{text}}
    '); - _.rootScope.context['text'] = 'works'; - _.rootScope.apply(); - expect(_.rootElement.text).toEqual('works'); - expect(logger).toEqual(['works']); - })); - - it('should replace {{}} in attribute', inject((Compiler $compile, Scope rootScope, Injector injector, DirectiveMap directives) { var element = $('
    '); var template = $compile(element, directives); @@ -120,17 +110,6 @@ main() { } -@NgDirective( - selector: '[listener]', - publishTypes: const [TextChangeListener], - visibility: NgDirective.DIRECT_CHILDREN_VISIBILITY -) -class _ListenerDirective implements TextChangeListener { - Logger logger; - _ListenerDirective(Logger this.logger); - call(String text) => logger(text); -} - @NgFilter(name:'hello') class _HelloFilter { call(String str) { From b0dc20f9780117212778b07cc1cdf6926c061294 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Fri, 14 Feb 2014 20:45:24 -0800 Subject: [PATCH 29/35] WIP --- demo/bouncing_balls/bouncy_balls.dart | 12 +++- demo/bouncing_balls/index.html | 2 +- lib/change_detection/change_detection.dart | 20 +++++- .../dirty_checking_change_detector.dart | 7 +- lib/change_detection/watch_group.dart | 22 +++++-- lib/core/module.dart | 2 + lib/core/scope.dart | 64 ++++++++++++++++++- pubspec.lock | 8 +-- test/core/scope_spec.dart | 4 +- 9 files changed, 125 insertions(+), 16 deletions(-) diff --git a/demo/bouncing_balls/bouncy_balls.dart b/demo/bouncing_balls/bouncy_balls.dart index 621fc5c80..1e3663774 100644 --- a/demo/bouncing_balls/bouncy_balls.dart +++ b/demo/bouncing_balls/bouncy_balls.dart @@ -73,7 +73,7 @@ class BounceController { var start = window.performance.now(); digestTime = currentDigestTime; scope.rootScope.domRead(() { - currentDigestTime = (window.performance.now() - start).round(); + currentDigestTime = window.performance.now() - start; }); } @@ -118,6 +118,16 @@ class MyModule extends Module { MyModule() { type(BounceController); type(BallPositionDirective); + value(GetterCache, new GetterCache({ + 'x': (o) => o.x, + 'y': (o) => o.y, + 'bounce': (o) => o.bounce, + 'fps': (o) => o.fps, + 'balls': (o) => o.balls, + 'length': (o) => o.length, + 'digestTime': (o) => o.digestTime, + 'ballClassName': (o) => o.ballClassName + })); } } diff --git a/demo/bouncing_balls/index.html b/demo/bouncing_balls/index.html index 568d6336d..1f765d11f 100644 --- a/demo/bouncing_balls/index.html +++ b/demo/bouncing_balls/index.html @@ -50,7 +50,7 @@ - {{bounce.fps}} fps. ({{bounce.balls.length}} balls) [{{(1000/bounce.fps).round()}} ms]
    + {{bounce.fps}} fps. ({{bounce.balls.length}} balls) [{{1000/bounce.fps}} ms]
    Digest: {{bounce.digestTime}} ms
    +1 +10 diff --git a/lib/change_detection/change_detection.dart b/lib/change_detection/change_detection.dart index 2eb49fe2e..b33b11dbb 100644 --- a/lib/change_detection/change_detection.dart +++ b/lib/change_detection/change_detection.dart @@ -54,7 +54,8 @@ abstract class ChangeDetector extends ChangeDetectorGroup { * linked list of [ChangeRecord]s. The [ChangeRecord]s are returned in the * same order as they were registered. */ - ChangeRecord collectChanges([EvalExceptionHandler exceptionHandler]); + ChangeRecord collectChanges({ EvalExceptionHandler exceptionHandler, + AvgStopwatch stopwatch }); } abstract class Record { @@ -238,3 +239,20 @@ abstract class MovedItem extends CollectionChangeItem { abstract class RemovedItem extends CollectionChangeItem { RemovedItem get nextRemovedItem; } + +class AvgStopwatch extends Stopwatch { + int _count = 0; + + int get count => _count; + + void reset() { + _count = 0; + super.reset(); + } + + int increment(int count) => _count += count; + + double get ratePerMs => elapsedMicroseconds == 0 + ? 0 + : _count / elapsedMicroseconds * 1000; +} diff --git a/lib/change_detection/dirty_checking_change_detector.dart b/lib/change_detection/dirty_checking_change_detector.dart index 24a568341..032d0d9c3 100644 --- a/lib/change_detection/dirty_checking_change_detector.dart +++ b/lib/change_detection/dirty_checking_change_detector.dart @@ -216,11 +216,14 @@ class DirtyCheckingChangeDetector extends DirtyCheckingChangeDetectorGroup implements ChangeDetector { DirtyCheckingChangeDetector(GetterCache getterCache): super(null, getterCache); - DirtyCheckingRecord collectChanges([EvalExceptionHandler exceptionHandler]) { + DirtyCheckingRecord collectChanges({ EvalExceptionHandler exceptionHandler, + AvgStopwatch stopwatch}) { + if (stopwatch != null) stopwatch.start(); DirtyCheckingRecord changeHead = null; DirtyCheckingRecord changeTail = null; DirtyCheckingRecord current = _head; // current index + int count = 0; while (current != null) { try { if (current.check() != null) { @@ -230,6 +233,7 @@ class DirtyCheckingChangeDetector extends DirtyCheckingChangeDetectorGroup changeTail = changeTail.nextChange = current; } } + if (stopwatch != null) count++; } catch (e, s) { if (exceptionHandler == null) { rethrow; @@ -240,6 +244,7 @@ class DirtyCheckingChangeDetector extends DirtyCheckingChangeDetectorGroup current = current._nextWatch; } if (changeTail != null) changeTail.nextChange = null; + if (stopwatch != null) stopwatch..stop()..increment(count); return changeHead; } diff --git a/lib/change_detection/watch_group.dart b/lib/change_detection/watch_group.dart index 13ca937d9..ed7e8d2ec 100644 --- a/lib/change_detection/watch_group.dart +++ b/lib/change_detection/watch_group.dart @@ -334,12 +334,17 @@ class RootWatchGroup extends WatchGroup { * Each step is called in sequence. ([ReactionFn]s are not called until all * previous steps are completed). */ - int detectChanges({EvalExceptionHandler exceptionHandler, - ChangeLog changeLog}) { + int detectChanges({ EvalExceptionHandler exceptionHandler, + ChangeLog changeLog, + AvgStopwatch fieldStopwatch, + AvgStopwatch evalStopwatch, + AvgStopwatch processStopwatch}) { // Process the ChangeRecords from the change detector ChangeRecord<_Handler> changeRecord = - (_changeDetector as ChangeDetector<_Handler>) - .collectChanges(exceptionHandler); + (_changeDetector as ChangeDetector<_Handler>).collectChanges( + exceptionHandler:exceptionHandler, + stopwatch: fieldStopwatch); + if (processStopwatch != null) processStopwatch.start(); while (changeRecord != null) { if (changeLog != null) changeLog(changeRecord.handler.expression, changeRecord.currentValue, @@ -347,12 +352,15 @@ class RootWatchGroup extends WatchGroup { changeRecord.handler.onChange(changeRecord); changeRecord = changeRecord.nextChange; } + if (processStopwatch != null) processStopwatch.stop(); - int count = 0; + if (evalStopwatch != null) evalStopwatch.start(); // Process our own function evaluations _EvalWatchRecord evalRecord = _evalWatchHead; + int evalCount = 0; while (evalRecord != null) { try { + if (evalStopwatch != null) evalCount++; var change = evalRecord.check(); if (change != null && changeLog != null) { changeLog(evalRecord.handler.expression, @@ -364,10 +372,13 @@ class RootWatchGroup extends WatchGroup { } evalRecord = evalRecord._nextEvalWatch; } + if (evalStopwatch != null) evalStopwatch..stop()..increment(evalCount); // Because the handler can forward changes between each other synchronously // We need to call reaction functions asynchronously. This processes the // asynchronous reaction function queue. + int count = 0; + if (processStopwatch != null) processStopwatch.stop(); Watch dirtyWatch = _dirtyWatchHead; while(dirtyWatch != null) { count++; @@ -379,6 +390,7 @@ class RootWatchGroup extends WatchGroup { dirtyWatch = dirtyWatch._nextDirtyWatch; } _dirtyWatchHead = _dirtyWatchTail = null; + if (processStopwatch != null) processStopwatch..stop()..increment(count); return count; } diff --git a/lib/core/module.dart b/lib/core/module.dart index 70ee0c5ae..9400523b2 100644 --- a/lib/core/module.dart +++ b/lib/core/module.dart @@ -3,6 +3,7 @@ library angular.core; import 'dart:async' as async; import 'dart:collection'; import 'dart:mirrors'; +import 'package:intl/intl.dart'; import 'package:di/di.dart'; @@ -17,6 +18,7 @@ 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'; +export 'package:angular/change_detection/dirty_checking_change_detector.dart'; import 'package:angular/core/parser/utils.dart'; import 'package:angular/core/parser/syntax.dart'; diff --git a/lib/core/scope.dart b/lib/core/scope.dart index 4e4a8e038..f01050cff 100644 --- a/lib/core/scope.dart +++ b/lib/core/scope.dart @@ -281,6 +281,58 @@ class Scope { } +class ScopeStats { + bool report = true; + NumberFormat nf = new NumberFormat.decimalPattern(); + RootScope _rootScope; + + AvgStopwatch digestFieldStopwatch = new AvgStopwatch(); + AvgStopwatch digestEvalStopwatch = new AvgStopwatch(); + AvgStopwatch digestProcessStopwatch = new AvgStopwatch(); + int _digestLoopNo = 0; + + AvgStopwatch flushFieldStopwatch = new AvgStopwatch(); + AvgStopwatch flushEvalStopwatch = new AvgStopwatch(); + AvgStopwatch flushProcessStopwatch = new AvgStopwatch(); + + ScopeStats() { + nf.maximumFractionDigits = 0; + } + + void digestStart() { + _digestStopwatchReset(); + _digestLoopNo = 0; + } + + _digestStopwatchReset() { + digestFieldStopwatch.reset(); + digestEvalStopwatch.reset(); + digestProcessStopwatch.reset(); + } + + void digestLoop(int changeCount) { + _digestLoopNo++; + if (report) { + print('digest #$_digestLoopNo:' + 'Field: ${_stat(digestFieldStopwatch)} ' + 'Eval: ${_stat(digestEvalStopwatch)} ' + 'Process: ${_stat(digestProcessStopwatch)}'); + } + _digestStopwatchReset(); + } + + String _stat(AvgStopwatch s) { + return '${nf.format(s.count)}' + ' / ${nf.format(s.elapsedMicroseconds)} us' + ' = ${nf.format(s.ratePerMs)} #/ms'; + } + + void digestEnd() { + + } +} + + class RootScope extends Scope { static final STATE_APPLY = 'apply'; static final STATE_DIGEST = 'digest'; @@ -297,6 +349,8 @@ class RootScope extends Scope { _FunctionChain _domWriteHead, _domWriteTail; _FunctionChain _domReadHead, _domReadTail; + ScopeStats _stats = new ScopeStats(); + String _state; RootScope(Object context, this._astParser, this._parser, @@ -310,6 +364,7 @@ class RootScope extends Scope { digest(); flush(); }; + _stats._rootScope = this; } RootScope get rootScope => this; @@ -325,6 +380,7 @@ class RootScope extends Scope { List digestLog; var count; ChangeLog changeLog; + _stats.digestStart(); do { while(_runAsyncHead != null) { try { @@ -337,7 +393,11 @@ class RootScope extends Scope { digestTTL--; count = rootWatchGroup.detectChanges( - exceptionHandler: _exceptionHandler, changeLog: changeLog); + exceptionHandler: _exceptionHandler, + changeLog: changeLog, + fieldStopwatch: _stats.digestFieldStopwatch, + evalStopwatch: _stats.digestEvalStopwatch, + processStopwatch: _stats.digestProcessStopwatch); if (digestTTL <= LOG_COUNT) { if (changeLog == null) { @@ -353,8 +413,10 @@ class RootScope extends Scope { throw 'Model did not stabilize in ${_ttl.ttl} digests. ' 'Last $LOG_COUNT iterations:\n${log.join('\n')}'; } + _stats.digestLoop(count); } while (count > 0); } finally { + _stats.digestEnd(); _transitionState(STATE_DIGEST, null); } } diff --git a/pubspec.lock b/pubspec.lock index 89cc6390a..d51273b25 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -12,7 +12,7 @@ packages: benchmark_harness: description: benchmark_harness source: hosted - version: "1.0.3" + version: "1.0.4" browser: description: browser source: hosted @@ -48,11 +48,11 @@ packages: route_hierarchical: description: route_hierarchical source: hosted - version: "0.4.12" + version: "0.4.14" shadow_dom: description: shadow_dom source: hosted - version: "0.9.2" + version: "0.9.1" source_maps: description: source_maps source: hosted @@ -68,7 +68,7 @@ packages: unmodifiable_collection: description: unmodifiable_collection source: hosted - version: "0.9.2" + version: "0.9.2+1" utf: description: utf source: hosted diff --git a/test/core/scope_spec.dart b/test/core/scope_spec.dart index 5255fa8be..95253f53c 100644 --- a/test/core/scope_spec.dart +++ b/test/core/scope_spec.dart @@ -4,7 +4,7 @@ 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('scope', () { +main() => ddescribe('scope', () { beforeEach(module((Module module) { Map context = {}; module.value(GetterCache, new GetterCache({})); @@ -847,7 +847,7 @@ main() => describe('scope', () { })); - it(r'should run digest multiple times', inject( + iit(r'should run digest multiple times', inject( (RootScope rootScope) { // tests a traversal edge case which we originally missed var log = []; From 484a87253d54b0bea6b3f0e4e67f7b7933c2f56a Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Wed, 19 Feb 2014 11:45:33 -0800 Subject: [PATCH 30/35] fix(ng-event): don't double digest --- lib/directive/ng_events.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/directive/ng_events.dart b/lib/directive/ng_events.dart index 08e9f516c..930d9974a 100644 --- a/lib/directive/ng_events.dart +++ b/lib/directive/ng_events.dart @@ -152,7 +152,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) => handler({r"$event": event})); } } From 59c4b8804f5ca5046e3f7b5ad622448d9529499d Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Wed, 19 Feb 2014 11:45:52 -0800 Subject: [PATCH 31/35] chore(test): re-enable all tests. --- test/core/scope_spec.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/core/scope_spec.dart b/test/core/scope_spec.dart index 95253f53c..dbe4d6f20 100644 --- a/test/core/scope_spec.dart +++ b/test/core/scope_spec.dart @@ -4,7 +4,7 @@ import '../_specs.dart'; import 'package:angular/change_detection/change_detection.dart' hide ExceptionHandler; import 'package:angular/change_detection/dirty_checking_change_detector.dart'; -main() => ddescribe('scope', () { +main() => describe('scope', () { beforeEach(module((Module module) { Map context = {}; module.value(GetterCache, new GetterCache({})); From 778733e6a153016b1789988ef79a1bf7e83d4b48 Mon Sep 17 00:00:00 2001 From: Pavel Jbanov Date: Wed, 19 Feb 2014 16:34:41 -0500 Subject: [PATCH 32/35] fix(compiler): don't wait indefinitly for non-null value on =>! --- lib/core_dom/compiler.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core_dom/compiler.dart b/lib/core_dom/compiler.dart index 063f31420..501c8218c 100644 --- a/lib/core_dom/compiler.dart +++ b/lib/core_dom/compiler.dart @@ -210,8 +210,8 @@ class Compiler { (value, _) { if (dstPathFn.assign(controller, value) != null) { watch.remove(); - notify(); } + notify(); }); }; break; From d6785a6aca200b631db24dd7cf4b374e5683e4ff Mon Sep 17 00:00:00 2001 From: Pavel Jbanov Date: Wed, 19 Feb 2014 16:35:14 -0500 Subject: [PATCH 33/35] fix(parser_generator): use parser getter/setter generator instead --- lib/tools/expression_extractor.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/tools/expression_extractor.dart b/lib/tools/expression_extractor.dart index ae42c07f9..b50b9dfa4 100644 --- a/lib/tools/expression_extractor.dart +++ b/lib/tools/expression_extractor.dart @@ -1,5 +1,6 @@ library angular.tools.html_expression_extractor; +import 'dart:async'; import 'dart:io'; import 'package:angular/tools/html_extractor.dart'; import 'package:angular/tools/source_metadata_extractor.dart'; @@ -13,6 +14,7 @@ import 'package:di/dynamic_injector.dart'; import 'package:angular/core/module.dart'; import 'package:angular/core/parser/parser.dart'; +import 'package:angular/tools/parser_getter_setter/generator.dart'; import 'package:angular/tools/parser_generator/generator.dart'; main(args) { @@ -53,14 +55,19 @@ main(args) { printer.printSrc('// Found ${expressions.length} expressions'); Module module = new Module() ..type(Parser, implementedBy: DynamicParser) - ..type(ParserBackend, implementedBy: DynamicParserBackend) + ..type(ParserBackend, implementedBy: DartGetterSetterGen) ..type(FilterMap, implementedBy: NullFilterMap) ..value(SourcePrinter, printer); Injector injector = new DynamicInjector(modules: [module], allowImplicitInjection: true); - // Run the generator. - injector.get(ParserGenerator).generateParser(htmlExtractor.expressions); + runZoned(() { + // Run the generator. + injector.get(ParserGetterSetter).generateParser(htmlExtractor.expressions); + }, zoneSpecification: new ZoneSpecification(print: (_, __, ___, String line) { + printer.printSrc(line); + })); + // Output footer last. if (footerFile != '') { From f8788eadb462efb734da81ae1096ba4fcaaa4cd7 Mon Sep 17 00:00:00 2001 From: Pavel Jbanov Date: Wed, 19 Feb 2014 16:35:41 -0500 Subject: [PATCH 34/35] hack(scope): ignore nulls in > operator Closes #10 --- lib/core/scope.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core/scope.dart b/lib/core/scope.dart index f01050cff..420ffd6e5 100644 --- a/lib/core/scope.dart +++ b/lib/core/scope.dart @@ -875,7 +875,7 @@ _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_greater_then(left, right) => (left == null || right == null) ? false : 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; From a6de9ee0d0d261f2d0b014598a27d69a0372a1d3 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Wed, 19 Feb 2014 13:50:05 -0800 Subject: [PATCH 35/35] fix(introspection): warnings --- lib/introspection.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/introspection.dart b/lib/introspection.dart index 9af282267..0b1085a1b 100644 --- a/lib/introspection.dart +++ b/lib/introspection.dart @@ -100,8 +100,8 @@ js.JsObject _jsInjector(Injector injector) { js.JsObject _jsScope(Scope scope) { return new js.JsObject.jsify({ "apply": scope.apply, - "digest": scope.digest, - "flush": scope.flush, + "digest": scope.rootScope.digest, + "flush": scope.rootScope.flush, "context": scope.context, "get": (name) => scope.context[name], "set": (name, value) => scope.context[name] = value