Introducing Live Extensions For Better-DOM: What They Are And How They Work

About The Author

Maksim is a freelance full-stack Web developer who lives in Minsk, Belarus. In his spare time, he likes to learn Web standards and to contribute to open source … More about Maksim ↬

Email Newsletter

Weekly tips on front-end & UX.
Trusted by 200,000+ folks.

After writing his latest article, “Writing A Better JavaScript Library For The DOM”, Maksim Chemerisuk realized that it’s important to understand what exactly live extensions are and how they work since the topic is extremely complex. In today’s article, he will answer most questions that were asked regarding “live extensions”.

After recently writing an article on “Writing A Better JavaScript Library For The DOM”, I realized that the topic is indeed a very complex one and that it’s important to understand what exactly live extensions are and how they work. In today’s article, I will answer most questions that were asked regarding “live extensions” and help you get going with this new concept.

The Responsibilities Of Live Extensions

Event handling is one of the key principles of working with the DOM. Events are the primary means of receiving feedback from user interaction.

Simple Event Binding

In this first example, documentation and tutorials that cover DOM events is what I call “simple event binding”. You attach a listener for the desired event on the DOM element in which you expect it to happen on.


link.addEventListener("click", function(e) {
  // do something when the link is clicked
}, false);

The first argument indicates the type of an event, the second argument is a listener, and the third argument defines an event phase (so-called “bubbling” or “capturing”). The reason why the last argument exists is because most DOM events traverse the DOM tree from document node to target node (capture phase) and back to the document node (bubble phase). This process is called “event flow” and brings several powerful features.

Live and Delegated Events

Instead of attaching a handler for each element in a group, we can attach one listener onto an ancestor shared by all of the elements in that specific group. Then, we can determine where an event took place using the target property of the event object, passed into the listener. This is known as “event delegation”:


list.addEventListener("click", function(e) {
  if (e.target.tagName === "LI") {
    // do something when a child <li> element is clicked
  }
}, false);

By having all event handlers on a particular parent, we can update the innerHTML property of this element without losing the ability to listen to events for new elements. The feature was called “Live Events” in jQuery, and it quickly became popular because of its ability to filter events by a CSS selector. Later, delegated events replaced them due to their flexibility by allowing to bind a listener to any element within the document tree.

But even event delegation does not overcome the following problems:

  • When DOM mutation is required after a new element (that matches a specific selector) comes into the document tree,
  • When an element should be initialized on a excessive event such as scroll or mousemove,
  • Or on non-bubbling events, e.g. load, error, etc.

This is what live Extensions aim to solve.

Live Extensions Use Cases

Take a look at the following diagram that explains the responsibilities:

live-extension-respo

1. DOM Mutations For Existing And Future Elements

Imagine you want to develop a reusable datepicker widget. In HTML5, there is a standards-based <input type="date"> element that could be used to create a polyfill. But the problem is that this element looks and behaves very different from browser to browser:

dateinputs
Date input element in different browsers.

The only way to make the element behave consistently is to set the type attribute value to “text”. This will cancel a legacy implementation and enable JavaScript to make your own. Try defining a live extension with the example below:


DOM.extend("input[type=date]", {
  constructor: function() {
    // cancel browser-specific implementation
    this.set("type", "text");
    // make your own styleable datepicker,
    // attach additional event handlers etc.
  }
});

2. Media Query Callbacks

I highly recommend reading Paul Hayes’ article on how to “Use CSS transitions to link Media Queries and JavaScript”.

"A common problem in responsive design is the linking of CSS3’s media queries and JavaScript. For instance on a larger screen we can restyle, but it might be useful to use JavaScript and pull in different content at the same time, e.g. higher quality images."

Paul was probably the first who started to use “hidden force” of CSS3 animation events to solve mutation-related problems. Live extensions are powered by the same trick, therefore you can use them to make DOM modifications depending on the current viewport:


