todomvc
Version:
> Helping you select an MV\* framework
429 lines (393 loc) • 12.5 kB
JavaScript
/*!
* CanJS - 2.0.3
* http://canjs.us/
* Copyright (c) 2013 Bitovi
* Tue, 26 Nov 2013 18:21:22 GMT
* Licensed MIT
* Includes: CanJS default build
* Download from: http://canjs.us/
*/
define(["can/util/library", "can/util/bind", "can/util/batch"], function(can, bind) {
var names = ["__reading","__clearReading","__setReading"],
setup = function(observed){
var old = {};
for(var i =0; i < names.length; i++){
old[names[i]] = can[names[i]]
}
can.__reading = function(obj, attr){
// Add the observe and attr that was read
// to `observed`
observed.push({
obj: obj,
attr: attr+""
});
};
can.__clearReading = function(){
return observed.splice(0, observed.length);
}
can.__setReading = function(o){
[].splice.apply(observed, [0, observed.length].concat(o))
}
return old;
},
// empty default function
k = function(){};
// returns the
// - observes and attr methods are called by func
// - the value returned by func
// ex: `{value: 100, observed: [{obs: o, attr: "completed"}]}`
var getValueAndObserved = function(func, self){
var observed = [],
old = setup(observed),
// Call the "wrapping" function to get the value. `observed`
// will have the observe/attribute pairs that were read.
value = func.call(self);
// Set back so we are no longer reading.
can.simpleExtend(can,old);
return {
value : value,
observed : observed
};
},
// Calls `callback(newVal, oldVal)` everytime an observed property
// called within `getterSetter` is changed and creates a new result of `getterSetter`.
// Also returns an object that can teardown all event handlers.
computeBinder = function(getterSetter, context, callback, computeState){
// track what we are observing
var observing = {},
// a flag indicating if this observe/attr pair is already bound
matched = true,
// the data to return
data = {
// we will maintain the value while live-binding is taking place
value : undefined,
// a teardown method that stops listening
teardown: function(){
for ( var name in observing ) {
var ob = observing[name];
ob.observe.obj.unbind(ob.observe.attr, onchanged);
delete observing[name];
}
}
},
batchNum;
// when a property value is changed
var onchanged = function(ev){
// If the compute is no longer bound (because the same change event led to an unbind)
// then do not call getValueAndBind, or we will leak bindings.
if ( computeState && !computeState.bound ) {
return;
}
if(ev.batchNum === undefined || ev.batchNum !== batchNum) {
// store the old value
var oldValue = data.value,
// get the new value
newvalue = getValueAndBind();
// update the value reference (in case someone reads)
data.value = newvalue;
// if a change happened
if ( newvalue !== oldValue ) {
callback(newvalue, oldValue);
}
batchNum = batchNum = ev.batchNum;
}
};
// gets the value returned by `getterSetter` and also binds to any attributes
// read by the call
var getValueAndBind = function(){
var info = getValueAndObserved( getterSetter, context ),
newObserveSet = info.observed;
var value = info.value,
ob;
matched = !matched;
// go through every attribute read by this observe
for ( var i = 0, len = newObserveSet.length; i < len; i++ ) {
ob = newObserveSet[i];
// if the observe/attribute pair is being observed
if(observing[ob.obj._cid+"|"+ob.attr]){
// mark at as observed
observing[ob.obj._cid+"|"+ob.attr].matched = matched;
} else {
// otherwise, set the observe/attribute on oldObserved, marking it as being observed
observing[ob.obj._cid+"|"+ob.attr] = {
matched: matched,
observe: ob
};
ob.obj.bind(ob.attr, onchanged);
}
}
// Iterate through oldObserved, looking for observe/attributes
// that are no longer being bound and unbind them
for ( var name in observing ) {
var ob = observing[name];
if(ob.matched !== matched){
ob.observe.obj.unbind(ob.observe.attr, onchanged);
delete observing[name];
}
}
return value;
};
// set the initial value
data.value = getValueAndBind();
data.isListening = ! can.isEmptyObject(observing);
return data;
}
// if no one is listening ... we can not calculate every time
can.compute = function(getterSetter, context, eventName){
if(getterSetter && getterSetter.isComputed){
return getterSetter;
}
// stores the result of computeBinder
var computedData,
// how many listeners to this this compute
bindings = 0,
// the computed object
computed,
// an object that keeps track if the computed is bound
// onchanged needs to know this. It's possible a change happens and results in
// something that unbinds the compute, it needs to not to try to recalculate who it
// is listening to
computeState = {
bound: false,
// true if this compute is calculated from other computes and observes
hasDependencies: false
},
// The following functions are overwritten depending on how compute() is called
// a method to setup listening
on = k,
// a method to teardown listening
off = k,
// the current cached value (only valid if bound = true)
value,
// how to read the value
get = function(){
return value
},
// sets the value
set = function(newVal){
value = newVal;
},
// this compute can be a dependency of other computes
canReadForChangeEvent = true,
// save for clone
args = can.makeArray(arguments),
updater= function(newValue, oldValue){
value = newValue;
// might need a way to look up new and oldVal
can.batch.trigger(computed, "change",[newValue, oldValue])
},
// the form of the arguments
form;
computed = function(newVal){
// setting ...
if(arguments.length){
// save a reference to the old value
var old = value;
// setter may return a value if
// setter is for a value maintained exclusively by this compute
var setVal = set.call(context,newVal, old);
// if this has dependencies return the current value
if(computed.hasDependencies){
return get.call(context);
}
if(setVal === undefined) {
// it's possible, like with the DOM, setting does not
// fire a change event, so we must read
value = get.call(context);
} else {
value = setVal;
}
// fire the change
if( old !== value){
can.batch.trigger(computed, "change",[value, old] );
}
return value;
} else {
// Another compute wants to bind to this compute
if( can.__reading && canReadForChangeEvent ) {
// Tell the compute to listen to change on this computed
can.__reading(computed,'change');
// We are going to bind on this compute.
// If we are not bound, we should bind so that
// we don't have to re-read to get the value of this compute.
!computeState.bound && can.compute.temporarilyBind(computed)
}
// if we are bound, use the cached value
if( computeState.bound ) {
return value;
} else {
return get.call(context);
}
}
}
if(typeof getterSetter === "function"){
set = getterSetter;
get = getterSetter;
canReadForChangeEvent = eventName === false ? false : true;
computed.hasDependencies = false;
on = function(update){
computedData = computeBinder(getterSetter, context || this, update, computeState);
computed.hasDependencies = computedData.isListening
value = computedData.value;
}
off = function(){
computedData && computedData.teardown();
}
} else if(context) {
if(typeof context == "string"){
// `can.compute(obj, "propertyName", [eventName])`
var propertyName = context,
isObserve = getterSetter instanceof can.Map;
if(isObserve){
computed.hasDependencies = true;
}
get = function(){
if(isObserve){
return getterSetter.attr(propertyName);
} else {
return getterSetter[propertyName];
}
}
set = function(newValue){
if(isObserve){
getterSetter.attr(propertyName, newValue)
} else {
getterSetter[propertyName] = newValue;
}
}
var handler;
on = function(update){
handler = function(){
update(get(), value)
};
can.bind.call(getterSetter, eventName || propertyName,handler)
// use getValueAndObserved because
// we should not be indicating that some parent
// reads this property if it happens to be binding on it
value = getValueAndObserved(get).value
}
off = function(){
can.unbind.call(getterSetter, eventName || propertyName,handler)
}
} else {
// `can.compute(initialValue, setter)`
if(typeof context === "function"){
value = getterSetter;
set = context;
context = eventName;
form = "setter";
} else {
// `can.compute(initialValue,{get:, set:, on:, off:})`
value = getterSetter;
var options = context;
get = options.get || get;
set = options.set ||set;
on = options.on || on;
off = options.off || off;
}
}
} else {
// `can.compute(5)`
value = getterSetter;
}
can.cid(computed,"compute")
return can.simpleExtend(computed,{
/**
* @property {Boolean} can.computed.isComputed compute.isComputed
* @parent can.compute
* Whether the value of the compute has been computed yet.
*/
isComputed: true,
_bindsetup: function(){
computeState.bound = true;
// setup live-binding
// while binding, this does not count as a read
var oldReading = can.__reading;
delete can.__reading;
on.call(this, updater);
can.__reading = oldReading;
},
_bindteardown: function(){
off.call(this,updater)
computeState.bound = false;
},
/**
* @function can.computed.bind compute.bind
* @parent can.compute
* @description Bind an event handler to a compute.
* @signature `compute.bind(eventType, handler)`
* @param {String} eventType The event to bind this handler to.
* The only event type that computes emit is _change_.
* @param {function({Object},{*},{*})} handler The handler to call when the event happens.
* The handler should have three parameters:
*
* - _event_ is the event object.
* - _newVal_ is the newly-computed value of the compute.
* - _oldVal_ is the value of the compute before it changed.
*
* `bind` lets you listen to a compute to know when it changes. It works just like
* can.Map's `[can.Map.prototype.bind bind]`:
@codestart
* var tally = can.compute(0);
* tally.bind('change', function(ev, newVal, oldVal) {
* console.log('The tally is now at ' + newVal + '.');
* });
*
* tally(tally() + 5); // The log reads:
* // 'The tally is now at 5.'
* @codeend
*/
bind: can.bindAndSetup,
/**
* @function computed.unbind compute.unbind
* @parent can.compute
* @description Unbind an event handler from a compute.
* @signature `compute.unbind(eventType[, handler])`
* @param {String} eventType The type of event to unbind.
* The only event type available for computes is _change_.
* @param {function} [handler] If given, the handler to unbind.
* If _handler_ is not supplied, all handlers bound to _eventType_
* will be removed.
*/
unbind: can.unbindAndTeardown,
clone: function(context){
if(context){
if(form == "setter"){
args[2] = context
} else {
args[1] = context
}
}
return can.compute.apply(can,args);
}
});
};
// a list of temporarily bound computes
var computes,
unbindComputes = function(){
for( var i =0, len = computes.length; i < len; i++ ) {
computes[i].unbind("change",k)
}
computes = null;
}
// Binds computes for a moment to retain their value and prevent caching
can.compute.temporarilyBind = function(compute){
compute.bind("change",k)
if(!computes){
computes = [];
setTimeout(unbindComputes,10)
}
computes.push(compute)
};
can.compute.binder = computeBinder;
can.compute.truthy = function(compute){
return can.compute(function(){
var res = compute();
if(typeof res === "function"){
res = res()
}
return !!res;
})
}
return can.compute;
});