Skip to content

Commit 9b6996e

Browse files
feat(tree): Add editable state to the nodes
1 parent b0d8167 commit 9b6996e

File tree

6 files changed

+230
-21
lines changed

6 files changed

+230
-21
lines changed

angular/bootstrap/src/components/tree/tree.component.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type {SlotContent} from '@agnos-ui/angular-headless';
2-
import {BaseWidgetDirective, callWidgetFactory, ComponentTemplate, SlotDirective, UseDirective} from '@agnos-ui/angular-headless';
2+
import {auBooleanAttribute, BaseWidgetDirective, callWidgetFactory, ComponentTemplate, SlotDirective, UseDirective} from '@agnos-ui/angular-headless';
33
import {ChangeDetectionStrategy, Component, contentChild, Directive, inject, input, output, TemplateRef, viewChild} from '@angular/core';
4-
import type {TreeContext, TreeItem, NormalizedTreeItem, TreeSlotItemContext, TreeWidget} from './tree.gen';
4+
import type {NormalizedTreeItem, TreeContext, TreeItem, TreeSlotItemContext, TreeWidget} from './tree.gen';
55
import {createTree} from './tree.gen';
66

77
/**
@@ -97,12 +97,16 @@ export class TreeItemContentDirective {
9797

9898
@Component({
9999
changeDetection: ChangeDetectionStrategy.OnPush,
100-
imports: [SlotDirective, TreeItemContentDirective],
100+
imports: [SlotDirective, TreeItemContentDirective, UseDirective],
101101
template: `
102102
<ng-template auTreeItemContent #treeItemContent let-state="state" let-directives="directives" let-item="item" let-api="api">
103103
<span class="au-tree-item">
104104
<ng-template [auSlot]="state.itemToggle()" [auSlotProps]="{state, api, directives, item}" />
105-
{{ item.label }}
105+
@if (item.isEdited) {
106+
<input class="input input-sm w-32 min-w-0 flex-shrink" [auUse]="[directives.itemInputDirective, {item}]" />
107+
} @else {
108+
<span [auUse]="[directives.itemModifyDirective, {item}]">{{ item.label }}</span>
109+
}
106110
</span>
107111
</ng-template>
108112
`,
@@ -187,6 +191,7 @@ export class TreeComponent extends BaseWidgetDirective<TreeWidget> {
187191
},
188192
events: {
189193
onExpandToggle: (item: NormalizedTreeItem) => this.expandToggle.emit(item),
194+
onNodesChange: (nodes) => this.nodesChange.emit(nodes),
190195
},
191196
slotTemplates: () => ({
192197
structure: this.slotStructureFromContent()?.templateRef,
@@ -230,6 +235,12 @@ export class TreeComponent extends BaseWidgetDirective<TreeWidget> {
230235
* ```
231236
*/
232237
readonly ariaLabelToggleFn = input<(label: string) => string>(undefined, {alias: 'auAriaLabelToggleFn'});
238+
/**
239+
* If `true` the tree items can be modified from the tree itself, otherwise they are just displayed
240+
*
241+
* @defaultValue `false`
242+
*/
243+
readonly isEditable = input(undefined, {alias: 'auIsEditable', transform: auBooleanAttribute});
233244

234245
/**
235246
* An event emitted when the user toggles the expand of the TreeItem.
@@ -242,6 +253,17 @@ export class TreeComponent extends BaseWidgetDirective<TreeWidget> {
242253
* ```
243254
*/
244255
readonly expandToggle = output<NormalizedTreeItem>({alias: 'auExpandToggle'});
256+
/**
257+
* An event emitted when the nodes array is modified
258+
*
259+
* @param nodes - The updated nodes array
260+
*
261+
* @defaultValue
262+
* ```ts
263+
* () => {}
264+
* ```
265+
*/
266+
readonly nodesChange = output<TreeItem[]>({alias: 'auNodesChange'});
245267

246268
/**
247269
* Slot to change the default tree item content

angular/demo/bootstrap/src/app/samples/tree/basic.route.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {TreeComponent, type TreeItem} from '@agnos-ui/angular-bootstrap';
22
import {Component} from '@angular/core';
33

44
@Component({
5-
template: ` <au-component auTree [auNodes]="nodes" /> `,
5+
template: ` <au-component auTree [auNodes]="nodes" (auNodesChange)="onNodesChange($event)" auIsEditable /> `,
66
imports: [TreeComponent],
77
})
88
export default class BasicTreeComponent {
@@ -30,4 +30,8 @@ export default class BasicTreeComponent {
3030
],
3131
},
3232
];
33+
34+
onNodesChange(nodes: TreeItem[]) {
35+
console.log('nodes changed to', nodes);
36+
}
3337
}

angular/demo/daisyui/src/app/samples/tree/default.route.ts

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
1-
import {Component} from '@angular/core';
2-
import {TreeComponent} from './tree.component';
1+
import type {NormalizedTreeItem} from '@agnos-ui/angular-headless';
32
import {type TreeItem} from '@agnos-ui/angular-headless';
3+
import {Component, signal, viewChild} from '@angular/core';
4+
import {TreeComponent} from './tree.component';
45

56
@Component({
67
imports: [TreeComponent],
78
template: `
89
<div class="flex justify-center">
9-
<app-tree [nodes]="nodes" [navSelector]="navSelector" />
10+
<app-tree [nodes]="nodes()" [navSelector]="navSelector" />
1011
</div>
1112
`,
1213
})
1314
export default class BasicTreeComponent {
15+
readonly tree = viewChild(TreeComponent);
1416
readonly navSelector = (node: HTMLElement) => node.querySelectorAll<HTMLSpanElement>('span.au-tree-expand-icon');
15-
readonly nodes: TreeItem[] = [
17+
18+
readonly newItem: TreeItem = {
19+
label: 'New Item',
20+
};
21+
22+
readonly nodes = signal<TreeItem[]>([
1623
{
1724
label: 'resume.pdf',
1825
},
@@ -50,5 +57,35 @@ export default class BasicTreeComponent {
5057
{
5158
label: 'reports-final-2.pdf',
5259
},
53-
];
60+
]);
61+
62+
handleAddNode(targetParent: NormalizedTreeItem) {
63+
const tree = this.tree();
64+
if (!tree) return;
65+
66+
const originalNode = tree.api.getOriginalNode(targetParent);
67+
if (!originalNode) return;
68+
69+
const newItem: TreeItem = {
70+
label: 'New item',
71+
};
72+
73+
this.nodes.update((current) => {
74+
const updateNode = (items: TreeItem[]): TreeItem[] =>
75+
items.map((item) => {
76+
if (item === originalNode) {
77+
return {
78+
...item,
79+
children: item.children ? [...item.children, newItem] : [newItem],
80+
isExpanded: true,
81+
};
82+
}
83+
if (item.children) {
84+
return {...item, children: updateNode(item.children)};
85+
}
86+
return item;
87+
});
88+
return updateNode(current);
89+
});
90+
}
5491
}

angular/demo/daisyui/src/app/samples/tree/tree.component.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@ import {ChangeDetectionStrategy, Component, input, output} from '@angular/core';
1515
</ul>
1616
1717
<ng-template #treeItem let-item="item">
18-
<!-- eslint-disable-next-line @angular-eslint/template/role-has-required-aria -->
19-
<li role="treeitem">
20-
<span class="flex flex-wrap items-center" [auUse]="[directives.itemToggleDirective, {item}]">
18+
<li [auUse]="[directives.itemAttributesDirective, {item}]">
19+
<span class="flex flex-nowrap items-center" [auUse]="[directives.itemToggleDirective, {item}]">
2120
<span class="me-1">
2221
<ng-container [ngTemplateOutlet]="itemIcon" [ngTemplateOutletContext]="{item}" />
2322
</span>
24-
<span>{{ item.label }}</span>
23+
@if (item.isEdited) {
24+
<input class="input input-sm w-32 min-w-0 flex-shrink" [auUse]="[directives.itemInputDirective, {item}]" />
25+
} @else {
26+
<span [auUse]="[directives.itemModifyDirective, {item}]">{{ item.label }}</span>
27+
}
2528
@if (item.children.length > 0) {
2629
<span class="ms-auto">
2730
<svg

core/src/components/tree/tree.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,15 @@ describe(`Tree`, () => {
6262
ariaLabel: 'root',
6363
level: 0,
6464
isExpanded: false,
65+
isEdited: false,
6566
children: [
6667
{
6768
label: 'child',
6869
ariaLabel: 'child',
6970
level: 1,
7071
isExpanded: undefined,
7172
children: [],
73+
isEdited: false,
7274
},
7375
],
7476
},
@@ -87,4 +89,10 @@ describe(`Tree`, () => {
8789
expect(state.normalizedNodes[0].isExpanded).toBe(true);
8890
expect(itemExpands.length).toEqual(1);
8991
});
92+
93+
test(`should return the TreeItem based on the NormalizedTreeItem`, () => {
94+
const newNodes = [{label: 'root', ariaLabel: 'root', children: [{label: 'child', ariaLabel: 'child'}]}];
95+
tree.patch({nodes: newNodes});
96+
expect(tree.api.getOriginalNode(state.normalizedNodes[0])).toEqual(newNodes[0]);
97+
});
9098
});

0 commit comments

Comments
 (0)