bind-js
v0.4.2
Published
Fast, unobstrusive and simple Javascript Templating and Binding
Downloads
9
Readme
Bind JS
Fast, unobstrusive and simple Javascript Templating and Binding. Directly influenced by Pure and MooTools and reversely influenced by AngularJS and React. In 1.4 minified Ks.
If you end up using Bind, shoot me a line and let me know. I would love to hear use cases and suggestions on how to improve library.
Table of Contents
- Goals
- What is Bind
- Why Bind
- Hello World Example
- Features (through Examples)
- MVC Todos (Angular Comparison)
- API Documentation
- Installing
- Extended Rationale (how are goals achieved)
- Client/Server
- Known Limitations
- Testing Bind
- Contributing
- License
Goals
See the Extended Rationale section for an elaboration of these goals.
- Elegance
- KISS
- Pure HTML Markup
- CSS Selectors
- Compatibility
- Simple Two-Way binding (TBD)
- Speed
What is Bind
A very small and lean binding/rendering library. Separates presentation from data very cleanly and very fast. Implements two way binding optionally without kludges. Allows for a hooking up a statically web app to render dynamic content in a principled and economic way.
The main ideas are:
- Pure HTML (no
ng-bla
attributes) constitutes a (potential) template - Data (JSON/Javascript) data is bound through CSS selectors (i.e. put
object.name
indiv label#name
element) - Observe data and if it changes re-render bound template
- Observe forms and it they change, update (simply mapped) object attributes
- Spice CSS selectors with some syntactic sugar to make them more palatable
Why Bind
Given the (literally) hundreds of Javascript Template Engines out there, why am I creating a new one? In short, I believe most of them to be one or more of the following: ugly, slow, verbose, obtrusive, and/or Cognitive Load heavy with respect to the rest of the web stack. Most extend a server solution, or create a whole new set of concepts and vocabulary. Bind is my attempt at cleanly extending the current concepts in HTML/Javascript/CSS in a way that flows more naturally.
Hello World Example
Mandatory Hello World example (link takes you to JSFiddle):
HTML:
<h1>Hello <span>NAME</span>!</h1>
<p>My favorite fruits are:</p>
<ul>
<li>FRUIT</li>
</ul>
<p>My favorite color is: <i id="color" style="STYLE">COLOR</i></p>
<p>Today's date is <b id="date">DATE</b></p>
Javascript:
bind(
document.body,
{
"h1 span": "John Smith",
"li": ["Orange", "Pear", "Apple"],
"#color": {
"": "green",
"@style": "color: green"
},
"b[id=date]": new Date()
}
);
Result:
<h1>Hello <span>John Smith</span>!</h1>
<p>My favorite fruits are:</p>
<ul>
<li>Orange</li>
<li>Pear</li>
<li>Apple</li>
</ul>
<p>My favorite color is: <i id="color" style="color: green;">green</i></p>
<p>Today's date is <b id="date">Wed Jan 14 2015 22:40:57 GMT-0600 (CST)</b></p>
Mappings can be nested (see #color
selector), so if the value of a CSS selector is another mapping, the
context node becomes the one(s) matched by the selector.
The ALL CAPS text in the HTML is where the CSS selectors should match: it makes the template more readable.
The mapping uses the CSS extensions allowed, which is to able to match attributes (CSS does not contemplate the matching of attributes) and to match the current context node (dot syntax or simply empty selector). The extended syntax is inspired by the XSLT Recommendation (see current and attribute values).
Features
The following examples demonstrate the features (and caveats) of the engine. You can copy and paste them in first fiddle to run them.
Iteration
Simple iteration is done via the the mapping a CSS selector to an array. Bind iterated through the array creating a new HTML object copy for each element in the array. If the array is empty, the element is iterated through zero times, making it in effect disappear.
<h1>Hello <span>NAME</span>!</h1>
<p>The list of your friends is:</p>
<ul>
<li>LAST, FIRST NAME</li>
</ul>
var friends = [
"Einstein, Albert",
"Curie, Marie",
"Freud, Sigmund",
"Planck, Max",
"Watson, James"
];
bind(
document.body,
{ name: "John Smith", friends: friends },
function(m) { return { "h1 span": m.name, "li": m.friends }; }
);
Result:
<h1>Hello <span>John Smith</span>!</h1>
<p>The list of your friends is:</p>
<ul>
<li>Einstein, Albert</li>
<li>Curie, Marie</li>
<li>Freud, Sigmund</li>
<li>Planck, Max</li>
<li>Watson, James</li>
</ul>
Nested Iteration
When filling a table, you can map nested CSS selectors to an array of objects to create a table. In this case the labels in the first row are extracted from the first object, and then each row if filled via a nested mapping.
<style>
table { border-collapse: collapse; width: 100%; }
table th { border: 1px solid grey; font-weight: bold; }
table td { border: 1px solid grey; font-weight: normal; }
</style>
<table>
<thead>
<tr>
<td></td>
<th><span>COLUMN LABEL</span></th>
</tr>
</thead>
<tbody>
<tr>
<th>ROW LABEL</th>
<td>PROPERTY VALUE</td>
</tr>
</tbody>
</table>
var scientists = [
{ last: "Einstein", first: "Albert", country: "Germany", science: "Physics" },
{ last: "Curie", first: "Marie", country: "Poland", science: "Chemistry" },
{ last: "Freud", first: "Sigmund", country: "Austria", science: "Neurology" },
{ last: "Planck", first: "Max", country: "Germany", science: "Physics" },
{ last: "Watson", first: "James", country: "USA", science: "Biology" }
];
bind(
document.body,
scientists,
function(m) { return {
"table thead th": Object.keys(m[0]),
"table tbody tr": m.map(function(o) { return {
"th": o.first+' '+o.last,
"td": Object.values(o)
};})
};}
);
Result:
TBD
Attributes
Attribute binding is done via an XSLT inspired notation, where li@class
means the attribute class
at the
element li
. Note that li[class]
means a different thing in CSS: a li
element that contains an attribute class
.
The example uses an extended selector to display physicist names in bold. The mapper combines first and last name directly from the model.
<style>.Physics { font-weight: bold; }</style>
<p>Marking all *Physicists* with bold font face:</p>
<ul>
<li class="SCIENCE">LAST, FIRST NAME</li>
</ul>
var scientists = [
{ last: "Einstein", first: "Albert", country: "Germany", science: "Physics" },
{ last: "Curie", first: "Marie", country: "Poland", science: "Chemistry" },
{ last: "Freud", first: "Sigmund", country: "Austria", science: "Neurology" },
{ last: "Planck", first: "Max", country: "Germany", science: "Physics" },
{ last: "Watson", first: "James", country: "USA", science: "Biology" }
];
bind(
document.body,
scientists,
function(m) { return {
"li": m.map(function(s) { return {
".": s.last + ", " + s.first,
"@class": s.science
}})
}}
);
Result:
<style>.bold { font-weight: bold; }</style>
<ul>
<li class="bold">Einstein, Albert</li>
<li class="">Curie, Marie</li>
<li class="">Freud, Sigmund</li>
<li class="bold">Planck, Max</li>
<li class="">Watson, James</li>
</ul>
Conditionals
To show or not show a value, pass a bind.YES
or bind.NO
value. A bind.YES
value keeps it in,a bind.NO
value removes it.
<button id="login" onclick"login()">Login</button>
<button id="logout" onclick"logout()">Logout</button>
bind(
document.body,
{ authenticated: false },
function(m) { return {
"button#login": m.authenticated ? bind.NO : bind.YES,
"button#logout": m.authenticated ? bind.YES : bind.NO
};}
);
Result:
<button id="login" onclick"login()">Login</button>
In general you can take care of most conditionals at the data level, but it useful to know the example above works for
both elements and attributes (that is, if the attribute is false
it is removed: this obviates the need for things
like ng-disabled
in AngularJS).
Functional Values
Functions can be used instead of values, and their return is used as the value to display.
<p><b>NUMERATOR</b> ÷ <b>DENOMINATOR</b> = <b>QUOTIENT</b></p>
bind(
document.body,
{ numerator: 72, denominator: 8, quotient: 9 },
function(m) { return {
"b:nth-child(1)": m.numerator,
"b:nth-child(2)": m.denominator,
"b:nth-child(3)": function() { return m.numerator / m.denominator; }
};}
);
Result:
<p><b>72</b> ÷ <b>8</b> = <b>9</b></p>
Of course this example is more complicated than it needs to be for the results expected, but it makes for a good explanation of functional values and it shows also some slightly more complex CSS attributes.
Ambiguity
What happens if a CSS selector selects more than one element? All of them are treated equally, meaning that they are all potential template matches, and as such they are treated. It allows for some potentially mischievous code.
<h1>Hello <span>NAME</span>!</h1>
<span>This is the last element!</span>
bind(
document.body,
{ "span": "John Smith" }
);
Results:
<h1>Hello <span>John Smith</span>!</h1>
<span>John Smith</span>
Properties
Properties are attached to the element as direct object values (via the &
operator). You can read more on properties here:
The difference between attribute and property.
This is another case of a CSS syntax extension. The selector h1&model
is attaching the Javascript property model
to the
DOM object for the h1
tag. This is different than attaching an attribute via h1@model
. In this case we leave a Javascript
model representation behind (attached to the DOM) in case we need tlo reuse it.
The example below gives a flavor of the usefulness of this feature.
<h1>Hello <span>NAME</span>!</h1>
bind(
document.body,
{ last: "Smith", first: "John", country: "United States", science: "Plumbing" },
function(m) { return {
"h1&model": m,
"span": (m.first+" "+m.last),
};}
);
Results:
<h1>Hello <span>John Smith<span/>!</h1>
After running it, the value document.querySelector("h1").model
will be the model object. It can then be used
in events and further bindings.
Events
Events such as onclick
can be attached via addEventListener
in the following fashion. The example shows how click
the first name will result in an alert with the last name.
<ul>
<li>FIRST NAME</li>
</ul>
var scientists = [
{ last: "Einstein", first: "Albert", country: "Germany", science: "Physics" },
{ last: "Curie", first: "Marie", country: "Poland", science: "Chemistry" },
{ last: "Freud", first: "Sigmund", country: "Austria", science: "Neurology" },
{ last: "Planck", first: "Max", country: "Germany", science: "Physics" },
{ last: "Watson", first: "James", country: "USA", science: "Biology" }
];
bind(
document.body,
scientists,
function(m) { return {
"li": m.map(function(s) { return {
".": s.first,
"@onclick": function() { alert("You clicked on "+s.last); }
}})
};}
);
Result:
<ul>
<li>Albert</li>
<li>Marie</li>
<li>Sigmund</li>
<li>Max</li>
<li>James</li>
</ul>
And if we click on each li
we will get an alert with the last name of the scientist.
Filters
Bind does not really have filters, but they can be embedded via the mapper. For example, for an uppercase
filter:
<h1>Hello <span>NAME</span>!</h1>
bind(
document.body,
{ name: "John Smith" },
function(m) { return { "span": m.name.toUpperCase() } }
);
Results:
<h1>Hello <span>JOHN SMITH</span>!</h1>
MVC Todo
Following the example from the AngularJS page, we create a similar app in JSFiddle. The goal of recreating this little app, is to test how close one can get to the original functionality with Bind and how complex it is. A couple of notes:
- The code is around 50% longer
- It does not use special HTML markup, it uses the
mapper
function below to bind HTML to the model - It has two-way binding via observing the model every 100 milliseconds (same mechanism as Angular)
- It is mostly understandable by people familiar with basic web development
- It is simple enough
The mapper function:
function mapper(todos) {
return {
"i:nth-of-type(1)": remaining(),
"i:nth-of-type(2)": todos.length,
"li": todos.map(function(t, i) {
return {
"@model": t,
"@index": i,
"input@checked": (t.done ? "checked" : false),
"span": t.text,
"span@class": (t.done ? "done" : "")
};
})
};
}
Notice how the todo
model is bound to the li
element in an explicit way.
By no means this little app is meant as proof or even claim of equivalent functionality. Angular is a big project, with lots of functionality and features. It is meant as a simple comparison exercise to flex Bind's muscles with respect to a well known example.
API
There are two modes in the API:
bind(elem, model, mapper)
: use a model and a mapper to generate the mappingbind(elem, mapping)
: use a mapping directly
Providing a model allows for a more principled approach, the direct mapping is the quick and dirty version. When it is important (isn't always?) to write modular and readable code, you should prefer the first option.
If the element has the reset
attribute, this instructs the library to keep a copy of itself and to reset itself
to the intial state each time then element is bound. It is a very useful option when using the library on GUIs.
bind(elem, model, mapper)
Bind a DOM element elem
to a model
, via a mapper
function. When the mapper function is applied to the model, it
yields a mapping - i.e. mapping = mapper(model)
. The mapper
function should a return an object where keys are
extended CSS selectors and the values are derived from the model
.
Parameters:
{Element} elem
: DOM element to be bound (effectively the view){Object|Array} model
: plain JavaScript object or array (the model){Function} mapper
: function bridging the model and view (mapping between CSS selectors and values)
Returns:
The element elem
now modified with the model value via the mapping.
Example:
<h1>Hello <span>NAME</span>!</h1>
bind(
document.body,
{ name: "John Smith" },
function(model) { return { "h1 span": model.name }; }
);
bind(elem, mapping)
Bind a DOM element elem
to values expressed in a mapping
object. The mapping is a plain object where keys are
extended CSS selectors that map to the values to be used.
Parameters:
{Element} elem
: DOM element to be bound (effectively the view){Object} mapping
: map between CSS selectors and values
Returns:
The element elem
now modified with the values in the mapping.
Example:
<h1>Hello <span>NAME</span>!</h1>
bind(
document.body,
{ "h1 span": "John Smith" }
);
Installing
Server side to be used with Node.js:
npm install bind-js
Client Use to be included in a project:
curl -O http://acrodrig.github.io/bind/lib/bind.js
Extended Rationale
Since I am presenting a new JavaScript Template Engine in a crowded field, let me give a rationale, which hopefully will convince you (in conjuntion with the examples) of the merits of the current approach. The current templating libraries usually fall in one of three main camps:
ERB Type: add to HTML a couple of interpolation tokens (usually
<%= var %>
or${{ var }}
) (such as Mustache, Handlebars and Underscore).Attribute Type: add to special attributes to HTML (like
ng-repeat
ordata-bind
). Popular choices are KnockoutJS and AngularJS.External: They work through special objects that connect the HTML to the data objects. I think the most popular choice here is PureJS. This is the route chosen by Bind, as it provides the cleanest separation of concerns.
In my opinion the first two are fairly convoluted solutions. The first one (ERB style) fills HTML with extraneous tags and it does not make the model explicit. These seem to be solutions transplanted from the server days. The syntax becomes specially nasty once you have to do iteration or conditionals.
As for the special attributes, they do make the model explicit, but they force you to learn a new language which is
full of special cases to account for the many possibilities (see ng-disabled
rationale for example).
Finally the third option is my favorite, but it is also the least popular as far as I can tell. The options out there also have special cases for iteration and they are not as elegant as they could be if they took the model to heart. The elegance in this approach is that iteration, conditionals, formatting, they are all driven by the data or the mapping function.
The way that Bind attempts to fulfill its goals:
- Elegance: in the eye of the beholder, but hopefully it flows naturally from HTML/Javascript/CSS
- KISS: library is less than 50 lines of code, concepts are well known (plain objects, maps, CSS selectors, XSLT)
- Pure HTML Markup: there are no extraneous attributes polluting the templates, no need to learn new attributes
- CSS Selectors: the bridging of the model and view is done via a powerful and expressive language
- Compatibility: works on top of simple templates as defined Web Components
- Speed: uses native browser implementations like
querySelector
and DOM manipulation with caching
Additionally further down below there is a JSPerf comparing Bind to different template engines. As with all micro-benchmarks, take with a grain of salt, but at the very least be convinced that we are in the best tier.
As the library progresses the merits of a virtual DOM (like React's) can be explored. There are things lost like native CSS matching with this approach.
Client/Server
Bind works both in the client in the server (with a compatible DOM implementation).
Known Limitations
- Cannot update partial text (something like
<li>name: <%= name %></li>"
). - Cannot update partial attributes (something like
style="color: <%= color %>; font-weight: bold;"
). - It can become verbose to provide mapper functions for all models. On the other hand that is the nature of the MVC beast.
- No two-way binding, although as shown in the MVC Todo example below, its value can be overstated
Testing Bind
There are unit tests that can be run by executing:
npm run test
They use the Mocha framework, which should be installed globally via npm
. The tests also use
Domino, a lightweight DOM implementation for the server as a development dependency.
Contributing
Do the usual GitHub fork and pull request dance. Add yourself to the contributors section of [package.json] too if you want to.
License
Released under the MIT license.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.