BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles The Datum Data Binding Library

The Datum Data Binding Library

Bookmarks

Key Takeaways

  • Most data binding libraries have coalesced around the same solution of defining new HTML elements in JavaScript, but there is an alternative approach.
  • Web frameworks have made it easier to write web applications, but they have also constrained the way we do so.
  • The interface between JavaScript and HTML is small and can be used to simplify webapps.
  • Datum is a data binding library designed to simplify application development by taking advantage of this interface.
  • In programming testability is paramount and Datum makes it a priority.

In recent years there has been an explosion of frameworks and libraries for making single page applications for the web. Angular, React, Vue and quite a few others have made it easier to develop on the web and helped fuel its growth as webapps have become more dynamic and interactive. However there may still be room for other libraries and new ideas about how to create web applications so without wishing to exacerbate the n-plus-one problem I will describe a simple data binding library I have authored with the hope of elucidating some of the ideas behind it and demonstrating a different approach to that which most developers are currently taking.

The two components of a web page built with Datum.js are the template and the view model. The template is the HTML between the <body> </body> tags before Datum.js has modified it. The developer is responsible for putting the template in the body of the DOM. Datum.js is agnostic about how you do this.

The view model is a single JavaScript object that contains all of the data that will get displayed on the web page and all of the logic for interacting with the web page. The programmer tells Datum.js what object this is by passing it to a constructor called BindingRoot.

var viewModel = {};

new BindingRoot(viewModel);

Declaring the binding root can be done only once, presumably as soon as the page loads.

The view model has two kinds of properties, data properties and bindings. The data properties contain the data that define the state of the view model. The bindings are special objects that define how that data gets displayed on the page. You can create a binding using the Binding constructor.

var viewModel = {

    myDatum: "Hello world.",
    myBinding: new Binding()
};

new BindingRoot(viewModel);

Each binding property has at least one corresponding binding attribute on the template. This attribute tells Datum.js which DOM element to bind each binding to. A binding attribute is always called data-bind and its value is the name of the binding property in the view model.

<body>
  <div data-bind="myBinding"></div>
</body>

The binding myBinding above will have no effect on the page. To make something happen you need to construct the binding with a callback. There are a few different callback options. The text callback gets used to put text on the page.

var viewModel = {

    myDatum: "Hello world.",
    myBinding: new Text(function() { return this.myDatum; })
};

new BindingRoot(viewModel);

The text callback sets the text content of the elements to which it is bound. The callback can contain any logic provided it returns the value which gets displayed. If any of the data properties used in the body of the callback changes, the text callback will get reevaluated and the text on the page gets updated. In the example above you can change the text on the page by assigning it to the myDatum property.

viewModel.myDatum = "Eh up planet.";

The value callback provides a two-way binding between an <input /> element and a data property. It is a single callback that gets called when either a change event gets triggered on the input element or one of the data dependencies of the callback changes. In the first case the value of the input element gets passed as the first parameter to the callback. In the second case undefined gets passed. The second parameter is always the element to which it gets bound.

<body>
  <input type="text" data-bind="myBinding" />
</body>
var viewModel = {

    myDatum: "Hello world.",
    myBinding: new Value(function(value) {

        if (value) {

            this.myDatum = value;
        }

        return this.myDatum;
    })
};

new BindingRoot(viewModel);

A standard two-way binding will typically have the above form, but functionality triggered by input changes such as data validation can also get called here.

These are the basics of using Datum.js and they are intentionally straightforward. Most of the motivation of Datum’s design is simply to make what we are already doing simpler, so starting somewhere simple is important. One thing you will not find in Datum.js is a mechanism for creating new HTML elements that can be referenced from within the template. Most popular data binding libraries have taken as their main abstraction mechanism the ability to define new HTML tags whose behaviour is defined locally in JavaScript. Such user-defined elements can have domain-specific behaviour such as displaying custom controls and arranging page content like text and images.

By putting JavaScript inside HTML tags it is hoped that HTML can become dynamic enough to satisfy the needs of modern web development but the problem with writing dynamic web pages with something like HTML is that HTML is not a true programming language. Web developers are having to grapple with the limits of their abilities to treat it like one. Even if you try to think of elements as constructors and properties as fields (don’t try, it doesn’t work) it falls down when you realise that you couldn’t possibly set one element as the property of another. The opportunities for abstraction are extremely limited. Modern programming languages on the other hand have extensive mechanisms for abstraction, objects, lambda expressions and good software development makes full use of them.

The alternative to thinking of webapps as HTML documents from which we hang the necessary JavaScript code to make them interactive is instead to think of them as JavaScript programs from which we hang the necessary markup to make them visible. In this view HTML gets relegated to what it has always done well; displaying text and other UI elements using that now extensive range of elements that are natively supported by browsers. The structure of the application gets represented by a nested JavaScript object called the view model. This view model will mirror the hierarchy of the DOM but be simpler with less nesting since the logical structure of an application is simpler than the DOM structure.

The advantage of doing things this way is that the full range of abstractions that are available to programming languages can now get performed with UI components. The bread and butter of object-oriented programming are techniques like polymorphism and dependency inversion, but it has never been possible to apply them to the visible elements of an application.

Datum bears a resemblance to earlier data binding libraries but it is carefully designed for writing data structures that display themselves in a browser. When the data structure gets updated then the page should update with few restrictions on how and when that gets done. Beyond that Datum does its best to get out of the way. It is avowedly not a framework, it is simple enough that I could write it myself, though complicated enough that it took me a while.

