Hello everyone!
I wanted to come forward with a problem I am having with my UX design, and see what advice people had on the matter.
Context:
Within my application, the user is able to create a list of categories/choices (these terms are interchangeable in my context). These choices can also have sub-choices, and a certain amount of allowed sub-levels. Effectively, I am allowing them to build a tree of choices with n levels, where n will be set by me at some point for the sake of not allowing too deep of a tree.
The way I have my data serialized is a JSON structure, containing a list of all top level nodes, and then each node as a “children” attribute, which contains more nodes with the same structure. This is then stored in an optional JSON field of a generic model object. Here is an example of a tree in this structure:
[{"name": "1", "children": [{"name": "1.1", "children": [{"name": "1.1.1", "children": []}]}]}, {"name": "2", "children": []}]
Problem:
I need a good way to handle the user building this and the problem lies in the demand for a recursive approach. I have attached my hardcoded solution with an image to help you better understand what I am aiming for. As you can see from my HTML, I rely heavily on Alpine directives and javascript functions to maintain the list as is. But how could I build a template that renders each level recursively and maintains the functionality of the add and delete buttons for their assigned nodes?
tree_choices_build.html
<div x-data="treeBuilder({{initialTree}})">
<h2 class="text-xl font-bold my-4">Manage Tree Choices</h2>
<div class="my-4">
<button type="button" @click="addNode(null)" class="bg-blue-500 text-white px-4 py-2 rounded">+</button>
</div>
<div id="tree" class="my-4">
<template x-for="(node, index) in tree" :key="index">
<div class="ml-4 border-l-2 pl-2">
<input type="text" x-model="node.name" placeholder="Category Name" class="border rounded px-2 py-1">
<button type="button" @click="addNode(index)" class="bg-green-500 text-white px-2 py-1 rounded-full ml-2"> + </button>
<button type="button" @click="removeNode(index)" class="bg-red-500 text-white px-2 py-1 rounded-full ml-2"> - </button>
<div x-show="node.children.length > 0" class="ml-4 mt-2" x-transition>
<template x-for="(child, childIndex) in node.children" :key="childIndex">
<div class="ml-4 border-l-2 pl-2">
<input type="text" x-model="child.name" placeholder="Subcategory Name" class="border rounded px-2 py-1">
<button type="button" @click="removeNode(index, childIndex)" class="bg-red-500 text-white px-2 py-1 rounded-full ml-2"> - </button>
</div>
</template>
</div>
</div>
</template>
</div>
<input type="hidden" name="tree_json" :value="JSON.stringify(tree)">
</div>
<script>
function treeBuilder(initialTree = []) {
return {
tree: initialTree.length ? initialTree : [],
addNode(parentIndex = null, childIndex = null) {
if (parentIndex === null) {
this.tree.push({ name: '', children: [] });
} else if (childIndex === null) {
this.tree[parentIndex].children.push({ name: '', children: [] });
} else {
this.tree[parentIndex].children[childIndex].children.push({ name: '', children: [] });
}
},
removeNode(parentIndex, childIndex = null) {
if (childIndex === null) {
this.tree.splice(parentIndex, 1);
} else {
this.tree[parentIndex].children.splice(childIndex, 1);
}
}
}
}
</script>
Current UI
I feel this is a lack of knowledge and experience on my end, but I’m also worried this is a limitation of my current FE tech stack (Django Templates, AlpineJS, HTMX, TailwindCSS). Any recommendations, critiques, etc. are more than welcome!!! I am also willing to bring in Javascript tools/libraries if necessary. Thank you in advance!
Disclaimer:
I apologize if this may not be an appropriate place to post a question like this, please correct me if so.