Skip to main content

Removing jQuery from your Drupal theme

In a previous article Using ES6 in your Drupal Components, we discussed writing our javascript using the modern ES6 methods and transpiling down for older browsers. It still used jQuery as an interim step to make the process of refactoring existing components a little easier. But let's go all the way now and pull out jQuery, leaving only modern, vanilla javascript.

by Rikki Bochow /

Why should we do this?

jQuery was first introduced 12 years ago, with the intention of making javascript easier to write. It had support for older browsers baked into it and improved the developer experience a great deal. It also adds 87KB to a page.

Today, modern vanilla javascript looks so much like jQuery! It’s support in the evergreen browsers is great and it’s so much nicer to write than it was 12 years ago. There are still some things that jQuery wins on but in the world of javascript frameworks, understanding the foundation on which they are built makes learning them so much easier.

And those older browsers? We don’t need jQuery for that either. You can support older browsers with a couple of polyfills. The polyfills I needed for the examples in this post only amounted to a 2KB file.

Drupal 8 and jQuery

One of the selling points of Drupal 8 (for us front-enders at least) was that jQuery would be optional for a theme. You choose to add it as a dependency. A lot of work has gone into rewriting core JS to remove the reliance on jQuery. There are still some sections of core that need work - Ajax related stuff is a big one. But even if you have a complex site which uses features that add jQuery in, it's still only going to be on the pages that need it. Plus we can help! Create issues and write patches for core or contrib modules that have a dependency on jQuery. 

So what does replacing jQuery look like?

In the Using ES6 blog post I had the following example for my header component.

// @file header.es6.js

const headerDefaults = {
  breakpoint: 700,
  toggleClass: 'header__toggle',
  toggleClassActive: 'is-active'
};

function header(options) {
  (($, this) => {
    const opts = $.extend({}, headerDefaults, options);
    return $(this).each((i, obj) => {
      const $header = $(obj);
      // do stuff with $header
    });
  })(jQuery, this);
}

export { header as myHeader }

and..

// @file header.drupal.es6.js

import { myHeader } from './header.es6';

(($, { behaviors }, { my_theme }) => {
behaviors.header = {
  attach(context) {
    myHeader.call($('.header', context), {
      breakpoint: my_theme.header.breakpoint
    });
  }
};
})(jQuery, Drupal, drupalSettings);

So let’s pull out the jQuery…

// @file header.es6.js

const headerDefaults = {
 breakpoint: 700,
 toggleClass: 'header__toggle',
 toggleClassActive: 'is-active'
};

function header(options) {
   const opts = Object.assign({}, headerDefaults, options);
   const header = this;
   // do stuff with header.
}

export { header as myHeader }

and...

// @file header.drupal.es6.js

import { myHeader } from './header.es6';

(({ behaviors }, { my_theme }) => {
 behaviors.header = {
   attach(context) {
     context.querySelectorAll('.header').forEach((obj) => {
       myHeader.call(obj, {
         breakpoint: my_theme.header.breakpoint,
       });
     });
   }
 };
})(Drupal, drupalSettings);

We’ve replaced $.extend with Object.assign for our default/overridable options. We use context.querySelectorAll('.header'') instead of $('.header', context) to find all instances of .header. We’ve also moved the .each((i, obj) => {}) to the .drupal file as .forEach((obj) => {}) to simplify our called function. Overall not very different at all!

We could go further and convert our functions to Classes, but if you're just getting started with ES6 there's nothing wrong with taking baby steps! Classes are just fancy functions, so upgrading to them in the future would be a great way to learn how they work.

Some other common things;

  • .querySelectorAll() works the same as .find()
  • .querySelector() is the same as .find().first()
  • .setAttribute(‘name’, ‘value’) replaces .attr(‘name’, ‘value’)
  • .getAttribute(‘name’) replaces .attr(‘name’)
  • .classList.add() and .classList.remove() replace .addClass() and .removeClass()
  • .addEventListener('click', (e) => {}) replaces .on('click', (e) => {})
  • .parentNode replaces .parent()
  • .children replaces .children()

You can also still use .focus(), .closest(), .remove(), .append() and .prepend(). Check out You Don't Need jQuery, it's a great resource, or just google “$.thing as vanilla js”.

Everything I’ve mentioned here that’s linked to the MDN web docs required a polyfill for IE, which is available on their respective docs page.

If you’re refactoring existing JS it’s also a good time to make sure you have some Nightwatch JS tests written to make sure you’re not breaking anything :)

Polyfills and Babel

Babel is the JS transpiler we use and it can provide the polyfills itself (babel-polyfill), but due to the nature of our component library based approach, Babel would transpile the polyfills needed for each component into that components JS file. If you bundle everything into one file then obviously this won’t be an issue. But once we start having a couple of different components JS loaded on a page, all with similar polyfills in them you can imagine the amount of duplication and wasted KB.

I prefer to just put the polyfills I need into one file and load it separately. It means have full control over the quality of my polyfills (since not all polyfills are created equally). I can easily make sure I’m only polyfilling what I really need. I can easily pull them out when no longer needed, and I’m only loading that polyfill file to browsers that need it;

js/polyfill.min.js : { attributes: { nomodule: true, defer: true } }

This line is from my themes libraries.yml file, where I'm telling Drupal about the polyfill file. If I pass the nomodule attribute in browsers who DO support ES6 modules will ignore this file, but browsers like IE load it. We're also deferring the file so it's loading after everything else.

I should point out Babel is still needed. We can't polyfill everything (like Classes or Arrow functions) and we can't Transpile everything either. We need both, at least until IE stops requiring support.

Posted by Rikki Bochow
Front end Developer

Dated

Comments

Comment by gambry

Dated

Great article, as always!
Wondering if you still use Rollup.js as a bundler or along the way you found out a better tool?
(Or reverted to Webpack)
Thanks!
Gab

Comment by Rikki Bochow

Dated

Thanks Gab, yeah we still use Rollup.js for the most part. Some of the more app-like projects are using Webpack, though I'm curious to try out Parcel.js one day too.

Comment by Andrey

Dated

How to replace jquery.once when we're using vanilla js?

Comment by Rikki Bochow

Dated

The addEventLister() function has an option you can set to ensure it only runs once, though it also requires a polyfill. Check out this post (which also shows the alternative approach of using removeEventLister().

Pagination

Add new comment

Restricted HTML

  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <h2> <h3> <h4> <h5> <h6>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
Not sure where to start? Try typing "hello" or "help" if you get stuck.