Mike's corner of the web.

An experiment in reusable web widgets

Wednesday 31 July 2013 10:28

For the same reasons that breaking down programs into short, composable functions is a good idea, it seems like breaking down web code into short, composable web widgets would be a good idea. (By web widget, I mean the HTML, CSS and JavaScript that go together to implement a particular piece of functionality.) Having shorter snippets makes code easier to understand and change, with the potential for reuse.

Yet it feels like there's no good way of sharing the HTML, CSS and JavaScript that go together to implement a particular piece of functionality. For instance, the usual way of creating a web widget using JQuery is to create a JQuery plugin, but there's no natural way of using such a JQuery plugin from Knockout. Over the past few days, I've tried an experiment in creating web widgets that can be written and consumed independently of technology.

First of all, I've defined a widget as being a function that accepts a single options argument. That options argument must contain an element property, which is the element that will be transformed into the widget (for instance, we might turn an <input> element into a date picker). The options argument can also contain any number of other options for that widget. The interface is kept simple so it's easy to implement, while still being sufficiently general. It's not exactly something to write home about, but the value is in choosing a fixed interface.

Now that we've defined the notion of web widget, we'll want to start consuming and creating widgets. For instance, we can create a widget that will turn its message option to uppercase wrapped in <strong> tags:

function shoutingWidget(options) {
    var element = options.element;
    var contents = options.message.toUpperCase();
    // Assuming that we've defined htmlEscape elsewhere
    element.innerHTML = "<strong>" + htmlEscape(contents) + "</strong>";
}

We can use it like so:

shoutingWidget({
    element: document.getElementById("example"),
    message: "Hello!"
});

which will transform the following HTML:

<span id="example"></span>

into:

<span id="example"><strong>HELLO!</strong></span>

However, most of the time, I'm not writing web code using raw JavaScript. So, for any given web framework/library, we can start to answer two questions:

  • What's the easiest way we can consume a widget?
  • What's the easiest way we can create a widget?

In particular, when a widget is used, we shouldn't care about the underlying implementation. Whether it was created using jQuery or Knockout or something else, we should be able to use it with the same interface.

Let's see how this works with Knockout. To create a web widget, I call the function knockoutWidgets.widget() with an object with an init function, and I get back a widget (which is just a function). The init function is called with the options object each time the widget is rendered. The init function should return the view model and template for that widget. For instance, to implement the previous example using Knockout:

var shoutingWidget = knockoutWidgets.widget({
    init: function(options) {
        var contents = options.message.toUpperCase();
        return {
            viewModel: {contents: contents},
            template: '<strong data-bind="text: contents"></strong>'
        }
    }
});

To consume widgets from Knockout, we have to explicitly specify dependencies. By avoiding putting all widgets into a single namespace, we avoid collisions without using long, unwieldy names. For instance, to create an emphatic greeter widget that transforms:

<span id="example"></span>

into:

<span id="example">Hello <strong>BOB</strong>!</span>

we can write:

var emphaticGreeterWidget = knockoutWidgets.widget({
    init: function(options) {
        return {
            viewModel: {name: options.name},
            template: 'Hello <span data-bind="widget: \'shout\', widgetOptions: {message: name}"></span>!'
        }
    },
    dependencies: {
        shout: shoutingWidget
    }
});

emphaticGreeterWidget({
    element: document.getElementById("example"),
    name: "Bob"
});

Importantly, although we've created the widget using Knockout, any code that supports our general notion of a web widget should be able to use it. Similarly, emphaticGreeterWidget can use shoutingWidget regardless of whether it was written using Knockout, raw JavaScript, or something else altogether.

Although I've successfully used this style with Knockout for some small bits of work, there are still two rather major unsolved problems.

The first problem: how should data binding be handled? All the above examples have data flowing in one direction: into the widget. What if we want data to flow in both directions, such as a date picker widget?

The second problem: should content within widgets be allowed? Our shouting widget had the message passed in via the options argument, but it could have been specified in the body of the element that the widget was applied to. Using raw JavaScript, that means a definition that looks something like:

function shoutingWidget(options) {
    var element = options.element;
    var message = "message" in options ? options.message : element.textContent;
    var contents = message.toUpperCase();
    // Assuming that we've defined htmlEscape elsewhere
    element.innerHTML = "<strong>" + htmlEscape(contents) + "</strong>";
}

If we allow content within widgets, then we have to work out how the widget interacts with the content and the web library in use. For instance, if we're using Knockout, do we apply the bindings before or after the widget is executed? How should the widget detect changes when its children change as a result of those Knockout bindings?

Also notably absent from the examples was any mention of CSS, despite my earlier mention. The reason: it hasn't been needed in my small experiments so far, so I haven't thought that much about it! It's something that will need dealing with at some point though.

Thoughts on the overall idea or those specific problems are welcome! You can take a look at the code on GitHub.

Topics: HTML, JavaScript

Thoughts? Comments? Feel free to drop me an email at hello@zwobble.org. You can also find me on Twitter as @zwobble.