A Better jQuery by Extending the DOM

Not very long ago creating your own custom HTML was considered A Bad Thing™. If you deviate from standards you’re just as bad as table-based layouts and the <blink> tag. Now we have Custom Elements, Angular.js and Ember.js and Polymer. It’s alright now because it’s a standard. But before it was a standard I was trying to hack support for custom elements in Simpli5 with the __proto__ element. IE didn’t support __proto__ so it came to a halt. Note: Polymer’s Custom Element polyfill relies on the __proto__ property, and IE11 supports it now, even though it is non-standard. Oh, and we can look forward to using Object.setPrototypeOf.

What about extending the DOM? jQuery makes things really nice, but it must wrap everything to accomplish this, adding memory and slowing down (not significantly, so this may be a lame argument) access. Why can’t we just extend the DOM to add all the useful functionality we need? Prototype.js does it. Here are a bunch of easy wins, along with suggestions for some of the more in-depth ones. This won’t work with really old browsers such as IE8.

How to Extend

Before we get started, here’s a quick note on how you ought to extend the DOM. Make sure you use Object.defineProperty or Object.defineProperties on the prototypes of the DOM elements. This allows you to define getters and setters if needed, but it you can also leave the enumerable setting at its default of false so that for-i-in loops don’t see your methods. Otherwise you can mess up other libraries. To match the other methods, set configurable and writable to true. I’ll be using Element.prototype.method = xxx in my examples for brevity, but know that it really means

Object.defineProperty(Element.prototype, 'method', {
  configurable: true,
  writable: true,
  value: xxx
})

Also, remember to always return this from your methods if you’re not returning a value. That way they can be chainable like jQuery.

Finding Elements

document.querySelector(selector) and document.querySelectorAll(selector) return an element or all elements respectively, using the CSS selector provided. You don’t need jQuery to do this anymore, and you don’t even need to extend anything to have this. These same methods exist on every element, searching their children for the selector provided. And if you want to shorten the call, set Document.prototype.find = Document.prototype.querySelectorAll and Element.prototype.find = Element.prototype.querySelectorAll.

You’ve already got the built-in element.nextElementSibling, but if you really want it to be shorter, you can make a getter on Element.prototype.next to return this.nextElementSibling.

We can make sure the built-in element.matches exists without prefix by using a pollyfill:

if (!Element.prototype.matches) {
  Element.prototype.matches = Element.prototype.webkitMatchesSelector ||
  Element.prototype.mozMatchesSelector ||
  Element.prototype.msMatchesSelector ||
  Element.prototype.oMatchesSelector
}

which allows an element.closest method to match jQuery’s method of the same name.

Events

Why not add Element.prototype.on = Element.prototype.addEventListener and Element.prototype.off = Element.prototype.removeEventListener (and the same to Document and Window) to shorten our event listening? Or to really shine we make the on method accept a selector and use event.target.matches to optionally call the listener, like jQuery, but built in.

You can add a trigger method if you don’t like dispatchEvent. It can take a string and make a MouseEvent, KeyboardEvent, CustomEvent, etc. for you.

Lists of Things

jQuery is nice because you can do one action to a whole list of elements at a time. What about extending NodeList? You know, that non-array you get back from calls like element.children, and document.querySelectorAll. We could add a bunch of methods to NodeList.prototype that have the same name as methods we add to Element.prototype except that the NodeList versions iterate over their elements and call the element method of the same name. This would allow element.children.on('click', onClick) or document.find('.tag').removeClass('tag').

Dealing with Nulls

One things jQuery does nicely is it handles nicely when the thing you’re looking for doesn’t exist. $(‘dummy-element’).on(‘event’, function(){}) won’t break, even though there is no dummy-element. When we’re getting back NodeLists it’s not a big deal because iterating over 0 elements won’t cause any errors. But you may want to create a dummy element class to return from calls to next() or prev() which only return one element, so that you can continue to chain without breaking. I’ll let you figure out how this could best be done, but one idea is you might think of returning an empty NodeList from document.createElement('div').children or something.

More Complex Settings

Creating elements from strings of HTML is one thing that takes a little more doing. Fortunately, jQuery has already done the work for us and we can just add the method to Document.prototype.create or something. If you don’t want to go looking through the jQuery source, Krasimir has already done the work for you.

Another heavier item is setting a bunch of CSS properties at once, taking into account browser prefixes. Here is one solution that I’ve come up with which extends the style property of your elements.

// Allows setting a lot of CSS styles at once, doing translations for vendor-
// prefixed styles.
// E.g. element.style.set({ color: red, transform: 'rotate(7deg)' })
var prefixes = ['webkit', 'moz', 'ms', 'o']
Object.defineProperty(CSSStyleDeclaration.prototype, 'set', {
  configurable: true,
  value: function(properties) {
    var style = this
    Object.keys(properties).forEach(function(property) {
      var value = properties[property]
      if (typeof style[property] == 'string') {
        style[property] = value
      } else {
        var capitalized = property.charAt(0).toUpperCase() + property.slice(1)
        for (var i = 0; i < prefixes.length; i++) {
          property = prefixes[i] + capitalized
          if (typeof style[property] == 'string') {
            style[property] = value
            break
          }
        }
      }
    })
  }
})

The Rest

Now that you’ve got the hang of it, you can see how you might easily add append(), prepend(), addClass(), and any number of things that you need from jQuery. Even if you only use the library for pet projects, it will be a good exercise in learning the DOM methods and build your understanding of how jQuery works internally as well.

Extending the DOM is a lot of fun and it can be quite enjoyable to work with native objects everywhere, sprinkled with your own helper methods. I don’t know if something like this will ever become mainstream, but you shouldn’t have any issues doing it with your own applications.

Because of potential conflicts (the main reason for not going this route), it is best to avoid extending the DOM in libraries that you want to load on other websites. Imagine if Facebook used a library like this in their Facebook Like button, and Google did the same in their Google Plus button. One of them would likely break the other, and they are so often on the same page that it would always be broken.

MyStickies Uses it Minimally

I use element prototype extension for MyStickies, but only to pollyfill missing functionality such as element.remove, element.matches, and element.classList. This way I won’t conflict with any other libraries (the MyStickies script is loaded on every page you browse when you install the extension), but I can still rely on the useful functionality that is standardized, but not implemented yet in every browser I support.

What say you? Is extending the DOM the epitome of all evil? Or would you do it for your own applications?