Nicolas Petton

Nicolas Petton

Web developer, Lisper, Smalltalker & Emacs maniac.

Data binding with objectjs

Data binding with objectjs

Sept 6, 2016

Objectjs is a simple object model for JavaScript, you can read more about it here.

I've been thinking for a while about adding first-class properties to it to enable data binding in widgetjs.

The idea is not new: whenever a domain object property is updated, any widget rendering that property could be updated automatically. React has been doing this for a while, but I'll explain here a slightly different approach that we took.

The counter example revisited

Let's have a look at a simple counter widget with two buttons to increment and decrement a counter, as it is today in widgetjs.

    // The counter model
    var counter = objectjs.object.subclass(function(that, my) {
        my.initialize = function(spec) {
            my.super(spec);
            my.count = 0;
        };

        my.get("count");

        that.increment = function() {
            my.count++;
        };

        that.decrement = function() {
            my.count--;
        };
    });

    // The counter widget
    var counterWidget = widgetjs.widget.subclass(function(that, my) {
        my.initialize = function(spec) {
            my.super(spec);
            my.counter = counter();
        };

        that.renderContentOn = function (html) {
            html.h1(my.counter.getCount());
            html.button('+').click(function() {
                my.counter.increment();
                that.update();
            });
            html.button('-').click(function() {
                my.counter.decrement();
                that.update();
            });
        };
    });

As you can see, each time the count value changes, the widget is explicitly re-rendered with that.update().

While this approach works fine, the rendering logic quickly becomes hard to maintain as the complexity of the UI grows, leading to complicated and coupled updates of widgets.

Properties in objectjs

The first thing to get data binding is a way to listen to domain attribute changes. That's easy enough with ECMAScript5 object properties:

object.extend(function(that, my) {
    my.property = function(propName, initialValue) {
        var value = initialValue;
        Object.defineProperty(my, propName, {
            configurable: true,
            enumerable: true,
            get: function() {
                emitPropertyAccess(propName);
                return value;
            },
            set: function(newValue) {
                value = newValue;
                emitPropertyChange(propName, value);
            }
        });
    };
});

As you can see, we're defining a property by name, with a custom getter and setter that will trigger an event each time the property is accessed and updated. I'm leaving out the details about emitPropertyChange here, but you can have a look at the source.

With this in place, we can now listen for property changes on the widget side:

    // Register to property accesses for data binding
    objectjs.propertyEventEmitter.instance().onAccess(function(instance, propName) {
        var widget = currentWidget.get();
        if (widget) {
            instance.onPropertyChange(propName, widget.update);
        }
    });

The only fancy thing here being currentWidget: during the rendering phase, we keep track of the current widget being rendered. This way we know at any time during the rendering which widget should listen to property changes.

A better counter example

Now we can define properties and let the widgets update themselves whenever they change:

var counter = objectjs.object.subclass(function(that, my) {
    my.initialize = function(spec) {
        my.super(spec);
        my.property("count", 0);
    };

    my.get("count");

    that.increment = function() {
        my.count++;
    };

    that.decrement = function() {
        my.count--;
    };
});

var counterWidget = widgetjs.widget.subclass(function(that, my) {
    my.initialize = function(spec) {
        my.super(spec);
        my.counter = counter();
    };

    that.renderContentOn = function (html) {
        html.h1(my.counter.getCount());
        html.button('+').click(my.counter.increment);
        html.button('-').click(my.counter.decrement);
    };
});

Here's 2 counter widgets in action:

Benefits

The approach taken is a bit different from React, but I think it has its benefits:

  • We haven't setup anything to enable data-binding: simply using properties in rendering code enables it for us. We really don't have to think about it.
  • Even better, thanks to object properties, no matter how we change the value of my.count, being my.count = 10; or my.count++, the widget will be updated without having to think about keeping the UI in sync again.

Finally, there are many things that we could do with properties. With a bit of meta-description, we could use it to serialize/deserialize objects from/to server data, generate forms, tables and other UI elements, validate domain objects, and so on.

The code is not final, but have a look at the changes if you are interested!

comments powered by Disqus