A Beginners Guide to Antlers Arrays and Loops

This in-depth guide provides a beginner-friendly introduction to Antlers arrays and loops. In it, we look at the basics of Antlers arrays, techniques for working with named scopes and nested arrays, and methods for randomly inserting supplementary content into blog post listings.

A Beginners Guide to Antlers Arrays and Loops
Published

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:

Example 1.1
1items:
2 - 'Item 1'
3 - 'Item 2'
4 - 'Item 3'

We can iterate this array using the following Antlers:

Example 1.2
1<ul>
2 {{ items }}
3 <li>{{ value }}</li>
4 {{ /items }}
5</ul>

Which produces the following output:

Example 1.3
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:

Table 1.1 Default Array Variables
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:

Example 1.4 Example Loop Variables 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

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:

Example 1.5 Example Loop Variables Template Output
1Value: Item 1
2Name: Item 1
3Index: 0
4Count: 1
5Total: 3
6First: true
7Last: false
8
9Value: Item 2
10Name: Item 2
11Index: 1
12Count: 2
13Total: 3
14First: false
15Last: false
16
17Value: Item 3
18Name: Item 3
19Index: 2
20Count: 3
21Total: 3
22First: false
23Last: 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:

Example 1.6 Changing Output Using first/last Variables
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:

Example 1.7
1The first item is: Item 1
2The last item is: Item 3

Conditionally Rendering Wrapper Elements

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:

Example 1.8
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:

Example 1.9
1{{ items | as('my_items') }}
2 <ul>
3 {{ my_items }}
4 <li>{{ value }}</li>
5 {{ /my_items }}
6 </ul>
7{{ /items }}
The as Modifier

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:

Example 1.10
1<ul>
2</ul>

To solve this, we could use a condition like the one in Example 1.11:

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:

Example 1.12
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.

Working with Neighboring Elements

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:

Example 1.13
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:

Example 1.14
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:

Example 1.15
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 2
10Current Title: Post 3
11Next 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:

Example 1.16
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:

Example 1.17
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:

Example 1.18
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:

Example 1.19
1Current Title: Post 1
2Next Title: Post 2
3 
4Previous Title: Post 1
5Current Title: Post 2
6Next Title: Post 3
7 
8Previous Title: Post 2
9Current Title: Post 3

Accessing Array Elements by Index

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:

Example 1.20
1articles:
2 -
3 id: 1
4 title: 'Article 1'
5 -
6 id: 2
7 title: 'Article 2'
8 -
9 id: 3
10 title: 'Article 3'
11 -
12 id: 4
13 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:

Example 1.21 Array Access Using Square Bracket Syntax
1{{ articles[0]title }}
2{{ articles[1]title }}
3{{ articles[2]title }}

Which produces the following results:

Example 1.22
1Article 1
2Article 2
3Article 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:

Example 1.23 Example of Invalid Array Access
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:

Example 1.24 Dynamic Array Access
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:

Example 1.25
1Previous:
2Current: Article 1
3Next: Article 2
4
5Previous: Article 1
6Current: Article 2
7Next: Article 3
8
9Previous: Article 2
10Current: Article 3
11Next: Article 4
12
13Previous: Article 3
14Current: Article 4
15Next:

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:

Example 1.26
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:

Example 1.27
1The first title: Article 1
2
3Previous:
4Current: Article 1
5Next: Article 2
6
7Previous: Article 1
8Current: Article 2
9Next: Article 3
10
11Previous: Article 2
12Current: Article 3
13Next: Article 4
14
15Previous: Article 3
16Current: Article 4
17Next:

Associative Arrays and YAML Maps

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:

Example 1.28
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];
Example 1.29
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: 3
12 title: 'Article 3'
13 -
14 id: 4
15 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:

Example 1.30
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:

Example 1.31
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.

Managing Array Data Scope

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:

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:

Example 1.34
1{{ articles }}
2 {{ index }} - {{ title }}
3{{ /articles }}

We would receive some rather interesting output:

Example 1.35
10 - Article 1
21 - Article 2
32 - Article 3
43 - 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:

Example 1.36 Applying Named Scopes to an Array
1{{ articles | scope('article') }}
2 {{ index }} - {{ article.title }}
3{{ /articles }}

Our output would now change to the following:

Example 1.37
10 - Article 1
21 - Article 2
32 - Article 3
43 -

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:

Example 1.38
1{{ articles | scope('article') }}
2 {{ index }} - {{ article }} {{ title }} {{ /article }}
3{{ /articles }}

Looping Key/Value Pairs

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:

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:

Example 1.40
1Attribute: id: the-page-id
2Attribute: title: Page Title
3Attribute: 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:

Example 1.41
1page:
2 id: the-page-id
3 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:

Example 1.42
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:

Example 1.43 Using the foreach Tag
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:

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" => 2
11 "index" => 1
12 "total_results" => 3
13 "first" => false
14 "last" => false
15 ]
16 "page" => array:3 [
17 "id" => "the-page-id"
18 "title" => "Page Title"
19 "content" => "The page content"
20 ]
21 "prev" => null
22 "total_results" => 3
23 "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:

Example 1.45
1{{ foreach:page as="value_name" }}
2 {{ key }}: {{ value_name }}
3{{ /foreach:page }}

Which produces the following output:

Example 1.46
1id: the-page-id
2title: Page Title
3content: 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:

Example 1.47
1{{ foreach:page as="key_name|value_name" }}
2 {{ key_name }}: {{ value_name }}
3{{ /foreach:page }}
Passing Multiple Values to Parameters

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.

Accessing the First and Last Array Elements

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:

Example 1.48
1articles:
2 -
3 id: 1
4 title: 'Article 1'
5 -
6 id: 2
7 title: 'Article 2'
8 -
9 id: 3
10 title: 'Article 3'
11 -
12 id: 4
13 title: 'Article 4'

We can access the first and last elements like so:

Example 1.49
1{{ articles | first }}
2 First: {{ title }}
3{{ /articles }}
4 
5{{ articles | last }}
6 Last: {{ title }}
7{{ /articles }}

Which produces the following output:

Example 1.50
1First: Article 1
2Last: Article 4

We can compare this to the iterative approach, which uses a condition to check against the first and last loop variables:

Example 1.51
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.

Accessing Arbitrary Array Ranges

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:

Example 1.52
1articles:
2 -
3 id: 1
4 title: 'Article 1'
5 -
6 id: 2
7 title: 'Article 2'
8 -
9 id: 3
10 title: 'Article 3'
11 -
12 id: 4
13 title: 'Article 4'
14 -
15 id: 5
16 title: 'Article 5'
17 -
18 id: 6
19 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:

Example 1.54
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:

Example 1.55
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:

Example 1.56
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:

Example 1.57
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:

Example 1.58 slice Modifier Implementation
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 Modifier
11{
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:

Example 1.59
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:

Example 1.61
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:

Example 1.62
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>

Checking for Even/Odd Indexes

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:

Example 1.63
1posts:
2 -
3 id: 1
4 title: 'Post 1'
5 -
6 id: 2
7 title: 'Post 2'
8 -
9 id: 3
10 title: 'Post 3'
11 -
12 id: 4
13 title: 'Post 4'
14 -
15 id: 5
16 title: 'Post 5'
17 -
18 id: 6
19 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:

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:

Example 1.65
1{{ posts }}
2 <div class="{{ switch between='one|two|three' }}">
3 {{ title }}
4 </div>
5{{ /posts }}

Which produces the following output:

Example 1.66
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.

Example 1.67
1{{ posts }}
2 <div class="{{ switch between='|even' }}">
3 {{ title }}
4 </div>
5{{ /posts }}

Our template in Example 1.67 produces the following output:

Example 1.68
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:

Example 1.69
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:

Example 1.70
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:

Example 1.71
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:

Example 1.73
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 }}

Inserting Content at Arbitrary or Random Locations

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:

Example 1.74
1posts:
2 -
3 id: 1
4 title: 'Post 1'
5 -
6 id: 2
7 title: 'Post 2'
8 -
9 id: 3
10 title: 'Post 3'
11 -
12 id: 4
13 title: 'Post 4'
14 -
15 id: 5
16 title: 'Post 5'
17 -
18 id: 6
19 title: 'Post 6'

For example, the template in Example 1.75 will insert additional content before the third blog post:

Example 1.75
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:

Example 1.76
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 Post
10 </div>
11 {{ /if }}
12 
13{{ /posts }}

Our template in Example 1.76 would produce the following output:

Example 1.77
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:

Example 1.78
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:

Example 1.79
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:

Example 1.80
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:

Example 1.81
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:

Example 1.82
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>

Recent articles

Stillat

Covering topics from JavaScript to Laravel, and everything in-between since 2012.

Resources

© Stillat, LLC. All right reserved.
Code highlighting provided by Torchlight