15

DRY Templating with Twig and Craft CMS

 3 years ago
source link: https://www.basedesign.com/blog/dry-templating-with-twig-and-craft-cms
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

DRY Templating with Twig and Craft CMS

Published on 31.05.2017, by 

All of us at Base try to integrate the DRY (Don’t Repeat Yourself) programming principle as deeply as we can in our daily processes. It helps us being less repetitive and work faster, in a smarter way.

Lots of tools and methodologies exist to help designers and developers adopt a less repetitive workflow (BEM, OOCSS, Sass, Object Oriented Programming and Sketch Symbols are the first that come to mind) but no solution exists for us, Twig developers using Craft CMS.

Craft is the CMS of our choice. We love its versatility and easy-to-use interface. But as we created our first Craft websites, we struggled with Twig, its templating language. The code we produced was never as DRY as we wanted it to be and we found ourselves writing the same code over and over again. We felt as WET (Wickedly Enslaved by Twig) as one could be in a DRY world and had the impression of doing the same things over and over again.

dry-templating-twig-craft-camels.jpg?mtime=20181026141450&focal=none?crop=focalpoint&domain=basedesign.imgix.net&fit=clip&w=500&h=333.33333333333&q=82&fm=pjpg&fp-x=0.5&fp-y=0.5&ixlib=php-2.1.1

Some other wet animals in a dry environment. By James Ballard, via Unsplash

I started to look around, trying to find out how other Craft CMS developers solved this issue. But from the code I see shared on Github, I would argue that many teams are experiencing the exact same issue.

So we kept searching for the holy grail, and I’m happy to say that slowly but steadily we were able to implement a Twig system that made everything a hundred times better. We used it on several small-scale and large-scale projects and refined it up to a point where it finally looks mature.

For this reason, we finally feel confident to share it with anybody who might want to adopt the DRY principle in a Twig templating system.

Macros to the rescue

If you’ve been playing with Craft CMS and Twig before, you must have used macros at some point.

If you come to think of it, macros are DRY-heaven. You write a few lines of code, then call the macro as often as you want with different parameters and bits of information whenever it’s needed.

I often use macros to format a date for example:

{% macro date(myDate) %} <span class="o-date">{{ myDate|date('j F Y') }}</span> {% endmacro %}

{{ _self.date(eventDateStart) }} {{ _self.date(eventDateEnd) }}

But at one point I realised I could push macros a bit further. I started using them to output more complex blocks of HTML as well. Visual buttons are the perfect example of this. At some point every call to action in the website was rendered by calling this macro:

{% macro button(url, label, external, classes) %} <a href="{{ url }}" class="o-button {{ classes|join(' ') }}" {{ external == true ? 'target="_blank" rel="noopener"' }}> {{- label -}} </a> {% endmacro %}

{{ _self.button('https://google.com', 'Go to Google', false, ['o-button--large', 'o-button--primary']) }}

It worked perfectly and I couldn’t notice any performance issue. So again I pushed it further and started using macros to output responsive images.

One thing led to another, and at some point we realized we had finally found our way to embrace the DRY principle in Twig templates.

No more unnecessary repetitions! A revolution in our code was on the way… 🎉

Let me introduce to you… Twig components!

Twig components is the name we gave to this technique of using macros in order to output template components.

To give you an idea of what a Twig component looks like, let’s take a look at the video component below.

