Skip to content

Instantly share code, notes, and snippets.

@wycats
Last active December 18, 2015 15:59
Show Gist options
  • Save wycats/5808149 to your computer and use it in GitHub Desktop.
Save wycats/5808149 to your computer and use it in GitHub Desktop.
HTMLBars Binding

HTMLBars serves as a polyfill for a native implement of node.bind.

Instead of letting the browser parse the HTML and generate nodes, which leaves us at the mercy of the limitations of the parser, HTMLBars implements an HTML parser and generates the nodes itself.

As HTMLBars generates the nodes, it calls into node.bind if it detects mustache syntax.

Open questions:

  • MDV "syntaxes"

Wrappers

In order to avoid needing to monkey-patch HTMLElement in the polyfill, we will instantiate wrapper objects that manage the binding.

So instead of:

textNode.bind('textContent', model, 'firstName');

We will do this:

var textNodeWrapper = new WRAPPERS.Text(textNode);
fragWrapper.appendChildWrapper(textNodeWrapper);
textNodeWrapper.bind('textContent', model, 'firstName');

The wrappers keep references to their children, which enables recursive disposal when the root of a wrapper tree is disposed of.

Text nodes

This template:

<p>Enter {{foo}} Exit</p>

Will execute something like this code:

// <p>
var p = document.createElement('p');

// Text Node
p.appendChild(document.createTextNode('Enter ');

// Bound Text Node
var foo = document.createTextNode('');
var fooWrapper = new WRAPPERS.Text(foo);
fragWrapper.appendChildWrapper(fooWrapper);

// {{foo}}
fooWrapper.bind('textContent', model, 'foo');
p.appendChild(foo);

// Text Node
p.appendChild(document.createTextNode(' Exit');

// </p>
frag.appendChild(p);

In this example, fragWrapper is an object that was associated with the template instantiation. When the instance of the template is destroyed, child wrappers will be recursively destroyed as well.

NOTE: MDV uses "dispose" terminology where Ember typically uses "destroy"

Elements

This template:

<a href="{{url}}">{{foo}}</a>
<p title="static">Contents</p>

Will execute something like this code:

// <a>
var a = document.createElement('a');
var aWrapper = new WRAPPERS.HTMLAnchorElement(a);
fragWrapper.appendChildWrapper(aWrapper);

// href="{{url}}"
aWrapper.bind('href', model, 'url');

// Bound Text Node
var foo = document.createTextNode('');
var fooWrapper = new WRAPPERS.Text(foo);
aWrapper.appendChildWrapper(fooWrapper);

// {{foo}}
fooWrapper.bind('textContent', model, 'foo');
a.appendChild(foo);

// </a>
frag.appendChild(a);

// <p>
var p = document.createElement('p');

// title="static"
p.setAttribute('title', 'static');

// Text Node
p.appendChild(document.createTextNode('Contents'));

// </p>
frag.appendChild(p);

Paths

Paths would work by generating a PathBinding instead of binding directly to the model:

<a href="{{link.url}}">Click here</a>

Would generate code like this:

// <a>
var a = document.createElement('a');
var aWrapper = new WRAPPERS.HTMLAnchorElement(a);
fragWrapper.appendChildWrapper(aWrapper);

// href="{{link.url}}"
var linkUrlBinding = new PathBinding(model, 'link.url');
aWrapper.bind('href', linkUrlBinding, 'value');

// Text Node
a.appendChild(document.createTextNode('Click here');

// </a>
frag.appendChild(a);

The PathBinding is an object that updates its value property whenever any link in the chain changes. A normal Object.observe on its value property will "just work".

Compound Bindings

It's possible to have an attribute that contains both static and dynamic parts. These attributes are represented by CompoundBindings.

<a href="{{scheme}}://{{host}}:{{port}}/{{path}}">Click here</a>

This would generate code like this:

// <a>
var a = document.createElement('a');
var aWrapper = new WRAPPERS.HTMLAnchorElement(a);
fragWrapper.appendChildWrapper(aWrapper);

// href="{{scheme}}://{{host}}:{{port}}/{{path}}"
var compoundBinding = new CompoundBinding([
  new Binding(model, 'scheme'),
  { value: "://" },
  new Binding(model, 'host'),
  { value: ":" },
  new Binding(model, 'port'),
  { value: "/" },
  new Binding(model, 'path')
]);
aWrapper.bind('href', compoundBinding, 'value');

// Text Node
a.appendChild(document.createTextNode('Click here');

// </a>
frag.appendChild(a);

A CompoundBinding observes the value property of all elements in the Array and updates its value property whenever any of them change.

A Binding observes a property on an object and updates its value property whenever that property changes.

Note that a CompoundBinding may contain PathBindings, since PathBindings also expose an observable value property as its surface area.

It would be possible to special-case Binding and static strings to improve efficiency, but I've opted not to for now to keep the implementation of CompoundBinding simple.

Ember Views

Ember would register Ember.Views as the element wrappers.

The same strategy could work with node.bind:

  • register the elements themselves as the wrappers
  • implement a no-op appendChildWrapper if disposal is not required
@g13013
Copy link

g13013 commented Jan 18, 2014

Awesome, where can we watch/try the progress? the main repo is outdated I think

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