Creating a Solid Foundation - UI Core Concepts - Developing Web Components (2015)

Developing Web Components (2015)

Part I. UI Core Concepts

A solid foundation is the key to constructing a building that will withstand the abuses of nature over the course of time. The same is true for software, but the foundation is knowledge—a firm understanding of design concepts and implementation patterns—not concrete. Without this foundation, an engineer will inevitably create unstable software that eventually crumbles. The downfall may be code smell due to a misunderstanding of a core concept, a poor API resulting from an insufficient design period, poor implementation due to not knowing a standard pattern, etc. The intent of Part I is to help prevent this downfall.

These opening chapters are designed to help you create a better foundation by covering commonly misunderstood (or inadequately understood) concepts that are core to positioning, dragging, and resizing elements—core features required of most UI components. In addition to these concepts, basic design considerations are discussed to help ensure consistency across components and prevent common design mistakes.

If you already have this knowledge, then great—skip to Part II! If you want a refresher, then just skim Part I. If you want to understand the base component class that will be used throughout the book, then just read the last section of Chapter 2 (“Example Widget”). Remember, this is your adventure!

NOTE

Throughout Parts I and II of this book, the term widget will be used when referring to different libraries and the dialog example. You might find yourself asking, “Why are we building a widget? I thought this was a web components book!” That’s a valid question. The answer is that this book is more than just a rehashing of the Web Components specification. It is designed to provide a better understanding of the DOM by exploring the details underlying today’s widget interfaces. Knowledge of these details is required to build moderately complex web components, and what better way to understand these details than through a concrete example?

Chapter 2. Creating a Solid Foundation

Jason Strimpel

Before learning about and developing web components, there are a few things that you should consider. Some of these considerations are not unique to web components, but rather are important aspects of software design and frontend engineering in general.

VOLTRON—THE COMPONENT BASE CLASS

In this chapter we will be constructing a base class widget, Voltron. This widget will be used to construct a dialog widget that will be iteratively built throughout this book as new concepts are introduced. The purpose of this base widget is to provide a standard that can be leveraged for the construction of a DOM library, irrespective of the interface—JavaScript widget, pure web component, Polymer, etc.

The Importance of a DOM Abstraction Layer

In theory, the DOM and its API provide a good interface for working with an HTML document. However, in practice it can be a very frustrating interface. This is especially true across browsers. In his book Learning JavaScript Design Patterns (O’Reilly), Addy Osmani describes how jQuery abstracts away the details of element selection, specifically selecting elements by class name. It accomplishes this by providing a convenient facade, its query selector API. Selecting an element by class name can be done in one of three ways, and the method selected is dependent upon what the browser running jQuery supports. jQuery hides these details and automatically selects the most efficient method. In addition to abstracting away these types of details, a good DOM library provides convenient APIs for common traversal and manipulation use cases.

TIP

The examples in this book will utilize jQuery, since it is known and used by a larger audience than any other DOM facade library.

Suppose you wanted to find the closest ancestor element with a given tag name and set the text color for that node to red. Using native APIs this could be accomplished as follows:

function colorize(el, tagName) {

// get el's parent el/node

var parentEl = el.parentNode;

// ensure tag name passed is uppercase

tagName = tagName.toUpperCase();

// loop through ancestors starting with

// the el's parent until the node tag name

// matches the tagName argument or 'HTML'

while (parentEl.tagName !== 'HTML') {

if (parentEl.tagName === tagName) {

parentEl.style.color = '#ff0000';

break;

}

parentEl = parentEl.parentNode;

}

}

// get the element with an id of 'descendant' and set

// closest ancestor div's color to red

colorize(document.getElementById('descendant'), 'DIV');

Using jQuery this can be accomplished in a much more concise manner, and any valid query selectors can be used to match the elements, not just the tag name:

// get the element with an id of 'descendant' and set

// closest ancestor div's color to red

$('#descendant').closest('div').css({ 'color': '#ff0000' });

You do not want to have to continuously write these types of abstractions and unearth all the cross-browser oddities yourself, unless you intend to create the next jQuery!

API Design and Widget Life Cycle

Before writing a single line of code, you should have a rough idea of what your API will look like and a basic understanding of how your widget life cycle will flow. Without a consistent API and a common life cycle, others will not be able to easily utilize your widget library, and eventually you will find it difficult to work with your own library if you step away from the code for more than a day or two.

