Entry Refactoring

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Here's our mission: to get rid of all the JavaScript and CSS stuff from our public/ directory. Our next target is admin_article_form.js:

Dropzone.autoDiscover = false;
$(document).ready(function() {
... lines 4 - 33
});
// todo - use Webpack Encore so ES6 syntax is transpiled to ES5
class ReferenceList
{
... lines 39 - 123
}
/**
* @param {ReferenceList} referenceList
*/
function initializeDropzone(referenceList) {
... lines 130 - 148
}

This probably won't come as a huge shock, but this is used in the admin section. Go to /admin/article. If you need to log in, use admin1@thespacebar.com, password engage. Then click to edit any of the articles.

This page has JavaScript to handle the Dropzone upload and a few other things. Open the template: templates/article_admin/edit.html.twig and scroll down. Ok: we have a traditional <script> tag for admin_article_form.js as well as two external JavaScript files that we'll handle in a minute:

... lines 1 - 35
{% block javascripts %}
{{ parent() }}
<script src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.5.1/min/dropzone.min.js" integrity="sha256-cs4thShDfjkqFGk5s2Lxj35sgSRr4MRcyccmi0WKqCM=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.8.3/Sortable.min.js" integrity="sha256-uNITVqEk9HNQeW6mAAm2PJwFX2gN45l8a4yocqsFI6I=" crossorigin="anonymous"></script>
<script src="{{ asset('js/admin_article_form.js') }}"></script>
{% endblock %}

The Repeatable Process of Refactoring to an Entry

This is super similar to what we just did. First, move admin_article_form.js into assets/js. This will be our third entry. So, in webpack.config.js copy addEntry(), call this one admin_article_form and point it to admin_article_form.js:

... lines 1 - 2
Encore
... lines 4 - 21
.addEntry('admin_article_form', './assets/js/admin_article_form.js')
... lines 23 - 73
;
... lines 75 - 76

Finally, inside edit.html.twig, change this to use {{ encore_entry_script_tags('admin_article_form') }}:

... lines 1 - 35
{% block javascripts %}
... lines 37 - 40
{{ encore_entry_script_tags('admin_article_form') }}
{% endblock %}

Now, stop and restart Encore:

yarn watch

Perfect! 3 entries and a lot of good code splitting. But we shouldn't be too surprised that when we refresh, we get our favorite JavaScript error:

$ is not defined

Let's implement phase 2 of refactoring. In admin_article_form.js, import $ from 'jquery':

import $ from 'jquery';
... lines 2 - 152

And... we're good to go!

Refactoring the External script Tags

In addition to moving things out of public/, I also want to remove all of these external script tags:

... lines 1 - 35
{% block javascripts %}
... lines 37 - 38
<script src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.5.1/min/dropzone.min.js" integrity="sha256-cs4thShDfjkqFGk5s2Lxj35sgSRr4MRcyccmi0WKqCM=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.8.3/Sortable.min.js" integrity="sha256-uNITVqEk9HNQeW6mAAm2PJwFX2gN45l8a4yocqsFI6I=" crossorigin="anonymous"></script>
... line 41
{% endblock %}

Actually, there's nothing wrong with including external scripts - and you can definitely argue that including some things - like jQuery - could be good for performance. If you do want to keep a few script tags for external stuff, check out Webpack's "externals" feature to make it work nicely.

The reason I don't like them is that, in the new way of writing JavaScript, you never want undefined variables. If we need a $ variable, we need to import $! But check it out: we're referencing Dropzone:

... lines 1 - 2
Dropzone.autoDiscover = false;
... lines 4 - 152

Where the heck does that come from? Answer: it's a global variable created by this Dropzone script tag!

... lines 1 - 35
{% block javascripts %}
... lines 37 - 38
<script src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.5.1/min/dropzone.min.js" integrity="sha256-cs4thShDfjkqFGk5s2Lxj35sgSRr4MRcyccmi0WKqCM=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.8.3/Sortable.min.js" integrity="sha256-uNITVqEk9HNQeW6mAAm2PJwFX2gN45l8a4yocqsFI6I=" crossorigin="anonymous"></script>
... line 41
{% endblock %}

The same is true for Sortable further down. I don't want to rely on global variables anymore.

Trash both of these script tags. Then, find your terminal, go to your open tab and run:

yarn add dropzone sortablejs --dev

I already looked up those exact package names to make sure they're right. Next, inside admin_article_form.js, these variables will truly be undefined now. Try it: refresh. A most excellent error!

Dropzone is undefined

It sure is! Fix that with import Dropzone from 'dropzone' and also import Sortable from 'sortablejs':

import $ from 'jquery';
import Dropzone from 'dropzone';
import Sortable from 'sortablejs';
... lines 4 - 154

Now it works.

Importing the CSS

But there's one more thing hiding in our edit template: we have a CDN link to the Dropzone CSS!

... lines 1 - 29
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.5.1/min/dropzone.min.css" integrity="sha256-e47xOkXs1JXFbjjpoRr1/LhVcqSzRmGmPqsrUQeVs+g=" crossorigin="anonymous" />
{% endblock %}
... lines 35 - 41

We don't need that either. Instead, in admin_article_form.js, we can import the CSS from the Dropzone package directly. Hold Command or Control and click to open Dropzone. I'll double-click the dropzone directory to take us there.

