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'):
| ; | |
| 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!
tooltipis 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:
| <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:
| <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!
jQueryis 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!
31 Comments
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?
Alexey R. ,
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 {
}
}
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:
<br />{{ encore_entry_script_tags('application') }}<br />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?
Hey Kevin!
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/excellalabs/jquery.maskedinput/pull/3/files
As we explain in this section (https://symfonycasts.com/screencast/webpack-encore/require-css#requiring-bootstrap-amp-fontawesome-css), 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:'
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!
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 functionI have tried two different ways to 'require' it:
<br />require('jquery-validation');<br />and
<br />require('jquery-validation/dist/jquery.validate');<br />require('jquery-validation/dist/additional-methods');<br />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!
Hey Kevin!
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!
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.
Thank you!
require('jquery.maskedinput/src/jquery.maskedinput.js');Works good!
Thank you.
require('jquery.maskedinput/src/jquery.maskedinput.js');This solution for me works good.
Hey Alexey R.
Have you tried enabling "autoProvidejQuery"?
More info here: https://symfonycasts.com/sc...
Cheers!
Yes, but it not helped.
Hmm, that's weird, probably is because of something internal of that plugin. Try doing the other option explained here: https://symfonycasts.com/sc...
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.
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.
in the base.html.twig
to resolve javascript issue, as mentioned in the tutorial, I changed webpack.config.js file as below.
Any clue would be helpful.
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:
Then, require things in home.js
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: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:
If you have more questions, let us know!
Cheers!
What a pooper! :)
That's a good one. Lol
Thank you very much. Inclusion of runtime.js helped in solving the issue.
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?
Hey Jose M.!
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!
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
Hi Shaun T.,
It could be possible if you move your javascripts entry's in `base.html.twig` to the <head> block.
Cheers!
Thanks Vladimir Sadicov, I tried that but I now get:
jquery.js:3827 Uncaught ReferenceError: loadPost is not defined
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 :).
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!
Hey Shaun T.
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!
Thanks Diego :)
👍 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/jqgridwiki/doku.php). Still no joy. 😣
'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');
$.jqGrid
ƒ (jQuery) {
//@add
(function ($) {
"use strict";
$.jgrid = $.jgrid || {};
$.extend($.jgrid,{
$.jqgrid
ƒ (jQuery) {
//@add
(function ($) {
"use strict";
$.jgrid = $.jgrid || {};
$.extend($.jgrid,{
$("#table").jqGrid({....
Uncaught TypeError: jQuery(...).jqGrid is not a function
jQuery.readyException = function( error ) {
};
Hey Authoritas
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:
If you want to know more about that problem, check this thread: https://github.com/symfony/webpack-encore/issues/244
I hope this fix your problem, cheers!
A nice one! https://bossanova.uk/jexcel and more bootstrap-like https://bossanova.uk/jexcel...
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?
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/configuration/externals/. 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!
Got it. Thanks Ryan.
"Houston: no signs of life"
Start the conversation!