Last time, we explored the benefits of (1) unconditionally setting variables with the default
filter and (2) requiring fields. We'll wrap up with three more ways to remove control flow from your code.
3. Route your content
Let's say we are templating a structure with multiple entry types: neighborhood
, city
, and state
.
In our _entry
template, we might create a switch
statement to include sub templates that are unique to the entry type.
{# _entry #}
...
{% switch entry.type %}
{% case 'neighborhood' %}
{% include "_includes/neighborhood" %}
{% case 'city' %}
{% include "_includes/city" %}
{% case 'state' %}
{% include "_includes/state" %}
{% endswitch %}
...
Not bad. We are still keeping our code DRY by including only the content that is different.
The flaw here is subtle – notice that we are telling our template how to identify the correct template based on the entry type.
If we add another type called country
, we suddenly have two areas to maintain: the new country
include and our switch
statement.
Can we take the control flow—the how—out of the equation and just tell the template what we want?
Let's make two assumptions about these entry types:
- Their names cannot be influenced by the user
- They always have an associated
include
Both are realistic expectations. Now, we can dynamically declare the correct include
by concatenating the entry type to the template lookup.
{# _entry #}
...
{% include "_includes/" ~ entry.type %}
...
We just turned this template into a router.
This technique is a simple application of polymorphism—providing the same interface for different types. Pretty slick, huh?
Let's see how well this approach works for a Craft matrix
with three block types: plainText
, image
, and button
. Here's the switch
way...
{% for block in matrix %}
{% switch block.type %}
{% case 'plainText' %}
<p>{{ block.plainText }}</p>
{% case 'image' %}
<figure>
<img src="{{ block.img.first().getUrl() }}">
{% if block.caption %}
<figcaption>{{ block.caption }}</figcaption>
{% endif %}
</figure>
{% case 'button' %}
<a href="{{ block.websiteUrl }}" class="btn btn--large">
{{ block.text }}
</a>
{% endswitch %}
{% endfor %}
…and the declarative way:
{% for block in matrix %}
{% include "_includes/" ~ block.type %}
{% endfor %}
Which is better in this case? It depends on your matrix. The plain text include
would look like this:
<p>{{ block.plainText }}</p>
Feels a bit lonely, right? Even our image
sub template is a little sad:
<figure>
<img src="{{ block.img.first().getUrl() }}">
{% if block.caption %}
<figcaption>{{ block.caption }}</figcaption>
{% endif %}
</figure>
So is the lowly switch
better?
Let's say we have more than one type of matrix in Craft (we'll call it anotherMatrix
), but they share some of their block types. What if we stored all of our block type templates in a folder called _blocks
to be accessible by both?
Now routing really shows off its colors.
TL;DR: The more complex your data, the more benefits you get from routing instead of switching.
4. Block sections that may change
Blocks are to sections what the default
filter is to variables: forgiving.
If you find yourself rewriting a section of _layout
in only one or two sub templates, a block
is the perfect solution to declare an easily overridable default.
Marketing sites often share common sections between templates, like a subscribe form or a testimony carousel. Let's block
these below our content block (not inside—Twig will crash if you block within another block):
{# _layout #}
{% block content %}
{% endblock %}
{% block global %}
{% include "_includes/subscribe" %}
{% include "_includes/_testimony" %}
{% endblock %}
We may not want these default sections to show up in the blog index. No problem!
{# blog/index #}
{% extends "_layout" %}
{% block content %}
...
{% endblock %}
{% block global %}
{# empties block #}
{% endblock %}
We can also add to the block with {{ parent() }}
:
{% block global %}
{{ parent() }}
{% include "_includes/archive" %}
{% endblock %}
Blocks are also great for liquid sections like scripts, meta data, and the hero section.
TL;DR: If a section in your _layout
changes frequently but needs sane defaults, use a block
to DRY your code but allow sub template customization.
5. Relocate your code
Often, we can remove the question mark from our templating by simply migrating code up or down the tree structure to a template where we know certain variables exists. Here's the polite way:
{# _layout #}
{% if craft.request.getSegment(2) == 'blog' %}
{% set headline = global.fullName ~ "'s Blog" %}
{% elseif entry is defined and entry.section == 'calendar' %}
{% set headline = 'Event: ' ~ [entry.startDate, entry.endDate] | join(' - ') %}
{% elseif entry is defined %}
{% set headline = entry.title %}
{% else %}
{% set headline = siteName %}
{% endif %}
<header>
<h1>{{ headline }}</h1>
</header>
And here's the declarative way.
{# _layout #}
{% set headline = headline | default(entry.title | default(siteName)) %}
<header>
<h1>{{ headline }}</h1>
</header>
{# blog/index #}
{% set headline = global.fullName ~ "'s Blog" %}
{# calendar/_entry #}
{% set headline = 'Event: ' ~ [entry.startDate, entry.endDate] | join(' - ') %}
TL;DL: Before pulling out the if statement, ask yourself if the information is defined in a parent or child template.
Conclusion
You will never need to use an if statement after reading this article, right?
There are still many situations where "iffing" is the most idiomatic solution, but if you find yourself conditionally creating variables and nesting your if statements, there may be a more sustainable solution.
TL;DR: When you are unsure of your content, declare before you ask. Add dependable fallbacks with Twig's default
filter and require the fields you absolutely need. Route your data where you can, block
sections for easy overriding, and move your code to where your variable call is not a question.
I'm a green developer (it's my 1 year anniversary!), so if you've discovered other ways to simplify code and defeat non-existent content, comment below!