Modular Widget Design Architecture

I’ve previously mentioned that I work on the Platform team within my current company, as the caretaker of our central UI widget repository. As part of an ongoing effort, we’ve been in talks with our offices in Washington, D.C. about how to share some of the development effort between our teams, and get the most out of the finite front-end engineering resources we have at our disposal. In our most recent round of collaboration, there was some terminology brought up that I really liked as a method of conceptualizing the building of widgets, and how they should relate to each other. It was put into the context of the building blocks of life itself: Elements, Compounds, Cells, and Organisms.

Elements

Just like chemistry, at its most basic level (without getting into subatomic particles), you can think of low-level widgets as being analogous to chemical elements. Each element-level widget should perform a single, specific task. This kind of thinking is at the heart of a design philosophy called Separation of Concerns, which in turn is at the heart of modular design in general.

What does it mean for each widget to perform a single task? Think about some kind of standard UI widget you may encounter out in the wild, for example, the auto-completing text box that Facebook uses to create a list of recipients for a new message you may be sending. You click into a text box, start typing, and the page shoots that partial text to the server to get a list of items matching that partial string. It takes the result set that’s returned, and creates a dropdown list of those items for you to select from. Upon selection, it renders the chosen item as a new visual token in the text box, and waits for you to continue typing any other names.

Breaking that item down into its element-level components, you might have 1 widget that handles the keypress listener on your text box. You could have a secondary element that controls the ajax functionality that manages server request and response. A third element might handle rendering the dropdown list from some data set. Finally, the fourth element might handle rendering the visual token of the selected item. Together, each of these element-level widgets can interact with each other to form a higher-level widget, which we think of as the auto-complete widget we actual use in the page.

The logic and behavior of an element-level widget should be fairly simple and concise. In the case of the text box with the keypress listener, it could be as simple as you instantiating the keypress reporter on the text box, with the reporter doing nothing more than binding a keypress event to the input in question, and when its callback is executed, it alerts an event of its own, telling any interested parties that its target has pressed a key. The ajax elemental widget might do nothing more than listen for certain events it’s interested in, e.g. a set of filters that might change the currently-rendered data set, and when it’s alerted that a filter has been updated, it simply contacts the server and gets an updated data set. It might then broadcast its own event that a data set has been updated, along with the data set itself. Other elemental widgets would function similarly. That is, they would take some form of input, run a process, and emit some form of output. It’s how these elemental widgets function as a unit that makes that special.

Compounds

Compound widgets are the first case that we should start seeing interoperability between our elemental widgets. An elemental widget on its own isn’t very helpful, it’s just a building block of something larger and more useful. Going back to our Facebook auto-complete example, We could bundle some of our elemental widgets into compounds by doing things like bundling our keypress listener to our ajax module, such that whenever a keypress event is emitted, it causes our ajax module to automatically request a new data set from the server, matching our new filter string.

Once these 2 are bound together, they function as a compound component, with one controlling the behavior of another through targeted event transmissions. Similarly, we could bind our dropdown list widget together with our token-rendering widget, such that when we select an item from the dropdown, it automatically causes a visual token to be created and inserted into our view.

Cells

Going back to our Facebook auto-complete example, the auto-complete widget itself would be considered a cell-level widget. It’s made up from each of these lower-level elemental and/or compound-level widgets, all transmitting data back and forth, in order to perform a workflow task.

Our keypress listener alerts our ajax module that the text has been updated. The ajax module issues a new transaction to the server to request a new data set matching the new text we’ve entered. The server responds with the matching data set, and the ajax module broadcasts that there’s new data available. Another widget listens for that event, takes the new data set, and renders a new dropdown list from it. When a user selects a name from it, it emits an event stating that a selection has been made, and some other process creates a new token out of the selected item, injecting it into our text box. At that point, we start over, and wait for for more user input.

Organisms

Scaling these concepts up to higher levels, we can consider cells to be groupings of compound- and element-level components that interact with each other. If we had realtime form validation running, then we might have some kind of event being broadcast by our cellular auto-complete widget, and picked up by a form validation widget, which may enable or disable our form’s submit button depending on whether we’d added a valid recipient to our list of people, or possibly removed the last remaining token from that list of recipients.

Organisms can be made up from single- or multi-cell widgets, compounds, and/or elements, controlling the workflow of a page, and communicating back and forth between each other in order to accomplish the specific task or tasks involved in a page. So how do we properly set up some kind of event bus to allow these widgets to all communicate with each other, without muddying up the page with global broadcasts, muddying up our code with hundreds of spaghetti strands of event listeners, and while maintaining loose coupling between our individual widgets?

Top-down Widget Dependencies

We always want to strive for modular independence between the various facets of our code. However, there are times when we have to specify dependencies between our modules. The best way to do this is with a top-down approach. Applying the analogies of elements, compounds, cells, and organisms to our component designs, think about something like ammonia (NH3). You can have both nitrogen and hydrogen without having ammonia, and without even having knowledge of the existence of a compound called ammonia. However, you cannot have ammonia without both of those elements. Similarly, your low-level widget components should exist completely independently of any higher-level compound, cell, and organism components that might include them, but those higher-level components cannot function without their lower-level dependencies.

In the case of the auto-complete widget, without having a keypress-listening widget, or an ajax module widget, or a dropdown list rendering widget, or a tokenizing widget, you could not have the auto-complete widget, at least in its existing implementation. Dependencies should always be specified from the higher level of abstraction down to the lower level of abstraction, and not vice versa. Here’s an example of how we could set up something like this:

var myWidgetPrototype = {
    _create: function () {
        this.parentWidgets = [];
    },

    _registerWidget: function (element, widgetType) {
        element[widgetType]();
        instance.parentWidgets.push(element.data(widgetType));
    },

    _trigger: function (type, event, data) {
        $.each(this.parentWidgets, function (index, widget) {
            widget.trigger(event || type, data);
        });
        this._superApply(arguments);
    }
};

Using something like this, we can set up an internal event distribution chain so that our widgets always communicate their events up that chain to their parent elements, without having to be internally aware of who or what those parent elements are. The parent elements can take that handoff and determine what needs to be done with the data. In the case of our auto-complete widget example, an autocomplete widget shell could be created to act as a traffic cop between all of the individual pieces of widgets that comprise its makeup, like so:

$.widget('ui.autocomplete', myWidgetPrototype, {
    _create: function () {
        this._super();

        this.registerWidget(this.element, 'keypress');
        this.registerWidget(this.element, 'ajax');
        this.registerWidget(this.element, 'dropdownselector');
        this.registerWidget(this.element, 'tokenizer');

        // Traffic cop event handling
        this.element.on('keypressupdate', $.proxy(this._notifyAjax, this));
        this.element.on('ajaxsuccess', $.proxy(this._notifyDropdown, this));
        this.element.on('dropdownselectorselected', $.proxy(this._notifyTokenizr, this));
        this.element.on('tokenizeradded tokenizrremoved', $.proxy(this._emitUpdate, this));
    },

    _notifyAjax (e, data) {
        this.element.ajax('updateData', data);
        this.element.ajax('reload');
    },

    _notifyDropdown (e, data) {
        this.element.dropdown('render', data);
    },

    _notifyTokenizr (e, data) {
        this.element.tokenizr('addToken', data);
    },

    _emitUpdate (e, data) {
        this._trigger('update', {
            data: this.element.tokenizer('getTokens')
        });
    }
});

Now all we have to do is let each of the lower-level widgets do what they do well, and at a higher level, route the data back and forth across the individual components as needed to perform the duties of this autocomplete widget. We can combine any number of lower-level widgets to perform any number of higher-level tasks and workflows, and all we have to do is define the business rules about how these components relate to each other for this specific type of task.

I hope this has been informative for you, and that you’ll start to consider that widgets should be cumulatively built on top of one another. It makes testing individual components easier, as well as reducing time to market on new widgets you develop, as they’ll mostly be made up of existing widgets combined in new ways. If you have any questions about anything I’ve put forth here, feel free to leave them in the comment section below.

Leave a Reply

Your email address will not be published. Required fields are marked *