DOM.extend(".rwd-menu", {
  constructor: function() {
    var viewportWidth = DOM.find("html").get("clientWidth");

    if (viewportWidth < 768) {
      // hide <ul> and construct Emmet abbreviation for a
      // <select> element that should be used on small screens
      this.hide().after("select[onchange='location=this.value']>" +
        this.children("li").reduce(function(memo, item) {
          var text = item.get("textContent"),
            href = item.find("a").get("href");

          memo.push("option[value=" + href + "]>live-extension-respo{" + text + "}");
          return memo;
        }, []).join("^"));
    }
  }
});

3. Element Media Queries

Back in 2011, Andy Hume implemented a script for applying styles depending on the dimensions of a particular element (not viewport, like for media queries). Later, this technique was named “element media queries”:

"Media queries work really well when you want to adjust the core layouts of the site, but they're less suited to changing styles at a smaller more granular level."

With the help of live extensions, it’s easy to implement element media queries support using the offset method:


DOM.extend(".signup-form", {
  constructor: function() {
    var currentWidth = this.offset().width;
    // add extra class depending on current width
    if (currentWidth < 150) {
      this.addClass("small-signup-form");
    } else if (currentWidth > 300) {
      this.addClass("wide-signup-form");
    }
  }
});

4. Efficiently Attach A Global Listener To Frequent Events


DOM.extend(".detectable", {
  constructor: function() {
    // mousemove bubbles but it’s usually a very bad
    // idea to listen to such event on a document level
    // but live extensions help to solve the issue
    this.on("mousemove", this.onMouseMove, ["pageX", "pageY"]);
  },
  onMouseMove: function(x, y) {
    // just output current coordinates into console
    console.log("mouse position: x=" + x + ", y=" + y);
  }
});

5. Listing Non-Bubbling Events On A Document Level


DOM.extend("img.safe-img", {
  constructor: function() {
    // error event doesn’t bubble so it’s not
    // possible to do the same using live events
    this.on("error", this.onError);
  },
  onError: function() {
    // show a predefined png if an image download fails
    this.src = "/img/download-failed.png"
  }
});

Brief Look Into History

The problems which live extensions aim to solve is not entirely new, of course. There are different approaches that address the above-mentioned issues. Let’s have a quick look at some of them.

HTML Components

Internet Explorer started supporting DHTML behaviors with IE 5.5:

"DHTML behaviors are components that encapsulate specific functionality or behavior on a page. When applied to a standard HTML element on a page, a behavior enhances that element's default behavior."

To attach behavior to future elements, Internet Explorer used an *.htc file with a special syntax. Here’s an example illustrating how we used to make :hover work on elements instead of <a>:


<PUBLIC:COMPONENT URN="urn:msdn-microsoft-com:workshop" >
  <PUBLIC:ATTACH EVENT="onmouseover" ONEVENT="Hilite()" />
  <PUBLIC:ATTACH EVENT="onmouseout"  ONEVENT="Restore()"  />
  <SCRIPT LANGUAGE="JScript">
  var normalColor, normalSpacing;

  function Hilite() {
    normalColor  = currentStyle.color;
    normalSpacing= currentStyle.letterSpacing;

    runtimeStyle.color  = "red";
    runtimeStyle.letterSpacing = 2;
  }

  function Restore() {
    runtimeStyle.color  = normalColor;
    runtimeStyle.letterSpacing = normalSpacing;
  }
</SCRIPT>
</PUBLIC:COMPONENT>

If you provided the above-mentioned code into the hilite.htc file, you could access it within CSS through the behavior property:


li {
  behavior: url(hilite.htc);
}

I was really surprised to discover that HTML components supported creating custom tags (starting from version 5.5), have single domain limitations and tons of other stuff that you probably have never used before. Despite Microsoft submitting a proposal to W3C, other browser vendors decided not to support this feature. As a result, HTML components were removed from Internet Explorer 10.

Decorators

