Buy Access to Course
03.

JavaScript Modules

|

Share this awesome video!

|

Inspect element on this page and head over to the browser console. Ah, we've got a console log that says it comes from assets/app.js. And sure enough, if we spin over and open that file... there it is!

10 lines | assets/app.js
/*
* Welcome to your app's main JavaScript file!
*
* This file will be included onto the page via the importmap() Twig function,
* which should already be in your base.html.twig.
*/
import './styles/app.css'
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉')

But how is this file being loaded? To answer that, view the page source. There's some interesting stuff going on here, but I want to zoom in on one part: <script type="module">, import 'app';.

ECMAScript Modules

It turns out that all modern browsers - basically everything except for IE 11... and you should not be supporting IE 11 anymore - ahem all modern browsers support JavaScript modules, also known as ECMAScript modules or ESM. But they're nothing fancy: a JavaScript module is any JavaScript file that uses the import or export statements that you probably grew accustomed to in Webpack Encore.

The big news is that: browsers understand import and export all by themselves! No build step needed. If you open any HTML page and say <script type="module">, the code inside is allowed to use import and export statements.

Importmaps

So... the second question is: what the heck is app? How does app ultimately refer to assets/app.js? This is also a new trick of browsers called importmaps. And this has nothing to do with Symfony or AssetMapper. If, on your page, you have a <script type="importmap">, this becomes a key value map that's used by your browser when it loads modules. So if we say import 'app', it looks inside of this list, sees app and ultimately loads this file... which is served by AssetMapper. It's a nice bit of teamwork!

Importmaps are supported by all modern browsers... though it has slightly less support than JavaScript modules. Fortunately, there's a shim or polyfill so that if your user happens to use a browser that doesn't support importmaps, that shim will add it and everything will work.

The importmap() Function

The final question on my mind is: where the heck is this all coming from? To answer that, open templates/base.html.twig. It's entirely coming from this one line right here: {{ importmap('app') }}:

21 lines | templates/base.html.twig
<!DOCTYPE html>
<html>
<head>
// ... lines 4 - 10
{% block javascripts %}
{{ importmap('app') }}
{% endblock %}
</head>
// ... lines 15 - 19
</html>

Because we passed app, this will generate a <script type="module"> with import 'app' inside. But this also dumps the polyfill, some preloads - those are good for performance, but not required - and, of course, the importmap itself. The importmap is primarily, though not entirely (we'll get to that), generated from this importmap.php file:

22 lines | importmap.php
// ... lines 1 - 2
/**
* Returns the import map for this application.
*
* - "path" is a path inside the asset mapper system. Use the
* "debug:asset-map" command to see the full list of paths.
*
* - "entrypoint" (JavaScript only) set to true for any module that will
* be used as an "entrypoint" (and passed to the importmap() Twig function).
*
* The "importmap:require" command can be used to add new entries to this file.
*
* This file has been auto-generated by the importmap commands.
*/
return [
'app' => [
'path' => './assets/app.js',
'entrypoint' => true,
],
];

The importmap.php File

When we installed AssetMapper, its recipe gave us this file. And this is the reason that the importmap in our HTML has an app key that points to assets/app.js.

Writing Some JavaScript Modules

So I want to play a bit with this new system. Inside the assets/ directory - we can organize this however we want - create a lib/ directory with an alien-greeting.js file. Inside, I'm going to write some awesome, modern JavaScript: export default a function, give it message and inPeace arguments... then I'll log a message using a template literal - the fancy backticks - and some emojis:

export default function (message, inPeace = false) {
console.log(`${message}! ${inPeace ? '👽' : '👾'}`);
}

Cool! This new file lives inside assets/ so, technically, it's publicly available. But... nobody is using it yet.

Let's try something non-traditional, but fun to start. Go into the base layout and, anywhere, say <script type="module">. Inside, import alienGreeting... and I'll hit tab:

27 lines | templates/base.html.twig
<!DOCTYPE html>
<html>
<head>
// ... lines 4 - 10
{% block javascripts %}
{{ importmap('app') }}
<script type="module">
import alienGreeting from '{{ asset('lib/alien-greeting.js') }}';
// ... lines 16 - 17
</script>
{% endblock %}
</head>
// ... lines 21 - 25
</html>

Hmm: PhpStorm used ../assets for the path. That's not going to work. Instead, we can use the asset() function and the logical path: lib/alien-greeting.js. Then below, use that: alienGreeting(), a message and we will not come in peace!

27 lines | templates/base.html.twig
<!DOCTYPE html>
<html>
<head>
// ... lines 4 - 10
{% block javascripts %}
// ... lines 12 - 13
<script type="module">
import alienGreeting from '{{ asset('lib/alien-greeting.js') }}';
alienGreeting('Give us all your candy!', false);
</script>
{% endblock %}
</head>
// ... lines 21 - 25
</html>

Let's see if it works! Close that, and... it doesn't? I actually thought it would! We get a 404 for lib/alien-greeing.js - with no "t"...! Boop!

27 lines | templates/base.html.twig
// ... lines 1 - 14
import alienGreeting from '{{ asset('lib/alien-greeting.js') }}';
// ... lines 16 - 27

Now it works! No build, nice code, nothing special.

If you view the page source, we, of course, have this nice versioned filename in the import. So you can import simple things like app and rely on the importmap to point to the true filename, or you can include full paths.

Importing from JS Files

As fun as it was to hack this into the HTML, in reality, we're not usually going to write in-line code like this. Copy this, get rid of the <script type="module">:

21 lines | templates/base.html.twig
<!DOCTYPE html>
<html>
<head>
// ... lines 4 - 10
{% block javascripts %}
{{ importmap('app') }}
{% endblock %}
</head>
// ... lines 15 - 19
</html>

Then go into app.js. Paste the code here:

11 lines | assets/app.js
// ... lines 1 - 6
import './styles/app.css'
import alienGreeting from './lib/alien-greeting.js';
alienGreeting('Give us all your candy!', false);

And now that we're inside JavaScript, when we refer to a path, we can write it with normal, relative paths: ./alien-greeting.js:

11 lines | assets/app.js
// ... lines 1 - 7
import alienGreeting from './lib/alien-greeting.js';
// ... lines 9 - 11

This is the exact code that we would have in Webpack Encore, with one small difference. In Webpack, you don't need to have the .js on the end. It turns out that leaving off the extension is a Node-specific thing. In real JavaScript, you do need to have the extension. So you do need to add the .js.

And... it works!

PhpStorm: Auto-add Extension

By the way, if you let PhpStorm auto-complete the path to the imported JavaScript file, by default, it will not include the .js on the end. To fix that, open the settings... and search for "extensions". There we go: "Editor"=>"Code Style"=> "JavaScript". Right here, change this "use file extension" to "always".

Ok, day 3 is in the books! Tomorrow, we'll make our JavaScript set up much more powerful by learning how to install 3rd-party packages!