Skip to content

Instantly share code, notes, and snippets.

@justinbmeyer
Last active December 16, 2015 21:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save justinbmeyer/5497620 to your computer and use it in GitHub Desktop.
Save justinbmeyer/5497620 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 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.

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

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>

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

Conclusion

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.

@moschel
Copy link

moschel commented May 2, 2013

"This is because a compute is 3 API's in one." Isn't it 4? Its also doing init.

@moschel
Copy link

moschel commented May 2, 2013

can.compute(video,"currentTime","timeupdate")

You need to explain that API by showing the call signature: obj, prop, eventName

Is eventName new? I just saw it in the API but looks like you added it recently. No one will know what that third argument is for.

@justinbmeyer
Copy link
Author

yes, very recent. But I didn't want API docs in the article.

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