Computes are amazing, especially when consumed by low-level widgets. They are so amazing, I want to see them become an interoperable standard the same way that deferreds have become.
To demonstrate compute's awesome power, I'll build a slider first without computes and then with computes. But first a bit of widgeting theory:
IRUL (pronounced "I rule")
Almost every widget that operates on a value needs to expose four common APIs to make it generally useful:
- Initialize the widget with a value
- Read the current value of the widget
- Update the widget's value
- Listen to when the widget's value changes
UI frameworks tend to have a standard IRUL approach. Take jQueryUI:
Initialize
$(".slider").slider({value: 5})
Read
$(".slider").slider("value") //-> 5
Update
$(".slider").slider("value",10)
Listen
$(".slider").on("slidechange",handler)
Without Computes
The following shows a basic percent slider built with CanJS and jQuery++ that exposes a similar IRUL as jQueryUI:
The slider operates on numbers between 0 and 1. Lets see its IRUL:
Initialize
slider = new Slider("#slider",{
value: 0
});
Read
slider.value() //-> 0
Update
slider.value(0.5)
Listen
$("#slider").bind("change", function(ev){
})
This slider api is serviceable, but it's little verbose if you need to cross-bind the control's value to the value of an object's property. For example, consider hooking this slider value up to a task's progress:
var slider = new Slider("#slider",{
value: project.attr('progress')
})
// when the slider changes, the "progress" property updates
$("#slider").bind(function(){
project.attr('progress',slider.value() )
})
// when the "progress" property changes, update the slider's value
project.bind("progress",function(ev, newVal){
slider.value( newVal )
})
Nine lines of code to setup and cross-bind a value to a control ... Yuck! Making matters worse, if the control was removed, you MUST make sure to call project.unbind("progress")
or you will have a memory leak.
Using Compute
Instead, by making the slider accept value as a can.compute
you can turn those 9 lines into 3:
var slider = new Slider("#slider",{
value: project.compute('progress')
})
This is because a compute is 3 API's in one. A compute lets you:
- read its value
compute()
- update its value
compute(newValue)
- listen to value changes
compute.bind("change", handler)
Here's that slider:
Translating values
In weekly widget 3, I showed how to use computes to translate a pagination observe's limit
and offset
values into pageNum
and pageCount
values that the NextPrev widget needed.
Similarly, our application might contain task objects with a "progress" property ranging from 0 to 100. However, our abstract slider control requires a value ranging from 0 up to 1. We need a layer to translate from one format to the other.
We can create a compute function that translates the task's progress values into values our slider needs. We create a compute with a getter/setter function like:
var task = new can.Observe({progress: 50}); // 50
var progress = can.compute(function(newValue){
if(arguments.length) { // setter
task.attr('progress', newValue * 100)
} else {
return task.attr('progress') / 100
}
})
new Slider("#slider",{
value: progress
})
Similar to the example in the previous section, the slider will use the progress compute for all 4 parts of IRUL. To read the current value, it uses the getter by calling progress(). After changing the value, it sets the value by calling the setter with progress(newVal). And it binds on progress' change internally, so if the compute's value ever changes, the slider will update itself. Magical!
Check it out:
Computes derived from the DOM
What if we wanted to cross bind a compute to something other than an observe? Say ... an HTML5 video element? With the upcoming CanJS 1.1.6 release, you can create a compute from any object's value with:
can.compute(object, property, updatingEventName)
I'll use this to create a computes for a video element's time
and duration
properties and hook them up to the slider like:
var video = document.getElementById("myvideo");
// create a compute from currentTime property
var time = can.compute(video,"currentTime","timeupdate")
// create a compute for the duration
var duration = can.compute(video,"duration","durationchange");
var progress = can.compute(function(newPercent){
// can only do anything if duration is ready
var duration = duration();
if(typeof duration == "number" && !isNaN(duration)){
if(arguments.length){ // treat as a setter function
time(newPercent * duration)
} else { // treat as a getter function
return time() // duration;
}
}
})
new Slider("#slider",{
value: progress
})
Check it out:
If we update the slider to take a min and max value also as computes, we can create the slider even more succinctly:
var video = document.getElementById("myvideo");
new Slider("#slider",{
value: can.compute(video,"currentTime","timeupdate"),
min: can.compute(0),
max: can.compute(video,"duration","durationchange")
})
Check it out:
Conclusion
can.compute is powerful, but its most important feature is simplifying IRUL APIs. By accepting a compute
, a widget provides a single way to initialize, read, update, and listen to changes of a value.
I'd like to see computes
become an interoperable standard the same way deferreds have become. If someone wants to do a lot of good, they will work with us and the Knockout folks to create a compute specification. That way, a widget made in CanJS would work with Knockout's computes and vice-versa.
@getsetbro suggested I build a tree widget, so look out for that soon. Keep those widget suggestions coming.