Skip to content

Commit

Permalink
first cut of computes binding on things other than observes
Browse files Browse the repository at this point in the history
  • Loading branch information
justinbmeyer authored and andykant committed Apr 18, 2013
1 parent e7e335b commit e73635f
Show file tree
Hide file tree
Showing 2 changed files with 265 additions and 45 deletions.
166 changes: 122 additions & 44 deletions observe/compute/compute.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,57 +300,131 @@ steal('can/util', function(can) {
*
*
*/
can.compute = function(getterSetter, context){
can.compute = function(getterSetter, context, eventName){
if(getterSetter && getterSetter.isComputed){
return getterSetter;
}
// get the value right away
// TODO: eventually we can defer this until a bind or a read
// stores the result of computeBinder
var computedData,
// how many listeners to this this compute
bindings = 0,
// the computed object
computed,
canbind = true;
if(typeof getterSetter === "function"){
computed = function(value){
if(value === undefined){
// we are reading
if(computedData){
// If another compute is calling this compute for the value,
// it needs to bind to this compute's change so it will re-compute
// and re-bind when this compute changes.
if(bindings && can.Observe.__reading) {
can.Observe.__reading(computed,'change');
}
return computedData.value;
} else {
return getterSetter.call(context || this)
}
// 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 },
// The following functions are overwritten depending on how compute() is called
// a method to setup listening
on = function(){},
// a method to teardown listening
off = function(){},
// 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;
}

computed = function(newVal){
// setting ...
if(arguments.length){
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(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;
}

if( old !== value){
can.Observe.triggerBatch(computed, "change",[value, old] );
}
return value;
} else {
// always let others konw to listen to changes in this compute
if( can.Observe.__reading ) {
can.Observe.__reading(computed,'change');
}
// if we are bound, use the cached value
if( computeState.bound ) {
return value;
} else {
return getterSetter.apply(context || this, arguments)
return get.call(context);
}
}
}

if(typeof getterSetter === "function"){
set = getterSetter;
get = getterSetter;

} else {
// we just gave it a value
computed = function(val){
if(val === undefined){
// If observing, record that the value is being read.
if(can.Observe.__reading) {
can.Observe.__reading(computed,'change');
on = function(update){
computedData = computeBinder(getterSetter, context || this, update, computeState);
value = computedData.value;
}
off = function(){
computedData.teardown();
}

} else if(context) {

if(typeof context == "string"){
// `can.compute(obj, "propertyName", [eventName])`
var propertyName = context;
get = function(){
if(getterSetter instanceof can.Observe){
return getterSetter.attr(propertyName)
} else {
return getterSetter[propertyName]
}
return getterSetter;
} else {
var old = getterSetter;
getterSetter = val;
if( old !== val){
can.Observe.triggerBatch(computed, "change",[val, old]);
}
set = function(newValue){
if(getterSetter instanceof can.Observe){
getterSetter.attr(propertyName, newValue)
} else {
getterSetter[propertyName] = newValue;
}

return val;
}

var handler;
on = function(update){
handler = function(){
update(get(), value)
};
can.bind.call(getterSetter, eventName || propertyName,handler)
}
off = function(){
can.unbind.call(getterSetter, eventName || propertyName,handler)
}
value = get();

} 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;
}
canbind = false;




} else {
// `can.compute(5)`
value = getterSetter;
}
/**
* @attribute isComputed
Expand All @@ -359,19 +433,23 @@ steal('can/util', function(can) {
computed.isComputed = true;

can.cid(computed,"compute")
var computeState = { bound: false };
var updater= function(newValue, oldValue){
value = newValue;
// might need a way to look up new and oldVal
can.Observe.triggerBatch(computed, "change",[newValue, oldValue])

}
/**
* @function bind
* `compute.bind("change", handler(event, newVal, oldVal))`
*/
computed.bind = function(ev, handler){
can.addEvent.apply(computed, arguments);
if( bindings === 0 && canbind){
if( bindings === 0 ){
computeState.bound = true;
// setup live-binding
computedData = computeBinder(getterSetter, context || this, function(newValue, oldValue){
can.Observe.triggerBatch(computed, "change",[newValue, oldValue])
}, computeState);
on.call(this, updater)

}
bindings++;
}
Expand All @@ -382,8 +460,8 @@ steal('can/util', function(can) {
computed.unbind = function(ev, handler){
can.removeEvent.apply(computed, arguments);
bindings--;
if( bindings === 0 && canbind){
computedData.teardown();
if( bindings === 0 ){
off.call(this,updater)
computeState.bound = false;
}

Expand Down
144 changes: 143 additions & 1 deletion observe/compute/compute_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ test("compute a compute", function() {
return parseInt( project.attr('progress') * 100, 10);
}
});
percent.named = "PERCENT";

equals(percent(),50,'percent starts right');
percent.bind('change',function() {
Expand All @@ -107,6 +108,7 @@ test("compute a compute", function() {
return percent() + '/100';
}
});
fraction.named ="FRACTIOn"

fraction.bind('change',function() {
// noop
Expand Down Expand Up @@ -230,13 +232,17 @@ test("compute only updates once when a list's contents are replaced",function(){

var list = new can.Observe.List([{name: "Justin"}]),
computedCount = 0;

var compute = can.compute(function(){
computedCount++;
list.each(function(item){
item.attr('name')
})
})
});

equals(0,computedCount, "computes are not called until their value is read")


compute.bind("change", function(ev, newVal, oldVal){

})
Expand Down Expand Up @@ -332,4 +338,140 @@ test("compute doesn't rebind and leak with 0 bindings", function() {
equal(computedA, 3, "unbound, so didn't recompute A");
equal(computedB, 2, "unbound, so didn't recompute B");
});

/*
test("compute setter without external value", function(){
var age = can.compute(0,function(newVal, oldVal){
var num = +newVal
if(! isNaN(num) && 0 <= num && num <= 120 ){
return num;
} else {
return oldVal;
}
})
equal(age(), 0, "initial value set");
age.bind("change", function(ev, newVal, oldVal){
equal(ev, newVal)
});
age(5);
equal(age(), 5, "5 set")
age("invalid");
equal(age(), 5, "5 set")
})*/

test("compute value",function(){
expect(9)
var input = {
value: 1
}

var value = can.compute("",{
get: function(){
return input.value;
},
set: function(newVal){
input.value = newVal;
//input.onchange && input.onchange();
},
on: function(update){
input.onchange = update;
},
off: function(){
delete input.onchange;
}
})

equal(value(), 1, "original value");
ok(!input.onchange, "nothing bound");
value(2);

equal(value(), 2, "updated value");

equal(input.value, 2, "updated input.value");



value.bind("change", function(ev, newVal, oldVal){
equal(newVal, 3, "newVal");
equal(oldVal, 2, "oldVal");
value.unbind("change", arguments.callee);
})
ok(input.onchange, "binding to onchange");

value(3);
ok(!input.onchange, "removed binding")
equal(value(), 3);
});

test("compute bound to observe",function(){
var me = new can.Observe({name: "Justin"});

var bind = me.bind,
unbind = me.unbind,
bindCount = 0;
me.bind = function(){
bindCount ++;
bind.apply(this,arguments);
}
me.unbind = function(){
bindCount --;
unbind.apply(this,arguments);
}

var name = can.compute(me,"name")

equal(bindCount, 0);
equal(name(), "Justin");

var handler = function(ev, newVal, oldVal){
equal(newVal, "Justin Meyer");
equal(oldVal, "Justin")
}

name.bind("change",handler)

equal(bindCount, 1);

name.unbind("change",handler);

equal(bindCount, 0);
});

test("compute bound to input value",function(){
var input = $("<input value='Justin'/>");
$("#qunit-test-area").append(input);

var value = can.compute(input[0], "value","change")

equal(value(),"Justin");

value("Justin M.");

equal(input.val(),"Justin M.","input change correctly");


var handler = function(ev, newVal, oldVal){
equal(newVal, "Justin Meyer");
equal(oldVal, "Justin M.")
}

value.bind("change", handler);


input.val("Justin Meyer").change();

value.unbind("change", handler);

input.val("Brian Moschel").change();

equal(value(),"Brian Moschel");

})



})();

0 comments on commit e73635f

Please sign in to comment.