Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Modularize our Code

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

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

Exporting RepLogApp

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,

  • non-existent - global variable RepLogApp:

... lines 1 - 53
{% block javascripts %}
... lines 55 - 58
<script>
$(document).ready(function() {
... line 61
var repLogApp = new RepLogApp($wrapper);
});
</script>
{% endblock %}

Creating a new Entry File

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.

Moving into Components

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.

Leave a comment!

3
Login or Register to join the conversation
halifaxious Avatar
halifaxious Avatar halifaxious | posted 4 years ago | edited

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?

Reply

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!

Reply
halifaxious Avatar

Thanks Diego. It did.

1 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial explains the concepts of an old version of Webpack using an old version of Symfony. The most important concepts are still the same, but you should expect significant differences in new versions.

What PHP libraries does this tutorial use?

// 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
    }
}

What JavaScript libraries does this tutorial use?

// 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
    }
}
userVoice