Skip to content

Instantly share code, notes, and snippets.

@moschel
Last active December 31, 2015 12:08
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 moschel/7983789 to your computer and use it in GitHub Desktop.
Save moschel/7983789 to your computer and use it in GitHub Desktop.
Data Driven Controls

Live bound templates in CanJS completely change how we build UI controls. When used correctly, live binding gives you far greater separation of concerns and code that is easier to understand and refactor.

In this article, we'll illustrate the clear advantages to using data driven, live bound UI controls, and show how to implement this in your own apps.

Advantages

Live bound templates provide a greater ability to keep UI Control code as semantic as possible, removing most or all of the manual DOM manipulation code that was necessary without live binding.

With live binding, the UI Control strictly maintains the “state” of the control, leaving the visual representation of that state to the View.

https://docs.google.com/a/bitovi.com/drawings/d/1nAcWD0TNEg4jtQItU8kg7fwCPHhqOjTg3MV0EIA4WpU/edit

Without it, controls required far more logic that kept the DOM in sync with the control state, leading to messier code.

https://docs.google.com/a/jupiterjs.com/drawings/d/16y95MTP_ol8ZeSM50GVkA2wXxfwtF2FeSDYYwKLmwqU/edit?usp=sharing

How it works in CanJS

In CanJS, using can.Mustache, data driven controls are written with the following architecture:

  • Controls maintain UI state, via can.Map or can.compute observable objects.
  • Views (can.Mustache) are rendered once and represent the UI state visually
  • View helpers (can.Mustache helper functions) translate UI state to DOM state.
  • Event handlers in the UI Controls strictly maintain and update the UI state objects.

The advantage of this approach is simplicity. You worry about how to represent your data once, while creating your view (and associated helpers). Render and forget. Any time data or state changes, those changes are automatically reflected.

By contrast, in Backbone (where live bound templates aren’t supported), every time data or the UI state changes, you have to a) manually trigger an event that re-renders the entire app with the new data or b) write some "glue" code that specifically ties this change to a small part of the DOM. A lot more moving parts.

For example, consider the case of a Select All button in a todo list. This button should be visible when there are todos in the list, but not if the list is empty. We’ll contrast two ways of implementing this feature, with and without live binding.

Without Live Binding (DOM Logic)

The following code is part of a can.Control. Assume this.todos is a list of the current todos being shown.

// somewhere in the app, a Todo item was created
"{Todo} created": function(Todo, ev, newTodo){

  // add the todo to the DOM
  $(".todos").append(todoTemplate(newTodo));
  
  // check if "select all" should be updated
  this._updateSelectAll();
},
// show button if there are todos
_updateSelectAll: function(){

  if(this.todos.length > 0) {
    $(".select-all").show();
  } else {
    $(".select-all").hide();
  }
}

Similarly when a todo is destroyed...

"{Todo} destroyed": function(Todo, ev, oldTodo){

  // find the right element, remove it
  var el = $(".todos").find("todo-"+oldTodo.id);
  el.destroy();
  
  // check if "select all" should be updated
  this._updateSelectAll();
}

The reason this approach sucks is because, as you can see, you have to constantly write code that keeps your data in sync with the DOM manually.

What happens if there's another method where this.todos can be updated? We'd have to manually call this._updateSelectAll again.

What happens if there's another button that must be shown/hidden based on whether any todos are marked completed? We'd have to create another helper and call it from every place in the code that todo completions might happen.

This obviously doesn't scale and is very error prone. Adding features requires a lot of inherent knowledge, and something can easily be forgotten, leading to unexpected bugs.

Without Live Binding (Render Everything)

You could reduce the amount of DOM logic by simply repeatedly calling a render function. For example:

"{Todo} created": function(Todo, ev, newTodo){
  this._render();
},
// render the whole todo list
_render: function(){
  $(".todos").html(todoListTemplate(this.todos));
}

