Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/marrow16/binder

A pure Javascript template data binder
https://github.com/marrow16/binder

binder javascript template

Last synced: 13 days ago
JSON representation

A pure Javascript template data binder

Awesome Lists containing this project

README

        

# 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):
```javascript
var myBinder = new Binder('', {
'@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:
```javascript
var newNode = myBinder.bind({
'uid': '27e7a5284dee',
'name': 'My first item'
});
```
would produce a node with the following HTML:
```html
My first item
```


## Table of Contents

* Constructor
* Methods
* bind
* rebind
* getBoundData
* isInplace
* Binding Selectors
* Nested Binding Selectors
* Binding Instructions
* Binding Instructions Function Scope
* Cookie-cutter mode & In-place mode
* Examples
* How It Works
* Browser Compatibility


## 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.:
```javascript
var myBinder = new Binder('', {
'@href': 'url', // set @href attribute on
'img': { // nested binding selectors for
'@src': 'imageUrl', // set @src attribute on
'@width': 'imageWidth', // set @width attribute on
'@height': 'imageHeight' // set @width attribute on
}
});
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.:
```javascript
var myBinder = new Binder(
'

' + '' +
'
' +
'
' +
'' +
'
' +
'' +
'
' +
'
',
{
'> .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:
```javascript
var myBinder = new Binder('', {
'@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.:
```javascript
var myScope = {
someTestProperty: "foo",
say: function(what) {
console.log('Test says... ', what);
}
};
var myBinder = new Binder('', {
'@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.:
```javascript
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](./demo/inplace/index.html))


## Examples

##### Example 1 - Explict [#textContent](#selector-textcontent)
```javascript
var myBinder = new Binder('

', {
'#textContent': 'name'
});
var newNode = myBinder.bind({
'name': 'Foo Bar'
});
```

##### Example 2 - Implicit [#textContent](#selector-textcontent)
_As example #1 - but without explicitly using the #textContent token_
```javascript
var myBinder = new Binder('

', {
'': 'name' // empty binding selector implies #textContent
});
var newNode = myBinder.bind({
'name': 'Foo Bar'
});
```

##### Example 3 - [#innerHTML](#selector-innerhtml)
```javascript
var myBinder = new Binder('




    ', {
    '.name': 'name',
    '.favourites-list #innerHTML': function(data) {
    var builder = [];
    for (var pty in data.favourites) {
    if (data.favourites.hasOwnProperty(pty)) {
    builder.push('
  • Favourite ' + pty + ' is ' + data.favourites[pty] + '
  • ');
    }
    }
    return builder.join('');
    }
    });
    var newNode = myBinder.bind({
    'name': 'Foo Bar',
    'favourites': {
    'colour': 'Red',
    'fruit': 'Banana',
    'film': 'Star Wars'
    }
    });
    ```

    ##### Example 4 - [#append](#selector-append)
    ```javascript
    var myBinder = new Binder('




      ', {
      '.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_](#selector-attribute-name)
      ```javascript
      var myBinder = new Binder('', {
      '#textContent': 'name',
      '@href': 'url'
      });
      var newNode = myBinder.bind({
      'name': 'Foo Bar',
      'url': '/people/123456'
      });
      ```

      ##### Example 6 - [@_attribute-name_.remove](#selector-attribute-name-remove)
      ```javascript
      var myBinder = new Binder('

      ', {
      '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](#selector-class-add)
      ```javascript
      var myBinder = new Binder('', {
      '#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](#selector-class-add) (adding multiple classes)
      ```javascript
      var myBinder = new Binder('', {
      '#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](#selector-class-remove)
      ```javascript
      var myBinder = new Binder('', {
      '#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](#selector-class-remove) removing multiple classes
      ```javascript
      var myBinder = new Binder('', {
      '#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_](#selector-property-name)
      ```javascript
      var myBinder = new Binder('', {
      '#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_](#selector-dataset-name)
      ```javascript
      var myBinder = new Binder('', {
      '#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_](#selector-event-name)
      ```javascript
      var myBinder = new Binder('', {
      '.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](#selector-event-bound)
      ```javascript
      var myBinder = new Binder('', {
      '#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](https://jsperf.com/clonenode-vs-createelement-performance/2)).

      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