Skip to main content
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Ryan Jeffords
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Ryan Jeffords

Recursive Template Rendering In Alpine.js 3.13.5

By
Published in Comments (7)

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> &mdash;

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

A tree data structure being edited and rendered using recursive templates in Alpine.js 3.13.5.

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:

The scope chain of a given x-data binding in a recursive tree rendering in Alpine.js.

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

4 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.

15,902 Comments

@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.

5 Comments

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!

15,902 Comments

@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.

5 Comments

We want to allow the users to create custom filters for our service calls. For example (SQL):

where
  divisions like '%301%
  and status != 'close'
  and (
    pay_status = 'processing'
    or pay_status = 'paid'
  )

You could imagine the nesting as deep as the user would like.
It's coming together pretty nicely.

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel