Skip to content

Latest commit

 

History

History
333 lines (251 loc) · 9.4 KB

2012-01-13-implementing-semantic-anti-templating-with-jquery.md

File metadata and controls

333 lines (251 loc) · 9.4 KB

Implementing Semantic Anti-Templating With jQuery

Single-page web applications have been pretty much standard for quite a while, and I'm a strong advocate for numerous reasons (reduced latency, separation of concerns and ease of testing to name a few).

However, one point I haven't felt too good about is client side rendering. It's ridiculous how cumbersome it is to compile the template, render the data and finally manipulate the DOM. For example, with popular template engines like Handlebars or Mustache, you typically need do something like

<script id="entry-template" type="text/x-handlebars-template">
  <div class="entry">
    <h1>{{title}}</h1>
    <div class="body">
      {{body}}
    </div>
  </div>
</script>
var data     = {title: "My New Post", body: "This is my first post!"}
var source   = $("#entry-template").html();
var template = Handlebars.compile(source);
var html     = template(data);
$('container').empty().append(html);

Frustrated with the amount of labor, I decided to roll out my own and focus on simplicity. In this article, I walk through some of the main design decisions and corresponding implementation. For the impatient, here's the demo site.

No syntax, please!

I started with a modest goal: Given I have a static web page

<div class="container">
  <div class="hello"></div>
  <div class="goodbye"></div>
</div>

and a simple JavaScript object

data = {
  hello: "Hi there!"
  goodbye: "See ya!"
};

I want to render that on object on the page with a single function call. No template definition in script tags, no extra markup, no manual DOM manipulation. So, when I call $('.container').render(data);, I should see the following in the browser

<div class="container">
  <div class="hello">Hi there!</div>
  <div class="goodbye">See ya!</div>
</div>

We'll, it turned out, that wasn't too hard to implement. DOM manipulation is the bread and butter of jQuery, so all we need to do is

  1. Iterate over the key-value pairs of the javascript objects
  2. Render the value on the matching DOM element.

The initial implementation looked like something like this (in CoffeeScript):

jQuery.fn.render = (data) ->
  template = this

  for key, value of data
    for node in template.find(".#{key}")
      node     = jQuery(node)
      children = node.children().detach()
      node.text value
      node.append children

Getting rid of loops

The next logical step was support for collections. I wanted to keep the interface exactly the same, without explicit loops or partials. Given an object like

friends = [
  {name: "Al Pacino"},
  {name: "The Joker"}
]

And a web page like

<ul class="container">
  <li class="name"></li>
</ul>

When I call $('.container').render(friends), I should see

<ul class="container">
  <li class="name">Al Pacino</li>
  <li class="name">The Joker</li>
</ul>

Obviously, we need to extend the existing implementation with following steps

  1. Iterate through the list of data objects
  2. Take a new copy of the template for each object
  3. Append the result to the DOM
jQuery.fn.render = (data) ->
  template = this.clone()
  context  = this
  data     = [data] unless jQuery.isArray(data)
  context.empty()

  for object in data
    tmp = template.clone()

    for key, value of data
      for node in tmp.find(".#{key}")
        node     = jQuery(node)
        children = node.children().detach()
        node.text value
        node.append children

    context.append tmp.children()

It's worth noticing, that the rendering a single object is actually just an edge case of rendering a list of data objects. That gives us an opportunity to generalize the edge case by encapsulating the single object into a list as shown above.

Do it again!

The previous implementation works, kind of. However, if you call $('container').render(friends) twice, it fails.

Result after the first call

<ul class="container">
  <li class="name">Al Pacino</li>
  <li class="name">The Joker</li>
</ul>

Result after the second call

<ul class="container">
  <li class="name">Al Pacino</li>
  <li class="name">Al Pacino</li>
  <li class="name">The Joker</li>
  <li class="name">The Joker</li>
</ul>

