Weekly Widget 2 – 2-Way Mustache Helpers

by Justin Meyer

Weekly Widget 2 – 2-Way Mustache Helpers

Justin Meyer Learn how to make 2-way binding Mustache Helpers

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

{% raw %}

{% endraw %}

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:

{% raw %}


{% endraw %}

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.

{% raw %}

{% endraw %}

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:

{% raw %}


{% endraw %}

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:

{% raw %}

{% endraw %}

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:

{% raw %}
{{placeholder zipPlaceholder attendee.zip}}/>
{% endraw %}

fadeInWhen – fades in an element when a compute/property
is truthy.

Usage:

{% raw %}

<

div id=’address’ {{fadeInWhen attending}}>
{% endraw %}

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:

{% raw %}

{% endraw %}

The following fades in the address part of the form when
the attending returns true:

{% raw %}

<

div id=’address’ {{fadeInWhen attending}}>
{% endraw %}

The following 2-way binds attendee.zip to the
input’s value and live-binds zipPlaceholder as the
input’s placeholder:

{% raw %}
{{value attendee.zip}}
{{placeholder zipPlaceholder attendee.zip}}/>
{% endraw %}

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:

{% raw %}

{% endraw %}

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!

blog comments powered by Disqus

Getting Started with Cordova

Brian Moschelposted in Development on March 26, 2015 by Brian MoschelAt Bitovi, we’re big fans of building applications with web technologies and using build tools to target other platforms like iOS, Android, and desktop. This article will provide a quick guide to getting up and running quickly with Cordova.

Web Application Theory

Justin Meyerposted in Development on June 27, 2014 by Justin MeyerUnderstand the technology and architecture choices Bitovi make and why we make them. What Bitovi does and why.

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