November 1, 2022 —John Koster
Arrays are the most versatile datatype when working with Antlers templates. Throughout this article, we will look at various ways to iterate arrays, define and manipulate them, and the default helper variables that exist to help you develop your project's templates as fast as possible.
Perhaps the simplest thing we can do when working with Antlers arrays is to loop over them and print the value contained within each element. When beginning your Antlers journey, this task can initially seem confusing or unobvious, mainly because there are no language constructs like "for" (there is a foreach tag, but we use that to iterate key/value pairs, which we will get to later). Instead of using dedicated looping keywords, we can iterate arrays in Antlers by constructing a tag pair from our variable. Consider the following data:
1items:2 - 'Item 1'3 - 'Item 2'4 - 'Item 3'
We can iterate this array using the following Antlers:
1<ul>2 {{ items }}3 <li>{{ value }}</li>4 {{ /items }}5</ul>
Which produces the following output:
1<ul>2 <li>Item 1</li>3 <li>Item 2</li>4 <li>Item 3</li>5</ul>
Our data in Example 1.1 contains an array named items
which includes a list of three strings. The template in Example 1.2 contains our variable "tag pair." When we want to iterate over an array, our tag pair will have the variable name as both the opening and closing tag names. For example, on line 2, we have {{ items }}
, which begins our tag pair, and on line 4 we have {{ /items }}
, which closes our tag pair.
When we have simple arrays, such as the one in Example 1.1, that contain no key/value pairs, we can use the variable named value
to retrieve the contents of each element in the list. We do this within our template on line 3 to produce the three <li>
elements in our output. The critical thing to notice when iterating Antlers arrays is that, by default, all the content that appears between our opening and closing tag repeats for each element in the array. This behavior means we do not have to introduce unnecessary boilerplate code or temporary variables to render a list of items on our site.
In addition to the value
variable, Antlers will add several additional variables for us automatically. These variables can help us determine our position within the array, whether or not the current element is the first or last element, quickly access neighboring elements and more. In total, there are nine variables that Antlers will make available to us for every array that we loop over:
Variable Name | Data Type | Description |
---|---|---|
value | Mixed | The value of the current array element. |
name | Mixed | An alias of value. |
index | Number | The current zero-based position of the element. |
count | Number | The current one-based position of the element. |
total_results | Number | The total number of items in the array. |
first | Boolean | Indicates if the element is the first item in the array. |
last | Boolean | Indicates if the element is the last item in the array. |
prev | Array/null | Provides access to the previous element's data. |
next | Array/null | Provides access to the next element's data. |
We can see these variables in action using the following template:
1{{ items }}2 Value: {{ value }}3 Name: {{ name }}4 Index: {{ index }}5 Count: {{ count }}6 Total: {{ total_results }}7 First: {{ first | bool_string }}8 Last: {{ last | bool_string }}9{{ /items }}
The bool_string
modifier in Example 1.4 will return the text representation for a boolean value. It will return the words "true" or "false," depending on the variable's value.
Our sample template in Example 1.4 is relatively straightforward but will allow us to analyze the results:
1Value: Item 1 2Name: Item 1 3Index: 0 4Count: 1 5Total: 3 6First: true 7Last: false 8 9Value: Item 210Name: Item 211Index: 112Count: 213Total: 314First: false15Last: false1617Value: Item 318Name: Item 319Index: 220Count: 321Total: 322First: false23Last: true
If we look at the output in Example 1.5, we can see how each of our variables changes as we output the data of each array element. We can see an interesting thing happen with our count
and index
variable. The {{ index }}
for each array element will always be its zero-based position, and the {{ count }}
variable will always be its one-based position. The count
variable can be confusing at first, as it is easy to misinterpret the count
variable to mean the total number of items in the array, which is what the total_results
variable will provide.
Our sample template in Example 1.4 simply prints a string version of our first
and last
variables using the bool_string
modifier, but we can also use them inside a condition to modify our output. For example, in Example 1.6, we use two conditions to change our output dynamically:
1{{ items }}2 {{ if first }}3 The first item is: {{ value }}4 {{ /if }}5 6 {{ if last }}7 The last item is: {{ value }}8 {{ /if }}9{{ /items }}
Which produces output similar to the following:
1The first item is: Item 12The last item is: Item 3
When working with arrays and lists of items, a frequent task we will encounter when building our sites is wrapping the final output in some container element. For example, the following Antlers template surrounds our dynamic output in an unordered list:
1<ul>2{{ items }}3 <li>{{ value }}</li>4{{ /items }}5</ul>
Our approach in Example 1.8 works well if we don't mind the wrapper elements always appearing in the output. However, if we wanted only to render the container element when the list contains items, we may attempt to use the following:
1{{ items | as('my_items') }}2 <ul>3 {{ my_items }}4 <li>{{ value }}</li>5 {{ /my_items }}6 </ul>7{{ /items }}
Example 1.9 uses the as
modifier to create a copy of our original array. We can use the as
modifier on any array, which is useful whenever we want to control where our output items appear without explicitly creating temporary variables.
Our template in Example 1.9 uses the as
modifier to create a named copy of our items array. Inside the items
tag pair, we proceed with our container element and loop over our new named array. If the items
variable does not exist, or its value is null
, our example would work as we expect and not render anything. However, if the items
array is empty, our template would still produce an empty unordered list element:
1<ul>2</ul>
To solve this, we could use a condition like the one in Example 1.11:
1{{ if items | length > 0 }}2<ul>3 {{ items }}4 <li>{{ value }}</li>5 {{ /items }}6</ul>7{{ /if }}
Our condition uses the length
modifier to retrieve the total number of items in our array (this returns the same result as the total_results
variable when we are inside an array's tag pair). This approach works, and our output would only contain the unordered wrapper list when our items
variable is not null
and contains items. However, we do have an opportunity to clean up our template's logic and remove the modifier like so:
1{{ if items }}2<ul>3 {{ items }}4 <li>{{ value }}</li>5 {{ /items }}6</ul>7{{ /if }}
Our new template in Example 1.12 would produce the same output as in Example 1.11. The reason this works is due to how Antlers determines a variable's "truthiness."
A non-existent variable or one set to null
will always return false
when performing a condition. However, if the variable does exist, and it is an array that contains no items, it will also return false
, allowing us to skip the length
modifier entirely.
The two automatic array variables we have not yet looked at are the next
and prev
variables. These two variables provide a convenient way to access the next and previous elements of the array, respectively. For example, if we had the following data:
1posts:2 -3 title: 'Post 1'4 -5 title: 'Post 2'6 -7 title: 'Post 3'
We can use the prev
and next
variables to quickly access the details of the neighboring elements at any position within our array:
1{{ posts }}2 Previous Title: {{ prev.title }}3 Current Title: {{ title }}4 Next Title: {{ next.title }}5{{ /posts }}
The template in Example 1.14 would produce results similar to the following:
1Previous Title: 2Current Title: Post 1 3Next Title: Post 2 4 5Previous Title: Post 1 6Current Title: Post 2 7Next Title: Post 3 8 9Previous Title: Post 210Current Title: Post 311Next Title:
Our output in Example 1.15 is pretty exciting and showcases how our template responds when using the prev
and next
variables. We can improve our template by using a few conditions to only output the relevant information when we have neighboring items:
1{{ posts }} 2 {{ if prev }} 3 Previous Title: {{ prev.title }} 4 {{ /if }} 5 6 Current Title: {{ title }} 7 8 {{ if next }} 9 Next Title: {{ next.title }}10 {{ /if }}11{{ /posts }}
Because the prev
and next
values are either null
or an array, we can achieve the same results by relying on the fact that Antlers will ignore tag pairs if the variable is null
:
1{{ posts }} 2 {{ prev }} 3 Previous Title: {{ title }} 4 {{ /prev }} 5 6 Current Title: {{ title }} 7 8 {{ next }} 9 Next Title: {{ title }}10 {{ /next }}11{{ /posts }}
One thing to notice in Example 1.17 is that when we use the next
and prev
variables as a tag pair, we no longer have to prefix the title
variable. We do not have to specify a prefix because the title within the tag pair will take precedence over any previously defined title
variable.
If we wanted to reduce the number of lines in our example, we could use the gatekeeper operator, which will evaluate an expression if the initial variable evaluates to true
:
1{{ posts }}2 {{ prev ?= 'Previous Title: {prev.title}' }}3 Current Title: {{ title }}4 {{ next ?= 'Next Title: {next.title}' }}5{{ /posts }}
Our gatekeeper operators appear on lines 2 and 4. The variable we will check for truthiness appears before the ?=
, and the expression to evaluate when the variable is true
appears to the right. In our example, the expression is a simple string using interpolation to insert the title of the previous or next post dynamically.
All of the previous conditional examples would produce output similar to the following:
1Current Title: Post 12Next Title: Post 23 4Previous Title: Post 15Current Title: Post 26Next Title: Post 37 8Previous Title: Post 29Current Title: Post 3
We can access array data directly by its index in many different ways. The first and most recognizable is by using the square bracket syntax.
We will use the sample data in Example 1.20 for our discussions:
1articles: 2 - 3 id: 1 4 title: 'Article 1' 5 - 6 id: 2 7 title: 'Article 2' 8 - 9 id: 310 title: 'Article 3'11 -12 id: 413 title: 'Article 4'
If we already know the index of our array elements, we can use the square bracket syntax to access our array elements like so:
1{{ articles[0]title }}2{{ articles[1]title }}3{{ articles[2]title }}
Which produces the following results:
1Article 12Article 23Article 3
When working with the square bracket syntax, it is important to note that we do not need to add special characters such as :
or .
after the closing bracket. For example, the following examples would not be correct:
1{{# Invalid #}}2{{ articles[0]:title }}3{{ articles[1].title }}
We can also dynamically construct our indexes using the square bracket syntax by interpolating inside the square brackets. For example, we can implement the behavior of the prev
and next
variables using dynamic interpolation:
1{{ articles }}2 {{# Access the previous element. #}}3 Previous: {{ articles[{index - 1}]title }}4 5 Current: {{ title }}6 7 {{# Access the next element. #}}8 Next: {{ articles[{index + 1}]title }}9{{ /articles }}
Our example in Example 1.24 produces the results in Example 1.25, which is what we would expect:
1Previous: 2Current: Article 1 3Next: Article 2 4 5Previous: Article 1 6Current: Article 2 7Next: Article 3 8 9Previous: Article 210Current: Article 311Next: Article 41213Previous: Article 314Current: Article 415Next:
While we have successfully implemented dynamic array access, the combination of square and curly brackets can quickly become difficult to read. Luckily, we can use the dot syntax to retrieve known indexes in addition to dynamic indexes:
1The first title: {{ articles.0.title }} 2 3{{ articles }} 4 {{# Access the previous element. #}} 5 Previous: {{ articles.{index - 1}.title }} 6 7 Current: {{ title }} 8 9 {{# Access the next element. #}}10 Next: {{ articles.{index + 1}.title }}11{{ /articles }}
Which produces the following output:
1The first title: Article 1 2 3Previous: 4Current: Article 1 5Next: Article 2 6 7Previous: Article 1 8Current: Article 2 9Next: Article 31011Previous: Article 212Current: Article 313Next: Article 41415Previous: Article 316Current: Article 417Next:
The behavior of an array tag pair changes quite significantly when we work with associative arrays or YAML maps. When Antlers encounters these types of structures, it changes its behavior to make all key/value pairs available as new variables within the tag pair, overwriting any previous variables with the same name. This behavior injects variables from tags, such as the collection and form tags, as well as from specific modifiers.
Let us take a look at the PHP array in Example 1.28 and its equivalent YAML map in Example 1.29:
1<?php 2 3$data = [ 4 'results' => [ 5 'has_results' => true, 6 'articles' => [ 7 [ 'id' => 1, 'title' => 'Article 1' ], 8 [ 'id' => 2, 'title' => 'Article 2' ], 9 [ 'id' => 3, 'title' => 'Article 3' ],10 [ 'id' => 4, 'title' => 'Article 4' ],11 ]12 ]13];
1results: 2 has_results: true 3 articles: 4 - 5 id: 1 6 title: 'Article 1' 7 - 8 id: 2 9 title: 'Article 2'10 -11 id: 312 title: 'Article 3'13 -14 id: 415 title: 'Article 4'
We know from the previous section we can access values within the results
array by their index or sub-path like so:
1{{# Outputs "true" #}}2{{ results.has_results | bool_string }}3 4{{# Outputs "Article 3" #}}5{{ results.articles.2.title }}
However, if we were to use the results
variable as a tag pair, we would instead receive access to all nested key/value pairs as variables:
1{{ results }}2 {{ has_results | bool_string }}3 4 {{ articles }}5 {{ title }}6 {{ /articles }}7{{ /results }}
Notice that in Example 1.31, we no longer have to prefix all of our variables with the results. When we are inside the tag pair of an associative array or YAML map, we will get access to that array's or map's key/value pairs as new variables. For instance, on line 2, we use the variable name has_results
instead of results.has_results
. Similarly, between lines 4 and 6, we have a second tag pair for the nested articles
array.
Figure 1.32 provides a visual representation of where our data ends up in our template. We can see that everything inside the results tag pair is made available inside the tag pair. Once we leave the tag pair, our variable scope resets to what it was before we created the tag pair.
Whenever we are inside a tag pair and attempt to access a variable that does not exist, Antlers will attempt to locate it somewhere else by moving "up" what is called the Cascade. In simple terms, it will continue searching for a variable at all nesting levels until it finds a match. If Antlers cannot find a match, Antlers will then attempt to execute a tag with that same name if it exists, and if no tag exists, it will simply return null. Consider the data in Example 1.33:
1title: 'Page Title' 2articles: 3 - 4 title: 'Article 1' 5 - 6 title: 'Article 2' 7 - 8 title: 'Article 3' 9 -10 another_title: 'Article 4'
If we were to iterate the articles and output the value of the title
variable with the following template:
1{{ articles }}2 {{ index }} - {{ title }}3{{ /articles }}
We would receive some rather interesting output:
10 - Article 121 - Article 232 - Article 343 - Page Title
Our fourth article does not contain a title
variable, and our output instead displays the value "Page Title." This behavior can be confusing initially, especially if you anticipated the value being null
. However, this behavior makes it simpler to access entry data inside nested scopes, such as using an entry's title inside an image's alt text.
However, if we want to ensure that we receive a null
value if the variable does not exist for the current tag pair, we can use the scope modifier to create a new array containing the data we are interested in displaying. For example, if we modified our template to use the scope modifier like so:
1{{ articles | scope('article') }}2 {{ index }} - {{ article.title }}3{{ /articles }}
Our output would now change to the following:
10 - Article 121 - Article 232 - Article 343 -
When we use the scope modifier, it is essential to note that we must always use the nested variable syntax. If we were to access our new article
variable using a tag pair, the fallback logic would activate, leading to our undesired output:
1{{ articles | scope('article') }}2 {{ index }} - {{ article }} {{ title }} {{ /article }}3{{ /articles }}
In the previous section, we learned that creating a tag pair from an associative array or YAML map will extract the nested key/value pairs into new variables. However, there are times when it is desirable to loop over an associative array and dynamically print both the key and value to the screen.
For example, in PHP, we could use a foreach loop, like the one found in Example 1.39:
1<?php 2 3$data = [ 4 'id' => 'the-page-id', 5 'title' => 'Page Title', 6 'content' => 'The page content', 7]; 8 9foreach ($data as $key => $value) {10 echo 'Attribute: ', $key, ': ', $value, "\n";11}
Our loop in Example 1.39 would produce the following results:
1Attribute: id: the-page-id2Attribute: title: Page Title3Attribute: content: The page content
How can we accomplish something like this in Antlers? We will use the sample YAML data in Example 1.41 throughout the remainder of this section:
1page:2 id: the-page-id3 title: 'Page Title'4 content: 'The page content'
If we knew all of the keys ahead of time, we could hardcode them in our template like so:
1id: {{ page.id }}2title: {{ page.title }}3content: {{ page.content }}
Our sample template in Example 1.42 works, but it is not incredibly helpful if we don't know the keys ahead of time or want our templates to be dynamic. Luckily, we can use the foreach tag to achieve results similar to our PHP loop:
1{{ foreach:page }}2 {{ key }}: {{ value }}3{{ /foreach:page }}
But how does this work? If we were to look at the data the foreach tag returns for the first item, we would discover that the available data is similar to the data in Example 1.44:
1array:10 [ 2 "count" => 1 3 "first" => true 4 "index" => 0 5 "key" => "id" 6 "last" => false 7 "next" => array:7 [ 8 "key" => "title" 9 "value" => "Page Title"10 "count" => 211 "index" => 112 "total_results" => 313 "first" => false14 "last" => false15 ]16 "page" => array:3 [17 "id" => "the-page-id"18 "title" => "Page Title"19 "content" => "The page content"20 ]21 "prev" => null22 "total_results" => 323 "value" => "the-page-id"24]
From our sample data in Example 1.44, we can see that the foreach tag adds new array keys to our data and returns a modified array. We can rename the key/value names within the resulting array by supplying a value for the as
parameter. If we only supply one value to the as
parameter, it will rename the value of the key/value pair. As an example, let us consider the following template:
1{{ foreach:page as="value_name" }}2 {{ key }}: {{ value_name }}3{{ /foreach:page }}
Which produces the following output:
1id: the-page-id2title: Page Title3content: The page content
We can see from Example 1.45 that we can access our value by using the new value_name
variable. Suppose we were to supply two arguments to the as
parameter. In that case, the first argument will rename the key, and the second will rename the value:
1{{ foreach:page as="key_name|value_name" }}2 {{ key_name }}: {{ value_name }}3{{ /foreach:page }}
When we need to pass multiple values to tag parameters, we separate each parameter using the vertical pipe character (|
). This differs from modifiers, which have their arguments separated by commas, much like when calling PHP functions.
Suppose we wanted to access only the first and last items of an array without looping through all of the items in the list. We can use the first
and last
modifiers to easily access the data for these items. Consider the following data:
1articles: 2 - 3 id: 1 4 title: 'Article 1' 5 - 6 id: 2 7 title: 'Article 2' 8 - 9 id: 310 title: 'Article 3'11 -12 id: 413 title: 'Article 4'
We can access the first and last elements like so:
1{{ articles | first }}2 First: {{ title }}3{{ /articles }}4 5{{ articles | last }}6 Last: {{ title }}7{{ /articles }}
Which produces the following output:
1First: Article 12Last: Article 4
We can compare this to the iterative approach, which uses a condition to check against the first
and last
loop variables:
1{{ articles }} 2 3 {{ if first }} 4 First: {{ title }} 5 {{ /if }} 6 7 8 {{ if last }} 9 Last: {{ title }}10 {{ /if }}11 12{{ /articles }}
The template for our iterative approach is much longer than our example template in Example 1.49. It will also iterate every item within the articles array to achieve the same output, which may not be desirable if there are many articles.
In the previous section, we looked at how we can use the first and last modifiers to retrieve their individual items from an array. We can also use the limit and offset modifiers to retrieve ranges of elements from an array or a subset of the collection. Using all of these modifiers together enables us to produce some exciting results. Let us consider the following dataset containing six articles:
1articles: 2 - 3 id: 1 4 title: 'Article 1' 5 - 6 id: 2 7 title: 'Article 2' 8 - 9 id: 310 title: 'Article 3'11 -12 id: 413 title: 'Article 4'14 -15 id: 516 title: 'Article 5'17 -18 id: 619 title: 'Article 6'
Suppose we wanted to structure a template similar to that in Figure 1.53. In that case, it can be overwhelming initially to think of where we should begin.
We know from the previous section accessing the first and last items will be pretty trivial, as we can just use the modifiers to retrieve them:
1<div class="first_container"> 2 {{ articles.0 }} 3 First: {{ title }} 4 {{ /articles.0 }} 5</div> 6 7<div class="last_container"> 8 {{ articles | last }} 9 Last: {{ title }}10 {{ /articles }}11</div>
To render the list below the first and last items is a little more nuanced. We need to do the equivalent of PHP's array_slice
function, which is possible by combining the offset and limit modifiers. The offset modifier will effectively let us skip over a certain number of items, and limit will remove all items from the list over whatever length we want.
Our implementation of this may look like the following:
1<ul class="list_container">2 {{ articles | offset(1) | limit((articles | length) - 2) }}3 <li>{{ title }}</li>4 {{ /articles }}5</ul>
The templates in Example 1.54 and Example 1.55 would produce the following HTML output:
1<ul class="list_container">2 <li>Article 2</li>3 <li>Article 3</li>4 <li>Article 4</li>5 <li>Article 5</li>6</ul>
Looking at Example 1.55, we have a rather odd thing happening on line 2. We are subtracting two from the length of the "articles" array instead of one. We remove two to account for the first item we skipped and the last item we don't want to be included in our resulting array. This, however, can be somewhat confusing to look at, and we can improve the readability of our template significantly:
1<ul class="list_container">2 {{ articles | offset(1) | limit(-1) }}3 <li>{{ title }}</li>4 {{ /articles }}5</ul>
We can remove the inline calculation by utilizing a negative limit in Example 1.57. When using negative offsets, we can think of them as removing items from the end of the array. The absolute value of the negative offset is the number of things that will be removed for us.
We can create a new Antlers modifier that will help us simplify our template even more. Our new modifier will be named slice
. It will allow us to combine the behavior of both the offset and limit modifiers.
In app/Modifiers/Slice.php
:
1<?php 2 3namespace App\Modifiers; 4 5use Illuminate\Support\Arr; 6use Illuminate\Support\Collection; 7use Statamic\Facades\Compare; 8use Statamic\Modifiers\Modifier; 9 10class Slice extends Modifier11{12 public function index($value, $params, $context)13 {14 $offset = Arr::get($params, 0, 0);15 $length = Arr::get($params, 1, null);16 17 if (Compare::isQueryBuilder($value)) {18 $value = $value->get();19 }20 21 if ($value instanceof Collection) {22 return $value->slice($offset, $length);23 }24 25 return array_slice($value, $offset, $length);26 }27}
Using our new slice
modifier, we can refactor our template to the one in Example 1.59 while achieving the same end results:
1<ul class="list_container">2 {{ articles | slice(1, -1) }}3 <li>{{ title }}</li>4 {{ /articles }}5</ul>
As another example, let us suppose we wanted to produce a layout similar to Figure 1.60. However, let us all say that we need to output two different wrapper elements. The first will contain the first two items in our array, and the second container element will include the remaining items.
We could accomplish this using various conditions, but we can combine all the techniques we have encountered so far to achieve this:
1<div class="container_one"> 2 <div class="item_one"> 3 {{ articles.0 }} 4 {{ title }} 5 {{ /articles.0 }} 6 </div> 7 <div class="item_two"> 8 {{ articles.1 }} 9 {{ title }}10 {{ /articles.1 }}11 </div>12</div>13 14<div class="container_two">15 {{ articles | offset(2) }}16 <div class="item">17 {{ title }}18 </div>19 {{ /articles }}20</div>
Our template in Example 1.61 would produce output similar to the following:
1<div class="container_one"> 2 <div class="item_one"> Article 1 </div> 3 <div class="item_two"> Article 2 </div> 4</div> 5<div class="container_two"> 6 <div class="item"> Article 3 </div> 7 <div class="item"> Article 4 </div> 8 <div class="item"> Article 5 </div> 9 <div class="item"> Article 6 </div>10</div>
There are numerous ways to check if an array index is even or odd within an Antlers template. The first and simplest is to use the switch
tag to alternate between two classes. We will use the data in Example 1.63 throughout this section:
1posts: 2 - 3 id: 1 4 title: 'Post 1' 5 - 6 id: 2 7 title: 'Post 2' 8 - 9 id: 310 title: 'Post 3'11 -12 id: 413 title: 'Post 4'14 -15 id: 516 title: 'Post 5'17 -18 id: 619 title: 'Post 6'
If all we need to do is output an alternating class, the switch
tag is perfect for this. As an example, let us take a look at the template in Example 1.64:
1{{ posts }}2 <div class="{{ switch between='odd|even' }}">3 {{ title }}4 </div>5{{ /posts }}
Every time the switch
tag is evaluated by the Antlers engine, it will select the next class in our list, starting at the first class. When it is first evaluated, it will return "odd." The second time it is evaluated, it will return "even." Once we reach the end of the list of possible classes, it will start over.
The switch
tag is not limited to two arguments; we can supply as many as we want. For instance, in Example 1.65, we provide three classes. The switch
tag will rotate between all three of these:
1{{ posts }}2 <div class="{{ switch between='one|two|three' }}">3 {{ title }}4 </div>5{{ /posts }}
Which produces the following output:
1<div class="one"> Post 1 </div>2<div class="two"> Post 2 </div>3<div class="three"> Post 3 </div>4<div class="one"> Post 4 </div>5<div class="two"> Post 5 </div>6<div class="three"> Post 6 </div>
We can take advantage of this behavior by supplying empty values to effectively not emit any classes at specific indexes in our output.
1{{ posts }}2 <div class="{{ switch between='|even' }}">3 {{ title }}4 </div>5{{ /posts }}
Our template in Example 1.67 produces the following output:
1<div class=""> Post 1 </div>2<div class="even"> Post 2 </div>3<div class=""> Post 3 </div>4<div class="even"> Post 4 </div>5<div class=""> Post 5 </div>6<div class="even"> Post 6 </div>
Utilizing the switch
tag is beneficial when all we want to do is alternate between a list of possible outputs. However, if we want to dramatically change the structure of our HTML, that can be slightly more challenging to pull off with the switch
tag alone.
We can alternate between two structures within our template by using the modulo operator and checking if the current index is divisible by two. This is accomplished by checking if the result is zero. For example, we could use the following template to alternate between the "odd" and "even" class, just like our switch
tag implementation:
1{{ posts }}2 <div class="{{ if index % 2 == 0 }}odd{{ else }}even{{ /if }}">3 {{ title }}4 </div>5{{ /posts }}
Our template in Example 1.69 works to alternate between our classes, but the inline if
can feel clunky. We can simplify our template by using the ternary operator:
1{{ posts }}2 <div class="{{ (index % 2 == 0) ? 'odd' : 'even' }}">3 {{ title }}4 </div>5{{ /posts }}
At first, the logic in our template might seem counterintuitive. After all, we output the value "odd" when the expression is true. It is important to remember that the index
variable starts counting at zero, which will have the effect of reversing our logic. Suppose we were to use the count
variable instead, which begins counting at one. In that case, we could put our classes in a more understandable order:
1{{ posts }}2 <div class="{{ (count % 2 == 0) ? 'even' : 'odd' }}">3 {{ title }}4 </div>5{{ /posts }}
While our inline if
statement in Example 1.69 can make things difficult to read, we can use an if
statement to help produce output similar to that in Figure 1.72:
The template logic to implement something similar to Figure 1.72 might be similar to:
1{{ posts }} 2 3 {{ if count % 2 == 0 }} 4 <div class="even_layout"> 5 </div> 6 {{ else }} 7 <div class="odd_layout"> 8 </div> 9 {{ /if }}10 11{{ /posts }}
It is not an uncommon requirement to be able to insert additional content within a listing of collection entries. An example of this may be to insert an advertisement at specific locations or to randomly insert supplementary or featured content throughout the regular content listing.
We can insert supplementary content at fixed locations by checking against the current index and generating different outputs.
We will use the following sample blog posts for this section:
1posts: 2 - 3 id: 1 4 title: 'Post 1' 5 - 6 id: 2 7 title: 'Post 2' 8 - 9 id: 310 title: 'Post 3'11 -12 id: 413 title: 'Post 4'14 -15 id: 516 title: 'Post 5'17 -18 id: 619 title: 'Post 6'
For example, the template in Example 1.75 will insert additional content before the third blog post:
1{{ posts }} 2 3 {{ if count == 3 }} 4 <div class="ad post"> 5 Additional Content 6 </div> 7 {{ /if }} 8 9 <div class="post">10 {{ title }}11 </div>12 13{{ /posts }}
We can expand on this approach to insert content at other known locations by checking for different combinations of the index
or count
variables. In addition to inserting content at known locations, we can even reintroduce the modulus operator to insert content at fixed intervals, such as after every third blog post:
1{{ posts }} 2 3 <div class="post"> 4 {{ title }} 5 </div> 6 7 {{ if count % 3 == 0 }} 8 <div class="ad post"> 9 Additional Content After Every Third Post10 </div>11 {{ /if }}12 13{{ /posts }}
Our template in Example 1.76 would produce the following output:
1<div class="post"> Post 1 </div>2<div class="post"> Post 2 </div>3<div class="post"> Post 3 </div>4<div class="ad post"> Additional Content After Every Third Post </div>5<div class="post"> Post 4 </div>6<div class="post"> Post 5 </div>7<div class="post"> Post 6 </div>8<div class="ad post"> Additional Content After Every Third Post </div>
Our approach of checking the current index or count variable works but can quickly become tedious as the number of arbitrary locations grows. Not only that, but things become even more complicated if we need to randomly distribute supplementary content throughout our list of blog posts.
For this, I like to create a custom tag or modifier to handle this with PHP. With this approach, we can more easily express our intended logic and leverage PHP and framework features that may be difficult to express in Antlers. Suppose we want to insert a random entry from another collection and have them randomly distributed throughout our list of blog posts.
We can achieve this behavior by creating a custom modifier that we can use to insert random content into our blog posts.
In app/Modifiers/InsertContent.php
:
1<?php 2 3namespace App\Modifiers; 4 5use Statamic\Facades\Entry; 6use Statamic\Modifiers\Modifier; 7 8class InsertContent extends Modifier 9{10 public function index($value, $params, $context)11 {12 $items = collect($value)->map(function ($item) {13 return [14 'type' => 'content',15 'content' => $item,16 ];17 });18 19 $itemsToInsert = floor(count($value) * 0.25);20 21 if ($itemsToInsert < 1) {22 return $items->all();23 }24 25 $pages = Entry::whereCollection('pages')->shuffle()26 ->take($itemsToInsert)->all();27 28 foreach ($pages as $page) {29 30 // Get a random index to insert the content at.31 $index = rand(0, $items->count() - 1);32 33 // Use the splice method to insert our new item.34 $items->splice($index, 0, [35 [36 'type' => 'page',37 'content' => $page,38 ],39 ]);40 }41 42 return $items->all();43 }44}
Before we look at the Antlers template, let us spend some time discussing what our modifier is accomplishing in Example 1.78. Between lines 12 and 17, we convert all our blog posts into a nested array. This will behave similarly to the as
modifier and allow us to differentiate between our original blog posts and our inserted content.
On line 19, we are calculating the total amount of random items we want to insert into our list of blog posts. This logic can be adjusted to suit your project's specific needs. Our example will limit the number of inserted posts to 25% of the total number of blog posts. For example, if we had four blog posts, this logic would ensure we only insert one piece of supplementary content.
Because our total number of items to insert is dynamic, it may be zero. We account for this between lines 21 and 23.
Between lines 25 and 27, we are retrieving random entries from our pages
collection up to our total number of items to insert. We are using another Statamic collection here, but the source of our additional content could be anything from multiple collections to the results of some external API.
The actual insertion of our additional content happens between lines 28 and 40. On line 31, we calculate a random index to insert our other content. We use the splice
method to perform the actual insertion of our additional content. The vital thing to note on line 36 is that we are assigning a different type
value for our inserted content. This will let us structure our inserted content differently within our template.
Finally, on line 42, we return all items, including all inserted content.
Let us look at how we may use this modifier in our Antlers templates. In Example 1.79, we have added our new insert_content
modifier to apply its logic to our blog posts:
1{{ posts | insert_content }} 2 {{ if type == 'content' }} 3 <div class="post"> 4 {{ content.title }} 5 </div> 6 {{ elseif type == 'page' }} 7 <div class="inserted content"> 8 {{ content.title }} 9 </div>10 {{ /if }}11{{ /posts }}
It is also important to note how we have adjusted our template to check the current type
. This is similar to creating templates for the Bard or Replicator fieldtypes.
If we had less than four blog posts, our template would produce the following output every time:
1<div class="post"> Post 1 </div>2<div class="post"> Post 2 </div>3<div class="post"> Post 3 </div>
This is because our logic rounds down the total number of posts (using the floor
function). If we wanted to always at least insert one piece of additional content, we could use the ceil
function instead.
Running our template on four blog posts would always insert one piece of additional content at a random location. Example output for this behavior might be:
1<div class="post"> Post 1 </div>2<div class="inserted content"> Home </div>3<div class="post"> Post 2 </div>4<div class="post"> Post 3 </div>5<div class="post"> Post 4 </div>
The exciting thing, however, is that as the number of blog posts increases, the total number of additional content will always increase up to the total limit and availability of supplemental content. As an example, the following output was generated from a list of 16 blog posts, which caused 4 pages to be inserted randomly:
1<div class="post"> Post 1 </div> 2<div class="post"> Post 2 </div> 3<div class="inserted content"> Articles </div> 4<div class="post"> Post 3 </div> 5<div class="post"> Post 4 </div> 6<div class="inserted content"> About </div> 7<div class="post"> Post 5 </div> 8<div class="post"> Post 6 </div> 9<div class="post"> Post 7 </div>10<div class="inserted content"> Topics </div>11<div class="post"> Post 8 </div>12<div class="post"> Post 9 </div>13<div class="post"> Post 10 </div>14<div class="post"> Post 11 </div>15<div class="post"> Post 12 </div>16<div class="post"> Post 13 </div>17<div class="post"> Post 14 </div>18<div class="post"> Post 15 </div>19<div class="inserted content"> Home </div>20<div class="post"> Post 16 </div>
∎
The following amazing people help support this site and my open source projects ♥️
If you're interesting in supporting my work and want to show up on this list, check out my GitHub Sponsors Profile.