Extending HTML with widgets

What is a widget?

A widget is a chunk of user interface functionality. For example, scroll bars, weather maps, text editor fields, videos, search boxes, and so on. Well-defined widgets make it easier to define and revise user interfaces.

How should a widget behave?

It must be easy instantiate a widget anywhere on a web page. It must be possible to create multiple independent instances of the same kind of widget without any special care. Widgets should be nestable and composable. The source-level definition of a collection of widgets should be similar to HTML: easy to write, easy to edit. Operationally, widgets on different parts of the page must not be tightly coupled. It should be easy for a program to dynamically change the contents of a page: add, remove and reconfigure widgets.

A good way to design widgets is as objects whose interaction with the rest of the world is mediated by their parent context. This allows a widget's parent to reconfigure it, choose what events trigger activity in the widget, and customize the effects of events signaled by the widget.

Parent context routes all events to and from widgets:
input
events
state and
behavior
output
events

input
events
state and
behavior
output
events

Example: DragTilesView

See drag-tiles.html. The example contains two instances of a DragTilesView widget. Within each widget, you can drag the mouse to move tiles around. If you look at the source code of drag-tiles.html, you will see that the widgets are defined by very simple HTML specifications. All of the code complexity is isolated in the DragTilesView package.

For example, the first instance of DragTilesView is defined simply as follows:

<span widget="DragTilesView" id="container">
	<div class="tile">hello</div>
	<div class="tile">world</div>
	<div class="tile">where</div>
	<div class="tile">are</div>
	<div class="tile">you</div>
	<div class="tile">going?</div>
</span>

After reading the code below, you can use Firebug to step through the actual code of Widget.js used by the DragTilesView example.

Definition of the Widget class

// This is the base class for all Widget objects.
Widget = _class_(Object, {
	// Called by subclass constructors
	_new_: function (element) {
		this.element = element;
		if (this.element == null) {
			// If no HTML element is passed in, create a
			// new one to host the widget.
			this.element = document.createElement('span');
		}
		element.widgetObject = this;
		this.$protected = { };
		this.callbacks = { };
		// The owner of the widget may register callbacks, which will be called when
		// the widget calls this.signal()
	},
	// Called by methods of a widget to signal the widget's owner
	signal: function (cbName) {
		// Lookup cbName in the callbacks object; if it's
		// found, call the corresponding callback function. 
		// Ignore exceptions in callbacks.
		try {
			this.callbacks[cbName].apply(null, Array.prototype.slice.call(arguments, 1));
		} catch (e) {
			// Ignore exceptions in callbacks.
			var e2 = e; // allow a breakpoint here...
		}
	}
});

Automatically instantiating widgets from an HTML specification

// Instantiate all widgets on the HTML page.
// Assumes each widget is enclosed in a <span>...</span> tag
// Accepts optional arguments to pass to all widget constructors.
createWidgets = function (args) {
	// Find all the spans (potential widget) in the HTML document
	var spans = document.getElementsByTagName('span');

	// Traverse the widgets backwards, so that nested widgets
	// get instantiated before their container.
	for (var i = spans.length-1; i >= 0; i--) {
		var widgetType = spans[i].getAttribute('widget');
		if (widgetType != null && spans[i].widgetObject == null) {
		// If this span defines a widget that hasn't yet
		// been instantiated... instantiate it!
			try {
				// Find the constructor for the widget and call it.
				var widgetConstructor = eval(widgetType);
				new widgetConstructor(spans[i], args);
			} catch (e) {
				// Ignore exceptions in widget constructors.
				var e2 = e; // allow a breakpoint here...
			}
		}
	}
};