Permalink
Browse files

Added a computed value type object that can be used to represent seve…

…ral observe properties or a single static value. Much of this code was in EJS and has now been moved to can/observe/compute/compute.js. Compute will be very handy for making widgets that can auto-update but don't need to.

    can.compute(function(){
      return person.attr('first')+" "+person.attr('last')
    })
  • Loading branch information...
justinbmeyer committed Jun 16, 2012
1 parent 90e6228 commit 8eb7847d410c840da38f4dd5157726e560d0a5f5
Showing with 480 additions and 168 deletions.
  1. +309 −0 observe/compute/compute.js
  2. +73 −0 observe/compute/compute_test.js
  3. +18 −0 observe/compute/qunit.html
  4. +77 −165 view/ejs/ejs.js
  5. +3 −3 view/ejs/ejs_test.js
View
@@ -0,0 +1,309 @@
+steal('can/observe', 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 oldReading;
+ if (can.Observe) {
+ // Set a callback on can.Observe to know
+ // when an attr is read.
+ // Keep a reference to the old reader
+ // if there is one. This is used
+ // for nested live binding.
+ oldReading = can.Observe.__reading;
+ can.Observe.__reading = function(obj, attr){
+ // Add the observe and attr that was read
+ // to `observed`
+ observed.push({
+ obj: obj,
+ attr: attr
+ });
+ }
+ }
+
+ var 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.
+ if(can.Observe){
+ can.Observe.__reading = oldReading;
+ }
+ 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.
+ binder = function(getterSetter, context, callback){
+ // 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];
+ }
+ }
+ };
+
+ // when a property value is cahnged
+ var onchanged = function(){
+ // 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);
+ };
+ };
+
+ // 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;
+ matched = !matched;
+
+ // go through every attribute read by this observe
+ can.each(newObserveSet, function(ob){
+ // if the observe/attribute pair is being observed
+ if(observing[ob.obj._namespace+"|"+ob.attr]){
+ // mark at as observed
+ observing[ob.obj._namespace+"|"+ob.attr].matched = matched;
+ } else {
+ // otherwise, set the observe/attribute on oldObserved, marking it as being observed
+ observing[ob.obj._namespace+"|"+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
+ /**
+ * @function can.compute
+ * @parent can.util
+ *
+ * `can.compute( getterSetter, [context] )` creates a `computed` method that represents
+ * a single value. The value is often a composite value of one or
+ * more [can.Observe] property values, but it can
+ * also represent a static value.
+ *
+ * ## Computed static values
+ *
+ * `can.compute([value])` creates a `computed` with some value. For example:
+ *
+ * // create a compute
+ * var age = can.compute(29);
+ *
+ * // read the value
+ * console.log("my age is currently", age());
+ *
+ * // listen to changes in age
+ * age.bind("change", function(ev, newVal, oldVal){
+ * console.log("my age changed from",oldVal,"to",newVal)
+ * })
+ * // update the age
+ * age(30);
+ *
+ * Notice that you can __read__, __write__,
+ * and __listen__ to changes in any single value.
+ *
+ * _NOTE: can.Observe is similar to compute, but used for objects with multiple properties._
+ *
+ * ## Computed values
+ *
+ * Computes can represent a composite value of one
+ * or more `can.Observe` properties. The following
+ * creates a fullName compute that is the `person`
+ * observe's first and last name:
+ *
+ * var person = new can.Observe({
+ * first : "Justin",
+ * last : "Meyer"
+ * });
+ * var fullName = can.compute(function(){
+ * return person.attr("first") +" "+ person.attr("last")
+ * })
+ *
+ * Read from fullName like:
+ *
+ * fullName() //-> "Justin Meyer"
+ *
+ * Listen to changes in fullName like:
+ *
+ * fullName.bind("change", function(){
+ *
+ * })
+ *
+ * When an event handler is bound to fullName it starts
+ * caching the computes value so additional reads are faster!
+ *
+ * ## Computed converters
+ *
+ * `can.compute( getterSetter )` can be used to convert one observe's value into
+ * another value. For example, a `PercentDone` widget might accept
+ * a compute that needs to have values from `0` to `100`, but your project's
+ * progress is given between `0` and `1`. Pass that widget a compute!
+ *
+ * var project = new can.Observe({
+ * progress : 0.5
+ * });
+ * var percentage = can.compute(function(newVal){
+ * // are we setting?
+ * if(newVal !=== undefined){
+ * project.attr("progress", newVal / 100)
+ * } else {
+ * return project.attr("progress") * 100;
+ * }
+ * })
+ *
+ * // We can read from percentage.
+ * percentage() //-> 50
+ *
+ * // Write to percentage,
+ * percentage(75)
+ * // but it updates project!
+ * project.attr('progress') //-> 0.75
+ *
+ * // pass it to PercentDone
+ * new PercentDone({
+ * val : percentage
+ * })
+ *
+ * ## Using computes in building controls.
+ *
+ * Widgets that listen to data changes and automatically update
+ * themselves kick ass. It's what the V in MVC is all about.
+ *
+ * However, some enironments don't have observeable data. In an ideal
+ * world, you'd like to make your widgets still useful to them.
+ *
+ * `can.compute` lets you have your cake and eat it too. Simply convert
+ * all options to compute. Provide methods to update the compute
+ * values and listen to changes in computes. Lets see how that
+ * looks with `PercentDone`:
+ *
+ * var PercentDone = can.Control({
+ * init : function(){
+ * this.options.val = can.compute(this.options.val)
+ * // rebind event handlers
+ * this.on();
+ * this.updateContent();
+ * },
+ * val: function(value){
+ * return this.options.val(value)
+ * },
+ * "{val} change" : "updateContent",
+ * updateContent : function(){
+ * this.element.html(this.options.val())
+ * }
+ * })
+ *
+ *
+ */
+ can.compute = function(getterSetter, context){
+ if(getterSetter.isComputed){
+ return getterSetter;
+ }
+ // get the value right away
+ // TODO: eventually we can defer this until a bind or a read
+ var computedData,
+ bindings = 0,
+ computed,
+ canbind = true;
+ if(typeof getterSetter === "function"){
+ computed = function(value){
+ if(value === undefined){
+ // we are reading
+ if(computedData){
+ return computedData.value;
+ } else {
+ return getterSetter.call(context || this)
+ }
+ } else {
+ return getterSetter.apply(context || this, arguments)
+ }
+ }
+
+ } else {
+ // we just gave it a value
+ computed = function(val){
+ if(val === undefined){
+ return getterSetter;
+ } else {
+ var old = getterSetter;
+ getterSetter = val;
+ if( old !== val){
+ can.trigger(computed, "change",[val, old]);
+ }
+
+ return val;
+ }
+
+ }
+ canbind = false;
+ }
+
+ computed.isComputed = true;
+
+ // var bindings
+ computed.bind = function(ev, handler){
+ can.addEvent.apply(computed, arguments);
+ if( bindings === 0 && canbind){
+ // setup live-binding
+ computedData = binder(getterSetter, context || this, function(newValue, oldValue){
+ can.trigger(computed, "change",[newValue, oldValue])
+ });
+ }
+ bindings++;
+ }
+ computed.unbind = function(ev, handler){
+ can.removeEvent.apply(computed, arguments);
+ bindings--;
+ if( bindings === 0 && canbind){
+ computedData.teardown();
+ }
+
+ };
+ return computed;
+ };
+ can.compute.binder = binder;
+})
@@ -0,0 +1,73 @@
+module('can/observe/compute')
+
+test("Basic Compute",function(){
+
+ var o = new can.Observe({first: "Justin", last: "Meyer"});
+ var prop = can.compute(function(){
+ return o.attr("first") + " " +o.attr("last")
+ })
+
+ equals(prop(), "Justin Meyer");
+ var handler = function(ev, newVal, oldVal){
+ equals(newVal, "Brian Meyer")
+ equals(oldVal, "Justin Meyer")
+ }
+ prop.bind("change", handler);
+
+ o.attr("first","Brian");
+
+ prop.unbind("change", handler)
+ o.attr("first","Brian");
+});
+
+
+test("compute on prototype", function(){
+
+ var Person = can.Observe({
+ fullName : can.compute(function(){
+ return this.attr("first") + " " +this.attr("last")
+ })
+ })
+
+ var me = new Person({
+ first : "Justin",
+ last : "Meyer"
+ });
+
+ equals(me.fullName(), "Justin Meyer");
+
+ me.bind("fullName", function(){
+
+ })
+ // to make this work, we'd have to look for a computed function and bind to it's change ...
+ // maybe bind can just work this way?
+})
+
+
+test("setter compute", function(){
+ var project = new can.Observe({
+ progress: 0.5
+ });
+
+ // a setter compute that converts 50 to .5 and vice versa
+ var computed = can.compute(function(val){
+ if(val) {
+ project.attr('progress', val / 100)
+ } else {
+ return parseInt( project.attr('progress') * 100 );
+ }
+ });
+
+ equals(computed(), 50, "the value is right");
+ computed(25);
+ equals(project.attr('progress'), 0.25);
+ equals(computed(),25 );
+
+ computed.bind("change", function(ev, newVal, oldVal){
+ equals(newVal, 75);
+ equals(oldVal, 25)
+ })
+
+ computed(75);
+
+})
Oops, something went wrong.

0 comments on commit 8eb7847

Please sign in to comment.