Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
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
- Host: GitHub
- URL: https://github.com/marrow16/binder
- Owner: marrow16
- License: apache-2.0
- Created: 2017-06-16T17:54:12.000Z (over 7 years ago)
- Default Branch: master
- Last Pushed: 2021-03-29T07:12:01.000Z (over 3 years ago)
- Last Synced: 2023-03-10T05:06:38.354Z (over 1 year ago)
- Topics: binder, javascript, template
- Language: JavaScript
- Homepage:
- Size: 41 KB
- Stars: 1
- Watchers: 3
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
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, thetemplate
argument can be a string or existing DOM node (including a HTML<template>
element)
For in-place mode, thetemplate
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 isfalse
) -
bindWarnings
- whether, during binding, warnings are shown in the console about addressed data properties not being present (default isfalse
)
## Methods
bind
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
rebind
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
getBoundData
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
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 theevt.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('
}
}
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
Firefox
Safari
Internet
Explorer
Edge
Opera
49+
52+
10.1+
11 [1]
14
45+
[1] Does not support :scope
- so >
cannot be used on top-level binding selectors