Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
/// <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;
}*/
(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, _);
/// <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>;
}
(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, _);
/// <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
You can’t perform that action at this time.