jQuery Plugins / Bootstrap

Now that Webpack is handling layout.js, let's simplify it! Remove the self-executing function. And, of course, add const $ = require('jquery'):

8 lines public/assets/js/layout.js
'use strict';
const $ = require('jquery');
$(document).ready(function() {
$('[data-toggle="tooltip"]').tooltip();
});

Perfect, right? Well... we're in for a surprise! Go back to the main page and... refresh! Bah!

tooltip is not a function

Uh oh! The tooltip function comes from Bootstrap... and if you look in our base layout, yea! We are including jQuery and then Bootstrap:

110 lines templates/base.html.twig
<!DOCTYPE html>
<html lang="en">
... lines 3 - 19
<body>
... lines 21 - 98
{% block javascripts %}
<script src="https://code.jquery.com/jquery-3.1.1.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
... lines 102 - 105
{% endblock %}
</body>
</html>

Which should add this function to jQuery!

Trouble with jQuery Plugins

But be careful: this is where Webpack can get tricky! Internally, the Bootstrap JavaScript expects there to be a global jQuery variable that it can add the tooltip() function to. And there is a global jQuery variable! It's this jQuery that's included in the layout. So, Bootstrap adds .tooltip() to that jQuery object.

But, in layout.js, when we require('jquery'):

8 lines public/assets/js/layout.js
... lines 1 - 2
const $ = require('jquery');
... lines 4 - 8

This imports an entirely different jQuery object... and this one does not have the tooltip function!

To say this in a different way, if you look at just this file, we are not requiring bootstrap... so it should be no surprise that bootstrap hasn't been able to add its tooltip() function! What's the fix? Require Bootstrap!

Find your open terminal and run:

yarn add bootstrap@3 --dev

Bootstrap 4 just came out, but our app is built on Bootstrap 3. Now that it's installed, go back and add: require('bootstrap'):

9 lines public/assets/js/layout.js
... lines 1 - 2
const $ = require('jquery');
require('bootstrap');
... lines 5 - 9

And... that's it! Well, there is one strange thing... and it's really common for jQuery plugins: when you require bootstrap, it doesn't return anything. Nope, its whole job is to modify jQuery... not return something.

Now that it's fixed, go back and... refresh! What! The same error!!! This is where things get really interesting.

At this point, we're no longer using the global jQuery variable or Bootstrap JavaScript anywhere: all of our code now uses proper require statements. To celebrate, remove the two script tags from the base layout:

110 lines templates/base.html.twig
<!DOCTYPE html>
<html lang="en">
... lines 3 - 19
<body>
... lines 21 - 98
{% block javascripts %}
<script src="https://code.jquery.com/jquery-3.1.1.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
... lines 102 - 105
{% endblock %}
</body>
</html>

And now... refresh!

Fascinating!

jQuery is not defined

And it's coming from inside of Bootstrap!

Ah, ha! When we require bootstrap, internally in that file, it looks for a global variable called jQuery and then modifies it. But when you require jquery, it does not create a global variable: it just returns a value. And now that there is no global jQuery variable available, it fails! This is a really common situation for jQuery plugins... and there's a great fix. Actually, there are two ways to fix it... but only one good one.

The ugly fix is to say window.jQuery = $:

10 lines public/assets/js/layout.js
... lines 1 - 2
const $ = require('jquery');
window.jQuery = $;
require('bootstrap');
... lines 6 - 10

Try it! Go back and refresh! All better. Yep, we just made a global variable... so that when we require bootstrap, it uses it. But... come on! We're trying to remove global variables from our code - not re-add them!

9 lines public/assets/js/layout.js
... lines 1 - 2
const $ = require('jquery');
require('bootstrap');
... lines 5 - 9

So here's the better solution: go to webpack.config.js and add autoProvidejQuery():

21 lines webpack.config.js
... lines 1 - 2
Encore
... lines 4 - 14
// fixes modules that expect jQuery to be global
.autoProvidejQuery()
;
... lines 18 - 21

That's it. Find your terminal and restart Webpack:

yarn run encore dev --watch

And... refresh! Yes! It works! But... what the heck just happened? You've just experienced a crazy super power of Webpack. Thanks to autoProvidejQuery(), whenever Webpack finds a module that references an uninitialized global jQuery variable - yep, Webpack is smart enough to know this:

// node_modules/bootstrap/.../bootstrap.js

function ($) {
	// ...
} (jQuery)

It rewrites that code to require('jquery'):

// node_modules/bootstrap/.../bootstrap.js

function ($) {
	// ...
} (require('jquery'))

Yea... it basically rewrites the code so that it's written correctly! And so suddenly, Bootstrap requires the same jquery instance that we're using! This makes jQuery plugins work beautifully.

Tip

Not all jQuery plugins have this problem: some do behave properly out-fo-the-box.

Handling Legacy Template Code

Oh, but there's one other jQuery legacy situation I want to mention. If you're upgrading an existing app to Webpack, then you might not be able to move all of your JavaScript out of your templates at once. And that JavaScript probably needs jQuery. Here's my recommendation: remove jQuery from the base layout like we've already done. But then, in your layout.js file, require jquery and add: global.$ = $.

// ...
const $ = require('jquery');
global.$ = $;
require('bootstrap');
// ...

This global variable is special to Webpack - well... it's technically a Node thing, but that's not important. The point is, when you do this, it creates a global $ variable, which means that any JavaScript in your templates will be able to use it - as long as you make sure your code is included after your layout.js script tag.

Later, you should totally remove this when your code is refactored. But, it's a nice helper for upgrading.

Next, let's talk about how CSS fits into all of this!

Leave a comment!

  • 2018-04-27 Paul Hodel

    A nice one! https://bossanova.uk/jexcel and more bootstrap-like https://bossanova.uk/jexcel...

  • 2018-03-19 Cesar

    Got it. Thanks Ryan.

  • 2018-03-19 weaverryan

    Hey Cesar!

    GREAT question actually :). Like many things, it's just a trade-off. By using the normal link tag to the Bootstrap CDN, you have the advantages of using a really fast CDN and this file may already be cached by the user. On the con side, this is an extra web request to fetch this asset versus making the user download just ONE CSS file that contains everything they need. And yes, if you also use a CDN for your own assets, it helps *some* of this, but there is still some trade-off. I honestly don't know which would be faster, but are probably fast enough for most cases.

    Btw, Webpack actually has a feature to support using external CDN URLs (this is most relevant for JS files). You can read about "externals": https://webpack.js.org/conf.... It's basically a way for you to, for example, require('jquery'), but make Webpack smart enough NOT to package that file, because you will have a script tag already in your layout.

    Cheers!

  • 2018-03-15 Cesar

    I just finished the tutorial and my encore it's working great in my symfony app. However, I haver returned to the chapter 6 because I was wondering if keeping the CDN link for bootstrap it's better for a faster page speed. In theory, a CDN helps with that. I would like to know your opinion only to be sure. What can you recommend me?