If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeIn the last tutorial, when we created RepLogApp
, we put all of our code into a self-executing function: It starts up here... then all the way at the bottom, we call that function and pass in our dependencies which, before a recent change, included jQuery and SweetAlert:
... lines 1 - 6 | |
(function(window, Routing) { | |
... lines 8 - 217 | |
})(window, Routing); |
Why did we do this? Well, it gave our code a little bit of isolation: any variables we created inside the function are not available outside of it. And also, if we did something silly like saying $ = null
, well, it wouldn't actually set the global $
variable to null everywhere, it would only do it inside the function.
Now that I've told you about the amazing benefits of the self-executing function... I want you to delete it! What!? Inside a module system, each module is executed in isolation. Basically, you can imagine that Webpack wraps a self-executing function around our module for us. Actually, in the built file... that's exactly what happens! Sure, we can still use global variables - like this Routing
global variable - but we don't need to worry about any non-exported values leaking out of our module.
So get rid of the self-executing function! Woo! I'll un-indent everything to the root:
... lines 1 - 6 | |
let HelperInstances = new WeakMap(); | |
class RepLogApp { | |
... lines 10 - 196 | |
} | |
const rowTemplate = (repLog) => ` | |
<tr data-weight="${repLog.totalWeightLifted}"> | |
<td>${repLog.itemLabel}</td> | |
<td>${repLog.reps}</td> | |
<td>${repLog.totalWeightLifted}</td> | |
<td> | |
<a href="#" | |
class="js-delete-rep-log" | |
data-url="${repLog.links._self}" | |
> | |
<span class="fa fa-trash"></span> | |
</a> | |
</td> | |
</tr> | |
`; | |
window.RepLogApp = RepLogApp; |
This has no effect on our app... except looking a bit cleaner. Hmm, nice.
But at the bottom, huh, we still have window.RepLogApp = RepLogApp
:
... lines 1 - 214 | |
window.RepLogApp = RepLogApp; |
In other words, we're using the global window
variable that our browser makes available to create a global RepLogApp
variable. We need that because - in index.html.twig
- we're relying on RepLogApp
to be available globally:
... lines 1 - 53 | |
{% block javascripts %} | |
... lines 55 - 58 | |
<script> | |
$(document).ready(function() { | |
... line 61 | |
var repLogApp = new RepLogApp($wrapper); | |
}); | |
</script> | |
{% endblock %} |
Listen: we do not want to deal with global variables anymore. We can do better.
Since RepLogApp
is being loaded by webpack. it's already a module. So instead of using window.RepLogApp
, let's export a value properly: module.exports = RepLogApp
:
... lines 1 - 214 | |
module.exports = RepLogApp; |
Now, if anything requires this file, they will get the RepLogApp
class. And, we are no longer modifying anything in the global scope.
And as soon as we try that, our app is super broken! Thanks Ryan!
RepLogApp
is not defined
Yes! This makes sense: in our template, we're still trying to reference the now,
RepLogApp
:
... lines 1 - 53 | |
{% block javascripts %} | |
... lines 55 - 58 | |
<script> | |
$(document).ready(function() { | |
... line 61 | |
var repLogApp = new RepLogApp($wrapper); | |
}); | |
</script> | |
{% endblock %} |
How do we fix this? By fully modularizing our code and removing all JavaScript from our templates.
First, in the js/
directory, create a new file called rep_log.js
. This will be our new entry file. In fact, open webpack.config.js
right now and change the entry to this file: rep_log.js
:
... lines 1 - 2 | |
module.exports = { | |
entry: './web/assets/js/rep_log.js', | |
... lines 5 - 8 | |
}; |
This file will be the "entry point" for all the JavaScript that needs to run on this page. In other words, remove all of the JavaScript code from the template:
... lines 1 - 53 | |
{% block javascripts %} | |
... lines 55 - 56 | |
<script src="{{ asset('build/rep_log.js') }}"></script> | |
{% endblock %} |
And paste it here:
... lines 1 - 3 | |
$(document).ready(function() { | |
var $wrapper = $('.js-rep-log-table'); | |
var repLogApp = new RepLogApp($wrapper); | |
}); |
Now, when index.html.twig
includes the new built rep_log.js
file, it will hold all of the code that's needed to run this page.
Back in that file, if you look closely, we have two dependencies: $
and RepLogApp
. Add const $ = require('jquery')
. And then - thanks to the new module.exports
we added - const RepLogApp = require('./RepLogApp')
:
const $ = require('jquery'); | |
const RepLogApp = require('./RepLogApp'); | |
$(document).ready(function() { | |
var $wrapper = $('.js-rep-log-table'); | |
var repLogApp = new RepLogApp($wrapper); | |
}); |
So cool! If you look at the watch output in our terminal... it looks happy! But... remember.. we need to restart Webpack! Webpack's watch does not take into account changes to webpack.config.js
until we restart it. Hit Control
+C
and then re-run the command:
./node_modules/.bin/webpack --watch
Finally, let's try it! Refresh! Ha! Everything still works! Guys... this is big! We have zero code inside our template. This is a really common pattern: include one script tag to your entry file. And from that file, write just a little bit of code to require and boot up the rest of your application. This file is kind of like a controller in PHP: it's a thin layer of code that calls out to other layers.
By the way, to help keep my code clean, the entry file is the only file where I allow myself to reference the document
or window
objects that come from my browser. For all my true modules, I try to not rely on any global objects. If I need to use jQuery to find an element on the entire page, I do it here and pass that into my other modules.
If you look at our js/
directory now, rep_log.js
is our entry point. RepLogApp
and RepLogHelper
? Well, they're really components: independent modules that are meant to be used by other code. That's really cool!
To make that distinction a bit more clear, let's create a new directory called Components/
- that name isn't important. Then, drag those two files inside.
Oh, I like this: our entry file lives at the root, and the true modules live inside this new directory. To get this all working, all we need to do is update the path to ./Components/RepLogApp
:
... line 1 | |
const RepLogApp = require('./Components/RepLogApp'); | |
... lines 3 - 8 |
The require
statement in RepLogApp
to RepLogHelper
still works, because they live in the same directory.
Try it! Wow, we just can't seem to break our app! It still works, and our setup is starting to look pretty awesome.
Next! We need to talk about how we can have multiple entries... because right now, we can only create Webpacked JavaScript for the rep log page. But, what about the JavaScript on our login page? Or... any JavaScript in our layout? We need a way to handle all of that.
Hey halifaxious
I believe you are hitting a problem where a Jquery plugin modifies the global Jquery object. To overcome to this situation and for a deeper understanding of what's going on, I recommend you to watch this episode: https://symfonycasts.com/sc...
I hope it helps :)
Cheers!
// composer.json
{
"require": {
"php": "^7.2.0",
"symfony/symfony": "3.3.*", // v3.3.16
"twig/twig": "2.10.*", // v2.10.0
"doctrine/orm": "^2.5", // v2.7.0
"doctrine/doctrine-bundle": "^1.6", // 1.10.3
"doctrine/doctrine-cache-bundle": "^1.2", // 1.3.5
"symfony/swiftmailer-bundle": "^2.3", // v2.6.3
"symfony/monolog-bundle": "^2.8", // v2.12.1
"symfony/polyfill-apcu": "^1.0", // v1.4.0
"sensio/distribution-bundle": "^5.0", // v5.0.22
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.26
"incenteev/composer-parameter-handler": "^2.0", // v2.1.2
"friendsofsymfony/user-bundle": "^2.0", // v2.1.2
"doctrine/doctrine-fixtures-bundle": "~2.3", // v2.4.1
"doctrine/doctrine-migrations-bundle": "^1.2", // v1.3.2
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"friendsofsymfony/jsrouting-bundle": "^1.6" // 1.6.0
},
"require-dev": {
"sensio/generator-bundle": "^3.0", // v3.1.6
"symfony/phpunit-bridge": "^3.0" // v3.3.5
}
}
// package.json
{
"dependencies": [],
"devDependencies": {
"babel-core": "^6.25.0", // 6.25.0
"babel-loader": "^7.1.1", // 7.1.1
"babel-plugin-syntax-dynamic-import": "^6.18.0", // 6.18.0
"babel-preset-env": "^1.6.0", // 1.6.0
"bootstrap-sass": "^3.3.7", // 3.3.7
"clean-webpack-plugin": "^0.1.16", // 0.1.16
"copy-webpack-plugin": "^4.0.1", // 4.0.1
"core-js": "^2.4.1", // 2.4.1
"css-loader": "^0.28.4", // 0.28.4
"extract-text-webpack-plugin": "^3.0.0", // 3.0.0
"file-loader": "^0.11.2", // 0.11.2
"font-awesome": "^4.7.0", // 4.7.0
"jquery": "^3.2.1", // 3.2.1
"lodash": "^4.17.4", // 4.17.4
"node-sass": "^4.5.3", // 4.5.3
"resolve-url-loader": "^2.1.0", // 2.1.0
"sass-loader": "^6.0.6", // 6.0.6
"style-loader": "^0.18.2", // 0.18.2
"sweetalert2": "^6.6.6", // 6.6.6
"webpack": "^3.4.1", // 3.4.1
"webpack-chunk-hash": "^0.4.0", // 0.4.0
"webpack-dev-server": "^2.6.1", // 2.6.1
"webpack-manifest-plugin": "^1.2.1" // 1.2.1
}
}
I created a modalContent.js component that uses Bootstrap 4's modal function. When I have 2 entrypoints on a page (e.g. app and holding_table), the modalContent code gives a different result depending on whether it's included in app.js or in holding_table.js. In app.js, which imports Bootstrap, it works. In holding_table.js, which just imports jquery, it throws the following:
<blockquote>
TypeError: jqueryWEBPACK_IMPORTED_MODULE1default(...)(...).modal is not a function
at
jquery__WEBPACK_IMPORTED_MODULE_1___default()(this.target).modal('show');
</blockquote>
I can make modalContent.js work in holding_table.js if I
import {modal} from 'bootstrap'
inside modalContent.js. But that causes other Bootstrap functions (like dropdown menus) to stop working.It appears like the jQuery I have in holding_table.js (
import $ from 'jquery';
) does not have access to the Bootstrap that was added to jQuery in app.js. My understanding was that extensions to jQuery were accessible from wherever jQuery is accessed. Is this not correct?Any thoughts on how to fix this?