Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save justinbmeyer/5502291 to your computer and use it in GitHub Desktop.
Save justinbmeyer/5502291 to your computer and use it in GitHub Desktop.
Error in user YAML: (<unknown>): could not find expected ':' while scanning a simple key at line 5 column 1
--- 
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:

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:

<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 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:

<iframe style="width: 100%; height: 300px" src="http://jsfiddle.net/vz3Mx/4/embedded/result,html,js" allowfullscreen="allowfullscreen" frameborder="0">JSFiddle</iframe>

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:

<iframe style="width: 100%; height: 300px" src="http://jsfiddle.net/uMFRp/2/embedded/result,html,js" allowfullscreen="allowfullscreen" frameborder="0">JSFiddle</iframe>

Computes derived from the DOM

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){  // 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:

<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 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:

<iframe style="width: 100%; height: 300px" src="http://jsfiddle.net/pqeVQ/3/embedded/result,html,js" allowfullscreen="allowfullscreen" frameborder="0">JSFiddle</iframe>

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment