material-tree

General-purpose tree — expand/collapse, tri-state checkbox selection, lazy children. Nested markup or flat rows with level (django-mptt's native shape). Form-associated: posts one name=<value> entry per checked node.

Nested markup — categories

Items contain their children; depth is derived from nesting. Click a row to fire materialTreeActivate; arrows / Home / End / Enter work from the keyboard.

(no events)

Flat markup with level — the django-mptt shape

The same tree as an ordered list of rows, exactly what mptt iteration yields: <material-tree-item level="{{ node.level }}" {% if not node.is_leaf_node %}has-children{% endif %}>. No nesting in the markup at all.

Tri-state selection + form posting

selectable adds cascading checkboxes: a parent drives its subtree and shows indeterminate for partial selections. Checked values post as categories=… per node — request.POST.getlist('categories') on the Django side.

Submit Reset Clear selection
(submit to see FormData)

Lazy children — src endpoint

Nodes marked has-children fetch /demo-api/tree?parent=<value> on first expand (mocked, 500 ms). Selection cascades into lazily loaded children. Without src the tree emits materialTreeLoad instead, and you insert items yourself.

Trailing cells — aligned columns (BOM)

Fixed-width slot="trailing" cells line up one under another on every depth — only the label side indents. dense rows.

Component Qty Cost
1 $412.00 1 $189.00 1 $164.00 4 $168.00 2 $46.00 8 $70.00

RTL (hard-coded)

Indent, chevrons and arrow keys follow the reading direction.

Sample usage (Django + mptt)
<form method="post">
  <material-tree selectable name="categories" src="{% url 'category-children' %}">
    {% for node in nodes %}
      <material-tree-item level="{{ node.level }}" value="{{ node.pk }}"
                          label="{{ node.name }}"
                          {% if not node.is_leaf_node %}has-children{% endif %}
                          {% if node.level < 1 %}expanded{% endif %}
                          {% if node.pk in selected_ids %}checked{% endif %}>
      </material-tree-item>
    {% endfor %}
  </material-tree>
</form>

# views.py — lazy children + selection
def category_children(request):
    parent = Category.objects.get(pk=request.GET['parent'])
    return JsonResponse({'results': [
        {'value': c.pk, 'label': c.name, 'has_children': not c.is_leaf_node()}
        for c in parent.get_children()
    ]})

def save(request):
    ids = request.POST.getlist('categories')