From bf71b94bde8fc2abfebed8df9a948460727d45e3 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Mon, 24 Nov 2014 18:42:53 +0100 Subject: [PATCH] feat(Change Detector): Add support for collection content watch --- ...collection_changes.js => array_changes.js} | 32 +++- .../{map_changes.js => keyvalue_changes.js} | 133 +++++++++-------- modules/change_detection/src/parser/ast.js | 36 +++-- modules/change_detection/src/record.js | 130 ++++++++++++---- modules/change_detection/src/record_range.js | 36 ++++- ..._changes_spec.js => array_changes_spec.js} | 101 +++++++------ .../test/change_detector_spec.js | 140 ++++++++++++++++-- modules/change_detection/test/iterable.dart | 6 + modules/change_detection/test/iterable.es6 | 9 ++ ...anges_spec.js => keyvalue_changes_spec.js} | 107 +++++++++---- modules/change_detection/test/util.js | 29 ++++ modules/facade/src/collection.dart | 11 +- modules/facade/src/collection.es6 | 34 ++++- modules/facade/src/lang.dart | 7 +- modules/facade/src/lang.es6 | 4 + 15 files changed, 609 insertions(+), 206 deletions(-) rename modules/change_detection/src/{collection_changes.js => array_changes.js} (95%) rename modules/change_detection/src/{map_changes.js => keyvalue_changes.js} (75%) rename modules/change_detection/test/{collection_changes_spec.js => array_changes_spec.js} (71%) create mode 100644 modules/change_detection/test/iterable.dart create mode 100644 modules/change_detection/test/iterable.es6 rename modules/change_detection/test/{map_changes_spec.js => keyvalue_changes_spec.js} (50%) create mode 100644 modules/change_detection/test/util.js diff --git a/modules/change_detection/src/collection_changes.js b/modules/change_detection/src/array_changes.js similarity index 95% rename from modules/change_detection/src/collection_changes.js rename to modules/change_detection/src/array_changes.js index 9296be4f0a..8fd108807c 100644 --- a/modules/change_detection/src/collection_changes.js +++ b/modules/change_detection/src/array_changes.js @@ -1,4 +1,6 @@ import { + isListLikeIterable, + iterateListLike, ListWrapper, MapWrapper } from 'facade/collection'; @@ -12,20 +14,21 @@ import { looseIdentical, } from 'facade/lang'; -export class CollectionChanges { +export class ArrayChanges { _collection; _length:int; _linkedRecords:_DuplicateMap; _unlinkedRecords:_DuplicateMap; - _previousItHead:CollectionChangeRecord ; + _previousItHead:CollectionChangeRecord; _itHead:CollectionChangeRecord; - _itTail:CollectionChangeRecord ; + _itTail:CollectionChangeRecord; _additionsHead:CollectionChangeRecord; - _additionsTail:CollectionChangeRecord ; + _additionsTail:CollectionChangeRecord; _movesHead:CollectionChangeRecord; _movesTail:CollectionChangeRecord ; _removalsHead:CollectionChangeRecord; - _removalsTail:CollectionChangeRecord ; + _removalsTail:CollectionChangeRecord; + constructor() { this._collection = null; this._length = null; @@ -45,6 +48,10 @@ export class CollectionChanges { this._removalsTail = null; } + static supports(obj):boolean { + return isListLikeIterable(obj); + } + get collection() { return this._collection; } @@ -112,8 +119,19 @@ export class CollectionChanges { record = record._next; } } else { - // todo(vicb) implement iterators - throw "NYI"; + index = 0; + iterateListLike(collection, (item) => { + if (record === null || !looseIdentical(record.item, item)) { + record = this._mismatch(record, item, index); + mayBeDirty = true; + } else if (mayBeDirty) { + // TODO(misko): can we limit this to duplicates only? + record = this._verifyReinsertion(record, item, index); + } + record = record._next; + index++ + }); + this._length = index; } this._truncate(record); diff --git a/modules/change_detection/src/map_changes.js b/modules/change_detection/src/keyvalue_changes.js similarity index 75% rename from modules/change_detection/src/map_changes.js rename to modules/change_detection/src/keyvalue_changes.js index c2f2c69ba7..f43b981e82 100644 --- a/modules/change_detection/src/map_changes.js +++ b/modules/change_detection/src/keyvalue_changes.js @@ -1,19 +1,19 @@ -import {ListWrapper, MapWrapper} from 'facade/collection'; +import {ListWrapper, MapWrapper, StringMapWrapper} from 'facade/collection'; -import {stringify, looseIdentical} from 'facade/lang'; +import {stringify, looseIdentical, isJsObject} from 'facade/lang'; -export class MapChanges { +export class KeyValueChanges { _records:Map; - _map:Map; + _map:any; - _mapHead:MapChangeRecord; - _previousMapHead:MapChangeRecord; - _changesHead:MapChangeRecord; - _changesTail:MapChangeRecord; - _additionsHead:MapChangeRecord; - _additionsTail:MapChangeRecord; - _removalsHead:MapChangeRecord; - _removalsTail:MapChangeRecord; + _mapHead:KVChangeRecord; + _previousMapHead:KVChangeRecord; + _changesHead:KVChangeRecord; + _changesTail:KVChangeRecord; + _additionsHead:KVChangeRecord; + _additionsTail:KVChangeRecord; + _removalsHead:KVChangeRecord; + _removalsTail:KVChangeRecord; constructor() { this._records = MapWrapper.create(); @@ -28,57 +28,61 @@ export class MapChanges { this._removalsTail = null; } + static supports(obj):boolean { + return obj instanceof Map || isJsObject(obj); + } + 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); - } + forEachItem(fn:Function) { + var record:KVChangeRecord; + 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); - } + forEachPreviousItem(fn:Function) { + var record:KVChangeRecord; + 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); - } + forEachChangedItem(fn:Function) { + var record:KVChangeRecord; + 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); - } + forEachAddedItem(fn:Function){ + var record:KVChangeRecord; + 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); - } + forEachRemovedItem(fn:Function){ + var record:KVChangeRecord; + 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 oldSeqRecord:KVChangeRecord = this._mapHead; + var lastOldSeqRecord:KVChangeRecord = null; + var lastNewSeqRecord:KVChangeRecord = null; var seqChanged:boolean = false; - MapWrapper.forEach(map, (value, key) => { + this._forEach(map, (value, key) => { var newSeqRecord; if (oldSeqRecord !== null && key === oldSeqRecord.key) { newSeqRecord = oldSeqRecord; @@ -97,7 +101,7 @@ export class MapChanges { if (MapWrapper.contains(records, key)) { newSeqRecord = MapWrapper.get(records, key); } else { - newSeqRecord = new MapChangeRecord(key); + newSeqRecord = new KVChangeRecord(key); MapWrapper.set(records, key, newSeqRecord); newSeqRecord._currentValue = value; this._addToAdditions(newSeqRecord); @@ -124,7 +128,7 @@ export class MapChanges { _reset() { if (this.isDirty) { - var record:MapChangeRecord; + var record:KVChangeRecord; // Record the state of the mapping for (record = this._previousMapHead = this._mapHead; record !== null; @@ -171,7 +175,7 @@ export class MapChanges { } } - _truncate(lastRecord:MapChangeRecord, record:MapChangeRecord) { + _truncate(lastRecord:KVChangeRecord, record:KVChangeRecord) { while (record !== null) { if (lastRecord === null) { this._mapHead = null; @@ -189,20 +193,20 @@ export class MapChanges { record = nextRecord; } - for (var rec:MapChangeRecord = this._removalsHead; rec !== null; rec = rec._nextRemoved) { + for (var rec:KVChangeRecord = this._removalsHead; rec !== null; rec = rec._nextRemoved) { rec._previousValue = rec._currentValue; rec._currentValue = null; MapWrapper.delete(this._records, rec.key); } } - _isInRemovals(record:MapChangeRecord) { + _isInRemovals(record:KVChangeRecord) { return record === this._removalsHead || record._nextRemoved !== null || record._prevRemoved !== null; } - _addToRemovals(record:MapChangeRecord) { + _addToRemovals(record:KVChangeRecord) { // todo(vicb) assert //assert(record._next == null); //assert(record._nextAdded == null); @@ -218,7 +222,7 @@ export class MapChanges { } } - _removeFromSeq(prev:MapChangeRecord, record:MapChangeRecord) { + _removeFromSeq(prev:KVChangeRecord, record:KVChangeRecord) { var next = record._next; if (prev === null) { this._mapHead = next; @@ -232,7 +236,7 @@ export class MapChanges { //})()); } - _removeFromRemovals(record:MapChangeRecord) { + _removeFromRemovals(record:KVChangeRecord) { // todo(vicb) assert //assert(record._next == null); //assert(record._nextAdded == null); @@ -253,7 +257,7 @@ export class MapChanges { record._prevRemoved = record._nextRemoved = null; } - _addToAdditions(record:MapChangeRecord) { + _addToAdditions(record:KVChangeRecord) { // todo(vicb): assert //assert(record._next == null); //assert(record._nextAdded == null); @@ -268,7 +272,7 @@ export class MapChanges { } } - _addToChanges(record:MapChangeRecord) { + _addToChanges(record:KVChangeRecord) { // todo(vicb) assert //assert(record._nextAdded == null); //assert(record._nextChanged == null); @@ -288,7 +292,7 @@ export class MapChanges { var changes = []; var additions = []; var removals = []; - var record:MapChangeRecord; + var record:KVChangeRecord; for (record = this._mapHead; record !== null; record = record._next) { ListWrapper.push(items, stringify(record)); @@ -312,19 +316,29 @@ export class MapChanges { "changes: " + changes.join(', ') + "\n" + "removals: " + removals.join(', ') + "\n"; } + + _forEach(obj, fn:Function) { + if (obj instanceof Map) { + MapWrapper.forEach(obj, fn); + } else { + StringMapWrapper.forEach(obj, fn); + } + } } -export class MapChangeRecord { + + +export class KVChangeRecord { key; _previousValue; _currentValue; - _nextPrevious:MapChangeRecord; - _next:MapChangeRecord; - _nextAdded:MapChangeRecord; - _nextRemoved:MapChangeRecord; - _prevRemoved:MapChangeRecord; - _nextChanged:MapChangeRecord; + _nextPrevious:KVChangeRecord; + _next:KVChangeRecord; + _nextAdded:KVChangeRecord; + _nextRemoved:KVChangeRecord; + _prevRemoved:KVChangeRecord; + _nextChanged:KVChangeRecord; constructor(key) { this.key = key; @@ -345,5 +359,4 @@ export class MapChangeRecord { (stringify(this.key) + '[' + stringify(this._previousValue) + '->' + stringify(this._currentValue) + ']'); } - } diff --git a/modules/change_detection/src/parser/ast.js b/modules/change_detection/src/parser/ast.js index a0d3cc0dd9..0133276ef3 100644 --- a/modules/change_detection/src/parser/ast.js +++ b/modules/change_detection/src/parser/ast.js @@ -29,6 +29,21 @@ export class EmptyExpr extends AST { } } +export class Collection extends AST { + value:AST; + constructor(value:AST) { + this.value = value; + } + + eval(context) { + return value.eval(context); + } + + visit(visitor, args) { + visitor.visitCollection(this, args); + } +} + export class ImplicitReceiver extends AST { eval(context) { return context; @@ -386,20 +401,21 @@ export class TemplateBinding { //INTERFACE export class AstVisitor { - visitChain(ast:Chain, args){} - visitImplicitReceiver(ast:ImplicitReceiver, args) {} - visitConditional(ast:Conditional, args) {} visitAccessMember(ast:AccessMember, args) {} - visitKeyedAccess(ast:KeyedAccess, args) {} - visitBinary(ast:Binary, args) {} - visitPrefixNot(ast:PrefixNot, args) {} - visitLiteralPrimitive(ast:LiteralPrimitive, args) {} - visitFormatter(ast:Formatter, args) {} visitAssignment(ast:Assignment, args) {} + visitBinary(ast:Binary, args) {} + visitChain(ast:Chain, args){} + visitCollection(ast:Collection, args) {} + visitConditional(ast:Conditional, args) {} + visitFormatter(ast:Formatter, args) {} + visitFunctionCall(ast:FunctionCall, args) {} + visitImplicitReceiver(ast:ImplicitReceiver, args) {} + visitKeyedAccess(ast:KeyedAccess, args) {} visitLiteralArray(ast:LiteralArray, args) {} visitLiteralMap(ast:LiteralMap, args) {} + visitLiteralPrimitive(ast:LiteralPrimitive, args) {} visitMethodCall(ast:MethodCall, args) {} - visitFunctionCall(ast:FunctionCall, args) {} + visitPrefixNot(ast:PrefixNot, args) {} } var _evalListCache = [[],[0],[0,0],[0,0,0],[0,0,0,0],[0,0,0,0,0]]; @@ -410,4 +426,4 @@ function evalList(context, exps:List){ result[i] = exps[i].eval(context); } return result; -} \ No newline at end of file +} diff --git a/modules/change_detection/src/record.js b/modules/change_detection/src/record.js index 6ed117898e..464ca3e7c9 100644 --- a/modules/change_detection/src/record.js +++ b/modules/change_detection/src/record.js @@ -1,6 +1,8 @@ import {ProtoRecordRange, RecordRange} from './record_range'; import {FIELD, isPresent, isBlank, int, StringWrapper, FunctionWrapper, BaseException} from 'facade/lang'; import {List, Map, ListWrapper, MapWrapper} from 'facade/collection'; +import {ArrayChanges} from './array_changes'; +import {KeyValueChanges} from './keyvalue_changes'; var _fresh = new Object(); @@ -10,15 +12,15 @@ export const RECORD_TYPE_INVOKE_CLOSURE = 0x0001; export const RECORD_TYPE_INVOKE_FORMATTER = 0x0002; export const RECORD_TYPE_INVOKE_METHOD = 0x0003; export const RECORD_TYPE_INVOKE_PURE_FUNCTION = 0x0004; -export const RECORD_TYPE_LIST = 0x0005; -export const RECORD_TYPE_MAP = 0x0006; -export const RECORD_TYPE_MARKER = 0x0007; +const RECORD_TYPE_ARRAY = 0x0005; +const RECORD_TYPE_KEY_VALUE = 0x0006; +const RECORD_TYPE_MARKER = 0x0007; export const RECORD_TYPE_PROPERTY = 0x0008; +const RECORD_TYPE_NULL= 0x0009; const RECORD_FLAG_DISABLED = 0x0100; export const RECORD_FLAG_IMPLICIT_RECEIVER = 0x0200; - - +export const RECORD_FLAG_COLLECTION = 0x0400; /** * For now we are dropping expression coalescence. We can always add it later, but @@ -112,7 +114,6 @@ export class Record { this.dest = null; this.previousValue = null; - this.currentValue = _fresh; this.context = null; this.funcOrValue = null; @@ -125,6 +126,11 @@ export class Record { this._mode = protoRecord._mode; + // Return early for collections, further init delayed until updateContext() + if (this.isCollection) return; + + this.currentValue = _fresh; + var type = this.type; if (type === RECORD_TYPE_CONST) { @@ -150,15 +156,21 @@ export class Record { } } - get type() { + // todo(vicb): getter / setters are much slower than regular methods + // todo(vicb): update the whole code base + get type():int { return this._mode & RECORD_TYPE_MASK; } - get disabled() { + set type(value:int) { + this._mode = (this._mode & ~RECORD_TYPE_MASK) | value; + } + + get disabled():boolean { return (this._mode & RECORD_FLAG_DISABLED) === RECORD_FLAG_DISABLED; } - set disabled(value) { + set disabled(value:boolean) { if (value) { this._mode |= RECORD_FLAG_DISABLED; } else { @@ -166,23 +178,33 @@ export class Record { } } - get isImplicitReceiver() { + get isImplicitReceiver():boolean { return (this._mode & RECORD_FLAG_IMPLICIT_RECEIVER) === RECORD_FLAG_IMPLICIT_RECEIVER; } - static createMarker(rr:RecordRange) { + get isCollection():boolean { + return (this._mode & RECORD_FLAG_COLLECTION) === RECORD_FLAG_COLLECTION; + } + + static createMarker(rr:RecordRange):Record { return new Record(rr, null, null); } check():boolean { - this.previousValue = this.currentValue; - this.currentValue = this._calculateNewValue(); - - if (isSame(this.previousValue, this.currentValue)) return false; - - this._updateDestination(); - - return true; + if (this.isCollection) { + var changed = this._checkCollection(); + if (changed) { + this._notifyDispatcher(); + return true; + } + return false; + } else { + this.previousValue = this.currentValue; + this.currentValue = this._calculateNewValue(); + if (isSame(this.previousValue, this.currentValue)) return false; + this._updateDestination(); + return true; + } } _updateDestination() { @@ -194,13 +216,38 @@ export class Record { this.dest.updateContext(this.currentValue); } } else { - this.recordRange.dispatcher.onRecordChange(this, this.protoRecord.dest); + this._notifyDispatcher(); + } + } + + _notifyDispatcher() { + this.recordRange.dispatcher.onRecordChange(this, this.protoRecord.dest); + } + + // return whether the content has changed + _checkCollection():boolean { + switch(this.type) { + case RECORD_TYPE_KEY_VALUE: + var kvChangeDetector:KeyValueChanges = this.currentValue; + return kvChangeDetector.check(this.context); + + case RECORD_TYPE_ARRAY: + var arrayChangeDetector:ArrayChanges = this.currentValue; + return arrayChangeDetector.check(this.context); + + case RECORD_TYPE_NULL: + // no need to check the content again unless the context changes + this.recordRange.disableRecord(this); + this.currentValue = null; + return true; + + default: + throw new BaseException(`Unsupported record type (${this.type})`); } } _calculateNewValue() { - var type = this.type; - switch (type) { + switch (this.type) { case RECORD_TYPE_PROPERTY: return this.funcOrValue(this.context); @@ -219,17 +266,8 @@ export class Record { this.recordRange.disableRecord(this); return this.funcOrValue; - case RECORD_TYPE_MARKER: - throw new BaseException('Marker not implemented'); - - case RECORD_TYPE_MAP: - throw new BaseException('Map not implemented'); - - case RECORD_TYPE_LIST: - throw new BaseException('List not implemented'); - default: - throw new BaseException(`Unsupported record type ($type)`); + throw new BaseException(`Unsupported record type (${this.type})`); } } @@ -241,6 +279,34 @@ export class Record { updateContext(value) { this.context = value; this.recordRange.enableRecord(this); + + if (!this.isMarkerRecord) { + this.recordRange.enableRecord(this); + } + + if (this.isCollection) { + if (ArrayChanges.supports(value)) { + if (this.type != RECORD_TYPE_ARRAY) { + this.type = RECORD_TYPE_ARRAY; + this.currentValue = new ArrayChanges(); + } + return; + } + + if (KeyValueChanges.supports(value)) { + if (this.type != RECORD_TYPE_KEY_VALUE) { + this.type = RECORD_TYPE_KEY_VALUE; + this.currentValue = new KeyValueChanges(); + } + return; + } + + if (isBlank(value)) { + this.type = RECORD_TYPE_NULL; + } else { + throw new BaseException("Collection records must be array like, map like or null"); + } + } } get isMarkerRecord() { diff --git a/modules/change_detection/src/record_range.js b/modules/change_detection/src/record_range.js index 7ead66657c..e97dd702d3 100644 --- a/modules/change_detection/src/record_range.js +++ b/modules/change_detection/src/record_range.js @@ -1,6 +1,7 @@ import { ProtoRecord, Record, + RECORD_FLAG_COLLECTION, RECORD_FLAG_IMPLICIT_RECEIVER, RECORD_TYPE_CONST, RECORD_TYPE_INVOKE_CLOSURE, @@ -13,10 +14,26 @@ import { import {FIELD, IMPLEMENTS, isBlank, isPresent, int, toBool, autoConvertAdd, BaseException, NumberWrapper} from 'facade/lang'; import {List, Map, ListWrapper, MapWrapper} from 'facade/collection'; -import {AST, AccessMember, ImplicitReceiver, AstVisitor, LiteralPrimitive, - Binary, Formatter, MethodCall, FunctionCall, PrefixNot, Conditional, - LiteralArray, LiteralMap, KeyedAccess, Chain, Assignment} from './parser/ast'; import {ContextWithVariableBindings} from './parser/context_with_variable_bindings'; +import { + AccessMember, + Assignment, + AST, + AstVisitor, + Binary, + Chain, + Collection, + Conditional, + Formatter, + FunctionCall, + ImplicitReceiver, + KeyedAccess, + LiteralArray, + LiteralMap, + LiteralPrimitive, + MethodCall, + PrefixNot +} from './parser/ast'; export class ProtoRecordRange { headRecord:ProtoRecord; @@ -32,13 +49,16 @@ export class ProtoRecordRange { * @param ast The expression to watch * @param memento an opaque object which will be passed to WatchGroupDispatcher on * detecting a change. - * @param shallow Should collections be shallow watched + * @param content Wether to watch collection content (true) or reference (false, default) */ addRecordsFromAST(ast:AST, memento, - shallow = false) + content:boolean = false) { var creator = new ProtoRecordCreator(this); + if (content) { + ast = new Collection(ast); + } creator.createRecordsFromAST(ast, memento); this._addRecords(creator.headRecord, creator.tailRecord); } @@ -436,6 +456,12 @@ class ProtoRecordCreator { this.add(record); } + visitCollection(ast: Collection, dest) { + var record = this.construct(RECORD_FLAG_COLLECTION, null, null, null, dest); + ast.value.visit(this, new Destination(record, null)); + this.add(record); + } + visitConditional(ast:Conditional, dest) { var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _cond, 3, null, dest); ast.condition.visit(this, new Destination(record, 0)); diff --git a/modules/change_detection/test/collection_changes_spec.js b/modules/change_detection/test/array_changes_spec.js similarity index 71% rename from modules/change_detection/test/collection_changes_spec.js rename to modules/change_detection/test/array_changes_spec.js index bf2545b46f..7f5dc83405 100644 --- a/modules/change_detection/test/collection_changes_spec.js +++ b/modules/change_detection/test/array_changes_spec.js @@ -1,13 +1,11 @@ import {describe, it, iit, xit, expect, beforeEach, afterEach} from 'test_lib/test_lib'; - -import {CollectionChanges} from 'change_detection/collection_changes'; - -import {isBlank, NumberWrapper} from 'facade/lang'; - -import {ListWrapper} from 'facade/collection'; +import {ArrayChanges} from 'change_detection/array_changes'; +import {NumberWrapper} from 'facade/lang'; +import {ListWrapper, MapWrapper} from 'facade/collection'; +import {TestIterable} from './iterable'; +import {arrayChangesAsString} from './util'; // todo(vicb): UnmodifiableListView / frozen object when implemented -// todo(vicb): Update the code & tests for object equality export function main() { describe('collection_changes', function() { describe('CollectionChanges', function() { @@ -15,30 +13,62 @@ export function main() { var l; beforeEach(() => { - changes = new CollectionChanges(); + changes = new ArrayChanges(); }); afterEach(() => { changes = null; }); + it('should support list and iterables', () => { + expect(ArrayChanges.supports([])).toBeTruthy(); + expect(ArrayChanges.supports(new TestIterable())).toBeTruthy(); + expect(ArrayChanges.supports(MapWrapper.create())).toBeFalsy(); + expect(ArrayChanges.supports(null)).toBeFalsy(); + }); + + it('should support iterables', () => { + l = new TestIterable(); + + changes.check(l); + expect(changes.toString()).toEqual(arrayChangesAsString({ + collection: [] + })); + + l.list = [1]; + changes.check(l); + expect(changes.toString()).toEqual(arrayChangesAsString({ + collection: ['1[null->0]'], + additions: ['1[null->0]'] + })); + + l.list = [2, 1]; + changes.check(l); + expect(changes.toString()).toEqual(arrayChangesAsString({ + collection: ['2[null->0]', '1[0->1]'], + previous: ['1[0->1]'], + additions: ['2[null->0]'], + moves: ['1[0->1]'] + })); + }); + it('should detect additions', () => { l = []; changes.check(l); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(arrayChangesAsString({ collection: [] })); ListWrapper.push(l, 'a'); changes.check(l); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(arrayChangesAsString({ collection: ['a[null->0]'], additions: ['a[null->0]'] })); ListWrapper.push(l, 'b'); changes.check(l); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(arrayChangesAsString({ collection: ['a', 'b[null->1]'], previous: ['a'], additions: ['b[null->1]'] @@ -51,7 +81,7 @@ export function main() { l = [1, 0]; changes.check(l); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(arrayChangesAsString({ collection: ['1[null->0]', '0[0->1]'], previous: ['0[0->1]'], additions: ['1[null->0]'], @@ -60,7 +90,7 @@ export function main() { l = [2, 1, 0]; changes.check(l); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(arrayChangesAsString({ collection: ['2[null->0]', '1[0->1]', '0[1->2]'], previous: ['1[0->1]', '0[1->2]'], additions: ['2[null->0]'], @@ -76,7 +106,7 @@ export function main() { ListWrapper.push(l, 2); ListWrapper.push(l, 1); changes.check(l); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(arrayChangesAsString({ collection: ['2[1->0]', '1[0->1]'], previous: ['1[0->1]', '2[1->0]'], moves: ['2[1->0]', '1[0->1]'] @@ -90,7 +120,7 @@ export function main() { ListWrapper.removeAt(l, 1); ListWrapper.insert(l, 0, 'b'); changes.check(l); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(arrayChangesAsString({ collection: ['b[1->0]', 'a[0->1]', 'c'], previous: ['a[0->1]', 'b[1->0]', 'c'], moves: ['b[1->0]', 'a[0->1]'] @@ -99,7 +129,7 @@ export function main() { ListWrapper.removeAt(l, 1); ListWrapper.push(l, 'a'); changes.check(l); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(arrayChangesAsString({ collection: ['b', 'c[2->1]', 'a[1->2]'], previous: ['b', 'a[1->2]', 'c[2->1]'], moves: ['c[2->1]', 'a[1->2]'] @@ -112,14 +142,14 @@ export function main() { ListWrapper.push(l, 'a'); changes.check(l); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(arrayChangesAsString({ collection: ['a[null->0]'], additions: ['a[null->0]'] })); ListWrapper.push(l, 'b'); changes.check(l); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(arrayChangesAsString({ collection: ['a', 'b[null->1]'], previous: ['a'], additions: ['b[null->1]'] @@ -128,7 +158,7 @@ export function main() { ListWrapper.push(l, 'c'); ListWrapper.push(l, 'd'); changes.check(l); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(arrayChangesAsString({ collection: ['a', 'b', 'c[null->2]', 'd[null->3]'], previous: ['a', 'b'], additions: ['c[null->2]', 'd[null->3]'] @@ -136,7 +166,7 @@ export function main() { ListWrapper.removeAt(l, 2); changes.check(l); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(arrayChangesAsString({ collection: ['a', 'b', 'd[3->2]'], previous: ['a', 'b', 'c[2->null]', 'd[3->2]'], moves: ['d[3->2]'], @@ -149,7 +179,7 @@ export function main() { ListWrapper.push(l, 'b'); ListWrapper.push(l, 'a'); changes.check(l); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(arrayChangesAsString({ collection: ['d[2->0]', 'c[null->1]', 'b[1->2]', 'a[0->3]'], previous: ['a[0->3]', 'b[1->2]', 'd[2->0]'], additions: ['c[null->1]'], @@ -165,7 +195,7 @@ export function main() { var oo = 'oo'; ListWrapper.set(l, 1, b + oo); changes.check(l); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(arrayChangesAsString({ collection: ['a', 'boo'], previous: ['a', 'boo'] })); @@ -175,7 +205,7 @@ export function main() { l = [NumberWrapper.NaN]; changes.check(l); changes.check(l); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(arrayChangesAsString({ collection: [NumberWrapper.NaN], previous: [NumberWrapper.NaN] })); @@ -187,7 +217,7 @@ export function main() { ListWrapper.insert(l, 0, 'foo'); changes.check(l); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(arrayChangesAsString({ collection: ['foo[null->0]', 'NaN[0->1]', 'NaN[1->2]'], previous: ['NaN[0->1]', 'NaN[1->2]'], additions: ['foo[null->0]'], @@ -201,7 +231,7 @@ export function main() { ListWrapper.removeAt(l, 1); changes.check(l); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(arrayChangesAsString({ collection: ['a', 'c[2->1]'], previous: ['a', 'b[1->null]', 'c[2->1]'], moves: ['c[2->1]'], @@ -210,7 +240,7 @@ export function main() { ListWrapper.insert(l, 1, 'b'); changes.check(l); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(arrayChangesAsString({ collection: ['a', 'b[null->1]', 'c[1->2]'], previous: ['a', 'c[1->2]'], additions: ['b[null->1]'], @@ -224,7 +254,7 @@ export function main() { ListWrapper.removeAt(l, 0); changes.check(l); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(arrayChangesAsString({ collection: ['a', 'a', 'b[3->2]', 'b[4->3]'], previous: ['a', 'a', 'a[2->null]', 'b[3->2]', 'b[4->3]'], moves: ['b[3->2]', 'b[4->3]'], @@ -238,7 +268,7 @@ export function main() { ListWrapper.insert(l, 0, 'b'); changes.check(l); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(arrayChangesAsString({ collection: ['b[2->0]', 'a[0->1]', 'a[1->2]', 'b', 'b[null->4]'], previous: ['a[0->1]', 'a[1->2]', 'b[2->0]', 'b'], additions: ['b[null->4]'], @@ -255,7 +285,7 @@ export function main() { ListWrapper.push(l, 'a'); ListWrapper.push(l, 'c'); changes.check(l); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(arrayChangesAsString({ collection: ['b[1->0]', 'a[0->1]', 'c'], previous: ['a[0->1]', 'b[1->0]', 'c'], moves: ['b[1->0]', 'a[0->1]'] @@ -265,16 +295,3 @@ export function main() { }); } -function changesAsString({collection, previous, additions, moves, removals}) { - if (isBlank(collection)) collection = []; - if (isBlank(previous)) previous = []; - if (isBlank(additions)) additions = []; - if (isBlank(moves)) moves = []; - if (isBlank(removals)) removals = []; - - return "collection: " + collection.join(', ') + "\n" + - "previous: " + previous.join(', ') + "\n" + - "additions: " + additions.join(', ') + "\n" + - "moves: " + moves.join(', ') + "\n" + - "removals: " + removals.join(', ') + "\n"; -} diff --git a/modules/change_detection/test/change_detector_spec.js b/modules/change_detection/test/change_detector_spec.js index 66708d1110..c799e69db3 100644 --- a/modules/change_detection/test/change_detector_spec.js +++ b/modules/change_detection/test/change_detector_spec.js @@ -1,10 +1,11 @@ import {ddescribe, describe, it, iit, xit, expect} from 'test_lib/test_lib'; -import {isPresent} from 'facade/lang'; +import {isPresent, isBlank, isJsObject} from 'facade/lang'; import {List, ListWrapper, MapWrapper} from 'facade/collection'; import {ContextWithVariableBindings} from 'change_detection/parser/context_with_variable_bindings'; import {Parser} from 'change_detection/parser/parser'; import {Lexer} from 'change_detection/parser/lexer'; +import {arrayChangesAsString, kvChangesAsString} from './util'; import { ChangeDetector, @@ -22,9 +23,10 @@ export function main() { return parser.parseBinding(exp).ast; } - function createChangeDetector(memo:string, exp:string, context = null, formatters = null) { + function createChangeDetector(memo:string, exp:string, context = null, formatters = null, + content = false) { var prr = new ProtoRecordRange(); - prr.addRecordsFromAST(ast(exp), memo, false); + prr.addRecordsFromAST(ast(exp), memo, content); var dispatcher = new LoggingDispatcher(); var rr = prr.instantiate(dispatcher, formatters); @@ -35,8 +37,9 @@ export function main() { return {"changeDetector" : cd, "dispatcher" : dispatcher}; } - function executeWatch(memo:string, exp:string, context = null, formatters = null) { - var res = createChangeDetector(memo, exp, context, formatters); + function executeWatch(memo:string, exp:string, context = null, formatters = null, + content = false) { + var res = createChangeDetector(memo, exp, context, formatters, content); res["changeDetector"].detectChanges(); return res["dispatcher"].log; } @@ -194,10 +197,11 @@ export function main() { }); }); + describe("ContextWithVariableBindings", () => { it('should read a field from ContextWithVariableBindings', () => { var locals = new ContextWithVariableBindings(null, - MapWrapper.createFromPairs([["key", "value"]])); + MapWrapper.createFromPairs([["key", "value"]])); expect(executeWatch('key', 'key', locals)) .toEqual(['key=value']); @@ -205,7 +209,7 @@ export function main() { it('should handle nested ContextWithVariableBindings', () => { var nested = new ContextWithVariableBindings(null, - MapWrapper.createFromPairs([["key", "value"]])); + MapWrapper.createFromPairs([["key", "value"]])); var locals = new ContextWithVariableBindings(nested, MapWrapper.create()); expect(executeWatch('key', 'key', locals)) @@ -213,14 +217,132 @@ export function main() { }); it("should fall back to a regular field read when ContextWithVariableBindings " + - "does not have the requested field", () => { + "does not have the requested field", () => { var locals = new ContextWithVariableBindings(new Person("Jim"), - MapWrapper.createFromPairs([["key", "value"]])); + MapWrapper.createFromPairs([["key", "value"]])); expect(executeWatch('name', 'name', locals)) .toEqual(['name=Jim']); }); }); + + describe("collections", () => { + it("should support null values", () => { + var context = new TestData(null); + var c = createChangeDetector('a', 'a', context, null, true); + var cd = c["changeDetector"]; + var dsp = c["dispatcher"]; + + cd.detectChanges(); + expect(dsp.log).toEqual(['a=null']); + dsp.clear(); + + cd.detectChanges(); + expect(dsp.log).toEqual([]); + + context.a = [0]; + cd.detectChanges(); + expect(dsp.log).toEqual(["a=" + + arrayChangesAsString({ + collection: ['0[null->0]'], + additions: ['0[null->0]'] + }) + ]); + dsp.clear(); + + context.a = null; + cd.detectChanges(); + expect(dsp.log).toEqual(['a=null']); + }); + + it("should throw if not collection / null", () => { + var context = new TestData("not collection / null"); + var c = createChangeDetector('a', 'a', context, null, true); + expect(() => c["changeDetector"].detectChanges()) + .toThrowError("Collection records must be array like, map like or null"); + }); + + describe("list", () => { + it("should support list changes", () => { + var context = new TestData([1, 2]); + expect(executeWatch("a", "a", context, null, true)) + .toEqual(["a=" + + arrayChangesAsString({ + collection: ['1[null->0]', '2[null->1]'], + additions: ['1[null->0]', '2[null->1]'] + })]); + }); + + it("should handle reference changes", () => { + var context = new TestData([1, 2]); + var objs = createChangeDetector("a", "a", context, null, true); + var cd = objs["changeDetector"]; + var dispatcher = objs["dispatcher"]; + cd.detectChanges(); + dispatcher.clear(); + + context.a = [2, 1]; + cd.detectChanges(); + expect(dispatcher.log).toEqual(["a=" + + arrayChangesAsString({ + collection: ['2[1->0]', '1[0->1]'], + previous: ['1[0->1]', '2[1->0]'], + moves: ['2[1->0]', '1[0->1]'] + })]); + }); + }); + + describe("map", () => { + it("should support map changes", () => { + var map = MapWrapper.create(); + MapWrapper.set(map, "foo", "bar"); + var context = new TestData(map); + expect(executeWatch("a", "a", context, null, true)) + .toEqual(["a=" + + kvChangesAsString({ + map: ['foo[null->bar]'], + additions: ['foo[null->bar]'] + })]); + }); + + it("should handle reference changes", () => { + var map = MapWrapper.create(); + MapWrapper.set(map, "foo", "bar"); + var context = new TestData(map); + var objs = createChangeDetector("a", "a", context, null, true); + var cd = objs["changeDetector"]; + var dispatcher = objs["dispatcher"]; + cd.detectChanges(); + dispatcher.clear(); + + context.a = MapWrapper.create(); + MapWrapper.set(context.a, "bar", "foo"); + cd.detectChanges(); + expect(dispatcher.log).toEqual(["a=" + + kvChangesAsString({ + map: ['bar[null->foo]'], + previous: ['foo[bar->null]'], + additions: ['bar[null->foo]'], + removals: ['foo[bar->null]'] + })]); + }); + }); + + if (isJsObject({})) { + describe("js objects", () => { + it("should support object changes", () => { + var map = {"foo": "bar"}; + var context = new TestData(map); + expect(executeWatch("a", "a", context, null, true)) + .toEqual(["a=" + + kvChangesAsString({ + map: ['foo[null->bar]'], + additions: ['foo[null->bar]'] + })]); + }); + }); + } + }); }); }); } diff --git a/modules/change_detection/test/iterable.dart b/modules/change_detection/test/iterable.dart new file mode 100644 index 0000000000..f4dfaad368 --- /dev/null +++ b/modules/change_detection/test/iterable.dart @@ -0,0 +1,6 @@ +import 'dart:collection'; + +class TestIterable extends IterableBase { + List list = []; + Iterator get iterator => list.iterator; +} diff --git a/modules/change_detection/test/iterable.es6 b/modules/change_detection/test/iterable.es6 new file mode 100644 index 0000000000..eef0367b79 --- /dev/null +++ b/modules/change_detection/test/iterable.es6 @@ -0,0 +1,9 @@ +export class TestIterable { + constructor() { + this.list = []; + } + + [Symbol.iterator]() { + return this.list[Symbol.iterator](); + } +} diff --git a/modules/change_detection/test/map_changes_spec.js b/modules/change_detection/test/keyvalue_changes_spec.js similarity index 50% rename from modules/change_detection/test/map_changes_spec.js rename to modules/change_detection/test/keyvalue_changes_spec.js index f8fa3a4b5a..985a2d7fe4 100644 --- a/modules/change_detection/test/map_changes_spec.js +++ b/modules/change_detection/test/keyvalue_changes_spec.js @@ -1,20 +1,18 @@ 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 {KeyValueChanges} from 'change_detection/keyvalue_changes'; +import {NumberWrapper, isJsObject} from 'facade/lang'; import {MapWrapper} from 'facade/collection'; +import {kvChangesAsString} from './util'; // todo(vicb): Update the code & tests for object equality export function main() { - describe('map_changes', function() { - describe('MapChanges', function() { + describe('keyvalue_changes', function() { + describe('KeyValueChanges', function() { var changes; var m; beforeEach(() => { - changes = new MapChanges(); + changes = new KeyValueChanges(); m = MapWrapper.create(); }); @@ -27,14 +25,14 @@ export function main() { MapWrapper.set(m, 'a', 1); changes.check(m); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(kvChangesAsString({ map: ['a[null->1]'], additions: ['a[null->1]'] })); MapWrapper.set(m, 'b', 2); changes.check(m); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(kvChangesAsString({ map: ['a', 'b[null->2]'], previous: ['a'], additions: ['b[null->2]'] @@ -49,7 +47,7 @@ export function main() { MapWrapper.set(m, 2, 10); MapWrapper.set(m, 1, 20); changes.check(m); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(kvChangesAsString({ map: ['1[10->20]', '2[20->10]'], previous: ['1[10->20]', '2[20->10]'], changes: ['1[10->20]', '2[20->10]'] @@ -61,14 +59,14 @@ export function main() { MapWrapper.set(m, 'a', 'A'); changes.check(m); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(kvChangesAsString({ map: ['a[null->A]'], additions: ['a[null->A]'] })); MapWrapper.set(m, 'b', 'B'); changes.check(m); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(kvChangesAsString({ map: ['a', 'b[null->B]'], previous: ['a'], additions: ['b[null->B]'] @@ -77,7 +75,7 @@ export function main() { MapWrapper.set(m, 'b', 'BB'); MapWrapper.set(m, 'd', 'D'); changes.check(m); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(kvChangesAsString({ map: ['a', 'b[B->BB]', 'd[null->D]'], previous: ['a', 'b[B->BB]'], additions: ['d[null->D]'], @@ -86,7 +84,7 @@ export function main() { MapWrapper.delete(m, 'b'); changes.check(m); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(kvChangesAsString({ map: ['a', 'd'], previous: ['a', 'b[BB->null]', 'd'], removals: ['b[BB->null]'] @@ -94,7 +92,7 @@ export function main() { MapWrapper.clear(m); changes.check(m); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(kvChangesAsString({ previous: ['a[A->null]', 'd[D->null]'], removals: ['a[A->null]', 'd[D->null]'] })); @@ -112,7 +110,7 @@ export function main() { MapWrapper.set(m, f + oo, b + ar); changes.check(m); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(kvChangesAsString({ map: ['foo'], previous: ['foo'] })); @@ -123,25 +121,70 @@ export function main() { changes.check(m); changes.check(m); - expect(changes.toString()).toEqual(changesAsString({ + expect(changes.toString()).toEqual(kvChangesAsString({ map: ['foo'], previous: ['foo'] })); }); + + // JS specific tests (JS Objects) + if (isJsObject({})) { + describe('JsObject changes', () => { + it('should support JS Object', () => { + expect(KeyValueChanges.supports({})).toBeTruthy(); + expect(KeyValueChanges.supports("not supported")).toBeFalsy(); + expect(KeyValueChanges.supports(0)).toBeFalsy(); + expect(KeyValueChanges.supports(null)).toBeFalsy(); + }); + + it('should do basic object watching', () => { + m = {}; + changes.check(m); + + m['a'] = 'A'; + changes.check(m); + expect(changes.toString()).toEqual(kvChangesAsString({ + map: ['a[null->A]'], + additions: ['a[null->A]'] + })); + + m['b'] = 'B'; + changes.check(m); + expect(changes.toString()).toEqual(kvChangesAsString({ + map: ['a', 'b[null->B]'], + previous: ['a'], + additions: ['b[null->B]'] + })); + + m['b'] = 'BB'; + m['d'] = 'D'; + changes.check(m); + expect(changes.toString()).toEqual(kvChangesAsString({ + map: ['a', 'b[B->BB]', 'd[null->D]'], + previous: ['a', 'b[B->BB]'], + additions: ['d[null->D]'], + changes: ['b[B->BB]'] + })); + + m = {}; + m['a'] = 'A'; + m['d'] = 'D'; + changes.check(m); + expect(changes.toString()).toEqual(kvChangesAsString({ + map: ['a', 'd'], + previous: ['a', 'b[BB->null]', 'd'], + removals: ['b[BB->null]'] + })); + + m = {}; + changes.check(m); + expect(changes.toString()).toEqual(kvChangesAsString({ + previous: ['a[A->null]', 'd[D->null]'], + removals: ['a[A->null]', 'd[D->null]'] + })); + }); + }); + } }); }); } - -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/change_detection/test/util.js b/modules/change_detection/test/util.js new file mode 100644 index 0000000000..dc27ee747b --- /dev/null +++ b/modules/change_detection/test/util.js @@ -0,0 +1,29 @@ +import {isBlank} from 'facade/lang'; + +export function arrayChangesAsString({collection, previous, additions, moves, removals}) { + if (isBlank(collection)) collection = []; + if (isBlank(previous)) previous = []; + if (isBlank(additions)) additions = []; + if (isBlank(moves)) moves = []; + if (isBlank(removals)) removals = []; + + return "collection: " + collection.join(', ') + "\n" + + "previous: " + previous.join(', ') + "\n" + + "additions: " + additions.join(', ') + "\n" + + "moves: " + moves.join(', ') + "\n" + + "removals: " + removals.join(', ') + "\n"; +} + +export function kvChangesAsString({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/facade/src/collection.dart b/modules/facade/src/collection.dart index 4c20958f48..5a326f4d30 100644 --- a/modules/facade/src/collection.dart +++ b/modules/facade/src/collection.dart @@ -51,7 +51,7 @@ class ListWrapper { static filter(List list, fn) => list.where(fn).toList(); static find(List list, fn) => list.firstWhere(fn, orElse:() => null); static any(List list, fn) => list.any(fn); - static forEach(list, fn) { + static forEach(list, Function fn) { list.forEach(fn); } static reduce(List list, Function fn, init) { @@ -68,6 +68,15 @@ class ListWrapper { static void clear(List l) { l.clear(); } } +bool isListLikeIterable(obj) => obj is Iterable; + +void iterateListLike(iter, Function fn) { + assert(iter is Iterable); + for (var item in iter) { + fn (item); + } +} + class SetWrapper { static Set createFromList(List l) { return new Set.from(l); } static bool has(Set s, key) { return s.contains(key); } diff --git a/modules/facade/src/collection.es6 b/modules/facade/src/collection.es6 index 132a68aa08..7369e13bea 100644 --- a/modules/facade/src/collection.es6 +++ b/modules/facade/src/collection.es6 @@ -1,4 +1,4 @@ -import {int} from 'facade/lang'; +import {int, isJsObject} from 'facade/lang'; export var List = window.Array; export var Map = window.Map; @@ -25,12 +25,17 @@ export class MapWrapper { static clear(m) { m.clear(); } } -// TODO: cannot export StringMap as a type as Dart does not support -// renaming types... +// TODO: cannot export StringMap as a type as Dart does not support renaming types... +/** + * Wraps Javascript Objects + */ export class StringMapWrapper { - // Note: We are not using Object.create(null) here due to - // performance! - static create():Object { return { }; } + static create():Object { + // Note: We are not using Object.create(null) here due to + // performance! + // http://jsperf.com/ng2-object-create-null + return { }; + } static get(map, key) { return map.hasOwnProperty(key) ? map[key] : undefined; } @@ -45,7 +50,9 @@ export class StringMapWrapper { } static forEach(map, callback) { for (var prop in map) { - callback(map[prop], prop); + if (map.hasOwnProperty(prop)) { + callback(map[prop], prop); + } } } } @@ -117,6 +124,19 @@ export class ListWrapper { } } +export function isListLikeIterable(obj):boolean { + if (!isJsObject(obj)) return false; + return ListWrapper.isList(obj) || + (!(obj instanceof Map) && // JS Map are iterables but return entries as [k, v] + Symbol.iterator in obj); // JS Iterable have a Symbol.iterator prop +} + +export function iterateListLike(obj, fn:Function) { + for (var item of obj) { + fn(item); + } +} + export class SetWrapper { static createFromList(lst:List) { return new Set(lst); } static has(s:Set, key):boolean { return s.has(key); } diff --git a/modules/facade/src/lang.dart b/modules/facade/src/lang.dart index 76abd1e2ad..a0f9982d21 100644 --- a/modules/facade/src/lang.dart +++ b/modules/facade/src/lang.dart @@ -161,6 +161,11 @@ dynamic getMapKey(value) { return value.isNaN ? _NAN_KEY : value; } -normalizeBlank(obj) { +dynamic normalizeBlank(obj) { return isBlank(obj) ? null : obj; } + +bool isJsObject(o) { + return false; +} + diff --git a/modules/facade/src/lang.es6 b/modules/facade/src/lang.es6 index 58f2c40f74..e7143b14b2 100644 --- a/modules/facade/src/lang.es6 +++ b/modules/facade/src/lang.es6 @@ -202,3 +202,7 @@ export function getMapKey(value) { export function normalizeBlank(obj) { return isBlank(obj) ? null : obj; } + +export function isJsObject(o):boolean { + return o !== null && (typeof o === "function" || typeof o === "object"); +}