NOTE

A widget life cycle consists of, but is not limited to, initialization, rendering, event bindings, and destruction. The jQuery UI and the widget factory are excellent examples of well-defined life cycle management.

A good example of the need for API and life cycle consistency is how a widget cleans up after itself. Anyone who has worked with jQuery plugins written by different authors knows just how frustrating this can be. Some have a “destroy” method. Others have a “remove” method. Some do not even account for cleanup. None of them seem to follow a common pattern or have the same behavior. This makes cleaning up from a parent widget or object much more difficult, because there is no common API to fall back on. The following code block is an example stub for a life cycle that will be used for widgets in this book:

// utility function for enabling and disabling element

function disable ($el, disabled) {

this.$el[disabled ? 'addClass' : 'removeClass']('disabled')

.find('input, textarea, select, button')

.prop('disabled', disabled);

}

// constructor function for creating an instance of a widget

function WidgetName (options) {

this.options = $.extend({}, this.defaults, options);

// initialize widget

this.init();

// return reference to instance

return this;

}

// options defaults

WidgetName.prototype.defaults = {

width: 200 // example option

};

// placeholder for widget initialization logic

WidgetName.prototype.init = function () {

this.$el = $(options.$el);

this.bind();

};

// placeholder for rendering logic

WidgetName.prototype.render = function () {

// appropriate events could be triggered here and in other methods, e.g.

this.$el.trigger('rendered', [this]);

return this;

};

// bind events using delegation and namespace

WidgetName.prototype.bind = function () {

this.$el.on('click.cmp-name', function (e) {});

return this;

};

// unbind events

WidgetName.prototype.unbind = function () {

this.$el.off('click.cmp-name');

return this;

};

// placeholder disabling a widget

WidgetName.prototype.disable = function () {

disable(this.$el, true);

return this;

};

// placeholder enabling a widget

WidgetName.prototype.enable = function () {

disable(this.$el, false);

return this;

};

// custom cleanup logic can be added here

WidgetName.prototype.destroy = function () {

this.unbind();

this.$el.remove();

};

Whatever names you select for your API methods, and however you decide the life cycle should flow, just be consistent! Most developers would much rather work with an API that is slightly less descriptive, but consistent, than with a self-describing API that behaves erratically.

Additionally, a good API should provide sensible defaults and hook points with useful function arguments so that developers can easily override methods to meet their needs without affecting the life cycle.

Lastly, not everything is a jQuery plugin! Your API design should not start with $.fn.*. You should have a solid API design and adopt a sensible inheritance pattern. Then, if it makes sense, you can turn your widget into a jQuery plugin to make it easier for others to consume.

The Inheritance Pattern

JavaScript uses prototypal inheritance. This often confuses engineers who are transitioning over from other languages because in prototypal inheritance objects inherit directly from other objects, as opposed to classical inheritance, where a new instance is created from a class that is essentially a blueprint for creating an object. Initially, most view JavaScript as inferior to statically typed languages that use classical inheritance. I am not going debate the finer points, but in my opinion JavaScript is more flexible because it allows engineers to implement inheritance patterns of their choosing. Personally, I am a fan of Backbone’s inheritance pattern, which was inspired by goog.inherits from Google Closure, because it abstracts away the mechanism of inheritance, allowing developers to focus on creating objects. Additionally, it implements constructs with which most developers are familiar: instance properties, static properties, and access to super properties.

The examples in this book will use a simple constructor pattern as to not detract from the main focus of the book, developing web components:

// base class constructor

function BaseWidget(options) {

// see previous code example on life cycles

// for an explaination of the line below

this.options = augment(this.defaults, options);

}

// extend base class

SubClassWidget.prototype = new BaseWidget();

SubClassWidget.prototype.constructor = SubClassWidget;

function SubClassWidget(options) {

BaseWidget.call(this, options);

}

Dependencies

When creating a web widget, you should always do your best to limit dependencies on other modules. I am not advocating against DRY or code reuse; I’m just saying that you should evaluate whether adding a library as a dependency is providing value that is worth its weight in terms of file size and maintenance, and the dependency burden it will place on consumers of your widget. The examples in this book will rely on jQuery only. Each example will pass dependencies into a closure. For instance:

(function (root, $) {

// widget awesomeness goes here

})(window, jQuery);

