---
title: Weekly Widget 7 - Computes and Sliders
tags: open-source canjs
author: justinbmeyer
lead: Learn why can.compute is the last API you will ever need as we explore using it
in a slider.
layout: post
---
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:
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)
The following shows a basic percent slider built with CanJS and jQuery++ that exposes a similar IRUL as jQueryUI:
<iframe style="width: 100%; height: 300px" src="http://jsfiddle.net/rZLbu/4/embedded/result,html,js" allowfullscreen="allowfullscreen" frameborder="0">JSFiddle</iframe>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 servicable, but it's little
unweildy if you need to cross-bind it to a value on
some object (or can.Observe).
Consider hooking it up
to a project's progress
property:
var slider = new Slider("#slider",{
value: project.attr('progress')
})
$("#slider").bind(function(){
project.attr('progress',slider.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.
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:
<iframe style="width: 100%; height: 300px" src="http://jsfiddle.net/vz3Mx/4/embedded/result,html,js" allowfullscreen="allowfullscreen" frameborder="0">JSFiddle</iframe>Sometimes the values a widget operates on match what a
widget provides. 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, we might get a task progress value that ranges from 0 to 100. We can create a compute 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
})
Check it out:
<iframe style="width: 100%; height: 300px" src="http://jsfiddle.net/uMFRp/2/embedded/result,html,js" allowfullscreen="allowfullscreen" frameborder="0">JSFiddle</iframe>What if wanted to cross bind a compute to something other than a observe? Say ... an HTML5 video element? You can do this 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){
time(newPercent * duration)
} else {
return time() / duration;
}
}
})
new Slider("#slider",{
value: progress
})
Check it out:
<iframe style="width: 100%; height: 300px" src="http://jsfiddle.net/cG88f/1/embedded/result,html,js" allowfullscreen="allowfullscreen" frameborder="0">JSFiddle</iframe>If we update the slider to take a min and max value also as a compute, we can create the slider even easier:
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:
<iframe style="width: 100%; height: 300px" src="http://jsfiddle.net/pqeVQ/3/embedded/result,html,js" allowfullscreen="allowfullscreen" frameborder="0">JSFiddle</iframe>can.compute is
powerful, but its most important feature is simplifies IRUL APIs.
By accepting a compute
, a widget provides 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. But keep those suggestions coming.
"This is because a compute is 3 API's in one." Isn't it 4? Its also doing init.