Alpine.js x-template: Interactive Tree
by rameel
HTML
<template id="treeitem">
<div x-format @click="toggle" @dblclick="add_child">
<span x-show="is_folder">{{ open ? '📂' : '📁' }}</span>
<span :class="{ folder: is_folder }">{{ model.name }}</span>
</div>
<template x-if="is_folder">
<ul x-show="open">
<template x-for="item in model.children">
<li x-template="treeitem" x-data="TreeItem(item)"></li>
</template>
<li class="folder-icon" @click="add_child">+</li>
</ul>
</template>
</template>
<p>Double click an item to turn it into a folder.</p>
<div x-data="TreeItem(treedata)" x-template="treeitem"></div>
<script>
const treedata = {
name: 'Project',
children: [
{
name: 'src',
children: [
{
name: 'components',
children: [
{ name: 'Header.js' },
{ name: 'Footer.js' }
]
},
{ name: 'main.js' },
{ name: 'app.css' },
]
},
{ name: 'index.html' },
{ name: 'README.md' },
{ name: 'package.json' }
]
};
document.addEventListener("alpine:init", () => {
Alpine.data("TreeItem", model => {
return {
model: model || {},
open: false,
get is_folder() {
return this.model.children?.length;
},
toggle() {
this.is_folder && (this.open = !this.open);
},
add_child() {
if (!this.is_folder) {
this.model.children = [];
}
this.model.children.push({ name: "new item" });
this.open = true;
}
};
});
});
</script>
<script src="https://cdn.jsdelivr.net/npm/@ramstack/alpinegear-format@1/alpinegear-format.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/@ramstack/alpinegear-template@1/alpinegear-template.min.js" defer></script>
<script...
CSS
div {
user-select: none;
}
ul,
li {
margin: 0;
padding: 0;
list-style: none;
}
ul {
margin-left: 1rem;
padding-left: 0.5rem;
border-left: 1px dashed #cfcfcf;
}
[x-format] {
cursor: pointer;
padding: 4px 8px;
display: flex;
align-items: center;
gap: 0.4rem;
line-height: 1.5;
&:hover {
background: #f0f7f0;
}
}
.folder-icon {
color: #4a8c2e;
cursor: pointer;
&:hover {
color: #2e5d1d;
}
}