A pure Javascript template data binder - with high performance and easy to use CSS selector style syntax.
Creating a new binder (using HTML string):
var myBinder = new Binder('<a></a>', {
'@id': 'item-link-{{uid}}',
'@href': '/items/{{uid}}',
'#textContent': 'name'
});
As above, when constructing a binder, the first argument is the HTML template (either as a string or an exsiting node from the DOM). The second argument is the actual bindings - an object where the property names are a CSS selector to indentify the node/attribute to be populated and the property values are the binding instruction of what to bind... how to construct the value to be be inserted into the generated HTML.
and then to use the binder to generate a new node:
var newNode = myBinder.bind({
'uid': '27e7a5284dee',
'name': 'My first item'
});
would produce a node with the following HTML:
<a id="item-link-27e7a5284dee" href="/items/27e7a5284dee">My first item</a>
- Constructor
- Methods
- Binding Selectors
- Binding Instructions
- Binding Instructions Function Scope
- Cookie-cutter mode & In-place mode
- Examples
- How It Works
- Browser Compatibility
new Binder(template, bindings[, bindingsScope [, inplaceMode[, [options]]])
|
||
Parameter | Type | Description |
---|---|---|
template
|
string | node |
The template node or template HTML string. For cookie-cutter mode, the template argument can be a string or existing DOM node (including a HTML <template> element)For in-place mode, the template argument must be an existing DOM element (and cannot be a HTML <template> element). |
bindings
|
object | An object containing the bindings - where the property names are the binding selectors and the property values are the binding instructions. |
bindingScope
|
object | [optional] An object to be used as the binding instructions function scope. |
inplaceMode
|
boolean |
[optional] Flag indicating whether the binder is created as in-place mode (true ) or cookie-cutter mode (false default).
|
options
|
object |
[optional] An object containing additional binder (debugging) options. The object can conatin the following boolean properties:
|
Populates a template node with data Syntax: binder.bind(data) => node Parameters: data - an object containing the data to be boundReturns: node - the node with data populated
|
|
Re-populates an existing node with new data Syntax: binder.rebind(data, node) => node Parameters: data - an object containing the data to be boundnode - the node to be re-boundReturns: node - the node with data populated
|
|
Gets the currently bound data from a node Syntax: binder.getBoundData(node) => object Parameters: node - the previously bound nodeReturns: object - the data that was bound to the node
|
|
Returns whether the binder was created as cookie-cutter mode or in-place mode. Syntax: binder.isInplace() => boolean Returns: boolean - whether the binder was created as in-place mode (true ) or
cookie-cutter mode (false )
|
The binding selectors are the property names of the object passed to the binding constructor. These property names use 'standard' CSS query syntax - as used by .querySelectror()
or .querySelectorAll()
. Each specified binding selector (CSS query) must only select one node from the template - if more than one node within the template for the binding selector is found, the Binder constructor will throw an exception.
To allow for bindings to attributes, properties and events some additional 'special' tokens can be added to the end of the binding selectors - these are:
Token | Description |
---|---|
#textContent
|
Sets the text content for the selected node This is the default for all selectors when no other special token present (see Example 1 and Example 2) |
#innerHTML
|
Sets the inner HTML for the selected node (see Example 3) |
#append
|
Appends nodes to the selected node (see Example 4) |
@attribute-name
|
Sets a specific named attribute on the selected node (see Example 5) |
@@attribute-name.remove
|
Removes a specific named atrribute from the selected node (see Example 6) |
@class.add
|
Adds class token(s) to the specified node The binding instructtion returns a string name of the class token to add or an array of string class tokiens to add (see Example 7 and Example 8) |
@class.remove
|
Removes class token(s) from the specified node The binding instructtion returns a string name of the class token to remove or an array of string class tokiens to remove (see Example 9 and Example 10) |
@property.property-name
|
Sets a specific named property on the selected node (see Example 11) |
@dataset.name
|
Sets a specific named data- attribute on the selected node(see Example 12) |
@event.event-name
|
Adds a specified event listener to the selected node The binding instruction must be a function that is the event listener. (see Example 13) |
@event.bound
|
Adds an after bound event to the binding (one only per binder) The binding instruction must be a function that is the event listener. (see Example 14) |
If the value (binding instruction) of a binding selector is an object
, it is treated as descendant binding selectors - this enables you to structure your bindings without having to repeat selectors.
A simple example of using nested binding selectors is to set multiple attributes on the same selected node, e.g.:
var myBinder = new Binder('<a><img></a>', {
'@href': 'url', // set @href attribute on <a>
'img': { // nested binding selectors for <img>
'@src': 'imageUrl', // set @src attribute on <img>
'@width': 'imageWidth', // set @width attribute on <img>
'@height': 'imageHeight' // set @width attribute on <img>
}
});
var newNode = myBinder.bind({
'url': 'https://en.wikipedia.org/wiki/Albert_Einstein',
'imageUrl': 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/3e/Einstein_1921_by_F_Schmutzer_-_restoration.jpg/220px-Einstein_1921_by_F_Schmutzer_-_restoration.jpg',
'imageWidth': 220,
'imageHeight': 289
});
By default, the nested binding selectors are treated as CSS descendant selectors - but you can use explicit child selectors using the usual CSS >
selector, e.g.:
var myBinder = new Binder(
'<div>' + '' +
'<div class="sub-div-1">' +
'<div class="sub-div-2">' +
'<span class="foo"></span>' +
'</div>' +
'<span class="foo"></span>' +
'</div>' +
'</div>',
{
'> .sub-div-1': {
'> .sub-div-2 .foo': 'name',
'> .foo': 'description'
}
});
Note: Immediate child >
selector can only be used at top-level binding selectors if the browser supports :scope
pseud-class. (see Browser Compatibility)
The binding instructions are the property values of the object passed to the binding constructor. These values can be different types - documented as:
Type | Description |
---|---|
string
|
Choice of:
|
function
|
For data binding selectors: A function with one argument that receives the data being bound, e.g. function(data) and returns the string to be injected into the template. For event binding selectors: A function with four arguments that receive information about the event and data being bound, e.g. function(evt, boundNode, eventNode, data) where the arguments are:
|
object
|
The value is an object containing descendant binding selectors (see Nested Binding Selectors) |
The binding instruction functions (including event listener functions) are, by default, bound to the bindings object passed to the constructor - for example, the following code:
var myBinder = new Binder('<a></a>', {
'@id': function(data) {
console.log("this['@href'] =", this['@href']);
return 'item-link-' + data.uid;
},
'@href': '/items/{{uid}}',
'#textContent': 'name'
});
var newNode = myBinder.bind({
'uid': '27e7a5284dee',
'name': 'My first item'
});
will show output in the console of:
this['@href'] = /items/{{uuid}}
Which really isn't of great use - which is why the binder constructor provides a third argument which allows you to supply an object for the function scope, e.g.:
var myScope = {
someTestProperty: "foo",
say: function(what) {
console.log('Test says... ', what);
}
};
var myBinder = new Binder('<a></a>', {
'@id': function(data) {
console.log("this.someTestProperty =", this.someTestProperty);
this.say('Hello World!');
return 'item-link-' + data.uid;
},
'@href': '/items/{{uid}}',
'#textContent': 'name'
}, myScope);
var newNode = myBinder.bind({
'uid': '27e7a5284dee',
'name': 'My first item'
});
will show output in the console of:
this.someTestProperty = foo
Test says... Hello World!
That's a whole lot more useful! You can now use the binding function scope to access information outside the bound data.
By default, Binder runs in 'cookie-cutter' mode - i.e. everytime you call bind(data)
on your binder it returns a newly created node from your template and binding instructions. However, Binder also provides an 'in-place' mode - which allows data to be bound and re-bound to an existing static node in the DOM.
To create a binder for 'in-place' mode simply use the fourth argument of the constructor, e.g.:
var myBinder = new Binder(document.getElementById('my-inplace-node'),
{
'#textContent': 'name'
},
null, /* we don't want a binding function scope for now */
true /* make it an in-place mode binder */);
(see also In-place Demo)
Example 1 - Explict #textContent
var myBinder = new Binder('<p></p>', {
'#textContent': 'name'
});
var newNode = myBinder.bind({
'name': 'Foo Bar'
});
Example 2 - Implicit #textContent
As example #1 - but without explicitly using the #textContent
token
var myBinder = new Binder('<p></p>', {
'': 'name' // empty binding selector implies #textContent
});
var newNode = myBinder.bind({
'name': 'Foo Bar'
});
Example 3 - #innerHTML
var myBinder = new Binder('<div><p class="name"></p><ul class="favourites-list"></ul></div>', {
'.name': 'name',
'.favourites-list #innerHTML': function(data) {
var builder = [];
for (var pty in data.favourites) {
if (data.favourites.hasOwnProperty(pty)) {
builder.push('<li>Favourite ' + pty + ' is ' + data.favourites[pty] + '</li>');
}
}
return builder.join('');
}
});
var newNode = myBinder.bind({
'name': 'Foo Bar',
'favourites': {
'colour': 'Red',
'fruit': 'Banana',
'film': 'Star Wars'
}
});
Example 4 - #append
var myBinder = new Binder('<div><p class="name"></p><ul class="favourites-list"></ul></div>', {
'.name': 'name',
'.favourites-list #append': function(data) {
var favNodes = [], favNode;
for (var pty in data.favourites) {
if (data.favourites.hasOwnProperty(pty)) {
favNode = document.createElement('li');
favNode.textContent = 'Favourite ' + pty + ' is ' + data.favourites[pty];
favNodes.push(favNode);
}
}
return favNodes;
}
});
var newNode = myBinder.bind({
'name': 'Foo Bar',
'favourites': {
'colour': 'Red',
'fruit': 'Banana',
'film': 'Star Wars'
}
});
Example 5 - @attribute-name
var myBinder = new Binder('<a></a>', {
'#textContent': 'name',
'@href': 'url'
});
var newNode = myBinder.bind({
'name': 'Foo Bar',
'url': '/people/123456'
});
Example 6 - @attribute-name.remove
var myBinder = new Binder('<div><input id="" type="text" disabled></div>', {
'input @id': 'uid',
'input @disabled.remove': function(data) {
// return whether to remove disabled attribute or not...
return data.enabled;
}
});
var newNode1 = myBinder.bind({
'uid': 1,
'enabled': true
});
var newNode2 = myBinder.bind({
'uid': 2,
'enabled': false
});
Example 7 - @class.add
var myBinder = new Binder('<a></a>', {
'#textContent': 'name',
'@href': 'url',
'@class.add': function(data) {
if (data.active) {
// return 'active' class token when data is active...
return 'show-active';
}
}
});
var newNode = myBinder.bind({
'name': 'Foo Bar',
'url': '/people/123456',
'active': true
});
Example 8 - @class.add (adding multiple classes)
var myBinder = new Binder('<a></a>', {
'#textContent': 'name',
'@href': 'url',
'@class.add': function(data) {
var classTokens = [];
if (data.active) {
classTokens.push('show-active');
}
if (data.important) {
classTokens.push('show-important');
}
return classTokens;
}
});
var newNode = myBinder.bind({
'name': 'Foo Bar',
'url': '/people/123456',
'active': true,
'important': true
});
Example 9 - @class.remove
var myBinder = new Binder('<a class="show-active"></a>', {
'#textContent': 'name',
'@href': 'url',
'@class.remove': function(data) {
if (!data.active) {
// return 'active' class token to remove when data is not active...
return 'show-active';
}
}
});
var newNode = myBinder.bind({
'name': 'Foo Bar',
'url': '/people/123456',
'active': false
});
Example 10 - @class.remove removing multiple classes
var myBinder = new Binder('<a class="show-active show-important"></a>', {
'#textContent': 'name',
'@href': 'url',
'@class.remove': function(data) {
var classTokens = [];
if (!data.active) {
classTokens.push('show-active');
}
if (!data.important) {
classTokens.push('show-important');
}
return classTokens;
}
});
var newNode = myBinder.bind({
'name': 'Foo Bar',
'url': '/people/123456',
'active': false,
'important': false
});
Example 11 - @property.property-name
var myBinder = new Binder('<a></a>', {
'#textContent': 'name',
'@href': 'url',
'@property.dataPropertyAddedToNode': 'additionalData'
});
var newNode = myBinder.bind({
'name': 'Foo Bar',
'url': '/people/123456',
'additionalData': {
'status': 'ready',
'fixed': true,
'modified': false
}
});
Example 12 - @dataset.name
var myBinder = new Binder('<a></a>', {
'#textContent': 'name',
'@href': 'url',
// set data-internal-id attribute...
'@dataset.internalId': 'uid'
});
var newNode = myBinder.bind({
'name': 'Foo Bar',
'uid': 123456,
'url': '/people/123456'
});
Example 13 - @event.event-name
var myBinder = new Binder('<button><img width="32" height="32"><span class="label"></span></button>', {
'.label': 'browser-name',
'img @src': 'browser-icon',
'@event.click': function(evt, boundNode, eventNode, data) {
console.log('You clicked the button' + (eventNode === evt.target ? '' : ' - or something inside it!'));
}
});
var newNode = myBinder.bind({
'browser-name': 'Chrome',
'browser-icon': 'https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/chrome/chrome_512x512.png'
});
Example 14 - @event.bound
var myBinder = new Binder('<a></a>', {
'#textContent': 'name',
'@href': 'url',
'@event.bound': function(evt, boundNode, eventNode, data) {
console.log('You just bound data: ', data, ' to node: ', boundNode);
}
});
var newNode = myBinder.bind({
'name': 'Foo Bar',
'url': '/people/123456'
});
Binder is designed to be fast and easy to use. Its speed is derived from the way it utilises node cloning (which out performs element creation on almost all browsers - see jsPerf - cloneNode vs createElement Performance).
When a new binder is instantiated, it compiles the bindings into stored pointers to the nodes to be populated and functions for obtaining the values used to populate - so that when the bind()
occurs everything is known (no re-interpreting of the bindings). Even the templated string binding instructions (strings containing {{}}
) are compiled into functions that are re-used at bind time.
Chrome |
Firefox |
Safari |
Internet Explorer |
Edge |
Opera |
49+ | 52+ | 10.1+ |
11 [1] |
14 | 45+ |
:scope
- so >
cannot be used on top-level binding selectors