Skip to content

Commit 7b90786

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

File tree

10 files changed

+368
-21
lines changed

10 files changed

+368
-21
lines changed

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

Lines changed: 29 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,10 @@ export class TreeComponent extends BaseWidgetDirective<TreeWidget> {
187191
},
188192
events: {
189193
onExpandToggle: (item: NormalizedTreeItem) => this.expandToggle.emit(item),
194+
onNodesChange: (nodes) => {
195+
console.log('Nodes changed', nodes);
196+
this.nodesChange.emit(nodes);
197+
},
190198
},
191199
slotTemplates: () => ({
192200
structure: this.slotStructureFromContent()?.templateRef,
@@ -230,6 +238,12 @@ export class TreeComponent extends BaseWidgetDirective<TreeWidget> {
230238
* ```
231239
*/
232240
readonly ariaLabelToggleFn = input<(label: string) => string>(undefined, {alias: 'auAriaLabelToggleFn'});
241+
/**
242+
* If `true` the tree items can be modified from the tree itself, otherwise they are just displayed
243+
*
244+
* @defaultValue `false`
245+
*/
246+
readonly isEditable = input(undefined, {alias: 'auIsEditable', transform: auBooleanAttribute});
233247

234248
/**
235249
* An event emitted when the user toggles the expand of the TreeItem.
@@ -242,6 +256,17 @@ export class TreeComponent extends BaseWidgetDirective<TreeWidget> {
242256
* ```
243257
*/
244258
readonly expandToggle = output<NormalizedTreeItem>({alias: 'auExpandToggle'});
259+
/**
260+
* An event emitted when the nodes array is modified
261+
*
262+
* @param nodes - The updated nodes array
263+
*
264+
* @defaultValue
265+
* ```ts
266+
* () => {}
267+
* ```
268+
*/
269+
readonly nodesChange = output<TreeItem[]>({alias: 'auNodesChange'});
245270

246271
/**
247272
* Slot to change the default tree item content
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {TreeComponent} from '@agnos-ui/angular-bootstrap';
2+
import type {TreeItem} from '@agnos-ui/angular-headless';
3+
import {Component, signal} from '@angular/core';
4+
5+
@Component({
6+
template: ` <au-component auTree [auNodes]="nodes()" auIsEditable (auNodesChange)="nodesChange($event)" /> `,
7+
imports: [TreeComponent],
8+
})
9+
export default class EditableTreeComponent {
10+
readonly nodes = signal([
11+
{
12+
label: 'Node 1',
13+
isExpanded: true,
14+
children: [
15+
{
16+
label: 'Node 1.1',
17+
children: [
18+
{
19+
label: 'Node 1.1.1',
20+
},
21+
],
22+
},
23+
{
24+
label: 'Node 1.2',
25+
children: [
26+
{
27+
label: 'Node 1.2.1',
28+
},
29+
],
30+
},
31+
],
32+
},
33+
]);
34+
35+
nodesChange(nodes: TreeItem[]) {
36+
// handle the change of the nodes manually in order to avoid the redraw of the tree
37+
console.log('changed in editable', nodes);
38+
}
39+
}

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: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ type TestingTreeState = Omit<TreeState, 'expandedMap'>;
1010
const defaultState: () => TestingTreeState = () => ({
1111
className: '',
1212
normalizedNodes: [],
13+
isEditable: false,
1314
});
1415

1516
describe(`Tree`, () => {
@@ -62,13 +63,15 @@ describe(`Tree`, () => {
6263
ariaLabel: 'root',
6364
level: 0,
6465
isExpanded: false,
66+
isEdited: false,
6567
children: [
6668
{
6769
label: 'child',
6870
ariaLabel: 'child',
6971
level: 1,
7072
isExpanded: undefined,
7173
children: [],
74+
isEdited: false,
7275
},
7376
],
7477
},
@@ -87,4 +90,10 @@ describe(`Tree`, () => {
8790
expect(state.normalizedNodes[0].isExpanded).toBe(true);
8891
expect(itemExpands.length).toEqual(1);
8992
});
93+
94+
test(`should return the TreeItem based on the NormalizedTreeItem`, () => {
95+
const newNodes = [{label: 'root', ariaLabel: 'root', children: [{label: 'child', ariaLabel: 'child'}]}];
96+
tree.patch({nodes: newNodes});
97+
expect(tree.api.getOriginalNode(state.normalizedNodes[0])).toEqual(newNodes[0]);
98+
});
9099
});

0 commit comments

Comments
 (0)