import { NgTemplateOutlet } from '@angular/common'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; export interface TreeNodeData { id?: string; label: string; badge?: string; children?: TreeNodeData[]; } @Component({ selector: 'vsp-tree', imports: [NgTemplateOutlet], template: `
@for (n of data; track $index) { }
@if (hasKids(n)) { } @else { } @if (hasKids(n)) { } @else { } {{ n.label }} @if (n.badge != null) { {{ n.badge }} }
@if (hasKids(n) && expandedSet.has(id(n))) {
@for (c of n.children; track $index) { }
}
`, }) export class VspTree implements OnInit { @Input() data: TreeNodeData[] = []; @Input() defaultExpanded: string[] = []; /** Controlled expanded node ids. Omit for uncontrolled. */ @Input('expanded') expandedInput?: string[]; @Output() expandedChange = new EventEmitter(); /** Controlled selected node id. Omit for uncontrolled. */ @Input('selected') selectedInput?: string | null; @Output() select = new EventEmitter(); private internalExpanded = new Set(); private internalSelected: string | null = null; ngOnInit(): void { this.internalExpanded = new Set(this.defaultExpanded); } get expandedSet(): Set { return this.expandedInput !== undefined ? new Set(this.expandedInput) : this.internalExpanded; } get selectedId(): string | null { return this.selectedInput !== undefined ? this.selectedInput : this.internalSelected; } id(n: TreeNodeData): string { return n.id ?? n.label; } hasKids(n: TreeNodeData): boolean { return (n.children ?? []).length > 0; } rowCls(n: TreeNodeData): string { const i = this.id(n); return ( 'ui-tree-row' + (this.expandedSet.has(i) ? ' open' : '') + (this.selectedId === i ? ' sel' : '') ); } activate(n: TreeNodeData): void { const i = this.id(n); if (this.hasKids(n)) { const next = new Set(this.expandedSet); if (next.has(i)) next.delete(i); else next.add(i); if (this.expandedInput === undefined) this.internalExpanded = next; this.expandedChange.emit([...next]); } if (this.selectedInput === undefined) this.internalSelected = i; this.select.emit(i); } }