Created
January 24, 2014 18:09
-
-
Save lcorneliussen/8602787 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /// <reference path="../../Scripts/knockout.d.ts" /> | |
| interface KnockoutSubscribableArray<T> { | |
| deltas(callback: (added: T[]) => void): KnockoutSubscription; | |
| deltas(callback: (added: T[], removed: T[]) => void): KnockoutSubscription; | |
| deltas(callback: (added: T[], removed: T[], all: T[]) => void): KnockoutSubscription; | |
| } | |
| // redefined until bug is fixed: https://typescript.codeplex.com/workitem/1351 | |
| /*interface KnockoutObservableArray<T> { | |
| deltas(callback: (added: T[]) => void): KnockoutSubscription; | |
| deltas(callback: (added: T[], removed: T[]) => void): KnockoutSubscription; | |
| deltas(callback: (added: T[], removed: T[], all: T[]) => void): KnockoutSubscription; | |
| }*/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (function (ko, Logger, _) { | |
| var log = Logger.get("ITOps/WebShell/Deltas"); | |
| log.setLevel(Logger.WARN); | |
| var notifyAboutDeltas = function (target, current, changes) { | |
| target.deltasData.previousList = current; | |
| var newItems = _.map(_.filter(changes, function (change) { | |
| return change.status === "added"; | |
| }), function (change) { return change.value; }); | |
| var deletedItems = _.map(_.filter(changes, function (change) { | |
| return change.status === "deleted"; | |
| }), function (change) { return change.value; }); | |
| _.each(target.deltasData.callbacks, function (c) { | |
| c(newItems, deletedItems, current); | |
| }); | |
| }; | |
| ko.subscribable.fn.deltas = function (callback) { | |
| var target = this; | |
| if (!target.deltasData) { | |
| target.deltasData = { | |
| callbacks: [], | |
| previousList: ((ko.hasEvaluated(target) ? target.peek() : null) || []).slice(0) | |
| }; | |
| /*var subscription = target.subscribe(function(current) { | |
| current = (current || []).slice(0); | |
| var previous = target.deltasData.previousList; | |
| target.deltasData.previousList = current; | |
| if (previous.length != current.length || _.any(_.zip(previous, current), function(x) { | |
| return x[0] !== x[1]; | |
| })) { | |
| var newItems = _.filter(current, function(item) { | |
| return !_.contains(previous, item); | |
| }); | |
| var deletedItems = _.filter(previous, function(item) { | |
| return !_.contains(current, item); | |
| }); | |
| _.each(target.deltasData.callbacks, function(c) { | |
| c(newItems, deletedItems, current); | |
| }); | |
| } | |
| });*/ | |
| var subscription; | |
| if (target.indexOf) { | |
| subscription = target.subscribe(function(changes) { | |
| var current = target.peek(); | |
| notifyAboutDeltas(target, current, changes); | |
| }, null, "arrayChange"); | |
| } else { | |
| subscription = target.subscribe(function (current) { | |
| var changes = ko.utils.compareArrays(target.deltasData.previousList, current, { sparse: true }); | |
| notifyAboutDeltas(target, current, changes); | |
| }); | |
| } | |
| subscription.deferUpdates = false; | |
| target.deltasData.subscription = subscription; | |
| /*var innerDispose = target.dispose; | |
| target.dispose = function() { | |
| subscription.dispose(); | |
| if (innerDispose) { | |
| innerDispose.apply(target); | |
| } | |
| };*/ | |
| } | |
| var dispose = disposable(); | |
| target.deltasData.callbacks.push(callback); | |
| disposable(dispose, "subscription", function() { | |
| target.deltasData.callbacks = _.without(target.deltasData.callbacks, callback); | |
| if (target.deltasData.callbacks.length == 0) { | |
| //console.log("dispose deltas subscription"); | |
| target.deltasData.subscription.dispose(); | |
| } | |
| }); | |
| return dispose; | |
| }; | |
| })(ko, Logger, _); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /// <reference path="../../Scripts/knockout.d.ts" /> | |
| interface KnockoutMapFromOptions { | |
| compact?: boolean; | |
| unwrap?: boolean; | |
| unordered?: boolean; | |
| lazy?: boolean; | |
| waitForSource?: boolean; | |
| } | |
| interface KnockoutMapFromSingle<TIn, TOut> extends KnockoutMapFromOptions { | |
| transform: (item: TIn) => TOut; | |
| } | |
| interface KnockoutMapFromManyOptions<TIn, TOut> extends KnockoutMapFromOptions{ | |
| flattenDeep?: boolean; | |
| transformMany: (item: TIn) => TOut[]; | |
| } | |
| interface KnockoutMapFromManySubscribableOptions<TIn, TOut> extends KnockoutMapFromOptions { | |
| flattenDeep?: boolean; | |
| transformMany: (item: TIn) => KnockoutSubscribableArray<TOut>; | |
| } | |
| interface KnockoutStatic { | |
| mapFrom<TIn, TOut>(source: KnockoutSubscribableArray<TIn>, options: KnockoutMapFromManySubscribableOptions<TIn, TOut>, target?: KnockoutObservable<TOut[]>): KnockoutSubscribableArray<TOut>; | |
| mapFrom<TIn, TOut>(source: KnockoutSubscribableArray<TIn>, options: KnockoutMapFromManyOptions<TIn, TOut>, target?: KnockoutObservable<TOut[]>): KnockoutSubscribableArray<TOut>; | |
| mapFrom<TIn, TOut>(source: KnockoutSubscribableArray<TIn>, options: KnockoutMapFromSingle<TIn, KnockoutSubscribable<TOut>>, target?: KnockoutObservable<TOut[]>): KnockoutSubscribableArray<TOut>; | |
| mapFrom<TIn, TOut>(source: KnockoutSubscribableArray<TIn>, options: KnockoutMapFromSingle<TIn, TOut>, target?: KnockoutObservable<TOut[]>): KnockoutSubscribableArray<TOut>; | |
| //mapFrom<TIn, TOut>(source: KnockoutSubscribableArray<TIn>, transform: (item: TIn) => KnockoutSubscribableArray<TOut>, target?: KnockoutObservable<TOut[]>): KnockoutSubscribableArray<TOut>; | |
| mapFrom<TIn, TOut>(source: KnockoutSubscribableArray<TIn>, transform: (item: TIn) => KnockoutSubscribable<TOut>, target?: KnockoutObservable<TOut[]>): KnockoutSubscribableArray<TOut>; | |
| //mapFrom<TIn, TOut>(source: KnockoutSubscribableArray<TIn>, transform: (item: TIn) => TOut[], target?: KnockoutObservable<TOut[]>): KnockoutSubscribableArray<TOut>; | |
| mapFrom<TIn, TOut>(source: KnockoutSubscribableArray<TIn>, transform: (item: TIn) => TOut, target?: KnockoutObservable<TOut[]>): KnockoutSubscribableArray<TOut>; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (function (ko, Logger, xHashtable, _) { | |
| var log = Logger.get("ITOps/WebShell/MapFrom"); | |
| log.setLevel(Logger.WARN); | |
| function Hashtable() { | |
| var self = this; | |
| self.items = []; | |
| var find = function (key) { | |
| for (var i = 0; i < self.items.length; i++) { | |
| if (self.items[i].key === key) { | |
| return { item: self.items[i], index: i }; | |
| } | |
| } | |
| return undefined; | |
| }; | |
| self.clear = function() { | |
| self.items = []; | |
| } | |
| self.put = function (key, value) { | |
| var found = find(key); | |
| if (!found) { | |
| self.items.push({ key: key, value: value }); | |
| } | |
| }; | |
| self.remove = function(key) { | |
| var found = find(key); | |
| if (found) { | |
| self.items.splice(found.index, 1); | |
| return true; | |
| } | |
| return false; | |
| }; | |
| self.containsKey = function (key) { | |
| return find(key) && true; | |
| }; | |
| self.get = function (key) { | |
| var found = find(key); | |
| if (found) { | |
| return found.item.value; | |
| } | |
| return undefined; | |
| }; | |
| self.keys = function () { | |
| return _.map(self.items, function(item) { | |
| return item.key; | |
| }); | |
| }; | |
| } | |
| var defaults = { | |
| compact: true, | |
| flatten: false, | |
| flattenDeep: false, | |
| unwrap: true, | |
| unordered: false, | |
| lazy: true, | |
| // if lazy is true and this is false, mapfrom will not map before the source has been touched by someone else | |
| touchUncomputedSource: true, | |
| autoDispose: false | |
| }; | |
| ko.mapFrom = function (source, optionsOrTransform, target) { | |
| if (!source) { | |
| throw "first parameter of mapFrom should be the list to subscribe to"; | |
| } | |
| if (!ko.isSubscribable(source)) { | |
| throw "list to map should be subscribable"; | |
| } | |
| var dispose = disposable(); | |
| if (!target) { | |
| target = ko.observable([]); | |
| } | |
| var options = _.extend({}, defaults, _.isObject(optionsOrTransform) ? optionsOrTransform : {}); | |
| if (_.isFunction(optionsOrTransform)) options.transform = optionsOrTransform; | |
| if (!options.transform) { | |
| if (options.transformMany) { | |
| options.flatten = true; | |
| options.transform = options.transformMany; | |
| } else { | |
| throw "need a transform function"; | |
| } | |
| } | |
| var orderedOriginals; | |
| var map = new Hashtable(); | |
| var originalWasUndefined; | |
| var originalWasNull; | |
| var patches = []; | |
| var isPatching = false; | |
| var set = function (original, transformed, unwrap) { | |
| var mapped = { | |
| transformed: transformed | |
| }; | |
| if (transformed && unwrap && ko.isSubscribable(transformed)) { | |
| mapped.transformed = transformed(); | |
| mapped.subscription = transformed.subscribe(function (newTransformed) { | |
| if (!mapped.subscription) { | |
| log.warn("change was fired after subscription has been disposed"); | |
| return; | |
| } | |
| mapped.transformed = newTransformed; | |
| log.debug("single transformed changed!", original, newTransformed); | |
| if (!isPatching) { | |
| update(); | |
| } | |
| }); | |
| mapped.subscription.deferUpdates = false; | |
| } | |
| if (_.isUndefined(original)) { | |
| originalWasUndefined = mapped; | |
| } else if (original === null) { | |
| originalWasNull = mapped; | |
| } else { | |
| map.put(original, mapped); | |
| } | |
| }; | |
| var deleteItem = function (deleted) { | |
| var mapped = map.get(deleted); | |
| if (!mapped) { | |
| return; | |
| } | |
| /*if (options.autoDispose) { | |
| var transformed = mapped.transformed; | |
| if (transformed && _.isFunction(transformed.dispose)) { | |
| transformed.dispose(); | |
| } | |
| }*/ | |
| if (mapped.subscription) { | |
| mapped.subscription.dispose(); | |
| log.debug("removed subscription"); | |
| mapped.subscription = null; | |
| } | |
| map.remove(deleted); | |
| }; | |
| var patch = function(newItems, deletedItems, allItems) { | |
| if (!_.isArray(allItems)) { | |
| debugger; | |
| throw "mapFrom does not support non-arrays anymore!"; | |
| } | |
| var transformAndSet = function(original) { | |
| var transformed = options.transform(original); | |
| // log.debug("transformed", original, "to", transformed, "with", options.transform); | |
| set(original, transformed, options.unwrap); | |
| }; | |
| orderedOriginals = allItems; | |
| // todo: fix workaround: for some unknown reason, newItems does not always contain all new items | |
| newItems = _.difference(allItems, map.keys()); | |
| // end of workaround | |
| isPatching = true; | |
| _.each(newItems, transformAndSet); | |
| _.each(deletedItems, deleteItem); | |
| update(); | |
| isPatching = false; | |
| }; | |
| var update = function () { | |
| var allTransformed = _.map(orderedOriginals, function (o) { | |
| var mapped = null; | |
| if (_.isUndefined(o)) { | |
| mapped = originalWasUndefined; | |
| } else if (o === null) { | |
| mapped = originalWasNull; | |
| } else { | |
| mapped = map.get(o); | |
| } | |
| return mapped ? mapped.transformed : null; | |
| }); | |
| allTransformed = options.compact ? _.compact(allTransformed) : allTransformed; | |
| allTransformed = options.flatten ? _.flatten(allTransformed, !options.flattenDeep) : allTransformed; | |
| setIfChanged(allTransformed); | |
| }; | |
| var hasChanged = function (newList) { | |
| var current = target.peek() || []; | |
| if (current.length != newList.length) return true; | |
| if (options.unordered) { | |
| // since they are the same length | |
| // it is enough to check, that all items in one list exists in the other | |
| for (var i = 0; i < newList.length; i++) { | |
| if (!_.contains(current, newList[i])) return true; | |
| } | |
| return false; | |
| } else { | |
| return _.any(_.zip(current, newList), function (x) { | |
| return x[0] !== x[1]; | |
| }); | |
| } | |
| }; | |
| var setIfChanged = function (newList) { | |
| if (hasChanged(newList)) { | |
| target(newList); | |
| if (log.enabledFor(Logger.DEBUG)) { | |
| var difference = _.difference(newList, current); | |
| var msg = "updated " + difference.length + " of " + current.length; | |
| log.debug(msg, difference); | |
| } | |
| } else { | |
| log.debug("nothing to do; got no changes"); | |
| } | |
| }; | |
| var initialized = false; | |
| var transformAndTrack = function () { | |
| if (initialized) return; | |
| var originals = (options.touchUncomputedSource ? source() || [] : []).slice(0); | |
| var deltasSubscription = source.deltas(function (newItems, deletedItems, allItems) { | |
| if (options.debug) debugger; | |
| patch(newItems, deletedItems, allItems); | |
| }, options.debug); | |
| // will run in reverse order | |
| disposable(dispose, "cache", function() { | |
| map.clear(); | |
| }); | |
| disposable(dispose, "subscription", deltasSubscription); | |
| patch(originals, [], originals); | |
| initialized = true; | |
| }; | |
| var computed = ko.computed({ | |
| deferEvaluation: options.lazy, | |
| read: function () { | |
| ko.dependencyDetection.ignore(transformAndTrack); | |
| return target(); | |
| } | |
| }); | |
| computed.deferUpdates = false; | |
| computed.__mapping = { | |
| source: source, | |
| target: target, | |
| options: options | |
| }; | |
| disposable(computed, "inner", dispose); | |
| return computed; | |
| }; | |
| ko.subscribable.fn.mapFrom = function (other, optionsOrTransform) { | |
| var target = this; | |
| if (optionsOrTransform.lazy) { | |
| throw "Lazy is only available if you call ko.mapFrom directly!"; | |
| } | |
| log.warn("rather use ko.mapFrom directly"); | |
| return ko.mapFrom(other, _.isFunction(optionsOrTransform) ? { lazy: false, transform: optionsOrTransform } : _.extend({ lazy: false }, optionsOrTransform), target); | |
| }; | |
| })(ko, Logger, Hashtable, _); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /// <reference path="..\..\Scripts\logger.min.js" /> | |
| /// <reference path="..\..\Scripts\underscore-min.js" /> | |
| /// <reference path="..\..\Scripts\knockout-latest.debug.js" /> | |
| /// <reference path="..\..\Scripts\jshashset.js" /> | |
| /// <reference path="..\..\Scripts\jshashtable-2.1.js" /> | |
| /// <reference path="knockout.deltas.js" /> | |
| /// <reference path="knockout.mapFrom.js" /> | |
| describe("ko.mapFrom", function () { | |
| it("is available", function () { | |
| expect(ko.mapFrom).toBeDefined(); | |
| }); | |
| describe("with lazy mapping", function() { | |
| it("does not map on creation", function () { | |
| var source = ko.observableArray([{}]); | |
| var mapped = 0; | |
| var map = ko.mapFrom(source, function (i) { | |
| mapped++; | |
| return i; | |
| }); | |
| expect(mapped).toBe(0); | |
| }); | |
| it("does not map for new items, if never accessed", function () { | |
| var source = ko.observableArray(); | |
| var mapped = 0; | |
| var map = ko.mapFrom(source, function (i) { | |
| mapped++; | |
| return i; | |
| }); | |
| source.push({}); | |
| expect(mapped).toBe(0); | |
| }); | |
| it("maps on first access", function () { | |
| var source = ko.observableArray([{}]); | |
| var mapped = 0; | |
| var map = ko.mapFrom(source, function (i) { | |
| mapped++; | |
| return i; | |
| }); | |
| map(); | |
| expect(mapped).toBe(1); | |
| }); | |
| it("maps new items after first access", function () { | |
| var source = ko.observableArray(); | |
| var mapped = 0; | |
| var map = ko.mapFrom(source, function (i) { | |
| mapped++; | |
| return i; | |
| }); | |
| map(); | |
| source.push({}); | |
| expect(mapped).toBe(1); | |
| }); | |
| describe("chained", function() { | |
| it("does not map the mapped source on creation", function () { | |
| var source = ko.observableArray([{}]); | |
| var mapped = 0; | |
| var map = ko.mapFrom(source, function (i) { | |
| mapped++; | |
| return i; | |
| }); | |
| var map2 = ko.mapFrom(map, function(i) { return i; }); | |
| expect(mapped).toBe(0); | |
| }); | |
| it("bubbles changes", function () { | |
| var source = ko.observableArray([1]); | |
| var mapped = 0; | |
| var map = ko.mapFrom(source, function (i) { | |
| mapped++; | |
| return i+1; | |
| }); | |
| var map2 = ko.mapFrom(map, function (i) { return i + 1; }); | |
| map2(); | |
| source.push(2); | |
| expect(map()).toEqual([2, 3]); | |
| expect(map2()).toEqual([3, 4]); | |
| }); | |
| }); | |
| }); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment