This article shows how to make 2-way binding mustache helpers and use them in a basic form.
By default, CanJS supports 1-way binding templates. 1-way binding means that if data changes, the template is automatically updated. For example, if attendee.name
changes, the following input's value is updated:
<input type="text" value="{{attendee.name}}">
2-way binding means the reverse is also true. If the user types a new value, the attendee.name
is automatically updated. This article shows how roll your own 2-way mustache helpers that can be used like:
<input name="" type="text" value=""> <input checked="checked" type="radio" value="yes">
2-way binding is a heavily requested CanJS feature, but you can easily add your own today.
The app
I recently got engaged and because I'm a prideful programmer, I decided to build my own wedding site. I started with a save-the-date form where potential attendees can indicate if they are attending and enter their address.
The form below is a close approximation of my site:
What it does
It allows the user to indicate if they can attend the wedding. If they indicate "Yes", the user will be able to enter their address. When they change the "Country", the placeholder for the state and zip localize for the specific country.
When an input value is changed, it immediately changes the value of the attendee
object. This is immediately apparent by viewing the JSON data that is displayed under the widget. Similarly, if attendee
is updated, the form values automatically update. This is 2-way binding!
To see it in action, click on "Payal's Grandmother Preset" button. This will simply populate the attendee
object, which through the magic of live binding will immediately update the form.
Next, change the country input to "Canada". Updating the input updates the model, which again updates the JSON data displayed under the widget.
How it works
This example is mostly powered by using Mustache Helpers. I have created the following mustache helpers: value
, checked
, placeholder
, and fadeInWhen
.
Before we see how to build them, I want to show what they do and how they are used.
Mustache Helper APIs
value - 2-way binds an input to an Observe's property or compute.
<input name="" type="text" value="">
This updates attendee.name
when the input's value is changed. It also updates the input's value when attendee.name
is changed.
checked - 2-way binds a radio input's checked property to an Observe's property or compute.
Usage:
<input checked="checked" type="radio" value="yes"> <input checked="checked" type="radio" value="no">
This will check the first input element if attendee.attending
's value is "yes" and check the second input element if attendee.attending
's value is "no".
placeholder - Adds a dynamic placeholder for inputs. The placeholder value can be a string or property/compute.
Usage:
<input type="text" placeholder="">
However, because live-binding can change the value of the input, which requires cleaning up the placeholder, the live-bound value of the value
property needs to be passed like:
<input type="text" value="" placeholder="">
fadeInWhen - fades in an element when a compute/property is truthy.
Usage:
<div id='address' {{fadeInWhen attending}}>
Building the Form
Using the mustache helpers above, creating the form is simple. I create an attendee whose country defaults to "USA", create a map of placeholder data grouped by country, and make a helper function that returns the placeholders for a given country:
var attendee = new can.Observe({
country: "USA"
}),
placeholders = {
usa: {
state: "state",
zip: "zip code"
},
india: {
state: "state",
zip: "postal code"
},
canada: {
state: "province",
zip: "postal code"
}
},
countryPlaceholders = function(){
return placeholders[
(attendee.attr('country')).toLowerCase()
];
}
Next, I render the form template:
$("#attendee").html( can.view("attendeeMustache", {
attendee: attendee,
statePlaceholder: can.compute(function(){
return countryPlaceholders().state
}),
zipPlaceholder: can.compute(function(){
return countryPlaceholders().zip
}),
cityOrStateSetOrCountryNotUSA: can.compute(function(){
return attendee.attr('state') ||
attendee.attr('city') ||
( attendee.attr('country') &&
attendee.attr('country') !== "USA" );
}),
attending: can.compute(function(){
console.log("re-eval");
return attendee.attr('attending') === 'yes'
})
}));
The data passed into the template includes:
- attendee - the attendee Observe
- statePlaceholder - the
state
placeholder for the current attendee.country - zipPlaceholder - the
zip
placeholder for the current attendee.country - cityOrStateOrCountryNotUSA - returns true if we should be showing the city/state input elements
- attending - returns true if the user is attending
The template uses the data to setup the behavior of the form. The following 2-way binds the attendee's name:
<input name="name" type="text" value="" placeholder="name">
The following fades in the address part of the form when the attending
returns true:
<div id='address' {{fadeInWhen attending}}>
The following 2-way binds attendee.zip
to the input's value and live-binds zipPlaceholder
as the input's placeholder:
<input name="zip" type="text" value="" placeholder="">
To demonstrate 2-way binding, I show the properties of the attendee anytime they are updated with:
attendee.bind("change", function(){
$("#out").text(
JSON.stringify( attendee.attr() )
.replace(/,/g,",\n ")
);
})
And update all the properties at once with:
$("#ba").click(function(){
attendee.attr({
name: "Payal's Grandmother",
attending: "yes",
country: "India",
street: "1234 Punjab St",
apt: "4u",
city: "Hyderabad",
state: "Andhra Pradesh",
zip: "500001"
})
})
Now lets see how these mustache helpers are created!
Mustache Helper Code
Value
I'll start with the 2-way binding helper - value. To start, I'll create a Value can.Control constructor function that gets created like:
new Value(inputEl, {value: compute});
Where:
compute
is a compute (ex:can.compute(30)
) that can be set and listened to for updates in its value.inputEl
a form input element.
I create value like:
var Value = can.Control({
init: function(){
this.set()
},
"{value} change": "set",
set: function(){
this.element.val(this.options.value())
},
"change": function(){
this.options.value(this.element.val())
}
});
"{value} change"
listens to the compute's value being changed and calls set
which updates's the element's value. "change"
listens for when the user changes the value of the input element and updates the compute's value.
I register a mustache helper with:
can.Mustache.registerHelper('value', function(value){
return function(el){
new Value(el, {value: value});
}
});
Notice how the helper function returns a function. If a helper returns a function, that function gets called back with the element
the helper is called within, in this case the <input/>
element below:
<input name="name" type="text" value="">
So, when the inner function gets called back with the input element, I create a Value control that 2-way binds the compute that represents attendee.name
with the input element's value!
Also notice how this.options.value
is called as a setter and getter in the Value control above. If the argument passed to a mustache helper represents an Observe property, that property is converted to a can.compute
function that can get or sets that property.
This means the mustache helper is called back with the value
argument being something like:
value = can.compute(function(newVal){
if(newVal !== undefined){
attendee.attr('name',newVal)
} else {
return attendee.attr('name')
}
})
This allows us to call this.options.value(this.element.val())
to set it or this.options.value()
to get the current value inside the Value control.
I followed the same pattern for all the other mustache helpers.
Checked
The Checked
control is almost exactly like Value
except that it sets the checked
property of the element when:
this.options.value() === this.element.val()
And, when the radio button is clicked, it only sets the compute value if the radio button is checked:
if(this.element.is(":checked")){
this.options.value(this.element.val())
}
FadeInWhen
FadeInWhen
only binds one-way. It simply listens to when its value is truthy and animates; otherwise, it hides the element:
"{value} change": function(value, ev, newVal){
if( newVal ) {
this.element.fadeIn();
} else {
this.element.hide()
}
}
Placeholder
The Placeholder
control binds some text to the HTML5 placeholder attribute. For browsers that don't support it (IE8), it contains logic to display either the element's value or its placeholder.
The placeholder helper accepts two properties, placeholder
and value
. As placeholder
can be a String, the helper converts it to a compute before passing it to the Placeholder
control:
can.Mustache.registerHelper('placeholder',
function(placeholder, value){
return function(el){
if(typeof placeholder === "string"){
placeholder = can.compute(placeholder);
}
new Placeholder(el, {
placeholder: placeholder,
value: value
});
}
});
Placeholder
listens to the input element's focus, blur, and change events, and value
to determine if the placeholder
value should be displayed.
Conclusion
CanJS makes it super easy to roll your own 2-way live binding widgets. Collecting these and similar common widgets into a plugin is probably a smart idea.
With 2-way live binding, complex form logic becomes very simple to write and maintain.
This currently uses some fixes in 1.1.4 which should be dropping this week.
Give me some suggestions for next week!