Select Page

Recursive Template Rendering In Alpine.js 3.13.5

Ben Nadel
Published: March 1, 2024

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="https://www.bennadel.com/blog/./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.

Source: www.bennadel.com