Recursive Template Rendering In Alpine.js 3.13.5
Yesterday, I created a "template outlet" directive in Alpine.js. This directive allows me to take a template reference and render it at any arbitrary point within the document and provide it with local data bindings. This opens the door for recursive template rendering since the template definition can include a template outlet which is a reference back to the current template definition. Let's take a quick look at this recursive template rendering in Alpine.js 3.13.5.
To explore recursive rendering, we need a tree-based data structure that has a non-deterministic depth. I'm going to keep this tree as simple as possible. Each node within the tree structure will contain nothing more than an id
and a children
array.
The tree will start with a single root node. But, each rendering of a tree node will include two actions: add a new child and clear all children. This way, we can both grow and prune any given node in the tree structure from the rendered user interface.
The majority of the UI in this demo is tied to the recursive template. This template renders the given node; and is then recursively rendered for each element in the .children
array. Here's a truncated version of the HTML. Notice that the template itself is stored as treeNodeTemplate
using x-ref
; and, that the body of the template references that $refs.treeNodeTemplate
value:
<template x-ref="treeNodeTemplate">
<!-- Render NODE itself. -->
<template x-for="childNode in node.children">
<!-- RECURSIVE RENDERING of child nodes. -->
<template
x-template-outlet="$refs.treeNodeTemplate"
x-data="{ node: childNode }">
</template>
</template>
</div>
Every tree-node rendering expects to have a node
in the scope chain. When invoking the x-template-outlet
directive, I can use template's x-data
directive to setup that node
mapping. In the above HTML, you can see that within the x-for
iteration, I'm mapping each childNode
onto the local node
rendering for the tree.
Here's the full code for the demo. Notice that all of the logic is in the application controller - I don't actually need any additional controllers for the nodes as long as I pass-in the target node when performing operations:
<!doctype html>
<html lang="en">
<link rel="stylesheet" type="text/css" href="./main.css" />
<body>
<h1>
Recursive Template Rendering In Alpine.js 3.13.5
</h1>
<div x-data="app">
<div class="tree">
<!-- The ROOT rendering of the tree. -->
<template
x-template-outlet="$refs.treeNodeTemplate"
x-data="{ node: tree.rootNode }">
</template>
</div>
<!--
RECURSIVE TEMPALTE! This template represents a node within the tree. When
rendering the node children, this template will re-render itself using the
template-outlet directive.
-->
<template x-ref="treeNodeTemplate">
<!-- The "node" object is defined by template outlet's x-data binding. -->
<div class="tree__node">
<strong x-text="node.id"></strong> —
<button @click="addChild( node )">
add
</button>
<button x-show="node.children.length" @click="clearChildren( node )">
clear
</button>
<ul x-show="node.children.length" class="tree__children">
<template x-for="childNode in node.children" :key="childNode.id">
<li>
<!--
RECURSIVE RENDERING: This template is about to render
itself, using the CHILD node as the given root.
-->
<template
x-template-outlet="$refs.treeNodeTemplate"
x-data="{ node: childNode }">
</template>
</li>
</template>
</ul>
</div>
</template>
</div>
<script type="text/javascript" src="./alpine.template-outlet.js" defer></script>
<!-- Include my custom directive for template-outlet. -->
<script type="text/javascript" src="../vendor/alpine.3.13.5.js" defer></script>
<script type="text/javascript">
document.addEventListener(
"alpine:init",
function setupAlpineBindings() {
Alpine.data( "app", AppController );
}
);
/**
* I control the app component.
*/
function AppController() {
// Every new node will get a unique ID.
var id = 0;
return {
tree: {
rootNode: nodeNew()
},
addChild: addChild,
clearChildren: clearChildren
};
// ---
// PUBLIC METHODS.
// ---
/**
* I add a new child node to the given parent node.
*/
function addChild( parent ) {
parent.children.push( nodeNew() );
}
/**
* I clear all of the child nodes out of the given parent node.
*/
function clearChildren( parent ) {
parent.children = [];
}
// ---
// PRIVATE METHODS.
// ---
/**
* I generate a new node for the node tree.
*/
function nodeNew() {
return {
id: ++id,
children: []
};
}
}
</script>
</body>
</html>
If I run this Alpine.js code and start adding and removing tree nodes, we get the following output:
As you can see, the tree data structure is being rendered despite the fact that is has an arbitrary, non-deterministic depth. This is because each node is recursively rendering its own template using the x-template-outlet
directive.
It's interesting to see how Alpine.js maintains the scope tree. If we look at the properties of one of the nested nodes, we can see every node
and childNode
mapping in its scope chain:
At the top of that array is the current DOM node. And, at the bottom of that array is the application's tree
structure (defined in the root x-data
binding).
In case it wasn't clear from the intro, the x-template-outlet
directive that I'm using in this recursive rendering is not a native Alpine.js directive - it is one that I created yesterday. And, now that I see that it enables recursive rendering, I can try to build the type of JSON explorer that I've built in Svelte.js and in Angular.
Want to use code from this post? Check out the license.
Reader Comments
Thanks for posting your code here (I had seen the video elsewhere). I'm often fiddling with recursive trees using JSON and (especially) SQL. Have always wanted to experiment with a UI tool for importing / exporting such things. Everything in the universe seems to be a recursive list of recursive lists.
@Figital,
I haven't done much with recursive SQL - I think that's mostly a MS SQL Server technique; though, PostgreSQL probably has it too - people see to love Postgress for all the things; so, I just assume it can do this 😆
Recursion is fun and super helpful in the right places. But, not always the easiest to manage in your head.
Amazing work sir! I'm starting to put together an internal tool for my company to create custom query filters. I can't believe you came up with did just a couple days before I google "recursive components in alpinejs". This would have been a lot of work! Thank you very much for sharing this!
@Angelez,
Very good timing then! Though, to be clear, I'm really just learning Alpine.js by playing around and doing these experiments. So, understand that my implementation may have issues :) That said, have fun with it!
I'm curious what kind of recursive rendering do you need to do? It doesn't come up that often - usually just some sort of "tree"; so, I'm wondering what you're trying to build.
We want to allow the users to create custom filters for our service calls. For example (SQL):
You could imagine the nesting as deep as the user would like.
It's coming together pretty nicely.
@Angelez,
Ah, I see - so you have a GUI tool that allows them to build complex filters. Very cool 🙌
Now that I've unblocked recursive template rendering, I was finally able to create a JSON Explorer in Alpine.js:
www.bennadel.com/blog/4611-recursive-json-explorer-in-alpine-js-3-13-5.htm
This is an exercise I like to do because it is small enough to be doable; but, complex enough to force me to explore the edges of the language features. And, in Alpine.js, I had to create some other directives in order to make this possible.
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →