Skip to content

Instantly share code, notes, and snippets.

@xavierzwirtz
Created February 12, 2015 00:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xavierzwirtz/b99d46ca2ce365cf1107 to your computer and use it in GitHub Desktop.
Save xavierzwirtz/b99d46ca2ce365cf1107 to your computer and use it in GitHub Desktop.
//Copyright (c) 2015, Xavier Zwirtz
//
//Permission is hereby granted, free of charge, to any person obtaining a copy of
//this software and associated documentation files (the "Software"), to deal in
//the Software without restriction, including without limitation the rights to
//use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
//of the Software, and to permit persons to whom the Software is furnished to do
//so, subject to the following conditions:
//
//The above copyright notice and this permission notice shall be included in all
//copies or substantial portions of the Software.
//
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
//FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
//IN THE SOFTWARE.
define([
'lodash',
'knockout'
], function (_, ko) {
'use strict';
// source: https://github.com/knockout/knockout/pull/1576
// var notifySubscribers = (function (original) {
// return function (value, event) {
// if (!event || event == 'change' || event == 'awake' || event == 'asleep') {
// original.call(this, value, 'spectate');
// }
// original.apply(this, arguments);
// };
// }(ko.subscribable.fn.notifySubscribers));
var makeNotify = function (observable) {
var notifyQueue = [];
var processNotifyQueue = function () {
if (notifyQueue.length > 0) {
var notify = notifyQueue[0];
if (!notify.running) {
notify.running = true;
observable.notifySubscribers(notify.value, notify.event);
notifyQueue.shift();
notify.running = false;
processNotifyQueue();
}
}
};
return function (value, event) {
notifyQueue.push({
value: value,
event: event,
running: false
});
processNotifyQueue();
};
};
var makerPoker = function () {
var poker = {};
poker.value = ko.observable(1);
poker.poke = function () {
poker.value(poker.value() + 1);
};
return poker;
};
var makePokableValue = function (initialVal) {
var pokable = {};
var poker = makerPoker();
pokable.poke = poker.poke;
pokable.pokerValue = poker.value;
// pokable.pokerValue = poker.value;
pokable.value = initialVal;
pokable.computed = ko.computed(function () {
pokable.pokerValue();
return pokable.value;
});
// var notify = makeNotify(pokable.computed.notifySubscribers);
// pokable.computed.subscribe = (function(original){
// return function(callback, callbackTarget){
// original.call(pokable.computed, callback, callbackTarget, 'spectate');
// };
// });
return pokable;
};
/**
* Creates a new onDemandObservable.
* @param {object} opts The observable data.
* @config {object} value The value to initialize with.
* @config {object} target The value to bind to this in function calls.
* @config {callback} load The function to call to load data.
* @config {string=} type The type of observable to create. Can be array or observable. Defaults to observable.
*/
var onDemandObservable = function (opts) {
var _value; //private observable
if (_.isUndefined(opts.type) || opts.type === 'object') {
var instantiated;
instantiated = opts.value;
_value = ko.observable(instantiated);
} else if (opts.type === 'array') {
_value = ko.observableArray();
if (!_.isUndefined(opts.value)) {
_.each(ko.unwrap(opts.value), function (part) {
_value.push(part);
});
}
} else {
throw new Error('type not implemented: ' + opts.type);
}
var shouldNotify = function (newValue, oldValue, overrideCompare) {
if (_.isUndefined(overrideCompare)) {
overrideCompare = false;
}
return (
opts.type === 'array' ||
(newValue !== oldValue || overrideCompare)
);
};
var notifyBefore = function (newValue, oldValue, overrideCompare) {
if (shouldNotify(newValue, oldValue, overrideCompare)) {
notify(oldValue, 'beforeChange');
notify(oldValue, 'beforeSpectate');
}
};
var notifyAfter = function (newValue, oldValue, overrideCompare) {
if (shouldNotify(newValue, oldValue, overrideCompare)) {
notify(newValue, 'change');
notify(newValue, 'spectate');
}
};
var hasAccessed = false;
var setLoaded = function (newValue, oldValue) {
_pokableLoaded.value = true;
_pokableLoading.value = false;
resultPoker.poke();
allPoker.poke();
notifyAfter(newValue, oldValue);
};
var getData = function (notifyImmediate) {
_pokableLoading.value = true;
_pokableLoading.poke();
var load = function (data) {
var oldValue = _value();
notifyBefore(data, oldValue);
var notify = function () {
if (_.isUndefined(opts.type) || opts.type === 'object') {
_value(data);
} else if (opts.type === 'array') {
_value.removeAll();
_.each(ko.unwrap(data), function (part) {
_value.push(part);
});
} else {
throw new Error('type not implemented: ' + opts.type);
}
setLoaded(data, oldValue);
};
if (notifyImmediate()) {
notify();
} else {
setTimeout(function () {
notify();
}, 1);
}
};
opts.load.call(opts.target, load);
};
var allPoker = makerPoker();
var resultPoker = makerPoker();
var resultShouldRead = true;
var result = ko.computed({ // should have no other dependencies than resultPoker
read: function () {
resultPoker.value();
allPoker.value();
var value = _value.peek();
if (!resultShouldRead) {
throw new Error('should not have read');
}
var hasLeft = false;
if (!_pokableLoaded.value && !_pokableLoading.value) {
// cant fire load when `read` is executing. Knockout does not trigger the change events.
// Detecting whether the data was loaded async or not, have to do a setTimeout if it was.
getData(function () {
return hasLeft; //if hasLeft, notify immediatly
});
}
//always return the current value
hasAccessed = true;
hasLeft = true;
return value;
},
write: function (newValue) {
hasAccessed = true;
//indicate that the value is now loaded and set it
resultShouldRead = false;
var oldValue = _value();
notifyBefore(newValue, oldValue);
_value(newValue);
resultShouldRead = true;
setLoaded(newValue, oldValue);
},
deferEvaluation: true, //do not evaluate immediately when created
deferSubscribeEvaluation: true //do not evaluate when subscribed to
});
var notify = makeNotify(result);
var resultWrapper = function () {
resultPoker.value();
allPoker.value();
var firstAccess = !hasAccessed;
if (arguments.length > 0) { //writing
if (firstAccess) {
_pokableLoaded.value = true;
result();
setupArrayTracking();
}
return result.apply(result, arguments);
} else { //reading
var data = result.apply(result, arguments);
setupArrayTracking();
return data;
}
};
// hackery so that ko.unwrap and ko.isObservable work.
resultWrapper.__ko_proto__ = ko.observable;
resultWrapper._subscriptions = result._subscriptions;
resultWrapper.notifySubscribers = result.notifySubscribers;
var proxy = function (name, shouldNotify) {
resultWrapper[name] = function () {
if (!_pokableLoaded.value) {
throw new Error('data must be loaded before methods can be called.');
}
var value = _.clone(_value());
setupArrayTracking();
if (shouldNotify) {
notifyBefore(value, value, true);
}
_value[name].apply(_value, arguments);
resultPoker.poke();
if (shouldNotify) {
notifyAfter(resultWrapper(), value, true);
}
};
};
var originalSubscribe = result.subscribe;
var needsArrayTracking = false;
var arrayTrackingSetup = false;
var setupArrayTracking = function () {
if (!hasAccessed && !_pokableLoaded.value) {
throw new Error('cant setupArrayTracking when not loaded');
} else if (!hasAccessed) {
result();
}
if (needsArrayTracking && !arrayTrackingSetup) {
arrayTrackingSetup = true;
var sub = result.subscribe;
result.extend({
trackArrayChanges: true
});
var arrChangeSubscribe = result.subscribe;
result.subscribe = sub;
arrChangeSubscribe.call(result, function () {}, null, "arrayChange")
.dispose();
}
};
if (opts.type === 'array') {
// debugger;
// result.extend({
// "trackArrayChanges": true
// });
proxy('push', true);
proxy('remove', true);
proxy('removeAll', true);
proxy('indexOf', false);
}
result.subscribe = resultWrapper.subscribe = function (callback, callbackTarget, event) {
if (event === 'arrayChange') {
if (hasAccessed) {
needsArrayTracking = true;
setupArrayTracking();
} else {
//setup change tracking once target is accessed for the first time.
needsArrayTracking = true;
}
} else if (event === 'beforeChange') {
event = 'beforeSpectate';
} else {
event = 'spectate';
}
return originalSubscribe.call(result, callback, callbackTarget, event);
};
//expose the current state, which can be bound against
var _pokableLoading = makePokableValue(false);
resultWrapper.loading = ko.computed(function () {
allPoker.value();
_pokableLoading.pokerValue();
return _pokableLoading.value;
});
var _pokableLoaded = (function () {
var loaded;
if (opts.loaded === true) {
loaded = true;
} else if (opts.loaded === false) {
loaded = false;
} else {
loaded = !_.isUndefined(opts.value);
}
return makePokableValue(loaded);
})();
resultWrapper.loaded = ko.computed(function () {
allPoker.value();
_pokableLoaded.pokerValue();
return _pokableLoaded.value;
});
//executes callback once loaded. executes immediatly if the data is loaded.
resultWrapper.ensure = function (callback) {
if (_pokableLoaded.value) {
if (callback) {
callback(resultWrapper());
}
} else {
var subscription = result.subscribe(function () {
if (_pokableLoaded.value) {
subscription.dispose();
if (callback) {
callback(resultWrapper());
}
}
});
resultWrapper.refresh();
}
};
//load it again if already loaded, otherwise starts inital load
resultWrapper.refresh = function () {
//dont fire another load if its already loading
if (!_pokableLoading.value) {
if (hasAccessed === false) {
resultWrapper();
} else {
_pokableLoaded.value = false;
_pokableLoaded.poke();
getData(function () {
return false;
});
}
}
};
// Object.freeze(result);
// Object.freeze(resultWrapper);
return resultWrapper;
};
return onDemandObservable;
});
//Copyright (c) 2015, Xavier Zwirtz
//
//Permission is hereby granted, free of charge, to any person obtaining a copy of
//this software and associated documentation files (the "Software"), to deal in
//the Software without restriction, including without limitation the rights to
//use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
//of the Software, and to permit persons to whom the Software is furnished to do
//so, subject to the following conditions:
//
//The above copyright notice and this permission notice shall be included in all
//copies or substantial portions of the Software.
//
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
//FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
//IN THE SOFTWARE.
testDefine(['utils/onDemandObservable', 'lodash', 'knockout'], function (onDemandObservable, _, ko) {
'use strict';
var of;
var badLoad;
beforeEach(function () {
var ctorMap = [];
of = function (spec) {
var that = {};
that.name = spec.name;
ctorMap.push([that, of]);
return that;
};
badLoad = function () {
throw new Error('load should not fire');
};
of.instanceOf = function (val) {
if (ctorMap.length === 0) {
return false;
}
var ctors = _.filter(ctorMap, function (ctor) {
return ctor[0] === val;
});
if (ctors.length !== 1) {
return false;
}
return ctors[0][1] === of;
};
});
it("should pass ko.isObservable", function () {
var obs = onDemandObservable({
load: badLoad
});
expect(ko.isObservable(ko.observable()))
.toEqual(true);
expect(ko.isObservable(obs))
.toEqual(true);
});
describe('object', function () {
it("should create an object onDemandObservable with [default value]", function (done) {
var doneMaker = makeDoneMaker(done);
var loadedSubscribeDone = doneMaker();
var subscribeDone = doneMaker();
var afterRefreshDone = doneMaker();
var target = 'target';
var load = function (done) {
expect(this)
.toEqual(target);
done('bar');
};
var obs = onDemandObservable({
value: 'foo',
target: target,
load: load
});
expect(obs.loaded())
.toEqual(true);
expect(obs.loading())
.toEqual(false);
/* ensure loaded is not writeable */
expect(
function () {
obs.loaded(false);
})
.toThrow();
/* ensure loading is not writeable */
expect(
function () {
obs.loading(true);
})
.toThrow();
expect(obs())
.toEqual('foo');
var expectFullyLoaded = function () {
expect(obs.loaded())
.toEqual(true);
expect(obs.loading())
.toEqual(false);
expect(obs())
.toEqual('bar');
};
var loadedFireCount = 0;
obs.loaded.subscribe(function (loaded) {
loadedFireCount += 1;
if (loadedFireCount === 1) {
//should be set to false during refresh
expect(refreshRunning)
.toEqual(true);
expect(loaded)
.toEqual(false);
expect(obs())
.toEqual('foo');
} else if (loadedFireCount === 2) {
expect(refreshRunning)
.toEqual(false);
expect(loaded)
.toEqual(true);
expect(obs())
.toEqual('bar');
expectFullyLoaded();
loadedSubscribeDone();
} else {
throw new Error('hit to many times');
}
});
obs.subscribe(function (val) {
expect(obs.loaded())
.toEqual(true);
expect(val)
.toEqual('bar');
expectFullyLoaded();
subscribeDone();
});
// loadedSubscribeDone();
// subscribeDone();
var refreshRunning = true;
obs.refresh();
refreshRunning = false;
expect(obs.loaded())
.toEqual(false);
expect(obs.loading())
.toEqual(true);
afterRefreshDone();
});
it("should create an object onDemandObservable with [no value]", function (done) {
var doneMaker = makeDoneMaker(done);
var lazyBarSubscribeDone = doneMaker();
var lazyFooSubscribeDone = doneMaker();
var postSubscribeDone = doneMaker();
var target = 'target';
var loadAllowed = false;
var subscribeRunning = false;
var load = function (done) {
if (!loadAllowed) {
if (subscribeRunning) {
throw new Error('subscribe caused load to run');
} else {
throw new Error('load called to soon');
}
}
expect(this)
.toEqual(target);
done('fromload');
};
var obs = onDemandObservable({
target: target,
load: load
});
subscribeRunning = true;
var barSubscribe = obs.subscribe(function (newVal) {
expect(newVal)
.toEqual('fromload');
lazyBarSubscribeDone();
barSubscribe.dispose();
});
subscribeRunning = false;
expect(obs.loaded())
.toEqual(false);
expect(obs.loaded())
.toEqual(false);
expect(obs.loading())
.toEqual(false);
loadAllowed = true;
expect(obs())
.toEqual(undefined);
obs.ensure(function (value) {
expect(obs.loaded())
.toEqual(true);
expect(value)
.toEqual('fromload');
expect(obs())
.toEqual('fromload');
postSubscribeDone();
obs('mod1');
obs('mod2');
});
var subscribeHitCount = 0;
obs.subscribe(function (newVal) {
if (lazyBarSubscribeDone.isDone && postSubscribeDone.isDone) {
subscribeHitCount += 1;
if (subscribeHitCount === 1) {
expect(newVal)
.toEqual('fromload');
} else if (subscribeHitCount === 2) {
expect(newVal)
.toEqual('mod1');
} else if (subscribeHitCount === 3) {
expect(newVal)
.toEqual('mod2');
lazyFooSubscribeDone();
}
}
});
});
it("should create an object onDemandObservable with [default value, of type]", function (done) {
var target = 'target';
var loadAllowed = false;
var load = function (done) {
if (!loadAllowed) {
throw new Error('load called to soon');
}
expect(this)
.toEqual(target);
done({
name: 'bar'
});
};
var obs = onDemandObservable({
value: {
name: 'foo'
},
target: target,
load: load,
maker: of,
checkInstanceOf: of.instanceOf
});
expect(obs.loaded())
.toEqual(true);
expect(of.instanceOf(obs()));
expect(obs())
.toDeepEqual({
name: 'foo'
});
loadAllowed = true;
obs.refresh();
obs.loaded.subscribe(function () {
expect(obs.loaded())
.toEqual(true);
expect(of.instanceOf(obs()));
expect(obs())
.toDeepEqual({
name: 'bar'
});
done();
});
});
it("should create an object onDemandObservable with [default value, of type, use lazyMaker to load]", function (done) {
var target = 'target';
var loadAllowed = false;
var load = function (done) {
if (!loadAllowed) {
throw new Error('load called to soon');
}
expect(this)
.toEqual(target);
done({
name: 'bar'
}, 'baz');
};
var obs = onDemandObservable({
value: {
name: 'foo'
},
target: target,
load: load,
maker: of,
checkInstanceOf: of.instanceOf
});
expect(obs.loaded())
.toEqual(true);
expect(of.instanceOf(obs()));
expect(obs())
.toDeepEqual({
name: 'foo'
});
loadAllowed = true;
obs.refresh();
obs.loaded.subscribe(function () {
expect(obs.loaded())
.toEqual(true);
expect(of.instanceOf(obs()));
expect(obs())
.toDeepEqual({
name: 'bar',
});
done();
});
});
it("should create an object onDemandObservable with [no value, of type]", function (done) {
var target = 'target';
var load = function (done) {
expect(this)
.toEqual(target);
done({
name: 'bar'
});
};
var obs = onDemandObservable({
target: target,
load: load,
maker: of,
checkInstanceOf: of.instanceOf
});
expect(obs.loaded())
.toEqual(false);
expect(obs())
.toEqual(undefined);
obs.refresh();
obs.loaded.subscribe(function () {
expect(obs.loaded())
.toEqual(true);
expect(of.instanceOf(obs()));
expect(obs())
.toDeepEqual({
name: 'bar'
});
done();
});
});
it('should fire subscribe when written before accessing', function (done) {
var loadAllowed = false;
var load = function (done) {
if (!loadAllowed) {
throw new Error('load called to soon');
}
done('loaded');
};
var obs = onDemandObservable({
load: load,
});
obs('init');
obs.subscribe(function (val) {
expect(val)
.toEqual('loaded');
expect(obs())
.toEqual('loaded');
done();
});
loadAllowed = true;
obs.refresh();
});
it('should fire change and beforeChange when setting', function () {
var doTest = function (obs) {
obs('init');
var beforeChangeFired = false;
obs.subscribe(function (val) {
expect(val)
.toEqual('init');
expect(obs())
.toEqual('init');
beforeChangeFired = true;
}, null, 'beforeChange');
obs.subscribe(function (val) {
expect(val)
.toEqual('mod');
expect(obs())
.toEqual('mod');
beforeChangeFired = true;
});
obs('mod');
expect(beforeChangeFired)
.toEqual(true);
expect(obs())
.toEqual('mod');
};
doTest(ko.observable());
doTest(onDemandObservable({
load: badLoad,
}));
});
it('should fire change and beforeChange when loading', function (done) {
var doneMaker = makeDoneMaker(done);
var changeDone = doneMaker();
var beforeChangeDone = doneMaker();
var loadAllowed = false;
var load = function (done) {
if (!loadAllowed) {
throw new Error('load called to soon');
}
done('loaded');
};
var obs = onDemandObservable({
load: load,
});
obs('init');
obs.subscribe(function (val) {
expect(changeDone.isDone)
.toEqual(false);
expect(val)
.toEqual('init');
expect(obs())
.toEqual('init');
beforeChangeDone();
}, null, 'beforeChange');
obs.subscribe(function (val) {
expect(beforeChangeDone.isDone)
.toEqual(true);
expect(val)
.toEqual('loaded');
expect(obs())
.toEqual('loaded');
changeDone();
});
loadAllowed = true;
obs.refresh();
});
it('should not fire change and beforeChange when value is the same', function () {
var doTest = function (obs) {
obs('foo');
var beforeChangeFired = false;
var changeFired = false;
obs.subscribe(function () {
beforeChangeFired = true;
throw new Error('shouldnt have fired');
}, null, 'beforeChange');
obs.subscribe(function () {
changeFired = true;
throw new Error('shouldnt have fired');
});
obs('foo');
expect(beforeChangeFired)
.toEqual(false);
expect(changeFired)
.toEqual(false);
};
doTest(ko.observable());
doTest(onDemandObservable({
load: badLoad,
}));
});
describe('dependent computed', function () {
it('should update computed before firing subscribe', function () {
var parent = onDemandObservable({
load: badLoad
});
var poker = ko.observable(1);
var parentID = ko.computed({
read: function () {
poker();
if (parent.loaded()) {
return parent();
} else {
return 'not loaded';
}
}
});
var firstSubscribe = parent.subscribe(function (val) {
expect(parentID())
.toEqual('foo');
expect(val)
.toEqual('foo');
firstSubscribe.dispose();
});
parent('foo');
expect(parentID())
.toEqual('foo');
expect(parent())
.toEqual('foo');
poker(poker() + 1);
var secondSubscribe = parent.subscribe(function (val) {
expect(parentID())
.toEqual('bar');
expect(val)
.toEqual('bar');
secondSubscribe.dispose();
});
parent('bar');
expect(parentID())
.toEqual('bar');
expect(parent())
.toEqual('bar');
var thirdSubscribe = parent.subscribe(function (val) {
expect(parentID())
.toEqual('baz');
expect(val)
.toEqual('baz');
thirdSubscribe.dispose();
});
parent('baz');
expect(parentID())
.toEqual('baz');
expect(parent())
.toEqual('baz');
});
it('should fire change on dependent computed when set', function () {
var obs = onDemandObservable({
load: badLoad
});
var computedRunCount = 0;
var comp = ko.computed(function () {
computedRunCount += 1;
if (obs.loaded()) {
return obs();
} else {
return 'not loaded';
}
});
expect(computedRunCount)
.toEqual(1);
expect(comp())
.toEqual('not loaded');
expect(computedRunCount)
.toEqual(1);
var subscribeHitCount = 0;
comp.subscribe(function (newVal) {
subscribeHitCount += 1;
var expectedVal;
if (subscribeHitCount === 1) {
expectedVal = 'init';
} else if (subscribeHitCount === 2) {
expectedVal = 'mod';
} else {
throw new Error('to many hits ' + subscribeHitCount);
}
expect(newVal)
.toEqual(expectedVal);
expect(comp())
.toEqual(expectedVal);
expect(obs())
.toEqual(expectedVal);
});
obs('init');
expect(computedRunCount)
.toEqual(3);
expect(comp())
.toEqual('init');
expect(computedRunCount)
.toEqual(3);
obs('mod');
expect(computedRunCount)
.toEqual(6);
expect(comp())
.toEqual('mod');
expect(subscribeHitCount)
.toEqual(2);
expect(computedRunCount)
.toEqual(6);
});
it('should fire change on dependent computed when loaded', function (done) {
var loadAllowed = false;
var load = function (done) {
if (!loadAllowed) {
throw new Error('load called to soon');
}
done('loaded');
};
var obs = onDemandObservable({
load: load
});
var comp = ko.computed(function () {
if (obs.loaded()) {
return obs();
} else {
return 'not loaded';
}
});
expect(comp())
.toEqual('not loaded');
var subscribeFired = false;
comp.subscribe(function (val) {
expect(val)
.toEqual('loaded');
subscribeFired = true;
done();
});
expect(subscribeFired)
.toEqual(false);
loadAllowed = true;
obs();
// expect(subscribeFired)
// .toEqual(true);
// obs.ensure(function (val) {
// expect(val)
// .toEqual('loaded');
// expect(subscribeFired)
// .toEqual(true);
// });
});
});
});
describe('array', function () {
it('should create an array onDemandObservable with [default value]', function (done) {
var target = 'target';
var loadAllowed = false;
var load = function (done) {
if (!loadAllowed) {
throw new Error('load called to soon');
}
expect(this)
.toEqual(target);
done(['foo', 'bar']);
};
var obs = onDemandObservable({
value: ['foo'],
type: 'object',
target: target,
load: load
});
expect(obs.loaded())
.toEqual(true);
expect(obs())
.toEqual(['foo']);
loadAllowed = true;
obs.refresh();
obs.loaded.subscribe(function () {
expect(obs.loaded())
.toEqual(true);
expect(obs())
.toEqual(['foo', 'bar']);
done();
});
});
it("should create an array onDemandObservable with [no value]", function (done) {
var doneMaker = makeDoneMaker(done);
var subscribeDone = doneMaker();
var subscribeArrayChangeDone = doneMaker();
var loadedSubscribeDone = doneMaker();
var target = 'target';
var loadAllowed = false;
var load = function (done) {
if (!loadAllowed) {
throw new Error('load called to soon');
}
expect(this)
.toEqual(target);
done(['foo', 'bar']);
};
var obs = onDemandObservable({
type: 'array',
target: target,
load: load
});
expect(obs.loaded())
.toEqual(false);
expect(obs.loading())
.toEqual(false);
obs.subscribe(function (val) {
expect(val)
.toEqual(['foo', 'bar']);
expect(obs())
.toEqual(['foo', 'bar']);
subscribeDone();
});
obs.subscribe(function (changes) {
expect(changes)
.toDeepEqual([
{
status: 'added',
value: 'foo',
index: 0
},
{
status: 'added',
value: 'bar',
index: 1
}
]);
subscribeArrayChangeDone();
}, null, 'arrayChange');
loadAllowed = true;
expect(obs())
.toEqual([]);
expect(obs.loaded())
.toEqual(false);
expect(obs.loading())
.toEqual(true);
obs.loaded.subscribe(function (val) {
expect(val)
.toEqual(true);
expect(obs.loaded())
.toEqual(true);
expect(obs())
.toEqual(['foo', 'bar']);
loadedSubscribeDone();
});
});
it("should create an array onDemandObservable with [default value, of type]", function (done) {
var target = 'target';
var load = function (done) {
expect(this)
.toEqual(target);
done([
{
name: 'foo'
},
{
name: 'bar'
}
]);
};
var obs = onDemandObservable({
value: [
{
name: 'foo'
}
],
type: 'array',
target: target,
load: load,
maker: of,
checkInstanceOf: of.instanceOf
});
expect(obs.loaded())
.toEqual(true);
expect(of.instanceOf(obs()));
expect(obs())
.toDeepEqual([{
name: 'foo'
}]);
obs.refresh();
obs.loaded.subscribe(function () {
expect(obs.loaded())
.toEqual(true);
expect(of.instanceOf(obs()[0]));
expect(of.instanceOf(obs()[1]));
expect(obs())
.toDeepEqual([
{
name: 'foo'
},
{
name: 'bar'
}
]);
done();
});
});
it("should create an array onDemandObservable with [no value, of type]", function (done) {
var target = 'target';
var load = function (done) {
expect(this)
.toEqual(target);
done([
{
name: 'foo'
},
{
name: 'bar'
}
]);
};
var obs = onDemandObservable({
type: 'array',
target: target,
load: load,
maker: of,
checkInstanceOf: of.instanceOf
});
expect(obs.loaded())
.toEqual(false);
expect(obs())
.toDeepEqual([]);
obs.refresh();
obs.loaded.subscribe(function () {
expect(obs.loaded())
.toEqual(true);
expect(of.instanceOf(obs()[0]));
expect(of.instanceOf(obs()[1]));
expect(obs())
.toDeepEqual([
{
name: 'foo'
},
{
name: 'bar'
}
]);
done();
});
});
it("should subscribe to arrayChanges", function () {
// var doneMaker = makeDoneMaker(done);
// var subscribeDone = doneMaker();
// var subscribeArrayChangeDone = doneMaker();
var loadAllowed = false;
var load = function (done) {
if (!loadAllowed) {
throw new Error('load called to soon');
}
done([]);
};
var makeChanges = function (func, expectedChanges, expectedValues) {
var doTest = function (obs) {
var beforeChangeValues = [];
obs.subscribe(function (val) {
beforeChangeValues.push(_.cloneDeep(val));
}, null, 'beforeChange');
var allValues = [];
obs.subscribe(function (val) {
allValues.push(_.cloneDeep(val));
});
var allChanges = [];
obs.subscribe(function (changes) {
allChanges.push(changes);
}, null, 'arrayChange');
func(obs);
expect(allChanges)
.toDeepEqual(expectedChanges);
expect(allValues)
.toDeepEqual(expectedValues);
var expectedBeforeChangeValues = _(expectedValues)
.cloneDeep();
expectedBeforeChangeValues.unshift([]);
expectedBeforeChangeValues.pop();
expect(beforeChangeValues)
.toDeepEqual(expectedBeforeChangeValues);
};
doTest(onDemandObservable({
value: [],
type: 'array',
load: load
}));
/* verify that the onDemandObservable works like the stock knockout observableArray */
var koObs = ko.observableArray();
koObs.extend({
"trackArrayChanges": true
});
doTest(koObs);
};
makeChanges(function (obs) {
obs.push('foo');
obs.push('bar');
obs.push('baz');
obs.removeAll();
}, [
[{
status: 'added',
value: 'foo',
index: 0
}],
[{
status: 'added',
value: 'bar',
index: 1
}],
[{
status: 'added',
value: 'baz',
index: 2
}],
[
{
status: 'deleted',
value: 'foo',
index: 0
},
{
status: 'deleted',
value: 'bar',
index: 1
},
{
status: 'deleted',
value: 'baz',
index: 2
}
]
], [
['foo'],
['foo', 'bar'],
['foo', 'bar', 'baz'],
[]
]);
makeChanges(function (obs) {
obs.push('foo', 'bar', 'baz');
obs.remove('bar');
}, [
[{
status: 'added',
value: 'foo',
index: 0
}, {
status: 'added',
value: 'bar',
index: 1
}, {
status: 'added',
value: 'baz',
index: 2
}],
[{
status: 'deleted',
value: 'bar',
index: 1
}]
], [
['foo', 'bar', 'baz'],
['foo', 'baz']
]);
makeChanges(function (obs) {
obs(['foo', 'bar']);
obs(['foo']);
}, [
[{
status: 'added',
value: 'foo',
index: 0
}, {
status: 'added',
value: 'bar',
index: 1
}],
[{
status: 'deleted',
value: 'bar',
index: 1
}]
], [
['foo', 'bar'],
['foo']
]);
makeChanges(function (obs) {
obs(['foo', 'bar']);
obs.remove('bar');
obs(['foo']);
}, [
[{
status: 'added',
value: 'foo',
index: 0
}, {
status: 'added',
value: 'bar',
index: 1
}],
[{
status: 'deleted',
value: 'bar',
index: 1
}]
], [
['foo', 'bar'],
['foo'],
['foo']
]);
});
it("should subscribe fire arrayChange when inited with a value and value is modified using method", function () {
var obs = onDemandObservable({
value: ['foo'],
type: 'array',
load: badLoad
});
var onChange = jasmine.createSpy('onChange');
obs.subscribe(onChange, null, 'arrayChange');
expect(onChange.calls.count())
.toEqual(0);
obs.push('bar');
expect(onChange.calls.count())
.toEqual(1);
expect(onChange.calls.argsFor(0))
.toDeepEqual([[{
index: 1,
status: 'added',
value: 'bar'
}]]);
});
it("should subscribe fire arrayChange when inited with a value and value is modified write", function () {
var obs = onDemandObservable({
value: ['foo'],
type: 'array',
load: badLoad
});
var onChange = jasmine.createSpy('onChange');
obs.subscribe(onChange, null, 'arrayChange');
expect(onChange.calls.count())
.toEqual(0);
obs(['foo', 'bar']);
expect(onChange.calls.count())
.toEqual(1);
expect(onChange.calls.argsFor(0))
.toDeepEqual([[{
index: 1,
status: 'added',
value: 'bar'
}]]);
});
it('should fire change and beforeChange when value is the same', function () {
var doTest = function (obs) {
var val = ['foo'];
obs(val);
var beforeChangeFired = false;
var changeFired = false;
obs.subscribe(function (oldVal) {
expect(oldVal)
.toEqual(val);
beforeChangeFired = true;
}, null, 'beforeChange');
obs.subscribe(function (newVal) {
expect(newVal)
.toEqual(val);
changeFired = true;
});
expect(val === val)
.toEqual(true);
obs(val);
expect(beforeChangeFired)
.toEqual(true);
expect(changeFired)
.toEqual(true);
};
doTest(ko.observableArray());
doTest(onDemandObservable({
load: badLoad,
type: 'array'
}));
});
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment