Introduction

Building reusable abstractions for browser-based frontends is a widely addressed, but only partially solved problem. Libraries such as jQuery UI provide developers already with a useful collection of commonly used UI elements. This frees developers from having to implement, for example, interactive date picker widgets and allows them to focus more on the actual business logic. However, such libraries come at a price: as soon as something is required, which is not provided by the library, things get messy.

The new requirement can be fulfilled by extending the library, which requires knowledge of the library's inner workings. The benefit of this approach is consistency. The newly developed widget ideally behaves like the rest of the widgets provided by the library.

Alternatively, the requirement can be fulfilled by using another library or creating a custom implementation. This approach can quickly lead to an inconsistent codebase but usually provides a faster way to a first implementation (based on the assumption that writing code is easier than reading it).

I propose a set of rules, which are not tied to any framework or library, with the aim of facilitating the creation of reusable UI components. The secondary goal of these rules is to do this without breaking progressive enhancement.

The Rules

  1. Only one piece of code is allowed to mutate a given subtree of the DOM. This is called owning a subtree.

    Rationale: the DOM is global state and accessible to every piece of code. As such, the usual principles for handling global variables should be applied to prevent the problems associated with global state.

  2. All meaningful state is stored in the DOM. Meaningful state is state that carries a meaning for the application, e.g. for a blogging application, state related to articles is most likely considered meaningful.

    Rationale: Putting all meaningful state into the DOM enables the server to generate the state, and the client to work with this state. It eliminates the need to serialize and deserialize data (e.g. using JSON or XML). Also, the user agent does not need any capabilities beyond parsing HTML.

  3. Pieces of code communicate through events, using the DOM as the event bus. Every public function should trigger events in order to notify other parts of the application about the function call. Public functions should be invokable by triggering the appropriate event on the element owned by this piece of code.

    Rationale: Triggering events for every function call on the DOM allows for easy delegation using event bubbling. By making every public function available through the DOM, the DOM becomes the single interface to interact with the application.

Example

This is an example application built adhering to the rules described above. It is a simple todo list application, allowing the creation and removal of todo items as well as marking them as "done". The count of items marked as "done" and the count of all items is display in the headline, next to the word "Todos".

Todos 0 / 0

Create a new todo item

Implementation

Note: The implementation of the example presented above follows progressive enhancement only partially: the creation of new todo items is possible without JavaScript (given a matching server backend), the modification and deletion of items is not.

The example application builds on jQuery for DOM manipulation and event handling. Components are defined using the function Component.create(definition), which returns a constructor for a new UI component based on definition. Here are parts of the definition of a single todo item:

{
  _namespace: "todo-list",
  _references: {
    "done": ".todo-item-done"
  },
  _events: {
    "change .todo-item-done": "toggle"
  },
  toggle: function(data, event) {
    if (this.$done.is(":checked")) {
      this.markDone();
    } else {
      this.markNotDone();
    }
  },
  markNotDone: function(data, event) {
    this.$root.removeClass("is-done").addClass("is-not-done");
  },
  markDone: function(data, event) {
    this.$root.addClass("is-done").removeClass("is-not-done");
  },
  remove: function(data, event) {
    this.$root.remove();
  }
}

Component.create treats some of these keys specially:

_namespace
This key specifies the namespace to use for binding events.
_references
A map of identifiers to CSS selectors. During the initialization of a component, this map is used for creating cached jQuery collections on the instance. A dollar sign is prefixed to the name to indicate this. This is how the variable this.$done is defined in the example definition.
_events
A map of (event, selector)-tuples to method names. This map is used for setting up event handlers during initialization. In the example, the toggle method is invoked when the change event is triggered by an element in the todo item matching .todo-item-done.

All other keys starting with a lowercase letter and mapping to a function are recognized as method definitions. The method definitions are wrapped to trigger events on the root DOM element of the component instance whenever the method is called: one before the method call (before-$METHOD_NAME) and one after the method call (after-$METHOD_NAME). Additionally, the methods can be invoked by triggering a do-$METHOD_NAME event on said DOM element.