marrow16 / Binder

A pure Javascript template data binder

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Binder

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>

Table of Contents


Constructor

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:
  • compileWarnings - whether to compiler (constructor) shows in the console warnings about compile prroblems (default is false)
  • bindWarnings - whether, during binding, warnings are shown in the console about addressed data properties not being present (default is false)

Methods

bind


Populates a template node with data

Syntax:

binder.bind(data) => node

Parameters:

data - an object containing the data to be bound

Returns:

node - the node with data populated

rebind


Re-populates an existing node with new data

Syntax:

binder.rebind(data, node) => node

Parameters:

data - an object containing the data to be bound

node - the node to be re-bound

Returns:

node - the node with data populated

getBoundData


Gets the currently bound data from a node

Syntax:

binder.getBoundData(node) => object

Parameters:

node - the previously bound node

Returns:

object - the data that was bound to the node

isInplace


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)

Binding Selectors

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)

Nested Binding Selectors

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)


Binding Instructions

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:
  • A string containing the name a property from the bound data to use as the value. The propery name can contain . property path seperators to enable traversing the bound data object.
  • A string containing occurences of {{}} - parts of the string outside the curly braces are static values and parts inside the curly braces are the names of properties from the bound data
  • A string starting with $ - is taken as a Javascript expression and evaluated
    (these $ expressions can also be used within {{}} curly braces - see previous point)
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:
  • evt - the actual event
  • boundNode - the outer bound node
  • eventNode - the node to which the event was bound (i.e. as specified by the original binding selector)
    This node may be different from the evt.target node - for example, if a <button> contained an <img> and the user clicked on the image, this argument would still be the button node.
  • data - the bound data
object The value is an object containing descendant binding selectors
(see Nested Binding Selectors)

Binding Instructions Function Scope

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.


Cookie-cutter mode & In-place mode

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)


Examples

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'
});
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
});
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'
});

How It Works

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.


Browser Compatibility

Chrome
Chrome
Firefox
Firefox
Safari iOS
Safari
IE
Internet
Explorer
Edge
Edge
Opera
Opera
49+ 52+ 10.1+ 11 [1]

14 45+
[1] Does not support :scope - so > cannot be used on top-level binding selectors

About

A pure Javascript template data binder

License:Apache License 2.0


Languages

Language:JavaScript 97.2%Language:HTML 2.0%Language:CSS 0.8%