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;
  }
}