diff --git a/modules/change_detection/pubspec.yaml b/modules/change_detection/pubspec.yaml index c5150f851f..f124130c99 100644 --- a/modules/change_detection/pubspec.yaml +++ b/modules/change_detection/pubspec.yaml @@ -7,4 +7,4 @@ dependencies: dev_dependencies: test_lib: path: ../test_lib - guinness: ">=0.1.16 <0.2.0" \ No newline at end of file + guinness: ">=0.1.16 <0.2.0" diff --git a/modules/change_detection/src/collection_changes.js b/modules/change_detection/src/collection_changes.js index bd8ca52474..07e13b290e 100644 --- a/modules/change_detection/src/collection_changes.js +++ b/modules/change_detection/src/collection_changes.js @@ -54,7 +54,7 @@ export class CollectionChanges { forEachItem(fn:Function) { var record:CollectionChangeRecord; - for (record = this._itHead; record !== null; record = record.next) { + for (record = this._itHead; record !== null; record = record._next) { fn(record); } } @@ -348,7 +348,7 @@ export class CollectionChanges { } _remove(record:CollectionChangeRecord):CollectionChangeRecord { - this._addToRemovals(this._unlink(record)); + return this._addToRemovals(this._unlink(record)); } _unlink(record:CollectionChangeRecord):CollectionChangeRecord { diff --git a/modules/change_detection/src/map_changes.js b/modules/change_detection/src/map_changes.js new file mode 100644 index 0000000000..b1b4f4d635 --- /dev/null +++ b/modules/change_detection/src/map_changes.js @@ -0,0 +1,353 @@ +import {ListWrapper, MapWrapper} from 'facade/collection'; + +import {stringify, looseIdentical} from 'facade/lang'; + +export class MapChanges { + // todo(vicb) add as fields when supported + /* + final _records = new HashMap(); + Map _map; + + Map get map => _map; + + MapKeyValue _mapHead; + MapKeyValue _previousMapHead; + MapKeyValue _changesHead, _changesTail; + MapKeyValue _additionsHead, _additionsTail; + MapKeyValue _removalsHead, _removalsTail; + */ + + constructor() { + this._records = MapWrapper.create(); + this._map = null; + this._mapHead = null; + this._previousMapHead = null; + this._changesHead = null; + this._changesTail = null; + this._additionsHead = null; + this._additionsTail = null; + this._removalsHead = null; + this._removalsTail = null; + } + + get isDirty():boolean { + return this._additionsHead !== null || + this._changesHead !== null || + this._removalsHead !== null; + } + + forEachItem(fn:Function) { + var record:MapChangeRecord; + for (record = this._mapHead; record !== null; record = record._next) { + fn(record); + } + } + + forEachPreviousItem(fn:Function) { + var record:MapChangeRecord; + for (record = this._previousMapHead; record !== null; record = record._nextPrevious) { + fn(record); + } + } + + forEachChangedItem(fn:Function) { + var record:MapChangeRecord; + for (record = this._changesHead; record !== null; record = record._nextChanged) { + fn(record); + } + } + + forEachAddedItem(fn:Function){ + var record:MapChangeRecord; + for (record = this._additionsHead; record !== null; record = record._nextAdded) { + fn(record); + } + } + + forEachRemovedItem(fn:Function){ + var record:MapChangeRecord; + for (record = this._removalsHead; record !== null; record = record._nextRemoved) { + fn(record); + } + } + + check(map):boolean { + this._reset(); + this._map = map; + var records = this._records; + var oldSeqRecord:MapChangeRecord = this._mapHead; + var lastOldSeqRecord:MapChangeRecord = null; + var lastNewSeqRecord:MapChangeRecord = null; + var seqChanged:boolean = false; + + MapWrapper.forEach(map, (value, key) => { + var newSeqRecord; + if (oldSeqRecord !== null && key === oldSeqRecord.key) { + newSeqRecord = oldSeqRecord; + if (!looseIdentical(value, oldSeqRecord._currentValue)) { + oldSeqRecord._previousValue = oldSeqRecord._currentValue; + oldSeqRecord._currentValue = value; + this._addToChanges(oldSeqRecord); + } + } else { + seqChanged = true; + if (oldSeqRecord !== null) { + oldSeqRecord._next = null; + this._removeFromSeq(lastOldSeqRecord, oldSeqRecord); + this._addToRemovals(oldSeqRecord); + } + if (MapWrapper.contains(records, key)) { + newSeqRecord = MapWrapper.get(records, key); + } else { + newSeqRecord = new MapChangeRecord(key); + MapWrapper.set(records, key, newSeqRecord); + newSeqRecord._currentValue = value; + this._addToAdditions(newSeqRecord); + } + } + + if (seqChanged) { + if (this._isInRemovals(newSeqRecord)) { + this._removeFromRemovals(newSeqRecord); + } + if (lastNewSeqRecord == null) { + this._mapHead = newSeqRecord; + } else { + lastNewSeqRecord._next = newSeqRecord; + } + } + lastOldSeqRecord = oldSeqRecord; + lastNewSeqRecord = newSeqRecord; + oldSeqRecord = oldSeqRecord === null ? null : oldSeqRecord._next; + }); + this._truncate(lastOldSeqRecord, oldSeqRecord); + return this.isDirty; + } + + _reset() { + if (this.isDirty) { + var record:MapChangeRecord; + // Record the state of the mapping + for (record = this._previousMapHead = this._mapHead; + record !== null; + record = record._next) { + record._nextPrevious = record._next; + } + + for (record = this._changesHead; record !== null; record = record._nextChanged) { + record._previousValue = record._currentValue; + } + + for (record = this._additionsHead; record != null; record = record._nextAdded) { + record._previousValue = record._currentValue; + } + + // todo(vicb) once assert is supported + //assert(() { + // var r = _changesHead; + // while (r != null) { + // var nextRecord = r._nextChanged; + // r._nextChanged = null; + // r = nextRecord; + // } + // + // r = _additionsHead; + // while (r != null) { + // var nextRecord = r._nextAdded; + // r._nextAdded = null; + // r = nextRecord; + // } + // + // r = _removalsHead; + // while (r != null) { + // var nextRecord = r._nextRemoved; + // r._nextRemoved = null; + // r = nextRecord; + // } + // + // return true; + //}); + this._changesHead = this._changesTail = null; + this._additionsHead = this._additionsTail = null; + this._removalsHead = this._removalsTail = null; + } + } + + _truncate(lastRecord:MapChangeRecord, record:MapChangeRecord) { + while (record !== null) { + if (lastRecord === null) { + this._mapHead = null; + } else { + lastRecord._next = null; + } + var nextRecord = record._next; + // todo(vicb) assert + //assert((() { + // record._next = null; + // return true; + //})); + this._addToRemovals(record); + lastRecord = record; + record = nextRecord; + } + + for (var rec:MapChangeRecord = this._removalsHead; rec !== null; rec = rec._nextRemoved) { + rec._previousValue = rec._currentValue; + rec._currentValue = null; + MapWrapper.delete(this._records, rec.key); + } + } + + _isInRemovals(record:MapChangeRecord) { + return record === this._removalsHead || + record._nextRemoved !== null || + record._prevRemoved !== null; + } + + _addToRemovals(record:MapChangeRecord) { + // todo(vicb) assert + //assert(record._next == null); + //assert(record._nextAdded == null); + //assert(record._nextChanged == null); + //assert(record._nextRemoved == null); + //assert(record._prevRemoved == null); + if (this._removalsHead === null) { + this._removalsHead = this._removalsTail = record; + } else { + this._removalsTail._nextRemoved = record; + record._prevRemoved = this._removalsTail; + this._removalsTail = record; + } + } + + _removeFromSeq(prev:MapChangeRecord, record:MapChangeRecord) { + var next = record._next; + if (prev === null) { + this._mapHead = next; + } else { + prev._next = next; + } + // todo(vicb) assert + //assert((() { + // record._next = null; + // return true; + //})()); + } + + _removeFromRemovals(record:MapChangeRecord) { + // todo(vicb) assert + //assert(record._next == null); + //assert(record._nextAdded == null); + //assert(record._nextChanged == null); + + var prev = record._prevRemoved; + var next = record._nextRemoved; + if (prev === null) { + this._removalsHead = next; + } else { + prev._nextRemoved = next; + } + if (next === null) { + this._removalsTail = prev; + } else { + next._prevRemoved = prev; + } + record._prevRemoved = record._nextRemoved = null; + } + + _addToAdditions(record:MapChangeRecord) { + // todo(vicb): assert + //assert(record._next == null); + //assert(record._nextAdded == null); + //assert(record._nextChanged == null); + //assert(record._nextRemoved == null); + //assert(record._prevRemoved == null); + if (this._additionsHead === null) { + this._additionsHead = this._additionsTail = record; + } else { + this._additionsTail._nextAdded = record; + this._additionsTail = record; + } + } + + _addToChanges(record:MapChangeRecord) { + // todo(vicb) assert + //assert(record._nextAdded == null); + //assert(record._nextChanged == null); + //assert(record._nextRemoved == null); + //assert(record._prevRemoved == null); + if (this._changesHead === null) { + this._changesHead = this._changesTail = record; + } else { + this._changesTail._nextChanged = record; + this._changesTail = record; + } + } + + toString():string { + var items = []; + var previous = []; + var changes = []; + var additions = []; + var removals = []; + var record:MapChangeRecord; + + for (record = this._mapHead; record !== null; record = record._next) { + ListWrapper.push(items, stringify(record)); + } + for (record = this._previousMapHead; record !== null; record = record._nextPrevious) { + ListWrapper.push(previous, stringify(record)); + } + for (record = this._changesHead; record !== null; record = record._nextChanged) { + ListWrapper.push(changes, stringify(record)); + } + for (record = this._additionsHead; record !== null; record = record._nextAdded) { + ListWrapper.push(additions, stringify(record)); + } + for (record = this._removalsHead; record !== null; record = record._nextRemoved) { + ListWrapper.push(removals, stringify(record)); + } + + return "map: " + items.join(', ') + "\n" + + "previous: " + previous.join(', ') + "\n" + + "additions: " + additions.join(', ') + "\n" + + "changes: " + changes.join(', ') + "\n" + + "removals: " + removals.join(', ') + "\n"; + } +} + +export class MapChangeRecord { + // todo(vicb) add as fields + //final K key; + //V _previousValue, _currentValue; + // + //V get previousValue => _previousValue; + //V get currentValue => _currentValue; + // + //MapKeyValue _nextPrevious; + //MapKeyValue _next; + //MapKeyValue _nextAdded; + //MapKeyValue _nextRemoved, _prevRemoved; + //MapKeyValue _nextChanged; + + constructor(key) { + this.key = key; + this._previousValue = null; + this._currentValue = null; + + this._nextPrevious = null; + this._next = null; + this._nextAdded = null; + this._nextRemoved = null; + this._prevRemoved = null; + this._nextChanged = null; + } + + toString():string { + return looseIdentical(this._previousValue, this._currentValue) ? + stringify(this.key) : + (stringify(this.key) + '[' + stringify(this._previousValue) + '->' + + stringify(this._currentValue) + ']'); + } + +} diff --git a/modules/change_detection/test/map_changes_spec.js b/modules/change_detection/test/map_changes_spec.js new file mode 100644 index 0000000000..f8fa3a4b5a --- /dev/null +++ b/modules/change_detection/test/map_changes_spec.js @@ -0,0 +1,147 @@ +import {describe, it, iit, xit, expect, beforeEach, afterEach} from 'test_lib/test_lib'; + +import {MapChanges} from 'change_detection/map_changes'; + +import {isBlank, NumberWrapper} from 'facade/lang'; + +import {MapWrapper} from 'facade/collection'; + +// todo(vicb): Update the code & tests for object equality +export function main() { + describe('map_changes', function() { + describe('MapChanges', function() { + var changes; + var m; + + beforeEach(() => { + changes = new MapChanges(); + m = MapWrapper.create(); + }); + + afterEach(() => { + changes = null; + }); + + it('should detect additions', () => { + changes.check(m); + + MapWrapper.set(m, 'a', 1); + changes.check(m); + expect(changes.toString()).toEqual(changesAsString({ + map: ['a[null->1]'], + additions: ['a[null->1]'] + })); + + MapWrapper.set(m, 'b', 2); + changes.check(m); + expect(changes.toString()).toEqual(changesAsString({ + map: ['a', 'b[null->2]'], + previous: ['a'], + additions: ['b[null->2]'] + })); + }); + + it('should handle changing key/values correctly', () => { + MapWrapper.set(m, 1, 10); + MapWrapper.set(m, 2, 20); + changes.check(m); + + MapWrapper.set(m, 2, 10); + MapWrapper.set(m, 1, 20); + changes.check(m); + expect(changes.toString()).toEqual(changesAsString({ + map: ['1[10->20]', '2[20->10]'], + previous: ['1[10->20]', '2[20->10]'], + changes: ['1[10->20]', '2[20->10]'] + })); + }); + + it('should do basic map watching', () => { + changes.check(m); + + MapWrapper.set(m, 'a', 'A'); + changes.check(m); + expect(changes.toString()).toEqual(changesAsString({ + map: ['a[null->A]'], + additions: ['a[null->A]'] + })); + + MapWrapper.set(m, 'b', 'B'); + changes.check(m); + expect(changes.toString()).toEqual(changesAsString({ + map: ['a', 'b[null->B]'], + previous: ['a'], + additions: ['b[null->B]'] + })); + + MapWrapper.set(m, 'b', 'BB'); + MapWrapper.set(m, 'd', 'D'); + changes.check(m); + expect(changes.toString()).toEqual(changesAsString({ + map: ['a', 'b[B->BB]', 'd[null->D]'], + previous: ['a', 'b[B->BB]'], + additions: ['d[null->D]'], + changes: ['b[B->BB]'] + })); + + MapWrapper.delete(m, 'b'); + changes.check(m); + expect(changes.toString()).toEqual(changesAsString({ + map: ['a', 'd'], + previous: ['a', 'b[BB->null]', 'd'], + removals: ['b[BB->null]'] + })); + + MapWrapper.clear(m); + changes.check(m); + expect(changes.toString()).toEqual(changesAsString({ + previous: ['a[A->null]', 'd[D->null]'], + removals: ['a[A->null]', 'd[D->null]'] + })); + }); + + it('should test string by value rather than by reference (DART)', () => { + MapWrapper.set(m, 'foo', 'bar'); + changes.check(m); + + var f = 'f'; + var oo = 'oo'; + var b = 'b'; + var ar = 'ar'; + + MapWrapper.set(m, f + oo, b + ar); + changes.check(m); + + expect(changes.toString()).toEqual(changesAsString({ + map: ['foo'], + previous: ['foo'] + })); + }); + + it('should not see a NaN value as a change (JS)', () => { + MapWrapper.set(m, 'foo', NumberWrapper.NaN); + changes.check(m); + + changes.check(m); + expect(changes.toString()).toEqual(changesAsString({ + map: ['foo'], + previous: ['foo'] + })); + }); + }); + }); +} + +function changesAsString({map, previous, additions, changes, removals}) { + if (isBlank(map)) map = []; + if (isBlank(previous)) previous = []; + if (isBlank(additions)) additions = []; + if (isBlank(changes)) changes = []; + if (isBlank(removals)) removals = []; + + return "map: " + map.join(', ') + "\n" + + "previous: " + previous.join(', ') + "\n" + + "additions: " + additions.join(', ') + "\n" + + "changes: " + changes.join(', ') + "\n" + + "removals: " + removals.join(', ') + "\n"; +} diff --git a/modules/core/src/compiler/view.js b/modules/core/src/compiler/view.js index 362457bb87..f489ef329b 100644 --- a/modules/core/src/compiler/view.js +++ b/modules/core/src/compiler/view.js @@ -3,7 +3,10 @@ import {ListWrapper} from 'facade/collection'; import {ProtoWatchGroup, WatchGroup, WatchGroupDispatcher} from 'change_detection/watch_group'; import {Record} from 'change_detection/record'; import {ProtoElementInjector, ElementInjector} from './element_injector'; -import {ElementBinder} from './element_binder'; +// Seems like we are stripping the generics part of List and dartanalyzer +// complains about ElementBinder being unused. Comment back in once it makes it +// into the generated code. +// import {ElementBinder} from './element_binder'; import {SetterFn} from 'change_detection/parser/closure_map'; import {FIELD, IMPLEMENTS, int, isPresent, isBlank} from 'facade/lang'; import {List} from 'facade/collection'; diff --git a/modules/facade/src/lang.dart b/modules/facade/src/lang.dart index e83019673c..62d645d124 100644 --- a/modules/facade/src/lang.dart +++ b/modules/facade/src/lang.dart @@ -123,6 +123,8 @@ class BaseException extends Error { } } +const _NAN_KEY = const Object(); + // Dart can have identical(str1, str2) == false while str1 == str2 bool looseIdentical(a, b) => a is String && b is String ? a == b : identical(a, b);