Weekly Widget 2 - 2-Way Mustache Helpers

Learn how to make 2-way binding Mustache Helpers

posted in open-source, canjs, Development on January 27, 2013 by Justin Meyer

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 type='text' {{value attendee.name}}/>
<input value="yes" type="radio" {{checked attendee.attending}}/>

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 type='text' {{value attendee.name}}/>

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 value="yes" type="radio" {{checked attendee.attending}}/>
<input value="no" type="radio" {{checked attendee.attending}}/> 

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 {{placeholder zipPlaceholder}}/>

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 {{value attendee.zip}} 
       {{placeholder zipPlaceholder attendee.zip}}/>

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" {{value attendee.name}} 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'
     {{value attendee.zip}} 
     {{placeholder zipPlaceholder attendee.zip}}/>

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"  {{value attendee.name}}/>

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!

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