Installation

You can grab the latest stable release from github or install using the package manager of your choice.

npm install tinybind

Use in a script tag...

<script src="tinybind.js"></script>

... or import using a bundler like webpack

import tinybind from 'tinybind'

Usage

Templates

Templates describe your UI in plain HTML. You can define them directly in the document, use template elements or store and load them however you like. Just make sure you have a convenient way to reference your templates when you want to bind some data to them.

<section id="auction">
  <h3>{ auction.product.name }</h3>
  <p>Current bid: { auction.currentBid | money }</p>

  <aside rv-if="auction.timeLeft | lt 120">
    Hurry up! There is { auction.timeLeft | time } left.
  </aside>
</section>

The important parts to note here are the attributes prefixed with rv- and portions of text wrapped in { ... }. These are binding declarations and they are the sole way that tinybind ties data to your templates. The values of these declarations all follow the same minimal and expressive syntax.

(keypath | primitive) [formatters...]

Keypaths get observed and will recompute the binding when any intermediary key changes. A primitive can be a string, number, boolean, null or undefined.

Formatters can be piped to values using | and they follow a similarly minimal yet expressive syntax. Formatter arguments can be keypaths or primitives. Keypath arguments get observed and will recompute the binding when any intermediary key changes.

(formatter) [keypath | primitive...]

Binding

Simply call tinybind.bind on a template element with some data that you would like to bind.

tinybind.bind(document.getElementById('auction'), {auction: auction})

Every call to tinybind.bind returns a fully data-bound view that you should hold on to for later. You'll need it in order to unbind it's listeners using view.unbind().

Configuring

Use tinybind.configure to set the following configuration options for your app. Note that all configuration options can be overridden locally to a particular view if needed.

tinybind.configure({

  // Attribute prefix in templates
  prefix: 'rv',

  // Preload templates with initial data on bind
  preloadData: true,

  // Root sightglass interface for keypaths
  rootInterface: '.',

  // Template delimiters for text bindings
  templateDelimiters: ['{', '}'],

  // Augment the event handler of the on-* binder
  handler: function(target, event, binding) {
    this.call(target, event, binding.view.models)
  }

})

Binders

Binders are the sets of instructions that tell tinybind how to update the DOM when an observed property changes. tinybind.js comes bundled with a handful commonly-used binders for your conveneience. See the Binder Reference to learn more about the built-in binders that are available out of the box.

While you can accomplish most UI tasks with the built-in binders, it is highly encouraged to extend tinybind with your own binders that are specific to the needs of your application.

One-way binders

One-way binders simply update the DOM when a model property changes (model-to-view only). Let's say we want a simple binder that updates an element's color when the model property changes. Here we can define a one-way color binder as a single function. This function takes the element and the current value of the model property, which we will use to updates the element's color.

tinybind.binders.color = function(el, value) {
  el.style.color = value
}

With the above binder defined, you can now utilize the rv-color declaration in your views.

<button rv-color="label.color">Apply</button>

Two-way binders

Two-way binders, like one-way binders, can update the DOM when a model property changes (model-to-view) but can also update the model when the user interacts with the DOM (view-to-model), such as updating a control input, clicking an element or interacting with a third-party widget.

In order to update the model when the user interacts with the DOM, you need to tell tinybind.js how to bind and unbind to that DOM element to set the value on the model. Instead of defining the binder as a single function, two-way binders are defined as an object containing a few extra functions.

tinybind.binders.toggle = {
  bind: function(el) {
    adapter = this.config.adapters[this.key.interface]
    model = this.model
    keypath = this.keypath

    this.callback = function() {
      value = adapter.read(model, keypath)
      adapter.publish(model, keypath, !value)
    }

    $(el).on('click', this.callback)
  },

  unbind: function(el) {
    $(el).off('click', this.callback)
  },

  routine: function(el, value) {
    $(el)[value ? 'addClass' : 'removeClass']('enabled')
  }
}

API

binder.bind

This function will get called for this binding on the initial view.bind(). Use it to store some initial state on the binding, or to set up any event listeners on the element.

binder.unbind

This function will get called for this binding on view.unbind(). Use it to reset any state on the element that would have been changed from the routine getting called, or to unbind any event listeners on the element that you've set up in the binder.bind function.

binder.routine

The routine function is called when an observed attribute on the model changes and is used to update the DOM. When defining a one-way binder as a single function, it is actually the routine function that you're defining.

binder.publishes

Set this to true if you want view.publish() to call publish on these bindings.

binder.block

Blocks the current node and child nodes from being parsed (used for iteration binding as well as the if/unless binders).

Formatters

Formatters are functions that mutate the incoming and/or outgoing value of a binding. You can use them to format dates, numbers, currencies, etc. and because they work in a similar fashion to the Unix pipeline, the output of each feeds directly as input to the next one, so you can stack as many of them together as you like.

One-way formatters

This is by far the most common and practical way to use formatters — simple read-only mutations to a value. Taking the dates example from above, we can define a date formatter that returns a human-friendly version of a date value.

tinybind.formatters.date = function(value){
  return moment(value).format('MMM DD, YYYY')
}

Formatters are applied by piping them to binding declarations using | as a delimiter.

<span rv-text="event.startDate | date"></span>

Two-way formatters