{# explore/index.html #}

{% import '_macros/components' as components %}

{% set videos = entry.videoField %} // This is a Craft matrix {% for video in videos %} {{ components.video({ id: video.videoId, title: video.videoTitle, image: video.videoImage, textOnVideo: true, classes: [ 'o-video--large', 'u-align-left' ] }) }} {% endfor %}

As you can see, the explore/index.html file calls the video component. In the call, a single params object containing multiple (pre-defined) keys is passed:

{# _macros/components.html #}

...

{% macro video(params) %} {% include '_components/video' %} {% endmacro %}

...

The _macros/components.html file lists all Twig components, including the video macro. The video macro then consists of an include to the _components/video.html file. This is where our logic for the video component is defined. Here it comes:

{# _components/video.html #}

{# Video From the provided YouTube video ID, we generate the YouTube embed code. @param {string} id (YouTube video ID) @param {string} title (optional) @param {string} meta (optional) @param {object} image (AssetFileModel) (Poster image) @param {bool} textOnVideo (optional) (whether the video information is placed under or on top of the video) @param {array} classes (optional) [ {string} ] #}

{# Parameters #} {% set videoId = params.id ?? null %} {% set videoTitle = params.title ?? craft.youtubeInformation.getTitle(videoId) %} {% set videoMeta = params.meta ?? null %} {% set videoImage = params.image[0] ?? null %} {% set videoTextOnVideo = params.textOnVideo ?? false %} {% set videoClasses = params.classes ?? null %}

{# Variables #} {% set videoDuration = null %} {% if videoTextOnVideo|length %} {% set videoDuration = craft.youtubeInformation.getDuration(videoId) %} {% endif %} {% set videoCoverTransform = { width: 1200, height: 675 } %}

{# Output #} {% if videoId|length and videoImage|length %} <a class="c-video {{ videoClasses|join(' ') }} js-youtube-play" href="https://www.youtube.com/watch?v={{ videoId }}" target="_blank"> <div class="c-video__container"> <div class="c-video__player js-youtube-player" data-video="{{ videoId }}"></div> </div> <div class="c-video__placeholder" style="background-image: url('{{ videoImage.getUrl(videoCoverTransform) }}')"> <div class="c-video__controls"> <svg class="o-icon c-video__icon c-video__icon--play"> <use xlink:href="/_assets/dist/img/icons.svg#play"></use> </svg> </div> {% if videoTextOnVideo %} {{- videoTitle -}} {% if videoDuration|length %}<br>{{- videoDuration -}}{% endif %} {% endif %} </div> </a> {% if not videoTextOnVideo %} {% if videoMeta|length %}{{- videoMeta -}}<br>{% endif %} {{- videoTitle -}} {% endif %} {% endif %}

This is where things get really interesting.

The first thing you’ll notice is that our Twig component has 4 sections: Documentation, Parameters, Variables and Output. In our team we made it a rule that every single component must contain these 4 sections and match this template. We value uniformity in syntax, as it helps us getting closer to the illusion that our code was written by a single developer.

1. Documentation

The Documentation section explains what the component should be used for, which parameters are accepted in the component, the type of values every parameter requires and any other information that will help other developers understand what this Twig component is made of. Your future-self and other developers (both those who use Twig daily and those who are less familiar with it) will thank you for it.

dry-templating-twig-craft-fist-bump.jpg?mtime=20181026141457&focal=none?crop=focalpoint&domain=basedesign.imgix.net&fit=clip&w=500&h=341.17246080436&q=82&fm=pjpg&fp-x=0.5&fp-y=0.5&ixlib=php-2.1.1

Some potential co-workers of mine. By Rawpixel, via Unsplash

As every component is written into its own file, adding documentation is extremely easy and convenient (remember our _macros/components.html file: every macro was nothing more than an include to a file from the _componentsfolder).

We couldn’t find any official specifications to document Twig macros and their parameters so we created our own documentation syntax.

{# Name of the component Description of the component in a few words, ending with a dot. @param {string/bool/object/array/int} name (further information) ... #}

The kind of “further information” we are after when defining @params is:

  • A type/Model name, when relevant: (AssetFileModel)
  • A string's accepted values, when relevant: (values 'small'|'large')
  • Whether the parameter is optional: (optional)
  • A description, if the name is not enough: (YouTube video ID)

Add as many @params as needed but don't forget that having a large amount of parameters will bring more complexity into your code. Most of the components I create have roughly between 3 and 8 parameters. I've known situations where a component required a lot of flexibility and up to 12 parameters but I try to avoid this as much as possible. As a general rule, if something is too complex, think twice before turning it into a Twig component.

Finally, don’t forget to describe what arrays and objects (except Models) are made of. Without this information, other developers don’t know what information they can/must pass when calling a component. As an example, look at how we document this array of objects:

{# @param {array} srcSet (optional) [ {object} (Transform) ( { {string} mode {int} width (optional) {int} height (optional) } ] #}

2. Parameters

Once you’re done with the documentation, the time has finally come to write some code. In the Parameters section, we’ll define a variable for each and every parameter that we just documented. In other words, if we have defined 4 @params, we will create exactly 4 variables.

Each variable is named after the component + the name of the parameter and is written in camel case. For example, in a filtercomponent, we could have variables named filterLabel, filterName, filterValue and filterDefault.

Each variable is then assigned its own value that we get by calling the paramsobject (remember, it's the name of the object we pass to our macros defined in the _macros/components.html file) and its children, our parameters. As an example, this is what it looks like in a filter component we recently built at Base:

{% set filterLabel = params.label ?? null %} {% set filterName = params.name ?? null %} {% set filterValue = params.value ?? null %} {% set filterDefault = params.default ?? null %}

Every parameter must have a default value. In order to define our variable’s default value, we just add a double question mark followed by the value we want assigned. It’s important because when Twig can’t process the original value (often because an optional object key wasn’t defined) it won’t output any undefined error.

Here is another example with a project component:

{% set projectEntry = attr.entry ?? null %} {% set projectTitleLevel = attr.titleLevel ?? 2 %} {% set projectSize = attr.size ?? 'small' %}

You might wonder why we start our variable names with the component name. After all macros are closed environments so no need to be over-specific, right? That’s true but let’s not forget about error management: when Craft or Twig reports an error but doesn’t give much indication about which bit of code caused it, having the information that there is an issue with the titlevariable won't help you much if you have 21 title variables distributed across 27 components. Knowing that there is an issue with the projectTitlevariable makes it extremely easy to handle though.

3. Variables

At this point, we have all the information we need to start outputting content. Just before going down that road, take some time to fill the Variables section with any other information that you might need later. For example, based on the projectEntry parameter above, which is an ElementCriteriaModel, we can easily get some more information:

{% import '_macros/components' as components %} {% set projectTitleLevel = 2 %} {% set projectUrl = projectEntry.url ?? null %} {% set projectTitleMain = projectEntry.title ?? null %} {% set projectTitleSubtitle = projectEntry.subtitle ?? null %} {% set projectIntro = projectEntry.introduction ?? null %} {% set projectImage = projectEntry.image ?? null %}

Once again, we never define a variable without assigning a default value when it comes from an object. If things go astray, a value you can deal with will still be assigned to the variable. It helps reducing the amount of errors that Twig could stumble upon while rendering the code and results in a more stable website.

I sometimes leave the Variables section empty as some components don’t require more information than what is defined in the Parameters section. Recently though I find myself using it more and more so that the Output section (more about that in a few seconds) never requires me to go and look into object properties. That’s exactly why I have all these projectSomethingvariables in the code above. I find it helps me analysing the component and its content quicker.

4. Output

Finally, some HTML!

Let’s be honest, by now it feels like what you could have written in 5 minutes took you 15 instead. But consider it an investment because the Output section below is now way easier to deal with.

{% if videoId|length and videoImage|length %} <a class="c-video {{ videoClasses|join(' ') }} js-youtube-play" href="https://www.youtube.com/watch?v={{ videoId }}" target="_blank"> <div class="c-video__container"> <div class="c-video__player js-youtube-player" data-video="{{ videoId }}"></div> </div> <div class="c-video__placeholder" style="background-image: url('{{ videoImage.getUrl(videoCoverTransform) }}')"> <div class="c-video__controls"> <svg class="o-icon c-video__icon c-video__icon--play"> <use xlink:href="/_assets/dist/img/icons.svg#play"></use> </svg> </div> {% if videoTextOnVideo %} {{- videoTitle -}} {% if videoDuration|length %}<br>{{- videoDuration -}}{% endif %} {% endif %} </div> </a> {% if not videoTextOnVideo %} {% if videoMeta|length %}{{- videoMeta -}}<br>{% endif %} {{- videoTitle -}} {% endif %} {% endif %}

Nothing crazy happening here. What is interesting in this code piece though is that we always begin our output with a condition. If we don’t have some basic information there is no point in processing this component any further. In this case, without any value in the videoID and videoImage variables there is no need to go any further.

What Twig components changed for us

In the past few months we were able to embrace Twig components so deeply that the code in our templates is now basically 90% Twig components. And because of that, our development process has shifted drastically too.

Templating before front-end

The first thing we realised when adopting Twig components is that we couldn’t have anyone work on the frontend before a developer from the templating team created all needed Twig components. But that’s okay, we just shifted our process a bit and later even realized everyone works faster because of this.

Front-end developers do the job once

Instead of having to work on the HTML and add classes every time the equivalent of a component was outputted in a template, front-end developers can now do it only once in the component file. That’s huge!

When the same component needs to look different in different templates, the front-end developer can of course pass a classes parameter with multiple CSS classes.

...

@param {array} classes (optional) [ {string} ] ...

{% set ingredientClasses = params.classes ?? null %} ...

<div class="o-ingredient {{ ingredientClasses|join(' ') }}">

...

If I had to draw a chart of the efforts spent on frontend before and after we adopted Twig components, here is what it would look like:

dry-templating-twig-craft-graph.jpg?mtime=20181026141459&focal=none?crop=focalpoint&domain=basedesign.imgix.net&fit=clip&w=500&h=299.84662576687&q=82&fm=pjpg&fp-x=0.5&fp-y=0.5&ixlib=php-2.1.1

Twig components encourage documentation

We’ve had issues with documenting our code in the past but these issues disappeared as soon as we adopted Twig components and our 4-section template (Documentation + Parameters + Variables + Output).

Maintenance is so easy!

Well-documented Twig components make it 10 times easier to maintain and iterate on a project. Easier in the sense that it is less complex but also that I personally don’t postpone maintenance tasks anymore. Everything looks so easy and almost playful now!

That’s it!

Additionally, in the next few days I’ll share a Github demo repo that shows how to use Twig components in your next project. It may come in handy.

If you work with Craft CMS and Twig, I would love to hear what you think of this templating technique. Any feedback is appreciated. Please share it in the comments section below.

PS: By the way, I had to dig into the Craft CMS /app folder recently and I stumbled upon templates that follow a similar, albeit less far-reaching macros pattern. The fact that Pixel & Tonic, the People behind Craft CMS, do it as well makes me even more confident that the solution above is a very valuable way to work with Twig.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK