Single file components in Twig #
Why would we want to use Javascript code in templates, alongside HTML - instead of storing it in separate files? What about layer separation? Vue JS framework popularized concept of single file components - code living alongside javascript (and CSS) in one file.
Let's think of the image gallery component. Such component contains HTML markup, some Twig logic, like for
loop and image transforms. Then, there is a Javascript function call which is responsible for lightbox. If we store all of that in one file we gain some advantages:
- Twig and JS code is more tightly knit. You can easily transfer Twig variables into JS.
- Your code is more portable - you can easily reuse component by just copying a single file to other project.
- When you want to tweak something, you don't need to look for code in two separate places - Javascript files and Twig template - but only at one. Thanks to that, code is easier to maintain.
There are also disadvantages - the main one is that using Javascript directly in Twig takes away your ability to use preprocessors like Babel. This means that if you want to support legacy browsers, you will not be able to use ES6.
js Twig tag #
js Twig tag is essential to using JS in Twig. It takes JS code passed to it and appends it at end of the template, right before ending body
tag. Multiple js
tags can be used - content from each of them will be concated with the rest. You don't need to wrap code passed to js
into <script>
tags - js
will do that for you.
js
tag has one limitation - it cannot be used inside {% cache %}
tags. Caching specific parts of template saves HTML content generated by them to database - so Twig code does not need to run. This however stops js
tag from working.
Your Twig component doesn't need to rely only on inline code - if you want to include external JS file from within Twig, you can use registerJsFile function. Link to this file will also be included at the end of the template.
{% do view.registerJsFile('directory/some-file.js') %}
There is also CSS equivalent of js
- css
tag. It works pretty much the same as js
, except it puts its contents into <head>
section of the website. I don't really use it - there would be problems with CSS preprocessors like gulp.
Passing data from Twig to JS #
Now that your JS code lives alongside Twig, you can easily transfer Twig variables into Javascript. For example:
alert({{twig_variable|json_encode}});
From Twig point of view, JS code is just text string - that's why we can just concate Twig variables with text that make up JS code. json_encode
is used to transform Twig variables into a form that can be consumed by JS. Thanks to that, Twig true
will turn into JS true
, not 1
- which would happen if we just outputted {{true}}
into the template.
Such approach will work, but it's pretty messy to mix Twig and JS like this - it can be hard to recognize on first glance which parts of code is Twig and which is JS. It is better to define JS variables containing data from Twig before JS code starts. You can do that using Twig macro:
{% macro jsVar(variable, jsVariableName) %}
var {{jsVariableName}} = {{variable|json_encode|raw}};
{% endmacro %}
This macro takes two arguments:
variable
- twig variable or object that needs to be transferred to JS.jsVariableName
- JS variable name that will have assigned Twig variable value.
With jsVar
macro, our example with alert will look like this:
{{_self.jsVar(twig_variable, 'javascript_variable_name')}}
alert(javascript_variable_name);
Which will render this code - assuming that twig_variable
contains string "abc":
var javascript_variable_name = 'abc';
alert(javascript_variable_name);
Javascript helpers plugin #
Javascript helpers is small plugin that I created to make working with JS in Twig easier. It's main feature is transfering all static message translations into javascript array. Normally, you would need to define each message manually, like this:
var message_1 = {{'message_1'|t|json_encode}};
var message_2 = {{'message_2'|t|json_encode}};
With Javascript helpers plugin you can transfer all static message translations at once:
{{craft.jsHelpers.outputMessages('all_messages')}}
Parameter passed to function is name of array. Assuming that you have only two static message translations, rendered result will look like this:
var all_messages = {message_1: 'something 1', message_2: 'something 2'};
You can also use plugin to transfer Twig variables into Javascript by using Twig filter instead of macro:
{{some_twig_variable|jsVar('js_variable_name')}}
Component namespacing #
Some components containing JS code can be used in multiple parts of the website at the same time. Let's think on carousel component. It renders slides using for
loop. Carousel script uses carouselSpeed
setting which takes value from Twig carousel.speed
variable.
{% is carousel is defined %}
<ul class="js-carousel">
{% for slide in carousel.slides %}
<li class="single-slide">
<img src="{{slide.url}}" alt="">
</li>
{% endfor %}
</ul>
{% js %}
{{_self.jsVar(carousel.speed, 'speed')}}
$('.js-carousel').initialiseCarousel({
carouselSpeed: speed
})
{% endjs %}
{% endif %}
As you can see, we have initialized carousel script on the element with js-carousel
class. What if the same carousel component is used simultaneously somewhere else on a currently displayed page? It might display different images, have different carousel.speed
setting, but its class attribute stays identical. This means that initializing script with specific settings would affect two carousels at once.
To avoid that, elements in Twig components affected by Javascript should have their classes (or other attributes used by JS) namespaced. Let's add namespace variable to carousel example:
{% is carousel is defined %}
<ul class="js-carousel{{namespace ?? null}}">
{% for slide in carousel.slides %}
<li class="single-slide">
<img src="{{slide.url}}" alt="">
</li>
{% endfor %}
</ul>
{% js %}
{{_self.jsVar(namespace ?? null, 'carousel_namespace')}}
{{_self.jsVar(carousel.speed, 'speed')}}
$('.js-carousel'+carousel_namespace).initialiseCarousel({
carouselSpeed: speed
})
{% endjs %}
{% endif %}
Namespace should be passed into the component using with
keyword. You should also use only
keyword, to ensure that component is isolated from the variable context of its parent file - which might also has its own namespace.
{% include 'carousel_component' with {namespace: '-some-carousel'} only %}
If you use your component in for
loop, you can use loop.index
or element id
as a part of namespace variable:
{% for block in entry.matrixField %}
{% include 'carousel_component' with {namespace: '-some-carousel-'~block.id, block: block} only %}
{% endfor %}
What about js-
prefix that carousel class has? Using that prefix reminds us that this class is used as a javascript hook and NOT for styling. Thanks to that, styling and javascript functionality are decoupled and changing javascript related classes wont break any styling. It's really good convention to follow.