steal('can/util', 'can/util/bind', 'can/compute/read.js','can/compute/get_value_and_bind.js','can/util/batch', function (can, bind, read, ObservedInfo) {
Allows the creation of observablue values. This is a prototype based version of can.compute.
can.Computes come in different flavors:
can.Computes have public .get
, .set
, .on
, and .off
methods that call
internal methods that are configured differently depending on what flavor of
compute is being created. Those methods are:
_on(updater)
- Called the first time the compute is bound. This should bind to
any source observables. When any of the source observables have changed, it should call
updater(newVal, oldVal, batchNum)
.
_off(updater)
- Called when the compute has no more event handlers. This should unbind to any source observables.
_get
- Called to get the current value of the compute._set
- Called to set the value of the compute.Other internal flags and values:
value
- the cached value_setUpdates
- if calling _set
will have updated the cached value itself so _get
does not need to be called._canObserve
- if this compute can be observed.hasDependencies
- if this compute has source observable values.steal('can/util', 'can/util/bind', 'can/compute/read.js','can/compute/get_value_and_bind.js','can/util/batch', function (can, bind, read, ObservedInfo) {
can.Compute = function(getterSetter, context, eventName, bindOnce) {
can.cid(this, 'compute');
var args = [];
for(var i = 0, arglen = arguments.length; i < arglen; i++) {
args[i] = arguments[i];
}
var contextType = typeof args[1];
if (typeof args[0] === 'function') {
Getter/Setter functional computes.
new can.Compute(function(){ ... })
this._setupGetterSetterFn(args[0], args[1], args[2], args[3]);
} else if (args[1]) {
if (contextType === 'string') {
Property computes.
new can.Compute(object, propertyName[, eventName])
this._setupProperty(args[0], args[1], args[2]);
} else if(contextType === 'function') {
Setter computes.
new can.Compute(initialValue, function(newValue){ ... })
this._setupSetter(args[0], args[1], args[2]);
} else {
if(args[1] && args[1].fn) {
Async computes.
this._setupAsyncCompute(args[0], args[1]);
} else {
Settings computes.
new can.Compute(initialValue, {on, off, get, set})
this._setupSettings(args[0], args[1]);
}
}
} else {
Simple value computes.
new can.Compute(initialValue)
this._setupSimpleValue(args[0]);
}
this._args = args;
this._primaryDepth = 0;
this.isComputed = true;
};
can.simpleExtend(can.Compute.prototype, {
setPrimaryDepth: function(depth) {
this._primaryDepth = depth;
},
_setupGetterSetterFn: function(getterSetter, context, eventName) {
this._set = context ? can.proxy(getterSetter, context) : getterSetter;
this._get = context ? can.proxy(getterSetter, context) : getterSetter;
this._canObserve = eventName === false ? false : true;
The helper provides the on and off methods that use getValueAndBind
.
var handlers = setupComputeHandlers(this, getterSetter, context || this);
can.simpleExtend(this, handlers);
},
_setupProperty: function(target, propertyName, eventName) {
var isObserve = can.isMapLike( target ),
self = this,
handler;
If a can.Map
, setup to read and write to that property.
if(isObserve) {
We should pass the batchNum if there is one.
handler = function(ev, newVal,oldVal) {
self.updater(newVal, oldVal, ev.batchNum);
};
this.hasDependencies = true;
this._get = function() {
return target.attr(propertyName);
};
this._set = function(val) {
target.attr(propertyName, val);
};
} else {
This is objects that can be bound to with can.bind.
handler = function () {
self.updater(self._get(), self.value);
};
this._get = function() {
return can.getObject(propertyName, [target]);
};
this._set = function(value) {
allow setting properties n levels deep, if separated with dot syntax
var properties = propertyName.split("."),
leafPropertyName = properties.pop(),
targetProperty = can.getObject(properties.join('.'), [target]);
targetProperty[leafPropertyName] = value;
};
}
this._on = function(update) {
can.bind.call(target, eventName || propertyName, handler);
Set the cached value
this.value = this._get();
};
this._off = function() {
return can.unbind.call( target, eventName || propertyName, handler);
};
},
_setupSetter: function(initialValue, setter, eventName) {
this.value = initialValue;
this._set = setter;
can.simpleExtend(this, eventName);
},
Use whatever on
, off
, get
, set
the users provided
as the internal methods.
_setupSettings: function(initialValue, settings) {
this.value = initialValue;
this._set = settings.set || this._set;
this._get = settings.get || this._get;
This allows updater to be called without any arguments. selfUpdater flag can be set by things that want to call updater themselves.
if(!settings.__selfUpdater) {
var self = this,
oldUpdater = this.updater;
this.updater = function() {
oldUpdater.call(self, self._get(), self.value);
};
}
this._on = settings.on ? settings.on : this._on;
this._off = settings.off ? settings.off : this._off;
},
This is a special, non-documented form of a compute rhat can asynchronously update its value.
_setupAsyncCompute: function(initialValue, settings){
var self = this;
this.value = initialValue;
This compute will call update with the new value itself.
this._setUpdates = true;
An “async” compute has a lastSetValue
that represents
the last value compute.set
was called with.
The following creates lastSetValue
as a can.Compute so when
lastSetValue
is changed, the getter
can see that change
and automatically update itself.
this.lastSetValue = new can.Compute(initialValue);
Wires up setting this compute to set lastSetValue
.
If the new value matches the last setValue, do nothing.
this._set = function(newVal){
if(newVal === self.lastSetValue.get()) {
return this.value;
}
return self.lastSetValue.set(newVal);
};
Wire up the get to pass the lastNewValue
this._get = function() {
return getter.call(settings.context, self.lastSetValue.get() );
};
This is the async getter function. Depending on how many arguments the function takes, we setup bindings differently.
var getter = settings.fn,
bindings;
if(getter.length === 0) {
If it takes no arguments, it should behave just like a Getter compute.
bindings = setupComputeHandlers(this, getter, settings.context);
} else if(getter.length === 1) {
If it has a single argument, pass it the last setValue.
bindings = setupComputeHandlers(this, function() {
return getter.call(settings.context, self.lastSetValue.get() );
}, settings);
} else {
If the function takes 2 arguments, the second argument is a function
that should update the value of the compute (setValue
). To make this we need
the “normal” updater function because we are about to overwrite it.
var oldUpdater = this.updater,
setValue = function(newVal) {
oldUpdater.call(self, newVal, self.value);
};
Because setupComputeHandlers
calls updater
internally with its
readInfo.value as oldValue
and that might not be up to date,
we overwrite updater to always use self.value.
this.updater = function(newVal) {
oldUpdater.call(self, newVal, self.value);
};
bindings = setupComputeHandlers(this, function() {
Call getter, and get new value
var res = getter.call(settings.context, self.lastSetValue.get(), setValue);
If undefined is returned, don’t update the value.
return res !== undefined ? res : this.value;
}, this);
}
can.simpleExtend(this, bindings);
},
_setupSimpleValue: function(initialValue) {
this.value = initialValue;
},
When a compute is first bound, call the internal this._on
method.
can.__notObserve
makes sure if _on
is listening to any observables,
they will not be observed by any outer compute.
_bindsetup: can.__notObserve(function () {
this.bound = true;
this._on(this.updater);
}),
_bindteardown: function () {
this._off(this.updater);
this.bound = false;
},
bind: can.bindAndSetup,
unbind: can.unbindAndTeardown,
Copies this compute, but for a different context. This is mostly used for computes on a map’s prototype.
clone: function(context) {
if(context && typeof this._args[0] === 'function') {
this._args[1] = context;
} else if(context) {
this._args[2] = context;
}
return new can.Compute(this._args[0], this._args[1], this._args[2], this._args[3]);
},
_on: can.k,
_off: can.k,
get: function() {
var recordingObservation = can.__isRecordingObserves();
If an external compute is tracking observables and this compute can be listened to by “function” based computes ….
if(recordingObservation && this._canObserve !== false) {
… tell the tracking compute to listen to change on this computed.
can.__observe(this, 'change');
… if we are not bound, we should bind so that we don’t have to re-read to get the value of this compute.
if (!this.bound) {
can.Compute.temporarilyBind(this);
}
}
If computed is bound, use the cached value.
if (this.bound) {
if(recordingObservation && this.getDepth && this.getDepth() >= recordingObservation.getDepth()) {
ObservedInfo.updateUntil(this.getPrimaryDepth(), this.getDepth());
}
return this.value;
} else {
return this._get();
}
},
_get: function() {
return this.value;
},
Sets the value of the compute.
Depending on the type of the compute and what _set
returns, it might need to call _get
after
_set
to get the final value.
set: function(newVal) {
var old = this.value;
Setter may return the value if setter is for a value maintained exclusively by this compute.
var setVal = this._set(newVal, old);
If the setter updated this.value, just return that.
if(this._setUpdates) {
return this.value;
}
If the computed function has dependencies, we should call the getter.
if (this.hasDependencies) {
return this._get();
}
Setting may not fire a change event, in which case the value must be read
if (setVal === undefined) {
this.value = this._get();
} else {
this.value = setVal;
}
Fire the change
updateOnChange(this, this.value, old);
return this.value;
},
_set: function(newVal) {
return this.value = newVal;
},
updater: function(newVal, oldVal, batchNum) {
this.value = newVal;
updateOnChange(this, newVal, oldVal, batchNum);
},
toFunction: function() {
return can.proxy(this._computeFn, this);
},
_computeFn: function(newVal) {
if(arguments.length) {
return this.set(newVal);
}
return this.get();
}
});
var updateOnChange = function(compute, newValue, oldValue, batchNum){
var valueChanged = newValue !== oldValue && !(newValue !== newValue && oldValue !== oldValue);
Only trigger event when value has changed
if (valueChanged) {
can.batch.trigger(compute, {type: "change", batchNum: batchNum}, [
newValue,
oldValue
]);
}
};
A helper that creates an _on
and _off
function that
will bind on source observables and update the value of the compute.
var setupComputeHandlers = function(compute, func, context) {
The last observeInfo object returned by getValueAndBind.
var readInfo = new ObservedInfo(func, context, compute);
return {
readInfo: readInfo,
Call onchanged
when any source observables change.
_on: function() {
readInfo.getValueAndBind();
compute.value = readInfo.value;
compute.hasDependencies = !can.isEmptyObject(readInfo.newObserved);
},
Unbind onchanged
from all source observables.
_off: function() {
readInfo.teardown();
},
getDepth: function() {
return readInfo.getDepth();
},
getPrimaryDepth: function() {
return readInfo.getPrimaryDepth();
}
};
};
can.Compute.temporarilyBind = function (compute) {
var computeInstance = compute.computeInstance || compute;
computeInstance.bind('change', can.k);
if (!computes) {
computes = [];
setTimeout(unbindComputes, 10);
}
computes.push(computeInstance);
};
A list of temporarily bound computes
var computes,
Unbinds all temporarily bound computes.
unbindComputes = function () {
for (var i = 0, len = computes.length; i < len; i++) {
computes[i].unbind('change', can.k);
}
computes = null;
};
can.Compute.async = function(initialValue, asyncComputer, context){
return new can.Compute(initialValue, {
fn: asyncComputer,
context: context
});
};
Wraps a compute with another compute that only changes when
the wrapped compute’s truthiness
changes.
can.Compute.truthy = function(compute) {
return new can.Compute(function() {
var res = compute.get();
if(typeof res === 'function') {
res = res.get();
}
return !!res;
});
};
can.Compute.read = read;
can.Compute.set = read.write;
return can.Compute;
});