I just committed can.compute for CanJS:
https://github.com/bitovi/canjs/commit/8eb7847d410c840da38f4dd5157726e560d0a5f5. At it's simplest it looks like:
- // READ
- var age = can.compute(5)
- age() //-> 5
- // LISTEN
- age.bind("change", function( ev,newVal, oldVal ) { ... })
- // UPDATE
- age(7)
It lets you do the same with properties of an observe:
- var me = new can.Observe({ first: "Justin", last: "Meyer"})
- var fullName = can.compute(function(){
- return me.attr("first") + ' ' + me.attr('last')
- })
- fullName() // "Justin Meyer"
- fullName.bind("change", function( ev, newVal, oldVal ){ .. })
And, even let you translate one observe's property value into another value and vice versa (cross-bound values). The following example converts a progress float (0-1) to a percent (0-100):
- var project = new can.Observe({progress: 0.5})
- var percent = can.compute(function(newPercent){
- if( newVal !== undefined) // write
- project.attr('progress', newPercent / 100 )
- else
- return project.attr('progress') * 100
- })
You can create view-helper Constructors like:
- var ProjectPresenter = can.Construct({
- init : function(project){
- this.project = project
- },
- prettyDate : can.compute(function(){
- return new Date( this.project.attr("date") ).toString()
- })
- });
- var pp = new ProjectPresenter( new can.Observe({ date: new Date().getTime() }) );
- pp.prettyDate() //-> "Sat Jun 16 2012 13:12:56 GMT-0500 (CDT)"
- pp.prettyDate.bind("change", handler);
Now all the above is quite useful. You can start creating presenter layer / objects to pass to your templates.
In the long run, it will help me appease the template purists and support live-binding in something like Handlebars (which does not allow parameterized function calls).
But even now, it is useful for something you might not suspect ...
It allows you to create live-binding widgets that also double as basic configurable widgets.
By "configurable widgets", I mean something like jQueryUI's slider. You have to manually listen to its events and update your model data when the slider's values changes. You also have to listen to your model data changing and update the slider. "Configurable widgets" are great because they can be used by anyone, regardless of what their data looks like. But they suck if you have a strong model layer.
Live binding widgets have the opposite problem. They are great when working with a strong model layer. You just pass in your model/attribute and the widget automatically updates it's value and vice versa. But, live widgets can't be used by everyone.
can.compute solves this!
Using can.compute I updated MXUI (now CanUI's) slider here:
And you can see how slider's are created here:
You'll notice that you can setup the slider with static values like:
- new can.ui.nav.Slider("#basicSlider", {
- interval: 1,
- min: 1,
- max: 10,
- val: 5
- });
or a compute like
- new can.ui.nav.Slider("#computeSlider", {
- interval: 1,
- min: 0,
- max: 100,
- val: percentDone // HERE!
- });
Before can.compute, a widget like this would have to always check if it was passed an observe or a static value. Instead, the slider converts all its options to a can.compute like:
- for(var optionName in this.options){
- this.options[optionName] = can.compute(this.options[optionName])
- }
- // rebind
- this.on();
It binds on changes to it's val option like:
"{val} change"
And updates it like:
this.options.val(5)
This means you can translate any option passed to a widget into a can.compute and operate on that. If a user passed static values, or compute values, it will still just work. The widget just operates on can.compute values internally.
The user can even pass getter/setter methods as options that can translate one value to another value (similar to percentDone) like:
- new can.ui.nav.Slider("#computeSlider", {
- interval: 1,
- min: 0,
- max: 100,
- val: function(newVal){ ... }
- });
Just to give one more example. Say you kept pagination state in something like:
- var pagination = new can.Observe({ offset: 0, limit: 100, count: 1000 })
But a widget just wanted the page. You could pass it:
- new PageCount({
- page : function() {
- return pagination.attr('offset') / pagination.attr('limit') + 1
- },
- pageCount: function(){
- return pagination.attr('limit') / pagination.attr('count')
- }
- })
See how useful it is! Widgets don't have to care about accepting some particular format of data. can.compute can be used to translate from one value to another.
Our plan is to make CanUI do this type of conversion in all of it's widgets.
Thanks!