Rivets.js

Rivets.js is a declarative data binding facility that plays well with existing frameworks such as Backbone.js, Spine.js and Stapes.js. It aims to be lightweight (2.3KB minified and gzipped), extensible, and configurable to work with any event-driven model.

Rivets.js keeps your UI and model data in sync for you, so you can spend less time manually tying into the DOM and more time on your actual data and application logic.

Out-of-the-box it’s got:

  • One-way and two-way binding to/from DOM elements.
  • Computed properties with dependent attributes.
  • Formatters to allow mutatating values through piping.
  • Iteration binding with full context support.
  • Easy debugging and manual syncing of bindings.

Describe your UI in plain HTML using data attributes:

<div id="auction">
  <h1 data-text="auction.title"></h1>
  <img data-src="image.url">

  <div class='info' data-show='auction.endingSoon'>
    <p>Hurry up! This auction is ending soon.</p>
  </div>

  <dl>
    <dt>Current Bid</dt>
    <dd data-text='auction.currentBid | money'></dd>
    <dt>Time left</dt>
    <dt data-text='auction.remaining | time'></dt>
  </dl>
</div>

Then tell Rivets.js what model(s) to bind to it:

rivets.bind($('#auction'), {auction: auction})

Configure

Use rivets.configure to configure Rivets.js for your app (or you can set configuration options manually on rivets.config).

Adapter

Rivets.js is model interface-agnostic, meaning it can work with any event-driven model by way of defining an adapter. This is the only required configuration as it’s what Rivet.js uses to observe and interact with your model objects. An adapter is just an object that responds to subscribe, unsubscribe, read and publish. Here is a sample configuration with an adapter for using Rivets.js with Backbone.js.

rivets.configure({
  adapter: {
    subscribe: function(obj, keypath, callback) {
      obj.on('change:' + keypath, callback)
    },
    unsubscribe: function(obj, keypath, callback) {
      obj.off('change:' + keypath, callback)
    },
    read: function(obj, keypath) {
      return obj.get(keypath)
    },
    publish: function(obj, keypath, value) {
      obj.set(keypath, value)
    }
  }
})

Prefix and data preloading

To prevent data attribute collision, you can set the prefix option to something like ‘rv’ or ‘bind’ so that data attributes are prefixed like data-rv-text.

rivets.configure({
  prefix: 'rv'
})

Set the preloadData option to false if you don’t want your bindings to be bootstrapped with the current model values on bind. This option is set to true by default.

rivets.configure({
  preloadData: false
})

Extend

Rivets.js is easily extended by adding your own custom binders and formatters. Rivets.js comes bundled with a few commonly used bindings, but users are encouraged to add their own that are specific to the needs of their application.

Binders

Binders are used to define different types of bindings – they contain instructions for a specific type of binding and can be defined as a single function for simple one-way bindings or as an object literal for more intricate bindings.

Let’s say we wanted a simple binder that sets the element’s color. Since this would be a one-way binding, we can take the standard single function approach. This function is reffered to as the binding routine, and is what runs when an observed attribute changes – it’s sole concern is to describe what happens to the element with the new attribute value.

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

With that binder defined, you can now utilize it in your views with the data-color binding.

<span data-color="model.color">COLOR</span>

Available bindings out-of-the-box:

  • data-text
  • data-html
  • data-value
  • data-show
  • data-hide
  • data-enabled
  • data-disabled
  • data-checked
  • data-unchecked
  • data-[attribute]
  • data-class-[class]
  • data-on-[event]
  • data-each-[item]

Formatters

Formatters are simple one-way functions that mutate the incoming 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.

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

Pipe bindings to formatters by using | as a delimiter.

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

Want to pass arguments to your formatter? Not a problem.

<span data-text="billing.cardNumber | maskMiddle 4 4 ********"></span>

(Note that all arguments are passed in a Strings, so you will need to perform any conversion to primitives if necessary.)

rivets.formatters.maskMiddle = function(value, beginLength, endLength, maskString) {
  if (beginLength == null) {
    beginLength = 4
  }
  if (endLength == null) {
    endLength = 4
  }
  if (maskString == null) {
    maskString = ' ... '
  }
  formatted = value.substring(0, parseInt(beginLength))
  formatted += maskString
  formatted += value.substring(value.length - parseInt(endLength))

  return formatted
}

Usage Notes

Rivets.View and Rivets.Binding

The rivets.bind function returns a bound Rivets.View instance that you should hold on to for later. You may want to unbind it’s listeners with view.unbind() and/or rebuild it’s bindings with view.build(). You can also access the individual Rivets.Binding instances inside the view through view.bindings — this is useful for debugging purposes or if you want to unbind or manually set the value for certain bindings.

Adapter Bypass

If your model object encapsulates it’s attributes (e.g. model.attributes for Backbone.js models) and your adapter conforms to that object specifically, you can still utilize properties defined outside of that object, such as functions or other static values defined on the object root.

Just use model:property instead of model.property inside your binding declaration and Rivets.js will bypass the adapter completely and access that property as it’s defined on the object root. This obviously won’t sync any changes, but that is by design in this case as these properties should be mostly static and used in conjunction with other “dynamic” properties.

Computed Properties

Computed properties are functions that get re-evaluated when one or more dependent properties change. Declaring computed properties in Rivets.js is simple, just separate the function from it’s dependencies with a <. The following data-text binding will get re-evaluated with event.duration() when either the event’s start or end attribute changes.

<span data-text="event:duration < .start .end"></span>

The prepended . is a shorthand syntax for specifying dependencies that are on the same object as the target, so that the above declaration is effectively the same as event:duration < event.start event.end.

Iteration Binding

Use the data-each-[item] binding to have Rivets.js 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 data-each-todo="list.todos">
    <input type="checkbox" data-checked="todo.done">
    <span data-text="todo.summary"></span>
  </li>
<ul>

If the array you’re binding to contains non-model objects (they don’t conform to your adapter), you can still iterate over them, just make sure to use the adapter bypass syntax — in doing so, the iteration binding will still update when the array changes, however the individual items will not since they’d be bypassing the adapter.subscribe.

<ul>
  <li data-each-link="item.links">
    <a data-href="link:url" data-text="link:title"></a>
  </li>
</ul>

Also note that you may bind to the iterated item directly on the parent element.

<ul>
  <li data-each-tag="item.tags" data-text="tag:name"></li>
</ul>