Inside dist... there it is: dropzone.css. That's the path we want to import. How? With import 'dropzone/dist/dropzone.css':

... line 1
import Dropzone from 'dropzone';
import 'dropzone/dist/dropzone.css'
... lines 4 - 155

Most of the time, we're lazy and we say import then the package name. But it's totally legal to import the package name / a specific file path.

As soon as we do that, go check out the Encore watch tab. Wow! The code splitting is getting crazy! Hiding inside there is one CSS file: vendors~admin_article_form.css.

Flip back to the edit template and add {{ encore_entry_link_tags('admin_article_form') }}:

... lines 1 - 29
{% block stylesheets %}
{{ parent() }}
{{ encore_entry_link_tags('admin_article_form') }}
{% endblock %}
... lines 35 - 41

Try it! Find your browser and refresh! Ok, it looks like the Dropzone CSS is still working. I think we're good!

This same JavaScript & CSS code is needed on one other page. Go back to /admin/article and click create. Oof, we still have some problems here. I'll close up node_modules/ and open templates/article_admin/new.html.twig:

... lines 1 - 2
{% block javascripts %}
{{ parent() }}
<script src="https://cdn.jsdelivr.net/autocomplete.js/0/autocomplete.jquery.min.js"></script>
<script src="{{ asset('js/algolia-autocomplete.js') }}"></script>
<script src="{{ asset('js/admin_article_form.js') }}"></script>
{% endblock %}
... lines 10 - 24

Ah, cool. Replace the admin_article_form.js script with our helper Twig function:

... lines 1 - 2
{% block javascripts %}
... lines 4 - 7
{{ encore_entry_script_tags('admin_article_form') }}
{% endblock %}
... lines 10 - 25

Under stylesheets, the new page doesn't use Dropzone, so it didn't have that same link tag here. Add {{ encore_entry_link_tags('admin_article_form') }} anyways so that this page has all the JS and CSS it needs:

... lines 1 - 10
{% block stylesheets %}
{{ parent() }}
{{ encore_entry_link_tags('admin_article_form') }}
... line 15
{% endblock %}
... lines 17 - 25

But this does highlight one... let's say... "not ideal" thing. Some of the JavaScript on the edit page - like the Dropzone & Sortable stuff - isn't needed here... but it's part of admin_article_form.js anyways. And actually, the reverse is true! That autocomplete stuff? That's needed on the "new" page, but not the edit page. At the end of the tutorial, we'll talk about async imports, which is one really nice way to help avoid packaging code all the time that is only needed some of the time.

Anyways, if we refresh now... the page is still totally broken! Apparently this "autocomplete" library we're importing is trying to reference jQuery. Let's fix that next... which will involve a... sort of "magical" feature of Webpack and Encore.

Leave a comment!

This tutorial works great with Symfony5!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "aws/aws-sdk-php": "^3.87", // 3.91.4
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.1
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.9.0
        "league/flysystem-aws-s3-v3": "^1.0", // 1.0.22
        "league/flysystem-cached-adapter": "^1.0", // 1.0.9
        "liip/imagine-bundle": "^2.1", // 2.1.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.1.0
        "oneup/flysystem-bundle": "^3.0", // 3.0.3
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.3.1
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.2.5
        "symfony/console": "^4.0", // v4.2.5
        "symfony/flex": "^1.0", // v1.6.2
        "symfony/form": "^4.0", // v4.2.5
        "symfony/framework-bundle": "^4.0", // v4.2.5
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.2.5
        "symfony/serializer-pack": "^1.0", // v1.0.2
        "symfony/twig-bundle": "^4.0", // v4.2.5
        "symfony/validator": "^4.0", // v4.2.5
        "symfony/web-server-bundle": "^4.0", // v4.2.5
        "symfony/webpack-encore-bundle": "^1.4", // v1.5.0
        "symfony/yaml": "^4.0", // v4.2.5
        "twig/extensions": "^1.5" // v1.5.4
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.1.0
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.2.5
        "symfony/dotenv": "^4.0", // v4.2.5
        "symfony/maker-bundle": "^1.0", // v1.11.5
        "symfony/monolog-bundle": "^3.0", // v3.3.1
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.2.5
        "symfony/profiler-pack": "^1.0", // v1.0.4
        "symfony/var-dumper": "^3.3|^4.0" // v4.2.5
    }
}

What JavaScript libraries does this tutorial use?

// package.json
{
    "devDependencies": {
        "@symfony/webpack-encore": "^0.27.0", // 0.27.0
        "autocomplete.js": "^0.36.0",
        "autoprefixer": "^9.5.1", // 9.5.1
        "bootstrap": "^4.3.1", // 4.3.1
        "core-js": "^3.0.0", // 3.0.1
        "dropzone": "^5.5.1", // 5.5.1
        "font-awesome": "^4.7.0", // 4.7.0
        "jquery": "^3.4.0", // 3.4.0
        "node-sass": "^4.11.0", // 4.11.0
        "popper.js": "^1.15.0",
        "postcss-loader": "^3.0.0", // 3.0.0
        "sass-loader": "^7.0.1", // 7.1.0
        "sortablejs": "^1.8.4", // 1.8.4
        "webpack-notifier": "^1.6.0" // 1.7.0
    }
}