Re-implementing jQuery methods in the HTMLElement prototype
Jochem Stoel

Jochem Stoel @jochemstoel

About: Pellentesque nec neque ex. Aliquam at quam vitae lacus convallis pulvinar. Mauris vitae ullamcorper lacus. Cras nisi dui, faucibus non dolor quis, volutpat euismod massa. Donec et pulvinar erat.

Location:
Inner Earth
Joined:
Sep 30, 2017

Re-implementing jQuery methods in the HTMLElement prototype

Publish Date: Dec 6 '18
122 22

It is almost 2019 and people have generally come to the agreement that jQuery is deprecated. I for one have a very different perspective on that but that is for some other time. So why do people still use it? Old habits? Convenience? It turns out that the majority of people that use jQuery only use it for a select few methods.

I thought it'd be fun and educational for n00b programmers to re-implement those jQuery methods onto the HTMLElement prototype.

QuerySelector

Lets first define a shortcut for the document querySelectors. $ is for a single element and $$ is for all matches. We want to be able to provide a second context argument like jQuery that defaults to the whole document. I am assuming ES6+ where default values in function declarations are supported.

/**
 * $ for document.querySelector
 * $$ for document.querySelectorall
 * with optional context just like jQuery (defaults to document)
 */
window.$ = (query, ctx = document) => ctx.querySelector(query)
window.$$ = (query, ctx = document) => ctx.querySelectorAll(query)
Enter fullscreen mode Exit fullscreen mode
$('h2') // will return single _<h2>_ element
$$('h2') // will return array with all _<h2>_ elements
Enter fullscreen mode Exit fullscreen mode

Using the context argument we can select all <p> elements within another element (<article></article>)

$$('p', $('article'))
Enter fullscreen mode Exit fullscreen mode

NodeList iteration

You have to admit, the jQuery.prototype.each is pretty neat too. Add a property each to the NodeList prototype and set the value to a function that casts the NodeList to an Array and then iterates it using the callback function provided.

/**
 * This allows you to "forEach" a NodeList returned by querySelectorAll or $$
 * similar to jQuery.prototype.each
 * use: $$('li').each(callback)
 */
Object.defineProperty(NodeList.prototype, 'each', {
    value: function (fn) {
        return Array.from(this).forEach((node, index) => fn(node, index))
    }
})
Enter fullscreen mode Exit fullscreen mode

Attributes

Another common jQuery method is attr. We can use attr to read and write attributes of elements in the DOM using a single method. We will add one tiny feature to our method. It returns all attributes when no arguments are provided.
With attr also comes removeAttr (remove attribute) and has to determine an argument exists.

/** 
 * single method to get/set/list attributes of HTMLElement. 
 * get argument id:     $('div').attr('id')
 * set argument id:     $('div').attr('id', 'post123')
 * list all arguments:  $('div').attr()  // Fuck yeah
 */
HTMLElement.prototype.attr = function (key, value) {
    if (!value) {
        if (!key) {
            return this.attributes
        }
        return this.getAttribute(key)
    }
    this.setAttribute(key, value)
    return this
}

/**
 * remove attribute from HTMLElement by key
 */
HTMLElement.prototype.removeAttr = function (key) {
    this.removeAttribute(key)
    return this
}

/**
 * check whether a DOM node has a certain attribute.
 */
HTMLElement.prototype.has = function(attribute) {
    return this.hasAttribute(attribute)
}
Enter fullscreen mode Exit fullscreen mode

innerText and innerHTML

/** 
 * single function to get and set innerHTML
 * get:  $('body').html()
 * set:  $('body').html('<h1>hi!</h1>')
 */
HTMLElement.prototype.html = function (string) {
    if (!string)
        return this.innerHTML
    this.innerHTML = string
    return this
}

/** 
 * single function to get and set innerText
 * get:  $('body').text()
 * set:  $('body').text('hi!')
 */
HTMLElement.prototype.text = function (string) {
    if (!string)
        return this.textContent
    this.innerText = string
    return this
}
Enter fullscreen mode Exit fullscreen mode

Append and prepend

The following append method allows you to insert a HTML element at the end of the specified target element. The prepend method will insert it just before.

/**
 * append HTMLElement to another HTMLElement
 * like jQuery append()
 */
HTMLElement.prototype.append = function (child) {
    if (child instanceof HTMLElement) {
        this.appendChild(child)
        return this
    }
    this.append(child)
    return this
}

/**
 * prepend HTMLElement to another HTMLElement
 * like jQuery prepend()
 */
HTMLElement.prototype.prepend = function (sibling) {
    if (sibling instanceof HTMLElement) {
        this.parentNode.insertBefore(sibling, this)
        return this
    }
    this.parentNode.insertBefore(sibling, this)
    return this
}
Enter fullscreen mode Exit fullscreen mode

Removing elements

Removing an element in JavaScript happens by accessing its parent node to call removeChild(). Yeah weird I know.

HTMLElement.prototype.remove = function() {
    this.parentNode.removeChild(this)
}
Enter fullscreen mode Exit fullscreen mode

As you probably know, you can not use arrow functions in jQuery. However

$('#foo').remove()
// or
$$('div').each(element => element.remove()) 
Enter fullscreen mode Exit fullscreen mode

Parent

Get the parent of a node.

/** 
 * get a HTMLElement's parent node
 * use: $('h1').parent()
 */
HTMLElement.prototype.parent = function () {
    return this.parentNode
}
Enter fullscreen mode Exit fullscreen mode

Events

Modern JavaScript libraries implement on, off and emit to get, set and dispatch events.

/**
 * add event listener to HTMLElement
 * $(document).on('click', event => ...)
 */
HTMLElement.prototype.on = function (event, callback, options) {
    this.addEventListener(event, callback, options)
    return this
}

/**
 * remove event listener from HTMLElement
 * $(document).off('click', callback)
 */
HTMLElement.prototype.off = function (event, callback, options) {
    this.removeEventListener(event, callback, options)
    return this
}

/**
 * dispatch an event on HTMLElement without needing to instanciate an Event object.
 * $(document).emit('change', { foo: 'bar' })
 */
HTMLElement.prototype.emit = function (event, args = null) {
    this.dispatchEvent(event, new CustomEvent(event, {detail: args}))
    return this
}
Enter fullscreen mode Exit fullscreen mode

DataSet

And last but not least a nice method to access data attributes.

/**
 * single method to get/set/list HTMLElement dataset values
 * get:  $('div').data('color')     assuming <div data-color="..."></div>
 * set:  $('div').data('color', '#0099ff')
 */
HTMLElement.prototype.data = function (key, value) {
    if (!value) {
        if (!key) {
            return this.dataset
        }
        return this.dataset[key]
    }
    this.dataset[key] = value
    return this
}
Enter fullscreen mode Exit fullscreen mode

Define

This is unrelated to jQuery but still a nice shortcut.

/**
 * Convenient shortcut 
 * use:   define('property', { ...descriptor })
 */
Object.defineProperty(window, 'define', {
    value: (property, ...meta) => meta.length == 2 ? Object.defineProperty(meta[0], property, meta[1]) : Object.defineProperty(window, property, meta[0]),
    writable: false,
    enumerable: true
})
Enter fullscreen mode Exit fullscreen mode

Now we can do for instance this:

/** 
 * now | single statement accessor that returns current time
 * @returns {number} 
 */
define('now', {
    get: Date.now
})
Enter fullscreen mode Exit fullscreen mode

The identifier now will return the current time. You don't have to call it as a function, simply accessing it will do.

setInterval(() => console.log(now), 10)
/*
1543930325785
1543930325795
1543930325805
1543930325815
1543930325825
1543930325835
*/
Enter fullscreen mode Exit fullscreen mode

Gist

For your convenience, a gist with all that is above.
https://gist.github.com/jochemstoel/856d5b2735c53559372eb7b32c44e9a6

