diff --git a/src/observable/index.js b/src/observable/index.js index 828a6aea..3a250fde 100644 --- a/src/observable/index.js +++ b/src/observable/index.js @@ -5,6 +5,7 @@ import BaseObservableMap from "./map/BaseObservableMap.js"; // re-export "root" (of chain) collections export { default as ObservableArray } from "./list/ObservableArray.js"; export { default as SortedArray } from "./list/SortedArray.js"; +export { default as MappedList } from "./list/MappedList.js"; export { default as ObservableMap } from "./map/ObservableMap.js"; // avoid circular dependency between these classes diff --git a/src/observable/list/MappedList.js b/src/observable/list/MappedList.js new file mode 100644 index 00000000..6d5f87eb --- /dev/null +++ b/src/observable/list/MappedList.js @@ -0,0 +1,115 @@ +import BaseObservableList from "./BaseObservableList.js"; + +export default class MappedList extends BaseObservableList { + constructor(sourceList, mapper, updater) { + super(); + this._sourceList = sourceList; + this._mapper = mapper; + this._updater = updater; + this._sourceUnsubscribe = null; + this._mappedValues = null; + } + + onSubscribeFirst() { + this._sourceUnsubscribe = this._sourceList.subscribe(this); + this._mappedValues = []; + for (const item of this._sourceList) { + this._mappedValues.push(this._mapper(item)); + } + } + + onReset() { + this._mappedValues = []; + this.emitReset(); + } + + onAdd(index, value) { + const mappedValue = this._mapper(value); + this._mappedValues.splice(index, 0, mappedValue); + this.emitAdd(index, mappedValue); + } + + onUpdate(index, value, params) { + const mappedValue = this._mappedValues[index]; + if (this._updater) { + this._updater(mappedValue, value); + } + this.emitUpdate(index, mappedValue, params); + } + + onRemove(index) { + const mappedValue = this._mappedValues[index]; + this._mappedValues.splice(index, 1); + this.emitRemove(index, mappedValue); + } + + onMove(fromIdx, toIdx) { + const mappedValue = this._mappedValues[fromIdx]; + this._mappedValues.splice(fromIdx, 1); + this._mappedValues.splice(toIdx, 0, mappedValue); + this.emitMove(fromIdx, toIdx, mappedValue); + } + + onUnsubscribeLast() { + this._sourceUnsubscribe(); + } + + get length() { + return this._mappedValues.length; + } + + [Symbol.iterator]() { + return this._mappedValues.values(); + } +} + +export async function tests() { + class MockList extends BaseObservableList { + get length() { + return 0; + } + [Symbol.iterator]() { + return [].values(); + } + } + + return { + test_add(assert) { + const source = new MockList(); + const mapped = new MappedList(source, n => {return {n: n*n};}); + let fired = false; + const unsubscribe = mapped.subscribe({ + onAdd(idx, value) { + fired = true; + assert.equal(idx, 0); + assert.equal(value.n, 36); + } + }); + source.emitAdd(0, 6); + assert(fired); + unsubscribe(); + }, + test_update(assert) { + const source = new MockList(); + const mapped = new MappedList( + source, + n => {return {n: n*n};}, + (o, n) => o.m = n*n + ); + let fired = false; + const unsubscribe = mapped.subscribe({ + onAdd() {}, + onUpdate(idx, value) { + fired = true; + assert.equal(idx, 0); + assert.equal(value.n, 36); + assert.equal(value.m, 49); + } + }); + source.emitAdd(0, 6); + source.emitUpdate(0, 7); + assert(fired); + unsubscribe(); + } + }; +}