Weekly Widget 7 - Computes and Sliders

Learn why can.compute is the last API you will ever need as we explore using it.

posted in open-source, in canjs, in Development on April 24, 2013 by Justin Meyer

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.

comments powered by Disqus
Contact Us
(312) 620-0386 | contact@bitovi.com
 or cancel