Live extensions

chemerisuk edited this page Oct 9, 2014 · 11 revisions

There are only two methods to deal with: DOM.extend and DOM.mock. The second method is used for testing. In other words to get your task done you need to understand how DOM.extend works.

DOM.extend

DOM.extend declares a live extension. It accepts a CSS selector as the first argument that defines what elements you want to capture.

General advice: try to make the selector simpler. 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 Element#matches method.

The second argument is a live extension definition. All properties of the object will be mixed with an element wrapper interface. Except private functions, that are removed after constructor ends.

Live extension for modal dialog

Let’s look at a simple example. Assuming we have an element like below on a web page:

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

The task is to show it as a modal dialog which should work for any existing and future content. 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);
    // append backdrop to the DOM
    DOM.find("body").append(backdrop);
    // we will define event handlers later
  },
  showModal: function(backdrop) {
    this.show();
    backdrop.show();
  }
});

The constructor property

Constructor is usually the place where you attach event handlers and perform DOM mutations where necessary.

General advice is do not rely on a moment of time when an extension is initialized. The actual initialization of a live extension varies across browsers.

Let’s update our .signin-form live extension to handle click on a close button and 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");
    }
  }
});

Public members and private functions

showModal above is a public method. You can access it in any (present or future) element that has the modal-dlg class:

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

DOM.find(".signin-btn").on("click", function() {
  signinForm.showModal(); // => shows the signin dialog
});

In a live extension definition any function that starts with on or do plus an upper case letter are called private function. Such function have a special behaviour: when the constructor is finished all such members are removed from the interface. Also, starting from better-dom 2 it preserves this always to be the current element.

In our case despite the live extension definition contains methods onClose and onKeyDown they won’t be mixed into the element wrapper interface after constructor ends:

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

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

Extending * elements

On some projects it’s useful to extend all element wrappers with a particular method(s). You can 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 special behavior: all extension declaration properties will be injected directly into the element wrapper prototype except the constructor which is totally ignored. So there is no performance penalty that is usually associated with the universal selector.

Obviously it doesn't make sense to put private functions there.

*Never pass more specific selectors like ".some-class " into DOM.extend because they do not have the behavior above and therefore they are slow.

Multiple live extensions on the same element

Sometimes it makes sense to split a large live extension into several pieces to reduce complexity. For instance you may have an element like below 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 a new content. The .chat extension shows tooltips when a user hovers over a userpic, adds smiles into messages etc.

But be accurate with multiple extensions: despite all event handlers are removed from interface you still might have public methods that intersect with each other.

See also

Writing tests for a live extension