The reason is obvious. The current implementation finds two matching elements on the second call and renders the name on the both elements. That sucks, because it means you'd have to manually keep the original templates in safe.

To avoid the problem, we need to

  1. Cache the original template on the first .render()
  2. Use the cached template on the successive calls

Luckily, thanks to jQuery data(), the functionality is trivial to implement.

jQuery.fn.render = (data) ->
  context  = this
  data     = [data] unless jQuery.isArray(data)
  context.data('template', context.clone()) unless context.data 'template'
  context.empty()

  for object in data
    template = context.data('template').clone()

    # Render values
    for key, value of data
      for node in tmp.find(".#{key}")
        node     = jQuery(node)
        children = node.children().detach()
        node.text value
        node.append children

    context.append template.children()

My way or the highway

Rails has shown us how powerful it is to have strong conventions over configurations. Sometimes, however, you need to do the things differently and then it helps to have all the power. In JavaScript, that means functions.

I wanted to be able to hook into rendering and define by functions how the rendering should happen. Common scenarios would include, e.g., decorators and attribute assignment.

For example, given a template

<div class="container">
  <div class="name"></div>
</div>

I want be able render the following data object with the directive

person = {
  firstname: "Lucy",
  lastname:  "Lambda"
}

directives =
  name: -> "#{@firstname} #{@lastname}"

$('.container').render person, directives

And the result should be

<div class="container">
  <div class="name">Lucy Lambda</div>
</div>

At first, implementing directives might seem like a daunting task, but given the flexibility and and power of javascript functions and object literals, it isn't that bad. We only need to

  1. Iterate over the key-function pairs of the directive object
  2. Bind the function to the data object and execute it
  3. Assign the return value to the matching DOM element
jQuery.fn.render = (data, directives) ->
  context  = this
  data     = [data] unless jQuery.isArray(data)
  context.data('template', context.clone()) unless context.data('template')
  context.empty()

  for object in data
    template = context.data('template').clone()

    # Render values
    for key, value of data
      for node in tmp.find(".#{key}")
        renderNode node, value

    # Render directives
    for key, directive of directives
      for node in template.find(".#{key}")
        renderNode node, directive.call(object, node)

    context.append template.children()

renderNode = (node, value) ->
  node = jQuery(node)
  children = node.children().detach()
  node.text value
  node.append children

Generalizing to nested data objects, lists and directives

We'll, I bet you saw this coming. Why stop here, if we could easily support nested objects, lists and directives. For each child object, we should do exactly same operations that we did for the parent object. Sounds like recursion and, indeed, we need to add only couple of lines:

jQuery.fn.render = (data, directives) ->
  context  = this
  data     = [data] unless jQuery.isArray(data)
  context.data('template', context.clone()) unless context.data('template')
  context.empty()

  for object in data
    template = context.data('template').clone()

    # Render values
    for key, value of data when typeof value != 'object'
      for node in tmp.find(".#{key}")
        renderNode node, value

    # Render directives
    for key, directive of directives when typeof directive == 'function'
      for node in template.find(".#{key}")
        renderNode node, directive.call(object, node)

    # Render children
    for key, value of object when typeof value == 'object'
      template.find(".#{key}").render(value, directives[key])

    context.append template.children()

renderNode = (node, value) ->
  node = jQuery(node)
  children = node.children().detach()
  node.text value
  node.append children

Using Transparency in the real world applications

Writing Transparency has been a delightful experience. It gave me a chance to get my feet wet with node.js, CoffeeScript, Jasmine and jQuery plugin development. At Leonidas, we've used it in a numerous projects in the past couple of months, and so far, we've been happy with it.

The actual implementation is 66 lines of CoffeeScript, available at GitHub. If you want to give it a try, check the demo site.

Discussions regarding the article are at Reddit.

Cheers,

Jarno Keskikangas <jarno.keskikangas@leonidasoy.fi>

Leonidas Oy <http://leonidasoy.fi>