Reactive Programming for JavaScript

Dan Tillberg

Follow along at http://tbonejs.org/preso

What is this thing?

TBone's original purpose:

Provide live templates on top of Backbone.

Because who doesn't need another live templating library?

What is this thing?

TBone's second purpose:

Use the dependency graph of models to determine which models are actually used in the UI, and to put the others to sleep. Out of 70 models in our app, only 10-20 typically are active on the page at any one time.

What is this thing?

TBone's third (and now primary) purpose:

A simple, general-purpose reactive programming platform as a platform for applications and as plumbing for other JS libraries.

Why is reactive programming so awesome?

Conquer async.

No async callbacks. Instead, just write everything idempotently and TBone will rerun functions, models, and views as necessary.

Why else is reactive programming so awesome?

The hardest UI bugs often look something like:

Live templating already empowers you to trace where each HTML element, attribute, and piece of text comes from in your data.

Reactive programming extends this to data.

It's similar to functional programming in this way, though with reactive programming you can see and inspect the intermediate data; it's more akin to message-passing.

Crash Preview

Add in a formatting step...

Now with a View

Or with nested Views

Three Tenets of TBone

Get: T(prop)


TBone evaluates each of these by splitting the string and doing a recursive lookup in a for loop.

In addition to returning the current value, TBone binds the currently-executing T-function (more on that in a couple slides...) to changes in that value.

Set: T(prop, value)

T('activeStep', 5);
T('user.name.first', 'Bob');
T('widgets.4', {
    price: '$3.99',
    num: 5

These change the value and fire change events accordingly.

TBone does what you might expect here:

T('widgets.4');     // → { price: '$3.99', num: 5 }
T('widgets.4.num'); // → 5
T('user.name');     // → { first: 'Bob' }

Run: T(fn)

T(function () {
    var num = T('numItems');
    var mass = T('itemMass');
    $('#num').text('Quantity: ' + num);
    $('#totalMass').text('Total mass: ' + (mass * num));

T-functions are the binding scope for TBone.

Instead of binding change events to callbacks, you wrap T-references in T-functions.

Whenever any of the values referenced in a T-function change, the function will be run again.

Run: Nested T(fn)

T(function () {
    var num = T('numItems');
    $('#num').text('Quantity: ' + num);
    T(function () {
        var mass = T('itemMass');
        $('#totalMass').text('Total mass: ' + (mass * num));

T-functions can be nested. TBone will re-run the outer scope here when 'numItems' changes, but only the inner scope when 'itemMass' changes.

Run: Nesting T(fn)

function updateNumberOfItems (num) {
    T(function () {
        var mass = T('itemMass');
        $('#totalMass').text('Total mass: ' + (mass * num));
// ... and somewhere else completely different:
T(function () {
    var num = T('numItems');
    $('#num').text('Quantity: ' + num);

T-functions are nested dynamically, not lexically.

This allows locally binding references without the caller needing to know when the callee should be re-executed.

First example, revisited:

The T-function at the top runs every time T('now') changes.

And every 100ms, we update T('now') to a new Date object.

Live data providers

This code sets up a live data source for the current date with a precision of about 100ms:

setInterval(function () {
    T('now', new Date());
}, 100);

Anyone else can subscribe to it:

T(function () { console.log(T('now') && T('now').getTime()); });
T(function () { $('title').text(T('now')); });
T(function () { $('body').text('It is ' + T('now')); });

Screen dimension data provider

We can set up a data source for the current screen dimensions:

function update () {
    T('screen.width', $(window).width());
    T('screen.height', $(window).height());
$(window).bind('resize', update);
function timer () {
    setTimeout(timer, 1000);

This updates both whenever the window resize event triggers, and also every second.

We use this in our app to make some Views "responsive." Just add a reference to T('screen') and the View will re-render when the browser is resized.

Set w/fn: T(prop, fn)

TBone models don't store functions as values. If you set a TBone property to a function, instead that property will be live-bound to the return value of the function.

T('screen.totalPixels', function () {
    return T('screen.width') * T('screen.height');

T('formattedNow', function () {
    var now = T('now');
    return now ? (_.pad(now.getHours(), 2, '0') + ':' +
                  _.pad(now.getMinutes(), 2, '0') + ':' +
                  _.pad(now.getSeconds(), 2, '0')) : '';

Set w/model: T(prop, model)

These are equivalent:

T('screen.totalPixels', function () {
    return T('screen.width') * T('screen.height');

T('screen.totalPixels', tbone.models.bound.make({
    state: function () {
        return T('screen.width') * T('screen.height');

Setting to a function creates a 'bound' TBone model on the fly and assigns that model to the property.

When you get a property, e.g. T('screen.totalPixels'), TBone will recurse into the child model and return its value instead of returning the model itself.

T is a Model

And you can make more of them!

var myModel = tbone.models.base.make();
myModel(function () {
    var count = myModel('user.count');
    var price = T('row.price');
    $('#total').text('$' + (count * price));

If you don't like T, you can use tbone.

Or make your own and call it whatever you want.

Backbone Support

TBone was originally built on top of Backbone, and it still supports nesting Backbone models inside of TBone models.

> var user = new Backbone.Model();
> T('user', user);
> T('user.name', 'Bob');
> user.get('name')
→ "Bob"

Divergence from Backbone

TBone now has its own implementation of Views and Models, and it runs with or without Backbone.

The biggest difference is that TBone supports setting a model to any value, not just a single-level hashmap.

> var model = tbone.models.base.make();
> // '' means "root of model":
> model('', [{ name: "Sally" }, { name: "Susan" }]);
> model('1.name')
→ "Susan"
> model('', 42);
> model('');
→ 42

Location Model

This model can read & write the location.hash property:

tbone.models.location = tbone.models.base.extend({
    initialize: function () {
        var self = this;
        $(window).bind('hashchange', function () {
            self('hash', location.hash);
        self('hash', location.hash);
        self(function () {
            if (location.hash !== self('hash')) {
                location.hash = self('hash');

Try it out! This presentation creates an instance at T('location').

Ajax models

Ajax sleepy-time


Back to basics... Live templates & views.

TBone Views combine template rendering and a view "ready" function (kind of a "DOM-fragment-ready" callback) into a T-function scope.

    The <%-noun%> is .


tbone.createView('myTmpl', {
    ready: function () {

View & Template Example

Template auto-instrumenting magic

TBone includes a Regex-based partial JS parser that will auto-instrument live TBone references in templates passed to it as a string.

> tbone.addTemplate('myTmpl', '<%- template %> text <%= here %>')
> // force template init:
> tbone.render($('<div tbone="tmpl myTmpl">'))
> tbone.templates.myTmpl.parsed
→ '<%-view.denullText( view.query(2, "template") )%> text <%=view.denullText( view.query(2, "here") )%>'

You can opt out of this by passing a template function instead of a string.



Unit tests pass in IE7+, Chrome, Firefox, Opera, and Safari. Unit tests cover the reactive programming stuff well, and the view stuff a little less so. There may be some rough edges in IEs, particularly with the view/template stuff.




This presentation uses TBone

You can see this presentation online at tbonejs.org/preso.

Source at github.com/appneta/tbonejs.org.



Slides at tbonejs.org/preso




Dan Tillberg