Skip to content

Instantly share code, notes, and snippets.

@justinbmeyer
Created June 14, 2012 06:35
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save justinbmeyer/2928408 to your computer and use it in GitHub Desktop.
Save justinbmeyer/2928408 to your computer and use it in GitHub Desktop.
Why do you like Handlebars (and concerns about live-binding)

I'm considering adding an alternative to CanJS's EJS live-binding system. I really like EJS, it's extremely powerful, but ugly to look at. So, I'm trying to create a better mouse-trap.

Currently, the most popular mouse-trap seems to be Handlebars. What do you like about it?

  • Syntax, or
  • Prepared data

Syntax

Handlebars uses {{}} for insertion and {{#}} and {{/}} for widgets and control structures.

Prepared data

All data must be passed to handlebars (except accessed through a helper). You can not seemingly call helper methods that read data like {{person.age()}}. This limits logic that can be performed in the template (although a helper can still do pretty much anything).

Concerns about live-binding and handlebars

I'd like to learn what people like about handlebars and use that in the design of the alternative templating language. But, there's two issues I would have to overcome:

Prepared Data

Prepared data is something that has been strongly promoted on the server and it also is being promoted on the client. I don't consider it as much an imperative on the client. I've never seen anyone fire an ajax call in a template. The data is always prepared, it's just accessed through the model or other helpers.

Needing prepared data makes doing something like http://jsfiddle.net/qYdwR/36/light/ with Handlebars more difficult to write. You'd have to create a computed property that is the date merged with this updating time observable.

With EJS, you can just write a function and anything that uses it becomes live.

Limited "helpers" (this is no longer a concern ... Handlebars must support this)

I don't think it's possible in Handlebars, but EJS supports ERB-style sub template helpers (if you wanted to build them) like:

<%== columns(items, 4, function(item){ %>
  <li><%= item.attr('name') %></li>
<% }) %>

Notice that the function(item){ ... } is actually a template that is called out to by the columns helper implemented like:

columns = function(items, num, template){
    var cols = new Array(num);
    items.forEach(function(item, i){
      if( ! cols[i%num] ) { 
        cols[i%num] = "";
      }
      cols[i%num] += template(item)
    });
    return "<ul>"+cols.join("</ul><ul>")+"</ul>";
}

And this would also instantly become live.

@justinbmeyer
Copy link
Author

{{#columns items, 4 -}}
  <li>{{name}}</li>
{{/columns}}

@renehennig
Copy link

Template:

{{#columns items count="4"}}
  {{name}}
{{/columns}}

Helper:

Handlebars.registerHelper('columns', function(items, options) {
  var out = '<ul>', i,
  max = options.hash.count ? options.hash.count : items.length;
  if (max > items.length) max = items.length;

  for(i = 0; i < max; i++) {
      out += '<li>' + options.fn(items[i]) + '</li>';
  }
  return out + '</ul>';
});

Should work.

@iamnoah
Copy link

iamnoah commented Jun 14, 2012

We built a simple JMVC live Handlebars plugin (I'm waiting for approval to open source it.) But we had a few principles that I think are key to making living binding with handlebars helpful but not harmful:

  1. Only bind to and modify the existing DOM. Don't recreate HTML strings and overwrite with innerHTML. Don't add extra elements, hookup to existing ones.
  2. Don't try to do everything, just the common (and easy stuff).
  3. Don't worry about removing chunks of content. If you have stuff you don't want to show, just toggle a CSS class to hide it.
  4. Be explicit about what is bound.

If you keep all that in mind, you can have nice live bound handlebars templates that do 90% of the live binding you want. That other 10% would probably need some custom code anyway, so you're better off without it.

There were basically 3 classes of binding:

  1. Binding an attribute value to DOM property/attr/val/text/html. Allow interpolation ala $.String.sub e.g., <div {{bindAttr class="foo bar-{someAttr}"}}>... (also bindVal,bindProp, bindText, etc.)
  2. Toggling a class based on an attribute. Basically a special case of the above but works only on boolean values.
  3. Binding a list. You want the binding to create the new elements and remove the old ones. We do something like this:
    <ul {{bindList myList sortBy="name"}}>
        {{#bindItem}}
            <li {{bindClass foo="!bar">...
        {{/bindItem}}
    </ul>

That gives you all the power you need without being too magical. You do have to create custom $.Observe objects if you want to bind to a computed value, but we prefer that in any case as Models/Observes are a lot easier to test.

Hopefully in the next day or two I can get our plugin out in the wild.

@justinbmeyer
Copy link
Author

Very cool! Are you familiar with EJS's live-binding? There's a lot of reusable code in there that could be shared across many live-binding implementations.

I'm looking to add a can.prop computed helper as hinted at here: bitovi/canui#1

@iamnoah
Copy link

iamnoah commented Jun 14, 2012

I am familiar with the EJS way. I hope I can get the code out soon because it speaks more clearly about this than I can. The first problem is that Handlebars output is text, not a DOM. That means with stock Handlebars, a generic prop helper isn't possible, because you don't know when the helper runs if you're binding an attribute, value, property, text or html. Lists need two helpers.

That said, we probably could utilize the attribute finding magic (Observe.__reading) applied to method and attr calls, although Handlebars method calls don't allow parameters.

If you do add official Handlebars support, I hope that it's OK for it to use a different paradigm than EJS, because it really is a different style of templating (and I like it that way.)

@justinbmeyer
Copy link
Author

I'm excited to see it. Let me know if I can help in anyway.

The first problem is that Handlebars output is text, not a DOM.

So is EJS's output. However the output has special helpers that can.view knows about to call code for a particular element.

You don't know when the helper runs if you're binding an attribute, value, property, text or html. Lists need two helpers.

Yeah, I would add in a similar HTML parsing that is in EJS, making Handlebars aware where the magic tags are located.

Handlebars method calls don't allow parameters

This is the biggest problem (and I think what you mean by OK for it to use a different paradigm than EJS). I've got to look into how Ember manages this. It has to have some extra functionality that, on property lookup, looks at the context and determines if it's observable. If it is, it doesn't simply do context[prop] and instead, sets up binding.

It's very likely I could do something similar. This would allow basic observe properties to become live. The problem then is allowing live-binding to computed values.

IMO, the Observe.__reading trick is freaking awesome. I'd like to still allow that by either:

  • allowing param calls
  • allowing arbitrary function calls (BTW ... ¿is this already possible? {{ob.foo()}} or {{ob.foo}} where it knows to call foo? )
  • providing can.prop as a way to create computed props.

@iamnoah
Copy link

iamnoah commented Jun 14, 2012

When Handlebars finds a function, it calls it on the current context (so I think obj.foo would get the this wrong). You could do:

{{#with obj}}{{foo}}{{/with}}

or have prop be smart about nested properties:

{{prop "obj.foo"}}

I'd argue that not being able to pass parameters (and the lack of namespaces) is something Handlebars users are already used to, so there is no need for either.

My library uses $.View.Hookup and specialized helpers (bindAttr,bindText, etc.). If you can do the right thing in all cases with just {{prop "foo"}} and with DOM manipulation instead of re-rendering, then that's probably better.

@justinbmeyer
Copy link
Author

yeah, that would be my plan. I should just be able to do {{name}} for live-binding right? I should be able to hook into it's default lookup and avoid a special prop method.

@justinbmeyer
Copy link
Author

btw, how does handlebars handle default values:

there are {{items.length}} item{{items.length > 1 "s" : ""}}

?

@iamnoah
Copy link

iamnoah commented Jun 14, 2012

yeah, that would be my plan. I should just be able to do {{name}} for live-binding right? I should be able to hook into it's default lookup and avoid a special prop method.

I would argue that that is no longer Handlebars and is being too clever for the sake of saving a few characters. I would not want to use it.

Assuming that syntax though, how would you update the dom for a template like this?

 <input type="radio" {{#if foo}}checked{{/if}}>

I would hope that it would be something like:

radioEl.prop('checked',foo);

But I'm guessing you replace the whole element?

My issue with that syntax is that I don't know how you're going to interpret my inline attributes. If I write:

<input type="radio" {{bindProp selected="foo"}}>

It didn't take any longer, but I know my element is updated the right way.

@iamnoah
Copy link

iamnoah commented Jun 14, 2012

No defaults in Handlebars without a helper AFAIK. Again, we're used to that. We create observables upfront that do what we need.

@justinbmeyer
Copy link
Author

But isn't this what ember.js is doing? It allows binding without {{#prop }} and it's by Yehuda and I would argue still handlebars ....

Assuming that syntax though, how would you update the dom for a template like this?

In a similar way as it works in EJS, which does not replace the whole element. It sees the new content, parses it and sets or removes the attribute.

My issue with that syntax is that I don't know how you're going to interpret my inline attributes.

The same way EJS does it ...

@justinbmeyer
Copy link
Author

Here's the code for selected/ checked:

https://github.com/jupiterjs/canjs/blob/master/view/ejs/ejs.js#L377

It gets the new value, splits it by = and remove or sets the attribute name.

@justinbmeyer
Copy link
Author

Essentially, EJS already has the code that creates observables, and can:

@iamnoah
Copy link

iamnoah commented Jun 14, 2012

In a similar way as it works in EJS, which does not replace the whole element. It sees the new content, parses it and sets or removes the attribute.

Awesome. I need to take a closer look at EJS.

But isn't this what ember.js is doing? It allows binding without {{#prop }} and it's by Yehuda and I would argue still handlebars ....

It's antithetical to reasons I use Handlebars I would say then. I don't know exactly how Ember.js does it, but Handlebars is supposed to be simple and explicit. I think changing the syntax to do live binding is more confusing than it is helpful.

@justinbmeyer
Copy link
Author

On why you should work with us to open source this ....

  1. We will make it live-update just the part of the dom that needs changing
  2. We will make it work in many more situations
  3. We will provide a standard computed layer as part of CanJS
  4. We will make it work pretty like Ember (even if you might think this is a negative, I'm sure that it would help make it more popular option which benefits the last point)
  5. We'd help harden it. In the 2.5 months CanJS has been out, we've had over 55 reported issues https://github.com/jupiterjs/canjs/issues many of them on live-binding which we've turned around and fixed. You'll basically get bug fixes for free.

As I saw you responded to #4 while I was typing this ....

You're saying changing {{}} to also setup live-binding makes things more complex ...

Ok, so lets make this optional (but I'd say turned on by default to better compete with ember). Something like:

Handlebars.bindByDefault = false

And it doesn't overwrite reading properties to check for observes.

@justinbmeyer
Copy link
Author

on explicit .... you mentioned it will call a function if it finds it ... I'm not sure how that's explicit. I think this is the same thing. If it finds a function, it calls it. If it finds a property on an observe, it binds to it, if it finds a simple property, it writes it. It's just one more case, similar to when it finds functions.

@iamnoah
Copy link

iamnoah commented Jun 14, 2012

Parity with Ember is compelling I admit. I dislike changing the default behavior, but if there is a way to turn it off I can't complain.

You don't have to make a case to me for open sourcing code, I just have to run it up the chain. That said, my implementation is basically a $.View.hookup hack (and on 3.1 no less), but I hope we'll open source it and I'd love to help out on getting an implementation leveraging this cool EJS DOM-diff code. What handles list binding?

you mentioned it will call a function if it finds it

The function call behavior is admittedly a little weird (and undocumented AFAIK) but it makes sense. You're not going to want the toString() of a function in your templates.

@justinbmeyer
Copy link
Author

The function stuff certainly makes sense ... you see a different type and get a value from it slightly differently. I don't think seeing an observe and getting a value differently (and of course hooking up live-binding) violates the ideas behind handlebars (especially as this is almost certainly what Yehuda is doing).

But, if you are ok with functions being treated differently, I've just added a compute method for CanJS:

canjs/canjs@8eb7847

What handles list binding?

I think maybe how EJS does it's magic isn't clear (because it's so powerful it might seem like magic). There's really nothing special that "handles" list binding. It's just the part of code that updates an HTML chunk listed above at: https://github.com/jupiterjs/canjs/blob/master/view/ejs/ejs.js#L329

The secret is that can.Observe.List changes it's length property when items are removed or added and can.Observe.List.prototype.each calls this.attr('length'). So a block like:

<% list.each(function(){ %>
  CONTENT
<% }) %>

is just html getting updated that is live-bound to the list's length.

@justinbmeyer
Copy link
Author

@iamnoah
Copy link

iamnoah commented Jun 18, 2012

Right, that makes sense, you would just use a DOM diff for everything. can.compute will be very helpful.

The flag to turn it off though is going to be essential, and those of us who need to upgrade will need it per view() call. Right now we have a lot of manual update code that could interfere with live binding. That's why it would be better for us to have a helper instead.

@iamnoah
Copy link

iamnoah commented Jun 19, 2012

As promised, here is our plugin: https://github.com/Spredfast/jmvc-bind-handlebars

It's a different approach, but it gets us close without too much code.

@tommymorgan
Copy link

Justin and Noah, I am very sorry, but I had to make the repo private until we attach a license to the code. I will try to make sure this happens quickly so I can make it public again.

@tommymorgan
Copy link

It's public again. Sorry for the disruption.

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