In my previous article, I mentioned the Decorators which are a part of Web components. Here’s how you can implement the open/closed state indicator of the <details> element using decorators:


<decorator id="details-closed">
  <script>
    function clicked(event) {
      event.target.setAttribute('open', 'open');
    }
    [{selector: '#summary', type: 'click', handler: clicked}];
  </script>
  <template>
    <a id="summary">
      &blacktriangleright; <content select="summary"></content>
    </a>
  </template>
</decorator>

<decorator id="details-open">
  <script>
  function clicked(event) {
    event.target.removeAttribute('open');
  }
  [{selector: '#summary', type: 'click', handler: clicked}];
  </script>
  <template>
    <a id="summary">
      &blacktriangledown; <content select="summary"></content>
    </a>
    <content></content>
  </template>
</decorator>

Decorators are also applied using the special decorator property in CSS:


details {
  decorator: url(#details-closed);
}

details[open] {
  decorator: url(#details-open);
}

You’ll quickly notice that this is very close to what Microsoft proposed in HTML Components. The difference is that instead of separate HTC files, decorators are HTML elements that can be defined within the same document. The example above is only provided to show that the Web platform is working on these topics, since decorators aren’t properly specified just yet.

Live Extensions API

While designing APIs for live extensions, I decided to follow the following rules:

  1. Live extensions should be declared in JavaScript. I strongly believe that everything that somehow changes the behavior of an element should be presented in a JavaScript file. (Note that better-dom inserts a new CSS rule behind the scenes, but this includes only implementation details).
  2. APIs should be simple to use. No tricky file formats or new HTML elements: only a small amount of knowledge related to the constructor and event handlers is required to start developing a live extension (thus, the barrier to entry should be low).

As a result, there are only two methods to deal with: DOM.extend and DOM.mock.

DOM.extend

DOM.extend declares a live extension. It accepts a CSS selector as the first argument which defines what elements you want to capture. General advice: try to make the selector simple.

Ideally, you should only use a tag name, class or attribute with or without a value or their combinations with each other. These selectors can be tested quicker without calling an expensive matchesSelector method.

The second argument is a live extension definition. All properties of the object will be mixed with an element wrapper interface except constructor and event handlers.

Let’s look at a simple example. Let’s assume we have such an element on a Web page:


<div class="signin-form modal-dlg">...</div>

The task is to show it as a modal dialog. This is how the live extension could look like:


DOM.extend(".modal-dlg", {
  constructor: function() {
    var backdrop = DOM.create("div.modal-dlg-backdrop");
    // using bind to store reference to backdrop internally
    this.showModal = this.showModal.bind(this, backdrop);
    // we will define event handlers later
  },
  showModal: function(backdrop) {
    this.show();
    backdrop.show();
  }
});

Now you can access the public method showModal in any (present or future) element that has the modal-dlg class (in our case this is the signin-form div):


var signinForm = DOM.find(".signin-form");

DOM.find(".signin-btn").on("click", function() {
  // the signin button doesn’t have the modal-dlg class
  // so it’s interface doesn’t contain the showModal method
  console.log(this.showModal); // => undefined
  signinForm.showModal(); // => shows the signin dialog
});

Note: The better-dom-legacy.js file which is included conditionally for Internet Explorer versions 8 and 9, contains the es5-shim library so you can safely use standards-based EcmaScript 5 functions (such as Function.prototype.bind) in your code. I’ve been using the bind method heavily in my code to build testable methods easily.

The Constructor Property

The constructor function is called when an element becomes visible. This is because of the animationstart event that is used to implement DOM.extend. Browsers are clever so they don’t fire animation events for hidden elements. This lazy initialization saves resources sometimes, but be careful with accessing initially hidden elements.

In older Internet Explorers versions such as 8 and 9, contentready event from better-dom-legacy.htc is used to implement live extensions. Therefore, the constructor function executes immediately in these browsers — even for hidden elements.

Note: Keep in mind not to rely on time whenever an extension has been initialized. The actual initialization of a live extension varies across browsers!

Constructor is usually the place where you attach event handlers and perform DOM mutations where necessary. Once the function has been completed, all methods that begin with “on” (in better-dom 1.7 also “do”) followed by an uppercase letter, event handlers, will be removed from the element wrapper’s interface.

Let’s update our .signin-form live extension with the help of a close button and the ESC key:


DOM.extend(".modal-dlg", {
  constructor: function() {
    var backdrop = DOM.create("div.modal-dlg-backdrop"),
      closeBtn = this.find(".close-btn");

    this.showModal = this.showModal.bind(this, backdrop);
    // handle click on the close button and ESC key
    closeBtn.on("click", this.onClose.bind(this, backdrop));
    DOM.on("keydown", this.onKeyDown.bind(this, closeBtn), ["which"])
  },
  showModal: function(backdrop) {
    this.show();
    backdrop.show();
  },
  onClose: function(backdrop) {
    this.hide();
    frame.hide();
  },
  onKeyDown: function(closeBtn, which) {
    if (which === 27) {
      // close dialog by triggering click event
      closeBtn.fire("click");
    }
  }
});

Despite the fact that the live extension contains both onClose and onKeyDown methods, they won’t be mixed into the element wrapper interface:


var signinForm = DOM.find(".signin-form");

console.log(signinForm.onClose); // => undefined
console.log(signinForm.onKeyDown); // => undefined

This kind of behavior exists simply because you can have multiple live extensions for a single element that may overload public methods of each other and produce unexpected results. For event handlers, this is not possible; they exist only inside of the constructor function.

Extending * Elements

Sometimes it is useful to extend all of the element wrappers with a particular method (or methods). But then again, you can also use the universal selector to solve the problem:


DOM.extend("*", {
  gesture: function(type, handler) {
    // implement gestures support
  }
});
…
DOM.find("body").gesture("swipe", function() {
  // handle a swipe gesture on body
});

The * selector has a special behavior: all extension declaration properties will be injected directly into the element wrapper prototype except for the constructor which is totally ignored. Therefore, there is no performance penalty that is usually associated with the universal selector.

Note: Never pass more specific selectors such as .some-class * into DOM.extend because they are slow and do not have the same behavior as mentioned above.

Multiple Live Extensions on the Same Element

More often that not, it makes sense to split a large live extension into several pieces to reduce complexity. For instance, you may have such an element on your page:


<div class="infinite-scroll chat"></div>

There are two different extensions attached to it. The .infinite-scroll extension implements a well-known infinite scroll pattern, e.g. it’s responsible for loading new content. At the same time, the .chat extension shows tooltips whenever a user hovers over a userpic, adds smileys into messages, and so on. However, be accurate with multiple extensions: even though all event handlers may have been removed from the interface, you still may have public methods that intersect with each other.

Inheritance

Live extensions respect declaration order; you can use this to your advantage and develop your own component hierarchy. Late binding helps to declare overridable event handlers and method overloading allows to redefine a method implementation in a child extension:


DOM.extend(".my-widget", {
  constructor: function() {
    this.on("click", "_handleClick");
  },
  showMessage: function() { }
});

DOM.extend(".my-button", {
  _handleClick: function() {
    console.log("I am a button!");
  },
  showMessage: function() {
    alert("I am a button message!");
  }
});

If you take a closer look at the code above, you’ll notice that the .my-button extension does not attach a click listener. The registration is done with the help of late binding instead of a simple event handler in .my-widget. Late binding is a perfect choice here: even if a child does not implement _handleClick there won’t be any errors since the handler will be silently ignored.

While spreading functionality across multiple modules is possible, this is not recommended in everyday use. Double check if you really need to go in this direction, because it’s the most complex one.

Writing Tests with DOM.mock

One requirement for a high-quality widget is test coverage. New elements are captured by a live extension asynchronously, so it’s not that easy to simply make them in memory. To solve this problem, better-dom has the DOM.mock function:


var myButton = DOM.mock("button.my-button");

DOM.mock creates elements, just like DOM.create. Additionally, it synchronously applies the registered live extensions to the newly created elements. For even more convenience, all wrapper objects created by DOM.mock preserve event handlers (e.g. onClick), so you can test them.

From time to time, you may need to create a “fake” instance of an element. Use DOM.mock without arguments to make such an object:


console.log(DOM.mock().length); // => 0

A test for the modal dialog live extension introduced earlier could look like this (I use Jasmine):


describe(".modal-dlg", function() {
  var dlg, backdrop;

  beforeEach(function() {
    dlg = DOM.mock("div.modal-dlg");
    backdrop = DOM.mock();
  });

  it("should hide itself and backdrop on close", function() {
    var dlgSpy = spyOn(dlg, "hide"),
      backdropSpy = spyOn(backdrop, "hide");

    dlg.onClose(backdrop);
    expect(dlgSpy).toHaveBeenCalled();
    expect(backdropSpy).toHaveBeenCalled();
  });

  it("should show itself and backdrop on show", function() {
    var dlgSpy = spyOn(dlg, "show"),
      backdropSpy = spyOn(backdrop, "show");

    dlg.showModal(backdrop);
    expect(dlgSpy).toHaveBeenCalled();
    expect(backdropSpy).toHaveBeenCalled();
  });
});

Feature Detection (in better-dom 1.7)

There are some cases when filtering with a CSS selector is not flexible enough. For instance, let’s say you want to declare a live extension but only for browsers that support (or do not support) a particular feature. You may need to run tests in a headless browser like PhantomJS that support the feature natively. Starting with better-dom 1.7, DOM.extend supports the optional argument condition.

Assume we need to create a polyfill for the placeholder attribute. It doesn’t make sense to implement it for browsers that have built-in support. Below is an example of how the feature detection could look like:


var supportsPlaceholder = typeof DOM.create("input")
      .get("placeholder") === "string";

By using just a simple “If” statement as shown in the example below, we won’t have an ability to test the widget because PhantomJS supports the placeholder attribute and the live extension will never be declared.


if (!supportsPlaceholder) {
  DOM.extend("[placeholder]", {
    // implement placeholder support
  };
}

In order to solve this problem, you can use an extra condition argument in DOM.extend that might be Boolean or a function:


DOM.extend("[placeholder]", !supportsPlaceholder, {
  constructor: function() { … },
  onFocus: function() { … },
  onBlur: function() { … }
});

DOM.mock ignores the condition argument, so you can access all methods of the [placeholder] extension even if current browser passes the check:


var input = DOM.mock("input[placeholder=test]");

typeof input.onFocus; // => "function"

Conclusion

Live extensions — and better-dom as an implementation of the concept — are a good base to build upon whenever your target is uncertain, e.g. when creating a polyfill that may or may not be used on a particular site. Or regular widgets that may or may not be needed, depending upon some AJAX call.

Live extensions aim to separate declaration and the use of widgets. They bring loose coupling (or decoupling, rather) of any DOM-based component, and allow your code to become smaller, cleaner and easier to maintain. You can even combine such independent pieces with any existing framework within the market (or with the vanilla DOM, of course).

You may now be thinking, “But wait, there are projects like Polymer or x-tags, right?” Well, live extensions cover a different area; they are not about custom tags but rather about extending existing ones instead. I prefer a standards-based way (if possible) of creating UI widgets, so making polyfills is my choice.

Better-dom also has another advantage: a carefully crafted live extension does not force you to rewrite a website’s markup using different tags. All you need is to simply include a script file on your page. Standards-based elements can potentially work without JavaScript, so they degrade well when it’s disabled. And the library’s browser support allows you to start using live extensions straight away.

Feel free to share your thoughts in the comments section below or on the better-dom project home page.

Further Reading

Smashing Editorial (il, mrn)