npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@masum-nishat/alpine-template-outlet

v1.1.1

Published

A lightweight Alpine.js plugin that enables recursive template rendering for nested data structures

Readme

Alpine.js Template Outlet Plugin

npm version CI License: MIT npm downloads

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

Then, 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 started

Option 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>) for x-template-outlet elements inside x-for loops.
  • Always provide x-data on the x-template-outlet element to pass the initial scope for the cloned template.

💡 How It Works

The plugin operates in 4 key steps:

  1. Reference: You define a template using x-ref and then reference it with x-template-outlet.
  2. Clone: The plugin clones the content of the referenced template.
  3. Scope: It attaches a new Alpine.js scope, initialized from the x-data attribute on the x-template-outlet element, to the cloned content.
  4. 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

  1. x-data is Required: The x-template-outlet element MUST have an x-data attribute. This provides the initialization data for the cloned template.
  2. Template Reference: Always use $refs to reference templates (e.g., <div x-template-outlet="$refs.myTemplate" ...></div>).
  3. Scope Isolation: Each rendered instance gets its own isolated Alpine.js scope based on the x-data provided.

✅ Best Practices

DO

  • Use <div> for x-template-outlet elements inside x-for loops.
  • Always provide x-data on the x-template-outlet element.
  • Use unique :key attributes in x-for loops for efficient rendering.
  • Name your templates clearly (e.g., x-ref="menuItemTemplate").
  • Keep templates focused on a single component pattern.
  • Use x-if for conditional rendering to remove elements from the DOM when not needed.

DON'T

  • Forget x-data on your x-template-outlet element.
  • 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-data is present, the template reference ($refs) is correct, and the template has a single root element.
  • "Template outlet: Invalid template reference"? Check x-ref attribute and name matching, and ensure the template is within the same Alpine component scope.
  • Infinite recursion / Browser freezes? Verify your termination conditions (x-if or 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

  1. Use x-if for conditional rendering

    • Completely removes elements from DOM.
    • Better for items that rarely change.
  2. Use :key in loops

    • Helps Alpine track elements efficiently.
    • Prevents unnecessary re-renders.
  3. Limit nesting depth when possible

    • Deep nesting can impact performance.
    • Consider flattening data structure if possible.
  4. Use x-cloak to 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 build

This 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

  1. Edit Source: Make changes only in src/alpine-template-outlet.js.
  2. Build: Run npm run build to update the dist/ files.
  3. Test: Open examples/test.html in 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:

  1. Check the Troubleshooting section above.
  2. Review the provided examples and interactive demos.
  3. Consult the official Alpine.js documentation for general Alpine questions.

Happy Coding! 🎉