Comments 22 total

  • Niko Heikkilä
    Niko HeikkiläDec 6, 2018

    These are very nice! Have you also tried implementing Ajax calls and stuff without jQuery? For a long time those were a big reason for sticking with jQuery but then Fetch API came.

    • Jochem Stoel
      Jochem StoelDec 6, 2018

      No I did not, for the reason you mentioned. We have fetch nowadays. Would you like me to, though?

      • Casey Cole
        Casey ColeDec 6, 2018

        Yes that would be a cool post

  • Klemen Slavič
    Klemen SlavičDec 6, 2018

    Nice write-up of extending the base prototypes!

    I do have a bit of a hang-up with modifying prototypes of builtins without using Object.defineProperty, though. If any application is inspecting the prototype, those properties are enumerated, so it might be better to define them as non-enumerable on the prototype object, just to be on the safe side. A factory function that would augment a built in by taking a property name and a function would make the examples just as readable. :)

    I love articles like this that make it clear just how thin an abstraction layer you can have on top of the standard web API.

    • Basti Ortiz
      Basti OrtizDec 6, 2018

      It also gives you an idea on what Babel does under the hood.

  • Mihail Malo
    Mihail MaloDec 6, 2018

    A word of warning:
    ParentNode.append() and ParentNode.prepend(), which have bazonkers browser support (everything but IE), behave like Node.appendChild()(But can take strings, and multiple nodes) and not like $().append().
    The sibling addition behavior, like the $().append() is implemented as ChildNode.after() and ChildNode.before(). It has the same affordances as the ParentNode.append() but is less supported (No Safari, no mobile Edge)

  • Meghan (she/her)
    Meghan (she/her)Dec 6, 2018

    Some notes:

    • $$('p', $('article')) returns all <p> elements in the first <article>, not all <p>s that are children of <article>s. For that you'd still have to do $$("article > p")
    • HTMLElement.prototype.has = HTMLElement.prototype.hasAttribute doesn't bind the function properly, so this will not work properly
    • your .text() function should use .textContent not .innerText
    • HTMLElement.append should still check to see if child is an instance of Node, even if it's not an HTMLElement
    • Same with HTMLElement.prototype.prepend
    • Also HTMLElement.prototype.prepend, already exists
    • HTMLElement.prototype.remove already exists too
    • HTMLElement.prototype.parent should return .parentElement
    • HTMLElement.prototype.emit should have args=null as a parameter and use new CustomEvent(event, { detail:args })
    • Jonas
      JonasDec 7, 2018

      HTMLElement.prototype.append and NodeList.prototype.forEach already exist, too.

    • Jochem Stoel
      Jochem StoelDec 7, 2018

      Thanks I fixed the things you said in updated gist. Why does text need to return textContent and not innerText?

  • Fernando Sávio
    Fernando SávioDec 7, 2018

    I think you should test if string is undefined in html() and text(), otherwise it wouldn't be possible to set it empty.

    PS: I am commenting from my phone, so forgive the lack of details :)

    • Jochem Stoel
      Jochem StoelDec 7, 2018

      If HTML and/or TEXT is empty you want to return the innerHTML and innerText so no.

      • Fernando Sávio
        Fernando SávioDec 8, 2018

        How would you simulate jQuery $('#el').html(''); ?

        • Jochem Stoel
          Jochem StoelDec 8, 2018

          You can literally use my example already provided in the post:

          window.$ = (query, ctx = document) => ctx.querySelector(query)
          window.$$ = (query, ctx = document) => ctx.querySelectorAll(query)
          
          HTMLElement.prototype.html = function (string) {
              if (!string)
                  return this.innerHTML
              this.innerHTML = string
              return this
          }
          

          then

          $('#el').html('')
          
          • Fernando Sávio
            Fernando SávioDec 8, 2018

            That wouldn't work because '' would evalute to false and in your code it would be the same as $('#el').html().

            Check it running: codepen.io/fernandosavio/pen/qLWeWV

            • Jochem Stoel
              Jochem StoelDec 8, 2018

              Well whatta ya know. You are right.
              Change the line

              if (!string)
              

              into this

              if (typeof string == 'undefined')
              

              so that you get

              window.$ = (query, ctx = document) => ctx.querySelector(query)
              window.$$ = (query, ctx = document) => ctx.querySelectorAll(query)
              
              HTMLElement.prototype.html = function (string) {
                  if (typeof string == 'undefined')
                      return this.innerHTML
                  this.innerHTML = string
                  return this
              }
              
              $('#test').html('');
              

              codepen.io/jochemstoel/pen/OrJLbR

              You can also replace it with

              if(typeof string != 'string')
              

              or even

              if(string == undefined)
              
  • Gergely Polonkai
    Gergely PolonkaiDec 7, 2018

    I used jQuery exclusively for $.ajax() (yeah, i know. But i’m not a front-end dev) until i finally took some time and learn about XMLHttpRequest. And now there’s this Fetch thing…

  • Lajos Koszti
    Lajos KosztiDec 8, 2018

    elmenet.attr('min', 0) will return the attribute value.

    better check arguments.length

  • akosipau
    akosipauDec 8, 2018

    How can it be implemented in angular?

  • Daniel Lo Nigro
    Daniel Lo NigroDec 16, 2018

    Not sure if you've ever used it, but this is basically the approach Prototype.js took (and MooTools copied). The issue back in the Prototype.js era was that extending the native prototypes didn't work in IE6, so it had to also support a different approach (a wrapper object) in IE.

  • lorenz1989
    lorenz1989Dec 28, 2022

    Below is the suggestion from ChatGPT that I requested it.

    ChatGPT

Add comment