Two-way formatters are useful when you want to store a value in a particular format, such as a unix epoch time or a cent value, but still let the user input the value in a different format.

Instead of defining the formatter as a single function, you define it as an object containing read and publish functions. When a formatter is defined as a single function, tinybind assumes it to be in the read direction only. When defined as an object, tinybind uses it's read and publish functions to effectively serialize and de-serialize the value.

Using the cent value example from above, let's say we want to store a monetary value as cents but let the user input it in a dollar amount and automatically round to two decimal places when setting the value on the model. For this we can define a two-way currency formatter.

tinybind.formatters.currency = {
  read: function(value) {
    return (value / 100).toFixed(2)
  },
  publish: function(value) {
    return Math.round(parseFloat(value) * 100)
  }
}

You can then bind using this formatter with any one-way or two-way binder.

<input rv-value="item.price | currency">

Note that you can also chain bidirectional formatters with any other formatters, and in any order. They read from left to right, and publish from right to left, skipping any read-only formatters when publishing the value back to the model.

Formatter arguments

Formatters can accept any number of arguments in the form of keypaths or primitives. Keypath arguments get observed and will recompute the binding when any intermediary key changes. A primitive can be a string, number, boolean, null or undefined.

<span>{ alarm.time | time user.timezone 'hh:mm' }</span>

The value of each argument in the binding declaration will be evaluated and passed into the formatter function as an additional argument.

tinybind.formatters.time = function(value, timezone, format) {
  return moment(value).tz(timezone).format(format)
}

Builtins formatters

The following formatters are provided by default:

not (alias: negate)

Returns false to truthy values and true for falsy values

watch

Returns the value as is. Can be used to track changes on one or more dependent properties that must be passed as arguments

<span rv-text="dateRange | someFormatter | watch start end"></span>

In the example above the binding value will be updated when start or end properties changes

Components

Tinybind comes with a light web component implementation allowing to create reusable HTML elements.

Defining a template

A component must de defined as a class descendent of tinybind.Component with a template static property:

  class MyComponent extends tinybind.Component {
    static get template() {
      return `      
      <span>{ message }</span>
      `
    }    
  }

The template is bound to the element instance, so, in the example above, the value of element "message" property will be displayed and its changes tracked.

Tracking attributes

To use values passed to the element as attributes, is necessary to define a properties static property that must return a hash where the key is the property that will be used in the template and the value the attribute name or any other non string value.

  class MyComponent extends tinybind.Component {
    static get properties() {
      return {
        message: true,
        iconUrl: 'icon'
      }
    }
  }

Registering custom element

In order to use a component it must be registered once per application:

customElements.define('my-component', MyComponent)

Using

Just like any other html element:

<my-component message="Hello"></my-component>

Internet Explorer requires the use of a compiler like babel to transpile the class as well to use the webcomponents polyfill

Adapters

tinybind is agnostic about the objects that it can subscribe to. This makes it very flexible as it can adapt to work with virtually any library or framework, but it also means that you need to tell tinybind how to subscribe to those objects. This is where adapters come in to play. This feature is driven by the observer pattern.

Each adapter is defined to a unique interface (a single character) which is used to separate the keys in a keypath. The interfaces used in a keypath determine which adapter to use for each intermediary key.

user.address:city

The above keypath will use the . adapter to access the address key on the user object, and the : adapter to access the city key on the address object. If you can imagine for a second that address is just a normal property on the user object pointing to a Backbone model, but city is actually an attribute on that Backbone model, you can see how this kind of notation is actually very succint and expressive.

tinybind comes with a default . adapter based on custom property getter and setter

The built-in adapter

tinybind ships with a . adapter for subscribing to properties on plain JavaScript objects. The adapter is self-implemented using ES5 natives such as Object.defineProperty. In the future, this adapter will be implemented purely using Object.observe as soon as browser support permits.

If you need to support non-ES5 browsers (< IE 9), you can replace this adapter to use polyfills or with a third-party library that has the browser support you need. If you're only targetting Chrome Canary, feel free to replace it with an Object.observe adapter now and enter data binding bliss.

Creating an adapter

Adapters are defined on tinybind.adapters with the interface as the property name and the adapter object as the value. An adapter is just an object that responds to observe, unobserve, get and set.

The following : adapter works for Backbone.js models / Stapes.js modules.

tinybind.adapters[':'] = {
  observe: function(obj, keypath, callback) {
    obj.on('change:' + keypath, callback)
  },
  unobserve: function(obj, keypath, callback) {
    obj.off('change:' + keypath, callback)
  },
  get: function(obj, keypath) {
    return obj.get(keypath)
  },
  set: function(obj, keypath, value) {
    obj.set(keypath, value)
  }
}

Computed Properties

Computed properties are re-evaluated when one or more dependent properties change. Declaring computed properties in Tinybind.js is possible by using buitin watch formatter followed by its dependencies. The following text binding will get re-evaluated when either the event's start or end attribute changes.

<span rv-text="dateRange | watch start end"></span>

Iteration binding

Use the rv-each-[item] binder to have tinybind automatically loop over items in an array and append bound instances of that element. Within that element you can bind to the iterated item as well as any contexts that are available in the parent view.

<ul>
  <li rv-each-todo="list.todos">
    <input type="checkbox" rv-checked="todo.done">
    <span>{ todo.summary }</span>
  </li>
<ul>