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'):

'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:

<!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'):

... 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'):

... 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:

<!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 = $:

... 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!

... 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():

... 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!

  • 2019-02-11 Diego Aguiar

    That's a good one. Lol

  • 2019-02-11 Martin

    What a pooper! :)

  • 2019-02-01 Kevin C

    Whoa weaverryan , yeah these plugins are a bit of a mess. Your suggestion was spot on! I also noticed that some internal jquery-ui widgets have issue with just a simple require('jquery-ui'). I had to do things like require('jquery-ui/ui/widgets/tooltip'), as an example, in order to get $(...).tooltip to work in existing code that I'm migrating.
    Thanks again for being there for us.

  • 2019-02-01 weaverryan

    Hey Kevin C!

    These jQuery plugins will be the death of me :). But at least I'm quite good at debugging them now. This one is broken, for a different reason. Here is the conversation and fix (add the resolve.alias for jquery): https://github.com/symfony/...

    I'm pretty sure that's your problem. If you'd like a bit more background about "why" let me know, but the short answer is that the library has "jquery" as a "dependency" in package.json, and that is incorrect (should be a peerDependency).

    Cheers!

  • 2019-01-31 Diego Aguiar

    Hmm, that's weird, probably is because of something internal of that plugin. Try doing the other option explained here: https://symfonycasts.com/sc...

  • 2019-01-31 Kevin C

    weaverryan , Thanks Ryan, for the quick response and it did fix the maskedinput issue.
    Since this is somewhat related and I hope it will help someone else too, would you give some insight into the jquery-validation plugin through yarn?

    Complaint is that $(...).validate is not a function

    I have tried two different ways to 'require' it:

    require('jquery-validation');

    and

    require('jquery-validation/dist/jquery.validate');
    require('jquery-validation/dist/additional-methods');

    Their package.json "main" key points to the dist/jquery.validate.js. Is this another broken plugin for some other reason? And what might be the work around?
    Thanks again!

  • 2019-01-31 Alexey

    Thank you.
    require('jquery.maskedinput/src/jquery.maskedinput.js');
    This solution for me works good.

  • 2019-01-31 Alexey

    Thank you!
    require('jquery.maskedinput/src/jquery.maskedinput.js');
    Works good!

  • 2019-01-31 Alexey

    Yes, but it not helped.

  • 2019-01-31 weaverryan

    Hey Kevin C!

    The short answer is that this package is broken, at least when using through yarn or npm. Here is the pull request to fix the issue: https://github.com/excellal...

    As we explain in this section (https://symfonycasts.com/sc..., when you require a file, the "main" key is read from the package.json of that library to know which file to include. That library's main key is just plain wrong.

    So, my guess is that you main run into other problems, but I would start with this:'


    const $ = require('jquery');
    require('jquery.maskedinput/src/jquery.maskedinput.js');

    Actually, looking into that file quickly, I think this WILL be all you need to do. Let me know! I hate these old, broken jQuery plugins :(

    Cheers!

  • 2019-01-30 Kevin C

    weaverryan ,
    Similarly, also having issues with the jquery.maskedinput. However, my issue is: Cannot read property 'definitions' of undefined at the line: $.mask.definitions['~'] = '[+-]';
    Clearly, "mask" is undefined here.

    Now, this is inside a Javascript Class. Relevant code:

    'use strict';

    const $ = require('jquery');
    require('jquery.maskedinput');

    class Enrollment {
    init(params){
    $.mask.definitions['~'] = '[+-]';
    }
    }
    module.exports = Enrollment;

    Uses above class as, which is in application.js and registered with webpack.conf.js

    const Enrollment = require('./Enrollment');
    var enrollment = new Enrollment();

    enrollment.init({});

    Then called via twig template extension:

    {{ encore_entry_script_tags('application') }}

    I don't seem to be having a jQuery issue, it's more about the maskedinput plugin not really attaching itself to $ as $.mask

    Thoughts on what to try?

  • 2019-01-28 Diego Aguiar

    Hey Alexey

    Have you tried enabling "autoProvidejQuery"?
    More info here: https://symfonycasts.com/sc...

    Cheers!

  • 2019-01-25 Alexey

    Hello!
    Thank you for the tutorial!

    I've some error when tried to using jquery.masked input plugin. (https://yarn.pm/jquery.mask...

    I did
    > yarn add jquery.maskedinput

    Then required it in app.js file

    const $ = require('jquery');
    require('jquery.maskedinput');

    But when I tried to use it got error
    Uncaught TypeError: $(...).mask is not a function

    Is there any ideas how to fix it?

  • 2018-11-19 bharatesh regoudar

    Thank you very much. Inclusion of runtime.js helped in solving the issue.

  • 2018-11-19 weaverryan

    Hi bharatesh regoudar!

    Welcome to Encore :). There is a learning curve to the "new way of doing JavaScript", bit it is TOTALLY worth it!

    So, I think I see the problem. Basically, you should *remove* the script tags in your template for jquery, pooper and bootstrap. Instead, you should require all of these from inside your home.js file! First, install everything:


    yarn add --dev jquery bootstrap popper.js

    Then, require things in home.js


    // behind the scenes, bootstrap will require "jquery" and add all the new functions to it
    // so, after this, you should be able to do things like $('.some-element').tooltip(), for example
    require('bootstrap');

    // make sure to require jQuery and set it to a $ variable so you can use it below
    var $ = require('jquery');

    require('bootstrap/dist/css/bootstrap.css');
    require('../css/carousel.css');

    $(document).ready(function() {
    console.log('Test loading...!');
    });

    Also, there is one more thing that is new to Encore that may be causing you problem: while following this tutorial, comment-out the .enableSingleRuntimeChunk() line OR add a new script tag that points to a new runtime.js file:


    <script src="{{ asset('build/runtime.js') }}"></script>
    <script src="{{ asset('build/home.js') }}"></script>

    You're coding through this tutorial using a newer version of Encore, which has a few improvements. You can also, if you want, use some new Twig functions to print these script tags for you. The above 2 script tags could be replaced with:


    {{ encore_entry_script_tags('home') }}

    If you have more questions, let us know!

    Cheers!

  • 2018-11-18 bharatesh regoudar

    Hi there!

    Firstly great article, thanks to author.
    As I am new to symfony 4.x with encore. From beginning of this tutorial all went smooth. But got struck in the following issue.


    /**
    * add js libs
    */
    require('bootstrap');

    /**
    * add css files
    */
    require('bootstrap/dist/css/bootstrap.css');
    require('../css/carousel.css');

    $(document).ready(function() {
    console.log('Test loading...!');
    });

    in the file named home.js

    yarn encore dev works fine build the files.

    But when I include them in the twig file. It is not able to run the javascript.


    {% block javascripts %}

    {#
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://getbootstrap.com/docs/4.1/assets/js/vendor/popper.min.js" type="text/javascript"></script>
    <script src="https://getbootstrap.com/docs/4.1/dist/js/bootstrap.min.js" type="text/javascript"></script>
    #}

    {#
    #}
    <script src="{{ asset('build/home.js') }}"></script>
    {% endblock %}

    in the base.html.twig

    to resolve javascript issue, as mentioned in the tutorial, I changed webpack.config.js file as below.


    var Encore = require('@symfony/webpack-encore');

    Encore
    // directory where compiled assets will be stored
    .setOutputPath('public/build/')
    // public path used by the web server to access the output path
    .setPublicPath('/build')
    // only needed for CDN's or sub-directory deploy
    //.setManifestKeyPrefix('build/')

    /*
    * ENTRY CONFIG
    *
    * Add 1 entry for each "page" of your app
    * (including one that's included on every page - e.g. "app")
    *
    * Each entry will result in one JavaScript file (e.g. app.js)
    * and one CSS file (e.g. app.css) if you JavaScript imports CSS.
    */
    .addEntry('home', './assets/js/home.js')
    //.addEntry('page1', './assets/js/page1.js')
    //.addEntry('page2', './assets/js/page2.js')

    // will require an extra script tag for runtime.js
    // but, you probably want this, unless you're building a single-page app
    .enableSingleRuntimeChunk()

    /*
    * FEATURE CONFIG
    *
    * Enable & configure other features below. For a full
    * list of features, see:
    * https://symfony.com/doc/current/frontend.html#adding-more-features
    */
    .cleanupOutputBeforeBuild()
    .enableBuildNotifications()
    .enableSourceMaps(!Encore.isProduction())
    // enables hashed filenames (e.g. app.abc123.css)
    .enableVersioning(Encore.isProduction())

    // enables Sass/SCSS support
    //.enableSassLoader()

    // uncomment if you use TypeScript
    //.enableTypeScriptLoader()

    // uncomment if you're having problems with a jQuery plugin
    .autoProvidejQuery()
    ;

    module.exports = Encore.getWebpackConfig();

    Any clue would be helpful.

  • 2018-11-05 weaverryan

    Hey Jose Mojena!

    Very good! You are 100% correct! And so, we need to solve that :). We do that with createSharedEntry() - we talk about it a bit later - https://symfonycasts.com/sc...

    In the next version of Encore (released about 5 hours ago), there will also be a different way to handle this called splitChunks(). Look for a blog post on Symfony.com tomorrow about it.

    Cheers!

  • 2018-11-05 Jose Mojena

    Awesome tutorial.... I have one question, if I have two different files to be included in the same page, and each file use internally jquery, after webpack encore this files, each file will included jquery?

  • 2018-10-10 Shaun

    Thanks Diego :)

  • 2018-10-09 Diego Aguiar

    Hey Shaun

    A better way to do it would be to create a utility file where all the logic for loading posts live, and then, create an entry file per page where you can call directly the methods you need. at the end you would end up with 3 files (2 entry files, and 1 for the functionality).

    Cheers!

  • 2018-10-09 Shaun

    Thanks Ryan :)

    The reason I wanted to call the Javascript function from the twig template is that there are functions in the javascript file that are also required on another page (edited).

    I have post.html.twig and profile.html.twig which both show posts and use common functions within post_show.js

    The way I got around this was to add the following to the top of post_show.js

    if (window.location.pathname.match("/app/post")) {
    loadPost();
    } else if(window.location.pathname.match("/app/profile")) {
    loadPostsByUser();
    }

    Thanks again for you help!

  • 2018-10-08 weaverryan

    Hey Shaun!

    Vladimir was right about the first part. An alternative solution is to keep your script tag where it was originally, but move the code you’re trying to execute down into the javascripts block. Basically, you need to make sure your script tag is included before you run your code.

    About the second issue. Basically, this global function doesn’t exist! When you create a function in the world of Webpack, it does not become a global variable. You could force it with something like global.myFunc = function()

    But, there’s a better way :). If you want to run some JavaScript on page load of a specific page, just out that code inside that page’s entry file. There’s no reason to put this JavaScript inside Twig, and I think it’s making your life more difficult.

    If you have any issues - let us know! Webpack is a different “way” of thinking - but it’s worth it :).

  • 2018-10-08 Shaun

    Thanks Vladimir Sadicov, I tried that but I now get:

    jquery.js:3827 Uncaught ReferenceError: loadPost is not defined

  • 2018-10-08 Vladimir Sadicov

    Hi Shaun,

    It could be possible if you move your javascripts entry's in `base.html.twig` to the <head> block.

    Cheers!

  • 2018-10-08 Shaun

    Hey guys?

    Is it possible to call a Javascript function from a twig template?

    This is my twig template:

    {% extends 'base.html.twig' %}

    {% block body %}
    <div class="row">
    <div class="col-12 col-md-8 js-post-show" data-post-id="{{ id }}"></div>
    <div class="col-12 col-md-4 js-post-sidebar"></div>
    </div>
    <script>
    $(document).ready(function(){
    loadPost();
    });
    </script>
    {% endblock %}

    {% block javascripts %}
    {{ parent() }}
    <script src="{{ asset('build/js/post_show.js') }}"></script>
    <script src="{{ asset('build/js/following.js') }}"></script>
    {% endblock %}

    This is my webpack.config.js

    // webpack.config.js
    var Encore = require('@symfony/webpack-encore');

    Encore
    .setOutputPath('public/build/')
    .setPublicPath('/build')
    .cleanupOutputBeforeBuild()
    .enableSourceMaps(!Encore.isProduction())
    .autoProvidejQuery()
    .addEntry('js/app', [
    './assets/js/app.js',
    './node_modules/jquery/dist/jquery.slim.js',
    './node_modules/popper.js/dist/popper.min.js',
    './node_modules/bootstrap/dist/js/bootstrap.min.js',
    './node_modules/underscore/underscore-min.js',
    ])
    .addEntry('js/header', './assets/js/header.js')
    .addEntry('js/login', './assets/js/login.js')
    .addEntry('js/post_list', './assets/js/post_list.js')
    .addEntry('js/post_show', './assets/js/post_show.js')
    .addEntry('js/following', './assets/js/following.js')
    .addEntry('js/home', './assets/js/home.js')
    .addEntry('js/notification_list', './assets/js/notification_list.js')
    .addStyleEntry('css/app', [
    './node_modules/bootstrap/dist/css/bootstrap.min.css',
    './assets/css/app.css',
    ])
    ;

    module.exports = Encore.getWebpackConfig();

    And this is my assets/js/app.js

    require('../css/app.css');
    require('@fortawesome/fontawesome-free/css/all.min.css');
    require('@fortawesome/fontawesome-free/js/all.min.js');

    const $ = require('jquery');

    // create global $ and jQuery variables
    global.$ = global.jQuery = $;

    global.userId = '';

    However I try to load the twig template, I get the following error message:

    Uncaught ReferenceError: $ is not defined

  • 2018-09-20 Diego Aguiar

    Hey Matt O'Toole

    I believe you ended in an edge case where some jQuery plugins are not managing their dependencies correctly. In your webpack config replace your module export line by these:


    const config = Encore.getWebpackConfig();

    config.resolve.alias.jquery = path.join(__dirname, 'node_modules/jquery/dist/jquery');

    module.exports = config;

    If you want to know more about that problem, check this thread: https://github.com/symfony/...

    I hope this fix your problem, cheers!

  • 2018-09-20 Matt O'Toole

    👍 on these tutorials. They're the best I've found on Symfony anywhere on the web.

    However, I have this issue I was hoping you'd be able to help with, Ryan.

    I hate to think how long I've spent on trying to fix an issue with another jQuery plugin - in this case, jqgrid (http://www.trirand.com/blog/ and http://www.trirand.com/jqgr.... Still no joy. 😣


    #webpack.config.js

    const Encore = require('@symfony/webpack-encore');

    // const isProduction = process.env.NODE_ENV === 'production';
    const CopyWebpackPlugin = require('copy-webpack-plugin');

    Encore
    // the project directory where all compiled assets will be stored
    .setOutputPath('public/build/')

    // the public path used by the web server to access the previous directory
    .setPublicPath('/build')

    .createSharedEntry('layout', './assets/js/layout.js')
    .addEntry('app', './assets/js/app.js')
    .addEntry('tables', './assets/js/tables.js')

    .enableBuildNotifications()

    // fixes modules that expect jQuery to be global
    .autoProvidejQuery()

    .addPlugin(new CopyWebpackPlugin([
    // copies to {output}/static
    { from: './assets/static', to: 'static' }
    ]))

    .autoProvideVariables({
    $: "jquery",
    jQuery: "jquery",
    jqGrid: "jqGrid",
    jqgrid: "jqgrid"
    })

    .enableSassLoader()

    .enableSourceMaps(!Encore.isProduction())

    .cleanupOutputBeforeBuild()

    .enableVersioning()
    ;

    // export the final configuration
    module.exports = Encore.getWebpackConfig();


    'use strict';

    ...

    // $ = window.$ = window.jQuery = require("jquery");
    global.$ = global.jQuery = require('jquery');
    require('jquery-ui');
    ...

    window.$.jqgrid = window.$.jqGrid = require('jqgrid/js/jquery.jqGrid.src.js');

    In Chrome Dev Tools' console, I get the following:


    $.jqGrid
    ƒ (jQuery) {
    //@add
    (function ($) {
    "use strict";
    $.jgrid = $.jgrid || {};
    $.extend($.jgrid,{
    version : "4.6.0",
    htmlDecode : function(value){
    if(value && (value===' ' || value==='…
    $.jqgrid
    ƒ (jQuery) {
    //@add
    (function ($) {
    "use strict";
    $.jgrid = $.jgrid || {};
    $.extend($.jgrid,{
    version : "4.6.0",
    htmlDecode : function(value){
    if(value && (value===' ' || value==='…

    However, when I then try to call jqGrid on the same page:


    $("#table").jqGrid({....

    I get:


    Uncaught TypeError: jQuery(...).jqGrid is not a function

    If I click through to the actual part of the jQuery library throwing the error, it appears to be coming from:


    jQuery.readyException = function( error ) {
    window.setTimeout( function() {
    throw error;
    } );
    };

    🤔

    So it's saying jQuery isn't ready, but it certainly is:


    > jQuery
    ƒ ( selector, context ) {

    // The jQuery object is actually just the init constructor 'enhanced'
    // Need init if jQuery is called (just allow error to be thrown if not included)
    return new jQuery…

    So it's the same as if it returned 'undefined'?

    Any idea what is going wrong here?

    Presumably it's something to do with what you mentioned about jQuery plugins expecting a global jQuery object?

    Pls help, if you can. This has been driving me nuts! 🤯

  • 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?