UNPKG

can

Version:

MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.

487 lines (427 loc) 15.5 kB
// # 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. 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 // Checks the arguments and calls different setup methods. 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; }, // ## Setup getter / setter functional computes // Uses the function as both a getter and setter. _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); }, // ## Setup property computes // Listen to a property changing on an object. _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); }; }, // ## Setup Setter Computes // Only a setter function is specified. _setupSetter: function(initialValue, setter, eventName) { this.value = initialValue; this._set = setter; can.simpleExtend(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.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); }, // ## 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. _bindsetup: can.__notObserve(function () { this.bound = true; this._on(this.updater); }), // ## _bindteardown // When a compute has no other bindings, call the internal `this._off` method. _bindteardown: function () { this._off(this.updater); this.bound = false; }, // ## bind and unbind // A bind and unbind that calls `_bindsetup` and `_bindteardown`. bind: can.bindAndSetup, unbind: can.unbindAndTeardown, // ## 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 can.Compute(this._args[0], this._args[1], this._args[2], this._args[3]); }, // ## _on and _off // Default _on and _off do nothing. _on: can.k, _off: can.k, // ## get // Returns the cached value if `bound`, otherwise, returns // the _get value. 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 // 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 if (setVal === undefined) { this.value = this._get(); } else { this.value = setVal; } // Fire the change updateOnChange(this, this.value, 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; updateOnChange(this, newVal, oldVal, batchNum); }, // ## toFunction // Returns a proxy form of this compute. toFunction: function() { return can.proxy(this._computeFn, this); }, _computeFn: function(newVal) { if(arguments.length) { return this.set(newVal); } return this.get(); } }); // ## 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) { can.batch.trigger(compute, {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) { // 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(); } }; }; // ### temporarilyBind // Binds computes for a moment to cache their value and prevent re-calculating it. 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; }; // ### async // A simple helper that makes an async compute a bit easier. can.Compute.async = function(initialValue, asyncComputer, context){ return new can.Compute(initialValue, { fn: asyncComputer, context: context }); }; // ### truthy // 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; }); }; // ### compatability // Setting methods that should not be around in 3.0. can.Compute.read = read; can.Compute.set = read.write; return can.Compute; });