@masum-nishat/alpine-template-outlet
v1.1.1
Published
A lightweight Alpine.js plugin that enables recursive template rendering for nested data structures
Maintainers
Readme
Alpine.js Template Outlet Plugin
A lightweight Alpine.js plugin that enables recursive template rendering for nested data structures. Perfect for rendering hierarchical data like tree views, nested menus, threaded comments, and more.
✨ Key Features
- ✅ True recursive rendering
- ✅ Unlimited nesting depth
- ✅ Proper Alpine.js scope management
- ✅ Clean, maintainable code
- ✅ No external dependencies
- ✅ Extremely lightweight (~100 lines of code)
📦 Installation
Option 1: npm (Recommended for modern build tools)
Install the package:
npm install @masum-nishat/alpine-template-outletThen, import and register the plugin in your JavaScript entry file:
import Alpine from 'alpinejs';
import templateOutlet from '@masum-nishat/alpine-template-outlet';
Alpine.plugin(templateOutlet);
Alpine.start(); // If Alpine.js is not already startedOption 2: CDN (Direct Script Include)
Include Alpine.js and the plugin directly in your HTML. It's recommended to use the minified CDN version for production.
<!-- Include Alpine.js (ensure it's loaded before the plugin) -->
<script
defer
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"
></script>
<!-- Include the plugin from unpkg or jsDelivr -->
<script src="https://unpkg.com/@masum-nishat/alpine-template-outlet@latest/dist/alpine-template-outlet.cdn.min.js"></script>
<!-- Or from jsDelivr -->
<!-- <script src="https://cdn.jsdelivr.net/npm/@masum-nishat/alpine-template-outlet@latest/dist/alpine-template-outlet.cdn.min.js"></script> -->Option 3: Manual Registration (Advanced)
For advanced use cases, you can manually register the directive:
import { TemplateOutletDirective } from '@masum-nishat/alpine-template-outlet/dist/alpine-template-outlet.esm.js'; // Path to ESM build
document.addEventListener('alpine:init', () => {
Alpine.directive('template-outlet', TemplateOutletDirective);
});🚀 Quick Start
Here's a simple example of rendering a nested list:
<div
x-data="{ items: [
{ name: 'Parent 1', children: [
{ name: 'Child 1.1', children: [] },
{ name: 'Child 1.2', children: [] }
]},
{ name: 'Parent 2', children: [] }
]}"
>
<!-- Render root items -->
<template x-for="item in items" :key="item.name">
<div x-template-outlet="$refs.itemTemplate" x-data="{ item: item }"></div>
</template>
<!-- Recursive template -->
<template x-ref="itemTemplate">
<div>
<strong x-text="item.name"></strong>
<!-- Recursively render children -->
<template x-if="item.children && item.children.length > 0">
<div style="margin-left: 20px;">
<template x-for="child in item.children" :key="child.name">
<div
x-template-outlet="$refs.itemTemplate"
x-data="{ item: child }"
></div>
</template>
</div>
</template>
</div>
</template>
</div>Important:
- Use
<div>(not<template>) forx-template-outletelements insidex-forloops. - Always provide
x-dataon thex-template-outletelement to pass the initial scope for the cloned template.
💡 How It Works
The plugin operates in 4 key steps:
- Reference: You define a template using
x-refand then reference it withx-template-outlet. - Clone: The plugin clones the content of the referenced template.
- Scope: It attaches a new Alpine.js scope, initialized from the
x-dataattribute on thex-template-outletelement, to the cloned content. - Render: The cloned and scoped content is then inserted into the DOM with full Alpine.js reactivity.
The Magic Formula
<!-- 1. Define a template with x-ref -->
<template x-ref="myTemplate">
<!-- Your content here -->
</template>
<!-- 2. Render it with x-template-outlet inside x-for -->
<template x-for="item in items">
<div
x-template-outlet="$refs.myTemplate"
x-data="{ /* data for this instance */ }"
></div>
</template>📖 API Reference
Directive: x-template-outlet
Syntax:
<div x-template-outlet="expression" x-data="{ /* scope data */ }"></div>Parameters:
| Parameter | Type | Required | Description |
| ------------ | --------------------- | ------------ | ----------------------------------------------------- |
| expression | HTMLTemplateElement | Yes | Reference to a template element (usually via $refs) |
| x-data | Object | Required | Scope data for the cloned template instance |
Returns: Clones and renders the referenced template with the provided scope.
Important API Notes
x-datais Required: Thex-template-outletelement MUST have anx-dataattribute. This provides the initialization data for the cloned template.- Template Reference: Always use
$refsto reference templates (e.g.,<div x-template-outlet="$refs.myTemplate" ...></div>). - Scope Isolation: Each rendered instance gets its own isolated Alpine.js scope based on the
x-dataprovided.
✅ Best Practices
DO
- Use
<div>forx-template-outletelements insidex-forloops. - Always provide
x-dataon thex-template-outletelement. - Use unique
:keyattributes inx-forloops for efficient rendering. - Name your templates clearly (e.g.,
x-ref="menuItemTemplate"). - Keep templates focused on a single component pattern.
- Use
x-iffor conditional rendering to remove elements from the DOM when not needed.
DON'T
- Forget
x-dataon yourx-template-outletelement. - Create circular references in your data that could lead to infinite loops.
- Mutate props directly in nested components; pass parent context or use methods from the parent scope if mutation is necessary.
- Use multiple root elements within a single
<template x-ref="...">(ensure a single root element).
❓ Troubleshooting
Here are solutions to common issues:
- Template doesn't render? Ensure
x-datais present, the template reference ($refs) is correct, and the template has a single root element. - "Template outlet: Invalid template reference"? Check
x-refattribute and name matching, and ensure the template is within the same Alpine component scope. - Infinite recursion / Browser freezes? Verify your termination conditions (
x-ifor array length checks) and check for circular references in your data structure. - Data not updating? Ensure you're passing reactive data and that parent data is properly scoped.
🎮 Examples & Demos
Basic Examples
Example 1: Simple Tree Structure
<div
x-data="{
tree: {
name: 'Root',
children: [
{ name: 'Branch 1', children: [
{ name: 'Leaf 1.1', children: [] }
]},
{ name: 'Branch 2', children: [] }
]
}
}"
>
<!-- Render the tree -->
<div x-template-outlet="$refs.nodeTemplate" x-data="{ node: tree }"></div>
<!-- Node template -->
<template x-ref="nodeTemplate">
<div class="node">
<span x-text="node.name"></span>
<template x-if="node.children.length > 0">
<div class="children">
<template x-for="child in node.children" :key="child.name">
<div
x-template-outlet="$refs.nodeTemplate"
x-data="{ node: child }"
></div>
</template>
</div>
</template>
</div>
</template>
</div>Example 2: Nested Menu
<div
x-data="{
menu: [
{
label: 'Products',
url: '/products',
submenu: [
{ label: 'Laptops', url: '/products/laptops', submenu: [] },
{ label: 'Phones', url: '/products/phones', submenu: [] }
]
},
{ label: 'About', url: '/about', submenu: [] }
]
}"
>
<nav>
<template x-for="item in menu" :key="item.label">
<div x-template-outlet="$refs.menuItem" x-data="{ item: item }"></div>
</template>
</nav>
<template x-ref="menuItem">
<div class="menu-item">
<a :href="item.url" x-text="item.label"></a>
<template x-if="item.submenu.length > 0">
<div
style="margin-left: 20px; padding-left: 15px; border-left: 2px solid #cbd5e0;"
>
<template x-for="subitem in item.submenu" :key="subitem.label">
<div
x-template-outlet="$refs.menuItem"
x-data="{ item: subitem }"
></div>
</template>
</div>
</template>
</div>
</template>
</div>Advanced Examples
Example 3: Interactive Tree with Add/Remove
<div
x-data="{
tree: { id: 1, children: [] },
nextId: 2,
addChild(node) {
node.children.push({ id: this.nextId++, children: [] });
},
removeNode(parent, node) {
parent.children = parent.children.filter(c => c.id !== node.id);
}
}"
>
<div
x-template-outlet="$refs.nodeTemplate"
x-data="{ node: tree, parent: null }"
></div>
<template x-ref="nodeTemplate">
<div class="node">
<strong>Node <span x-text="node.id"></span></strong>
<button @click="addChild(node)">Add Child</button>
<button x-show="parent" @click="removeNode(parent, node)">Remove</button>
<template x-if="node.children.length > 0">
<div class="children">
<template x-for="child in node.children" :key="child.id">
<div
x-template-outlet="$refs.nodeTemplate"
x-data="{ node: child, parent: node }"
></div>
</template>
</div>
</template>
</div>
</template>
</div>Example 4: File System Explorer
<div
x-data="{
fileSystem: {
name: 'root',
type: 'folder',
children: [
{
name: 'Documents',
type: 'folder',
children: [
{ name: 'resume.pdf', type: 'file', children: [] }
]
},
{ name: 'image.jpg', type: 'file', children: [] }
]
},
expanded: {}
}"
>
<div x-template-outlet="$refs.fileNode" x-data="{ node: fileSystem }"></div>
<template x-ref="fileNode">
<div>
<div
@click="node.type === 'folder' && (expanded[node.name] = !expanded[node.name])"
:class="{ 'cursor-pointer': node.type === 'folder' }"
>
<span x-show="node.type === 'folder'">
<span x-text="expanded[node.name] ? '📂' : '📁'"></span>
</span>
<span x-show="node.type === 'file'">📄</span>
<span x-text="node.name"></span>
</div>
<template x-if="node.type === 'folder' && expanded[node.name]">
<div style="margin-left: 20px;">
<template x-for="child in node.children" :key="child.name">
<div
x-template-outlet="$refs.fileNode"
x-data="{ node: child }"
></div>
</template>
</div>
</template>
</div>
</template>
</div>Real-World Use Cases
Use Case 1: Comment Thread System
Perfect for Reddit-style nested comments:
<div x-data="commentSystem">
<template x-for="comment in comments" :key="comment.id">
<div
x-template-outlet="$refs.commentTemplate"
x-data="{ comment: comment, depth: 0 }"
></div>
</template>
<template x-ref="commentTemplate">
<div :style="{ marginLeft: (depth * 20) + 'px' }" class="comment">
<div class="comment-header">
<strong x-text="comment.author"></strong>
<span x-text="comment.timestamp"></span>
</div>
<p x-text="comment.text"></p>
<button @click="showReplyForm(comment)">Reply</button>
<!-- Nested replies -->
<template x-if="comment.replies && comment.replies.length > 0">
<div class="replies">
<template x-for="reply in comment.replies" :key="reply.id">
<div
x-template-outlet="$refs.commentTemplate"
x-data="{ comment: reply, depth: depth + 1 }"
></div>
</template>
</div>
</template>
</div>
</template>
</div>Use Case 2: Organization Chart
<div
x-data="{
org: {
name: 'CEO',
title: 'Chief Executive Officer',
reports: [
{
name: 'CTO',
title: 'Chief Technology Officer',
reports: [
{ name: 'Dev Lead', title: 'Development Lead', reports: [] }
]
},
{
name: 'CFO',
title: 'Chief Financial Officer',
reports: []
}
]
}
}"
>
<div x-template-outlet="$refs.employeeCard" x-data="{ employee: org }"></div>
<template x-ref="employeeCard">
<div class="employee-card">
<h3 x-text="employee.name"></h3>
<p x-text="employee.title"></p>
<template x-if="employee.reports && employee.reports.length > 0">
<div class="reports">
<template x-for="report in employee.reports" :key="report.name">
<div
x-template-outlet="$refs.employeeCard"
x-data="{ employee: report }"
></div>
</template>
</div>
</template>
</div>
</template>
</div>Use Case 3: Dynamic Navigation Menu (Laravel Blade + Alpine.js)
This is exactly what you implemented in your project:
<div x-data="{ menuItems: window.menuConfig }">
<template x-for="(item, index) in menuItems" :key="index">
<div x-template-outlet="$refs.menuItem" x-data="{ item: item }"></div>
</template>
<template x-ref="menuItem">
<!-- Link item -->
<template x-if="item.type === 'link' && getMenuVisibility(item.activeKey)">
<a :href="item.url" :class="getClass(getActiveState(item.activeKey))">
<span x-text="item.name"></span>
</a>
</template>
<!-- Group item with children -->
<template x-if="item.type === 'group' && getMenuVisibility(item.activeKey)">
<div x-data="{ isExpanded: false }">
<button @click="isExpanded = !isExpanded" x-text="item.name"></button>
<template x-if="item.children && item.children.length > 0">
<ul x-show="isExpanded">
<template x-for="child in item.children" :key="child.activeKey">
<li>
<div
x-template-outlet="$refs.menuItem"
x-data="{ item: child }"
></div>
</li>
</template>
</ul>
</template>
</div>
</template>
</template>
</div>For interactive demonstrations, open examples/plugin-demo.html in your browser to see these examples in action.
⚡ Performance Tips
Use
x-iffor conditional rendering- Completely removes elements from DOM.
- Better for items that rarely change.
Use
:keyin loops- Helps Alpine track elements efficiently.
- Prevents unnecessary re-renders.
Limit nesting depth when possible
- Deep nesting can impact performance.
- Consider flattening data structure if possible.
Use
x-cloakto prevent flash of unstyled content<style> [x-cloak] { display: none; } </style> <div x-cloak x-data="..."></div>
🛠️ Development & Build Process
This project uses esbuild to compile the plugin into various distribution formats. While users typically consume the plugin via npm or CDN, understanding the build process can be helpful for contributors or those needing custom builds.
Building the Project
To build the plugin from source, first install the development dependencies, then run the build script:
# Install dependencies (if you haven't already)
npm install
# Build all distribution formats
npm run buildThis command generates the following files in the dist/ folder:
alpine-template-outlet.cdn.js: IIFE format for direct browser inclusion.alpine-template-outlet.cdn.min.js: Minified IIFE for production CDN usage.alpine-template-outlet.esm.js: ES Module format for modern JavaScript bundlers.
Development Workflow
- Edit Source: Make changes only in
src/alpine-template-outlet.js. - Build: Run
npm run buildto update thedist/files. - Test: Open
examples/test.htmlin your browser to verify changes.
Publishing
The prepublishOnly npm script automatically runs npm run build before npm publish to ensure your package always includes the latest compiled assets.
📝 License
This project is licensed under the MIT License. See the LICENSE file for more details.
💖 Credits
- Plugin Author: Masum Nishat
- Inspired by: Alpine.js community patterns
- Based on: Recursive template rendering concepts
❓ Support
For issues or questions:
- Check the Troubleshooting section above.
- Review the provided examples and interactive demos.
- Consult the official Alpine.js documentation for general Alpine questions.
Happy Coding! 🎉
