can-compute
Version:
CanJS’s legacy way of composing observables. Use can-observation instead.
533 lines (475 loc) • 16.9 kB
JavaScript
"use strict";
// # can/compute/proto_compute (aka can.Compute)
//
// Allows the creation of observablue values. This
// is a prototype based version of [can.compute](compute.html).
//
// can.Computes come in different flavors:
//
// - [Getter / Setter functional computes](#setup-getter-setter-functional-computes).
// - [Property computes](#setup-property-computes).
// - [Setter computes](#setup-setter-computes).
// - [Async computes](#setup-async-computes).
// - [Settings computes](#setup-settings-computes).
// - [Simple value computes](#setup-simple-value-computes).
//
//
// 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.
var Observation = require('can-observation');
var ObservationRecorder = require("can-observation-recorder");
var eventQueue = require("can-event-queue/map/map");
var observeReader = require("can-stache-key");
var getObject = require('can-key/get/get');
var assign = require("can-assign");
var canReflect = require("can-reflect");
var singleReference = require("can-single-reference");
// ## can.Compute
// Checks the arguments and calls different setup methods.
var Compute = function(getterSetter, context, eventName, bindOnce) {
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] !== undefined) {
if (contextType === 'string' || contextType === 'number') {
// Property computes.
// `new can.Compute(object, propertyName[, eventName])`
var isListLike = canReflect.isObservableLike(args[0]) && canReflect.isListLike(args[0]);
var isMapLike = canReflect.isObservableLike(args[0]) && canReflect.isMapLike(args[0]);
if(isMapLike || isListLike) {
var map = args[0];
var propertyName = args[1];
var mapGetterSetter = function(newValue){
if(arguments.length) {
observeReader.set(map,propertyName, newValue);
} else {
// forces length to be read
if(isListLike) {
observeReader.get(map,"length");
}
return observeReader.get(map,""+propertyName);
}
};
this._setupGetterSetterFn(mapGetterSetter, args[1], args[2], args[3]);
} else {
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;
};
// ## Helpers
// ## updateOnChange
// A helper to trigger an event when a value changes
var updateOnChange = function(compute, newValue, oldValue, batchNum){
var valueChanged = newValue !== oldValue && !(newValue !== newValue && oldValue !== oldValue);
// Only trigger event when value has changed
if (valueChanged) {
compute.dispatch({type: "change", batchNum: batchNum}, [
newValue,
oldValue
]);
}
};
// ### setupComputeHandlers
// 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) {
var observation = new Observation(func, context, compute);
var updater = compute.updater.bind(compute);
//!steal-remove-start
if(process.env.NODE_ENV !== 'production') {
Object.defineProperty(updater,"name",{
value: canReflect.getName(compute) + ".updater",
});
}
//!steal-remove-end
compute.observation = observation;
return {
// Call `onchanged` when any source observables change.
_on: function() {
canReflect.onValue( observation, updater,"notify");
if (observation.hasOwnProperty("_value")) {// can-observation 4.1+
compute.value = observation._value;
} else {// can-observation < 4.1
compute.value = observation.value;
}
},
// Unbind `onchanged` from all source observables.
_off: function() {
canReflect.offValue( observation, updater,"notify");
},
getDepth: function() {
return observation.getDepth();
}
};
};
eventQueue(Compute.prototype);
assign(Compute.prototype, {
setPrimaryDepth: function(depth) {
this._primaryDepth = depth;
},
// ## Setup getter / setter functional computes
// Uses the function as both a getter and setter.
_setupGetterSetterFn: function(getterSetter, context, eventName) {
this._set = context ? getterSetter.bind(context) : getterSetter;
this._get = context ? getterSetter.bind(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);
assign(this, handlers);
},
// ## Setup property computes
// Listen to a property changing on an object.
_setupProperty: function(target, propertyName, eventName) {
var self = this,
handler;
// This is objects that can be bound to with can.bind.
handler = function () {
self.updater(self._get(), self.value);
};
this._get = function() {
return getObject(target, propertyName);
};
this._set = function(value) {
// allow setting properties n levels deep, if separated with dot syntax
var properties = propertyName.split("."),
leafPropertyName = properties.pop();
if(properties.length) {
var targetProperty = getObject(target, properties.join('.'));
targetProperty[leafPropertyName] = value;
} else {
target[propertyName] = value;
}
};
this._on = function(update) {
eventQueue.on.call(target, eventName || propertyName, handler);
// Set the cached value
this.value = this._get();
};
this._off = function() {
return eventQueue.off.call( target, eventName || propertyName, handler);
};
},
// ## Setup Setter Computes
// Only a setter function is specified.
_setupSetter: function(initialValue, setter, eventName) {
this.value = initialValue;
this._set = setter;
assign(this, eventName);
},
// ## Setup settings computes
// 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;
},
// ## Setup async computes
// This is a special, non-documented form of a compute
// rhat can asynchronously update its value.
_setupAsyncCompute: function(initialValue, settings){
var self = this;
// This is the async getter function. Depending on how many arguments the function takes,
// we setup bindings differently.
var getter = settings.fn;
var bindings;
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 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() );
};
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,
resolve = ObservationRecorder.ignore(function(newVal) {
oldUpdater.call(self, newVal, self.value);
});
// Because `setupComputeHandlers` calls `updater` internally with its
// observation._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(), resolve);
// If undefined is returned, don't update the value.
return res !== undefined ? res : this.value;
}, this);
}
assign(this, bindings);
},
// ## Setup simple value computes
// Uses the default `_get`, `_set` behaviors.
_setupSimpleValue: function(initialValue) {
this.value = initialValue;
},
// ## _bindsetup
// 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.
_eventSetup: ObservationRecorder.ignore(function () {
this.bound = true;
this._on(this.updater);
}),
// ## _bindteardown
// When a compute has no other bindings, call the internal `this._off` method.
_eventTeardown: function () {
this._off(this.updater);
this.bound = false;
},
// ## clone
// 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 Compute(this._args[0], this._args[1], this._args[2], this._args[3]);
},
// ## _on and _off
// Default _on and _off do nothing.
_on: function(){},
_off: function(){},
// ## get
// Returns the cached value if `bound`, otherwise, returns
// the _get value.
get: function() {
// If an external compute is tracking observables and
// this compute can be listened to by "function" based computes ....
var recordingObservation = ObservationRecorder.isRecording();
if(recordingObservation && this._canObserve !== false) {
// ... tell the tracking compute to listen to change on this computed.
ObservationRecorder.add(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) {
Compute.temporarilyBind(this);
}
}
// If computed is bound, use the cached value.
if (this.bound) {
// if it has dependencies ... it should do some stuff ...
if(this.observation) {
return this.observation.get();
} else {
return this.value;
}
} else {
return this._get();
}
},
// ## _get
// Returns the cached value.
_get: function() {
return this.value;
},
// ## set
// 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
this.updater(setVal === undefined ? this._get() : setVal, old);
return this.value;
},
// ## _set
// Updates the cached value.
_set: function(newVal) {
return this.value = newVal;
},
// ## updater
// Updates the cached value and fires an event if the value has changed.
updater: function(newVal, oldVal, batchNum) {
this.value = newVal;
var observation = this.observation;
if (observation) {
// it's possible the observation doesn't actually
// have any dependencies
if (observation.hasOwnProperty("_value")) {// can-observation 4.1+
observation._value = newVal;
} else {// can-observation < 4.1
observation.value = newVal;
}
}
updateOnChange(this, newVal, oldVal, batchNum);
},
// ## toFunction
// Returns a proxy form of this compute.
toFunction: function() {
return this._computeFn.bind( this);
},
_computeFn: function(newVal) {
if(arguments.length) {
return this.set(newVal);
}
return this.get();
}
});
Compute.prototype.on = Compute.prototype.bind = Compute.prototype.addEventListener;
Compute.prototype.off = Compute.prototype.unbind = Compute.prototype.removeEventListener;
var hasDependencies = function hasDependencies() {
return this.observation && this.observation.hasDependencies();
};
Object.defineProperty(Compute.prototype, "hasDependencies", {
get: hasDependencies
});
// ### temporarilyBind
// Binds computes for a moment to cache their value and prevent re-calculating it.
Compute.temporarilyBind = Observation.temporarilyBind;
// ### async
// A simple helper that makes an async compute a bit easier.
Compute.async = function(initialValue, asyncComputer, context){
return new Compute(initialValue, {
fn: asyncComputer,
context: context
});
};
// ### truthy
// Wraps a compute with another compute that only changes when
// the wrapped compute's `truthiness` changes.
Compute.truthy = function(compute) {
return new Compute(function() {
var res = compute.get();
if(typeof res === 'function') {
res = res.get();
}
return !!res;
});
};
canReflect.assignSymbols(Compute.prototype, {
"can.isValueLike": true,
"can.isMapLike": false,
"can.isListLike": false,
"can.setValue": Compute.prototype.set,
"can.getValue": Compute.prototype.get,
"can.valueHasDependencies": hasDependencies,
"can.onValue": function onValue(handler, queue) {
function translationHandler(ev, newValue, oldValue) {
handler(newValue, oldValue);
}
singleReference.set(handler, this, translationHandler);
//!steal-remove-start
if(process.env.NODE_ENV !== 'production') {
Object.defineProperty(translationHandler, "name", {
value: canReflect.getName(handler) + "::onValue"
});
}
//!steal-remove-end
this.addEventListener("change", translationHandler, queue);
},
"can.offValue": function offValue(handler, queue) {
this.removeEventListener(
"change",
singleReference.getAndDelete(handler, this),
queue
);
},
"can.getValueDependencies": function getValueDependencies() {
var ret;
if (this.observation) {
ret = {
valueDependencies: new Set([this.observation])
};
}
return ret;
}
});
module.exports = exports = Compute;