Created
February 12, 2015 00:08
-
-
Save xavierzwirtz/b99d46ca2ce365cf1107 to your computer and use it in GitHub Desktop.
This file contains 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
//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; | |
}); |
This file contains 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
//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