One reason that frameworks have proliferated is the abstractions available to them are not general enough to apply more widely. While frameworks try to wrap up the web’s technologies into one package for ease of development, the nature of some frameworks, and the way they create abstractions, dictates that once you take one of their APIs you end up doing everything their way. If you write an app in Angular you can take advantage of many ready made UI components but your app will end up looking like other Angular apps. You can’t break out of the angular world without a complete rewrite and components written for Angular can’t be applied elsewhere.

Efforts by some more recent frameworks and tools which leverage web components such as Stencil, Svelte, and modern Dojo attempt to avoid these pitfalls but a different abstraction could be more effective. If you could separate specific behaviours of UI components from how they look then libraries could provide those behaviours without bringing a large framework with them. You can’t write an HTML element that captures the abstract concept of sliding or fading page transitions but in JavaScript such abstractions are possible. This would take us back towards what the web used to be, a collection of decoupled tools, not heavyweight frameworks, a place where you can choose freely from the best tools available rather than being locked in to one way of doing things. Datum is not alone in this goal, but here we’ll focus on Datum.

I should explain that the tools necessary to make JavaScript objects visible on a web page are not at all complicated. If you enumerate the ways that JavaScript can interact with a web page you will find that there are relatively few. You can put text in an element; push and pull from input values; handle clicks; hide and show elements and change their CSS classes. Most of what Datum does is provide properties that perform those functions. Those properties can get added to plain JavaScript objects to make them visible and allow users to interact with them and the few cases that aren’t covered by the available properties can get added using Datum’s extension mechanism.

The click callback is called whenever a click event is raised on an element to which it gets bound. Again the element gets passed as the first parameter.

<body>
  <button id="button-id" data-bind="myBinding"></button>
</body>
var viewModel = {
    myDatum: "The text on the button",
    myBinding: new Binding({

        text: function() { return this.myDatum; },
        click: function(element) {

            alert("You just clicked the button with id " + element.id + ".");
        }
    })
};

new BindingRoot(viewModel);

The visible callback can get used to hide and show elements. If the callback returns false the element will have the style display: none; applied to it. If the callback returns true the display style will get set to the value it had when the element was first bound.

The view model will typically be a nested data structure with sub-objects bound to parts of the DOM. A sub-object bound to an element becomes the view model for the part of the DOM made up of that element and its sub-elements. Just like its parent view model, the sub-view model can have data properties, binding properties and its own sub-objects. Objects get bound to elements in the same way as bindings by putting a data-bind attribute on the element whose value is the name of the object property.

<body>
  <div data-bind="mySubobject">
    <span data-bind="text"></span>
  </div>
</body>
var viewModel = {

    mySubobject: {

        text: new Text(function() { return "Hello world."; })
    }
};

new BindingRoot(viewModel);

One significant advantage of carefully defining the interface between JavaScript and HTML is testability. Testing frontend code has always had some challenges and it can be particularly tricky with frameworks that create new HTML components. Some tools exist such as Enzyme for React and Dojo’s testing harness, but the default approach to testing UI components in frameworks is non-trivial. When you want to test the logic in a component you may need to render the element that contains the code and its child elements. You also need to programatically interact with the HTML in order to execute and verify your test.

As every object-oriented programmer knows, clear interfaces are the solution to good testing. Once you have defined a simple interface between JavaScript and HTML and you are confident that interface will always behave in consistent, predictable ways then the need to actually render your HTML (or some equivalent data structure) in order to test it disappears. You can just test what’s on the JavaScript side of the interface with confidence in how the HTML will ultimately behave. Of course such tests may not be able to verify exactly how a component will look on the page but then it has never been possible to unit test what a real eye will actually see. What is possible is to test all of the code you have written right up to the boundary of where it interacts with the DOM and no further.

In order to simulate user interaction Datum.js provides test handles that get called programmatically. To retrieve a test handle simply call a binding as a function.

var binding = new Click(function() { alert("clicked!"); });

var testHandle = binding();

The test handle gives you access to the callbacks you used to construct the binding.

testHandle.click();

A typical test might look like the following:

var viewModel = {

    label: "click me",
    button: new Binding({

        text: function() { return this.label; },
        click: function() { this.label = "clicked!"; }
    })
};

var testHandle = viewModel.button();

testHandle.click();

var buttonLabel = testHandle.text();

assertEqual(buttonLabel, "clicked!");

Since using Datum.js allows you to completely separate layout and logic, the entire functionality of an application can get tested in this way without ever binding the view model to a template.

As a web developer I will gladly say that the web is a wonderful place to work. No platform offers as much freedom and flexibility to so many programmers with varying levels of ability. But the web has moved a long way from relatively simple and decoupled set of tools that it used to be. Programming is all about finding the right abstractions. Good ones make code both simpler and more expressive but I feel like we've been missing an important one. JavaScript and HTML perform fundamentally different tasks in a web application so mixing them together makes little sense. By defining the relatively simple interface between the two we can separate the page layout from the logic of an application thus simplifying both. This is a powerful abstraction, one whose consequences take some time and thought to explore but I have found it is well worth the effort.

About the Author

Martin Rixham works in IT consulting, applying years of experience in web development and various JVM languages. He divides his time between London and Bangalore where he has the privilege of being part of two great IT communities.

Rate this Article

Adoption
Style

BT