When creating any JavaScript module that will be used by a large audience, it is best to offer alternate formats for including your module. For instance, I always create an AMD version. If the code runs on both the client and the server, then I ensure it is possible to easily include the code via CommonJS, so it can be run in Node.js.

NOTE

The Asynchronous Module Definition (AMD) API specifies a mechanism for defining modules such that the module and its dependencies can be loaded asynchronously. This is particularly well suited for the browser environment, where synchronous loading of modules incurs performance, usability, debugging, and cross-domain access problems.

The following examples use Grunt (http://gruntjs.com) to build the files via Jarrod Overson’s grunt-preprocess plugin (http://bit.ly/dwc-grunt-preprocess):

// https://github.com/jstrimpel/hermes/blob/master/src/hermes.amd.js

define(function () {

'use strict';

// @include hermes.js

return hermes;

});

// https://github.com/jstrimpel/hermes/blob/master/src/hermes.window.js

(function (window) {

'use strict';

// @include hermes.js

window.hermes = window.hermes || hermes;

})(window);

Optimization

Donald Knuth once said, “Premature optimization is the root of all evil.” I have never known a statement to ring truer and as frequently in software engineering. I cannot count how many times I have seen prematurely optimizing code introduce bugs.

People frequently advocate caching jQuery objects, which is a practice I am not against in principle. But imagine you have a node that is destroyed because a widget component has completely replaced its inner HTML by redrawing itself via a template. You now have a reference to an orphaned node. This orphaned node cannot be garbage collected, and your application has a memory leak. It is a small leak, but if you are applying the same pattern across all widgets, then it can become a much larger problem rather quickly. This is especially true if your application goes for an extended period of time without doing a hard refresh.

Aside from the leak, any code that was utilizing this reference to perform DOM manipulations is now broken. The obvious solution is to understand the entire life cycle of your application, but this becomes impossible once an application reaches a certain size. Even if you could keep track of the entire application, you are likely using third-party libraries. While these libraries provide great benefits by encapsulating solutions to common problems, they hide the implementation details of how they are manipulating the DOM.

Your design, life cycles, and API should be your first considerations. In the case of Voltron, our UI base widget, the specific primary considerations were rendering, UI (un)binding, and disabling, so we created a generic interface that could be extended to support these concepts. Additional concerns, common to most software components, were construction, inheritance, and initialization, so we used a well-known inheritance pattern and provided an extendable hook point for initialization. This allows us to periodically run performance tests as we are implementing the details the underlie the API.

TIP

If you have a well-designed API, then you should be able to make optimizations on a case-by-case basis at a later date if significant performance bottlenecks are found.

The bottom line is to code sensibly by adhering to some best practices (like performing batch updates on the DOM), performance test as you implement your API, and then wait until a performance issue arises before further optimizing.

A Web Component Is Not JavaScript Alone

The first section of this book covers the JavaScript details of developing a web component, but web components are not solely comprised of JavaScript. If they were, then adopting a module format such as AMD or CommonJS, or simply using <script> tags, would make web components unnecessary. Web components attempt to solve a much larger problem, which is how to encapsulate HTML, CSS, and JavaScript, and make this encapsulation extensible without any impact on surrounding nodes in the DOM. For example, if a developer wants to create an image slider, it should be possible to do this in a declarative manner via a custom element in the document:

<!-- http://css-tricks.com/modular-future-web-components/ -->

<div class="img-slider">

<img src="./rock.jpg" alt="an interesting rock">

<img src="./grooves.jpg" alt="some neat grooves">

<img src="./arch.jpg" alt="a rock arch">

<img src="./sunset.jpg" alt="a dramatic sunset">

</div>

The resources that support this custom element should be easily included in an import and the implementation details should not be a concern for the author of the document.

The realization of this encapsulated, declarative approach will transform the Web from a quasi development platform to a bona fide development platform.

Example Widget

A dialog widget will be constructed throughout the course of the book as new concepts are introduced in each chapter. The end result will be a fully functioning modal dialog that can be instantiated, initialized with data, moved, resized, and shown/hidden on demand. This example will include all the necessary CSS, HTML, and JavaScript.

The Voltron Widget Base Class

Typically, I create a base class for any object that creates instances of itself. The base class contains common methods that can be easily overridden by the classes that extend it. It also allows for easily calling the base class’s prototype method.

NOTE

We use classic object-oriented programming (OOP) terms such as “base” and “super” throughout the book. This is not because classic OOP in better than prototypal inheritance; it is because these terms provide a frame of reference for explaining concepts.

The base class for the Voltron widget looks like this:

// calling a base class method

Super.prototype.doSomething = function () {

Base.prototype.doSomething.call(this);

// now do some other stuff that is unique to the Super class

};

(function (root, $) {

// https://github.com/jashkenas/backbone/blob/master/backbone.js#L1027

// cached regex to split keys for `delegate`

var delegateEventSplitter = /^(\S+)\s*(.*)$/;

// constructor; creates instance

function Voltron(options) {

this.init(options);

return this;

}

// default options

Voltron.prototype.defaults = {};

// events hash

Voltron.prototype.events = {};

// initialization code

Voltron.prototype.init = function (options) {

this.options = $.extend({}, this.defaults, options);

this.$el = $(options.$el);

this.bind();

return this;

};

// heavily based on Backbone.View.delegateEvents

// https://github.com/jashkenas/backbone/blob/master/backbone.js#L1088

// bind using event delegation

Voltron.prototype.bind = function () {

var events = this.options.events ? Voltron.result(this.options.events) :

null;

if (!events) {

return this;

}

// prevent double binding of events

this.unbind();

// iterate over events hash

for (var key in events) {

var method = events[key];

// if value is not a function then

// find corresponding instance method

if (!$.isFunction(method)) {

method = this[events[key]];

}

// if a method does not exist move

// to next item in the events hash

if (!method) {

continue;

}

// extract event name and selector from

// property

var match = key.match(delegateEventSplitter);

var eventName = match[1];

var selector = match[2];

// bind event callback to widget instance

method = $.proxy(method, this);

if (selector.length) {

this.$el.on(eventName, selector, method);

} else {

this.$el.on(eventName, method);

}

}

};

// used to unbind event handlers

Voltron.prototype.unbind = function () {

this.$el.off();

return this;

};

// destroy instance

Voltron.prototype.destroy = function () {

this.unbind();

this.$el.remove();

};

// static util method for returning a value

// of an unknown type - if value is a function then execute

// and return value of function

Voltron.result = function (val) {

return $.isFunction(val) ? val() : val;

};

window.Voltron = window.Voltron || Voltron;

})(window, jQuery);

Dialog Class

The dialog widget will extend the Voltron base class example from the previous section. The source code that follows is the stub for the dialog widget, which will be filled in progressively over the next two chapters:

(function () {

'use strict';

// set prototype to base widget and assign

// Dialog constructor to the constructor prototype

Dialog.prototype = new Voltron({});

Dialog.prototype.constructor = Dialog;

// constructor

function Dialog (options) {

Voltron.call(this, options);

return this;

}

// defaults, e.g., width and height

Dialog.prototype.defaults = {};

// event listeners; this is processed by Voltron.prototype.bind

Dialog.prototype.events = {};

// process template for injection into DOM

Dialog.prototype.render = function () {};

// makes dialog visible in the UI

Dialog.prototype.show = function () {};

// makes dialog invisible in the UI

Dialog.prototype.hide = function () {};

window.Dialog = window.Dialog || Dialog;

})(window, jQuery, Voltron);

Dialog CSS and HTML

The HTML and CSS that follows is intended to provide the basic structure for rendering the dialog widget:

<div role="dialog" aria-labelledby="title" aria-describedby="content">

<h2 id="title">Dialog Title</h2>

<p id="content">I am the dialog content.</p>

</div>

[role="dialog"] {

width: 400px;

height: 200px;

position: absolute;

}

[role="dialog"] h2 {

background: #ccc;

font-size: 16px;

font-weight: normal;

padding: 10px;

margin: 0;

}

[role="dialog"] p {

margin: 0;

padding: 10px;

font-size: 12px;

}

Summary

In this chapter we covered the productivity and normalization benefits of a DOM abstraction library and selected a well-known library, jQuery, as the library to be used throughout the course of the book. We then discussed how defining a common life cycle and base class can help make applications easier to develop, maintain, and debug because it helps define expected outcomes and behaviors. Next we explored how to include library dependencies and described a common inheritance pattern that will be used throughout the book. Finally, we created Voltron, the base class widget, and extended it to create the dialog widget, which will be iteratively built in the following chapters to turn concepts into a working component. You now have the necessary knowledge to begin your web components journey!