Laravel Blade Recursive Partials with @each
In this tutorial, we’ll go through the process of implementing recursive partials in Laravel’s Blade templating engine by means of the @each
command. This will allow us to render data structures with an arbitrary number of nested children without needing to know the maximum depth of the array.
The Data
The data I’m talking about is data like folder structures which can go deep into many levels. For our case, let’s imagine we’re dealing with a predefined data set of “Projects” in a todo application like Todoist. Feel free to grab the sample data from this gist or the code embed below:
$a = array(
0 => array(
'indent' => 1,
'name' => 'Inbox',
'color' => '#dddddd',
'is_deleted' => 0,
'collapsed' => 0,
'inbox_project' => true,
'archived_date' => null,
'item_order' => 0,
'is_archived' => 0,
'archived_timestamp' => 0,
'user_id' => 3840103,
'id' => 138837507,
'children' => array(),
'parent' => 'root',
),
1 => array(
'indent' => 1,
'name' => 'Personal',
'color' => '#fc603c',
'is_deleted' => 0,
'collapsed' => 0,
'archived_date' => null,
'item_order' => 1,
'is_archived' => 0,
'archived_timestamp' => 0,
'user_id' => 3840103,
'id' => 138837508,
'children' => array(),
'parent' => 'root',
),
2 => array(
'indent' => 1,
'name' => 'Work',
'color' => '#a8c9e5',
'is_deleted' => 0,
'collapsed' => 0,
'archived_date' => null,
'item_order' => 2,
'is_archived' => 0,
'archived_timestamp' => 0,
'user_id' => 3840103,
'id' => 138837509,
'children' => array(
0 => array(
'indent' => 2,
'name' => 'Work indent 1-1',
'color' => '#a8c9e5',
'is_deleted' => 0,
'collapsed' => 0,
'archived_date' => null,
'item_order' => 3,
'is_archived' => 0,
'archived_timestamp' => 0,
'user_id' => 3840103,
'id' => 139576614,
'children' => array(
0 => array(
'indent' => 3,
'name' => 'Work indent 1-2',
'color' => '#dddddd',
'is_deleted' => 0,
'collapsed' => 0,
'archived_date' => null,
'item_order' => 4,
'is_archived' => 0,
'archived_timestamp' => 0,
'user_id' => 3840103,
'id' => 139576626,
'children' => array(),
'parent' => 139576614,
),
1 => array(
'indent' => 3,
'name' => 'Work indent 1-2 2nd',
'color' => '#dddddd',
'is_deleted' => 0,
'collapsed' => 0,
'archived_date' => null,
'item_order' => 5,
'is_archived' => 0,
'archived_timestamp' => 0,
'user_id' => 3840103,
'id' => 139576629,
'children' => array(),
'parent' => 139576614,
),
),
'parent' => 138837509,
),
1 => array(
'indent' => 2,
'name' => 'Work indent 2-1',
'color' => '#a8c9e5',
'is_deleted' => 0,
'collapsed' => 0,
'archived_date' => null,
'item_order' => 6,
'is_archived' => 0,
'archived_timestamp' => 0,
'user_id' => 3840103,
'id' => 139576622,
'children' => array(
0 => array(
'indent' => 3,
'name' => 'Work indent 2-2',
'color' => '#dddddd',
'is_deleted' => 0,
'collapsed' => 0,
'archived_date' => null,
'item_order' => 7,
'is_archived' => 0,
'archived_timestamp' => 0,
'user_id' => 3840103,
'id' => 139576636,
'children' => array(),
'parent' => 139576622,
),
),
'parent' => 138837509,
),
2 => array(
'indent' => 2,
'name' => 'Work indent 3-1',
'color' => '#dddddd',
'is_deleted' => 0,
'collapsed' => 0,
'archived_date' => null,
'item_order' => 8,
'is_archived' => 0,
'archived_timestamp' => 0,
'user_id' => 3840103,
'id' => 139576646,
'children' => array(),
'parent' => 138837509,
),
),
'parent' => 'root',
),
3 => array(
'indent' => 1,
'name' => 'Errands',
'color' => '#74e8d4',
'is_deleted' => 0,
'collapsed' => 0,
'archived_date' => null,
'item_order' => 9,
'is_archived' => 0,
'archived_timestamp' => 0,
'user_id' => 3840103,
'id' => 138837510,
'children' => array(),
'parent' => 'root',
),
4 => array(
'indent' => 1,
'name' => 'Shopping',
'color' => '#dddddd',
'is_deleted' => 0,
'collapsed' => 0,
'archived_date' => null,
'item_order' => 10,
'is_archived' => 0,
'archived_timestamp' => 0,
'user_id' => 3840103,
'id' => 138837511,
'children' => array(),
'parent' => 'root',
),
5 => array(
'indent' => 1,
'name' => 'Movies to watch',
'color' => '#e3a8e5',
'is_deleted' => 0,
'collapsed' => 0,
'archived_date' => null,
'item_order' => 11,
'is_archived' => 0,
'archived_timestamp' => 0,
'user_id' => 3840103,
'id' => 138837512,
'children' => array(),
'parent' => 'root',
),
);
Plain old PHP
When using plain old PHP for outputting such data, one would probably use a method like this one:
public function output($projects)
{
$string = "<ul>";
foreach ($projects as $i => $project) {
$string .= "<li>";
$string .= $project['name'];
if (count($project['children'])) {
$string .= $this->output($project['children']);
}
$string .= "</li>";
}
$string .= "</ul>";
return $string;
}
Ew. It works, but it’s highly inflexible and it mixes presentation with logic. Let’s not do this.
Blade Foreach
With Blade, things become a little simpler. We can use the foreach
construct to help us out.
@if (count($projects) > 0)
<ul>
@foreach ($projects as $project)
@include('partials.project', $project)
@endforeach
</ul>
@else
@include('partials.projects-none')
@endif
As Blade doesn’t really support defining functions, thus not letting us call them recursively like the output
function above, we need to define partials and have them call themselves:
-
partials/project.blade.php
<li>{{ $project['name'] }}</li> @if (count($project['children']) > 0) <ul> @foreach($project['children'] as $project) @include('partials.project', $project) @endforeach </ul> @endif
-
partials/projects-none.blade.php
You have no projects!
But… so much code for something so rudimentary. Is there no way to shorten this even further?
Blade @each
There is an un(der)documented feature of Laravel Blade that’ll help us decimate the LoC count in our template files, making the lives of both our devs and designers much easier. The feature is @each
and is used thusly:
@each('viewfile-to-render', $data, 'variablename','optional-empty-viewfile')
The first argument is the template to render. This will usually be a partial, like our project.blade.php
. The second one is the iterable dataset, in our case $projects
. Third is the variable name the elements will use when being iterated upon. For example, in foreach ($data as $element)
, this argument would be element
(without the $
). The fourth argument is an optional one – it’s the name of the template file which should be rendered when the second argument ($data
) is empty, i.e. has nothing to iterate over. If we apply all this to our case, we can replace this entire block:
@if (count($projects) > 0)
<ul>
@foreach ($projects as $project)
@include('partials.project', $project)
@endforeach
</ul>
@else
@include('partials.projects-none')
@endif
with
@each('partials.project', $projects, 'project', 'partials.projects-none')
Conclusion
In this short tutorial, we saw how we can leverage an underdocumented feature of Laravel Blade to drastically reduce the number of lines in our template code. By using @each
and relying on partials and their ability to recursively call themselves, we have an amazing arsenal of tools at our disposal for outputting all manners of data – it’s just a matter of putting the building blocks in the right order.
You can use this approach of partials recursion to echo out directory trees, content management categories, employee directories, and much, much more.
Did you know about @each
? Do you know of any other hidden gems? Let us know in the comments!