This is the architecture of a typical Backbone app. When Backbone views often wire up model change events to the render method (http://backbonejs.org/#View-render), which re-renders the whole view.

However, this is no better, because this comes at the cost of performance. Rendering the whole todo list over and over means every little action will cause a slow re-render and possibly show some screen flicker.

With Live Binding

A simple mustache template that would render the todos in this.todos:

<ul class='todos'>
{{#todos}}
  <li class='todo' {{data 'todo'}}>{{title}}</li>
{{/todos}}
</ul>
{{#if todos.length}}
  <div class='select-all'></div>
{{/if}}

can.Mustache creates event handlers that listen on change events for this.todos (really it listens for changes on the 'length' property of this.todos). Three things therefore happen automatically that in our first example happened manually:

  1. When a new todo is pushed to this.todos, can.Mustache appends a new LI.
  2. When a todo is removed from this.todos, can.Mustache removes the corresponding LI.
  3. When todos.length becomes zero, the "select-all" button will hide itself (and will show itself again if more todos are added).

This greatly simplifies our control code:

// somewhere in the app, a Todo item was created
"{Todo} created": function(Todo, ev, newTodo){
  this.todos.push(newTodo);
}

By pushing the todo, the DOM will reflect the change automatically. A few concrete advantages are:

  • Remove selector strings from your UI code. These have a tendency to change often, breaking brittle selector strings.
  • Create strict separation of View/Control logic. Previously, writing a Control required intimate knowledge of the DOM structure and rules connecting the state to the DOM. Code like this is harder to read and maintain. With live binding, the view (or helpers) contain all of this logic. The Control just maintains application logic.
  • Comparing this to the "Render Everything" example above, the performance will be much better. can.Mustache renders just the smallest portion of the template that is required. If a todo is pushed, a single LI will be created and appended to the UL.

DIY

To use this pattern yourself, a few rules to live by:

No DOM manipulation code in the control (except view helpers)

This includes adding classes.

For example, imagine you need to keep track of the currently "active" todo. You could set the className directly in the control. If you do this, you'd have to query the DOM to figure out which todo is active (or worse, keep track of this information twice). This is bad!

Instead, keep track of state on the data itself, and use Mustache helpers to tie that state to the DOM. In this example:

In the template:

<li class='todo {{#active}}active{{/active}}' {{data 'todo'}}>{{title}}</li>

And in the control:

".todo click": function(el, ev){
  var todo = el.data('todo');
  this.todos.each(function(todo){
    todo.attr('active', false);
  });
  todo.attr('active', true);
}

Render templates only once (during control initialization).

Avoid re-rendering templates. Pre-live binding, the pattern was to render the control template each time something changed. The pattern now is to render templates in your init method, only once.

init: function(){
  this.element.html(renderTemplate(data));
}

Connect complex state to the DOM with a Mustache helper

Any attributes accessed in a Mustache helper will set up a live binding, so translate any non-trivial state logic to the DOM with helpers like:

this.element.html(renderTemplate(data, 
// helpers are the second argument
{
  // if there's an active todo, set class to 'show'
  editButtonVisible: function(){
    var active = false;
    this.todos.each(function(todo){
      if(todo.attr('active' === true){
        active = true;
      }
    });
    if(active) return 'show';
  }
}));

In the template, use the helper like:

<div class='edit {{editButtonVisible}}'></div>

Hopefully this simple example illustrated the correct way to use live binding to improve performance, maintainability, and ease of development for your application.

@moschel
Copy link
Author

moschel commented Jan 17, 2014

Stan's feedback: expand the example to show jquery dom manip adding classes, show the same with a helper.

@matthewp
Copy link

I think the "Without Live Binding" example isn't convincing that it's really that bad. I would recommend starting out with a very small example of code that doesn't look too bad, then show an example of adding more logic to it, then another of adding even more and explaining how the complexity increases as you add more DOM manipulation logic to your code.

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