Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions angular/bootstrap/src/components/tree/tree.component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type {SlotContent} from '@agnos-ui/angular-headless';
import {BaseWidgetDirective, callWidgetFactory, ComponentTemplate, SlotDirective, UseDirective} from '@agnos-ui/angular-headless';
import {auBooleanAttribute, BaseWidgetDirective, callWidgetFactory, ComponentTemplate, SlotDirective, UseDirective} from '@agnos-ui/angular-headless';
import {ChangeDetectionStrategy, Component, contentChild, Directive, inject, input, output, TemplateRef, viewChild} from '@angular/core';
import type {TreeContext, TreeItem, NormalizedTreeItem, TreeSlotItemContext, TreeWidget} from './tree.gen';
import type {NormalizedTreeItem, TreeContext, TreeItem, TreeSlotItemContext, TreeWidget} from './tree.gen';
import {createTree} from './tree.gen';

/**
Expand Down Expand Up @@ -97,12 +97,16 @@ export class TreeItemContentDirective {

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [SlotDirective, TreeItemContentDirective],
imports: [SlotDirective, TreeItemContentDirective, UseDirective],
template: `
<ng-template auTreeItemContent #treeItemContent let-state="state" let-directives="directives" let-item="item" let-api="api">
<span class="au-tree-item">
<ng-template [auSlot]="state.itemToggle()" [auSlotProps]="{state, api, directives, item}" />
{{ item.label }}
@if (item.isEdited) {
<input class="input input-sm w-32 min-w-0 flex-shrink" [auUse]="[directives.itemInputDirective, {item}]" />
} @else {
<span [auUse]="[directives.itemModifyDirective, {item}]">{{ item.label }}</span>
}
</span>
</ng-template>
`,
Expand Down Expand Up @@ -187,6 +191,10 @@ export class TreeComponent extends BaseWidgetDirective<TreeWidget> {
},
events: {
onExpandToggle: (item: NormalizedTreeItem) => this.expandToggle.emit(item),
onNodesChange: (nodes) => {
console.log('Nodes changed', nodes);
this.nodesChange.emit(nodes);
},
},
slotTemplates: () => ({
structure: this.slotStructureFromContent()?.templateRef,
Expand Down Expand Up @@ -230,6 +238,12 @@ export class TreeComponent extends BaseWidgetDirective<TreeWidget> {
* ```
*/
readonly ariaLabelToggleFn = input<(label: string) => string>(undefined, {alias: 'auAriaLabelToggleFn'});
/**
* If `true` the tree items can be modified from the tree itself, otherwise they are just displayed
*
* @defaultValue `false`
*/
readonly isEditable = input(undefined, {alias: 'auIsEditable', transform: auBooleanAttribute});

/**
* An event emitted when the user toggles the expand of the TreeItem.
Expand All @@ -242,6 +256,17 @@ export class TreeComponent extends BaseWidgetDirective<TreeWidget> {
* ```
*/
readonly expandToggle = output<NormalizedTreeItem>({alias: 'auExpandToggle'});
/**
* An event emitted when the nodes array is modified
*
* @param nodes - The updated nodes array
*
* @defaultValue
* ```ts
* () => {}
* ```
*/
readonly nodesChange = output<TreeItem[]>({alias: 'auNodesChange'});

/**
* Slot to change the default tree item content
Expand Down
39 changes: 39 additions & 0 deletions angular/demo/bootstrap/src/app/samples/tree/editable.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {TreeComponent} from '@agnos-ui/angular-bootstrap';
import type {TreeItem} from '@agnos-ui/angular-headless';
import {Component, signal} from '@angular/core';

@Component({
template: ` <au-component auTree [auNodes]="nodes()" auIsEditable (auNodesChange)="nodesChange($event)" /> `,
imports: [TreeComponent],
})
export default class EditableTreeComponent {
readonly nodes = signal([
{
label: 'Node 1',
isExpanded: true,
children: [
{
label: 'Node 1.1',
children: [
{
label: 'Node 1.1.1',
},
],
},
{
label: 'Node 1.2',
children: [
{
label: 'Node 1.2.1',
},
],
},
],
},
]);

nodesChange(nodes: TreeItem[]) {
// handle the change of the nodes manually in order to avoid the redraw of the tree
console.log('changed in editable', nodes);
}
}
47 changes: 42 additions & 5 deletions angular/demo/daisyui/src/app/samples/tree/default.route.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import {Component} from '@angular/core';
import {TreeComponent} from './tree.component';
import type {NormalizedTreeItem} from '@agnos-ui/angular-headless';
import {type TreeItem} from '@agnos-ui/angular-headless';
import {Component, signal, viewChild} from '@angular/core';
import {TreeComponent} from './tree.component';

@Component({
imports: [TreeComponent],
template: `
<div class="flex justify-center">
<app-tree [nodes]="nodes" [navSelector]="navSelector" />
<app-tree [nodes]="nodes()" [navSelector]="navSelector" />
</div>
`,
})
export default class BasicTreeComponent {
readonly tree = viewChild(TreeComponent);
readonly navSelector = (node: HTMLElement) => node.querySelectorAll<HTMLSpanElement>('span.au-tree-expand-icon');
readonly nodes: TreeItem[] = [

readonly newItem: TreeItem = {
label: 'New Item',
};

readonly nodes = signal<TreeItem[]>([
{
label: 'resume.pdf',
},
Expand Down Expand Up @@ -50,5 +57,35 @@ export default class BasicTreeComponent {
{
label: 'reports-final-2.pdf',
},
];
]);

handleAddNode(targetParent: NormalizedTreeItem) {
const tree = this.tree();
if (!tree) return;

const originalNode = tree.api.getOriginalNode(targetParent);
if (!originalNode) return;

const newItem: TreeItem = {
label: 'New item',
};

this.nodes.update((current) => {
const updateNode = (items: TreeItem[]): TreeItem[] =>
items.map((item) => {
if (item === originalNode) {
return {
...item,
children: item.children ? [...item.children, newItem] : [newItem],
isExpanded: true,
};
}
if (item.children) {
return {...item, children: updateNode(item.children)};
}
return item;
});
return updateNode(current);
});
}
}
11 changes: 7 additions & 4 deletions angular/demo/daisyui/src/app/samples/tree/tree.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ import {ChangeDetectionStrategy, Component, input, output} from '@angular/core';
</ul>

<ng-template #treeItem let-item="item">
<!-- eslint-disable-next-line @angular-eslint/template/role-has-required-aria -->
<li role="treeitem">
<span class="flex flex-wrap items-center" [auUse]="[directives.itemToggleDirective, {item}]">
<li [auUse]="[directives.itemAttributesDirective, {item}]">
<span class="flex flex-nowrap items-center" [auUse]="[directives.itemToggleDirective, {item}]">
<span class="me-1">
<ng-container [ngTemplateOutlet]="itemIcon" [ngTemplateOutletContext]="{item}" />
</span>
<span>{{ item.label }}</span>
@if (item.isEdited) {
<input class="input input-sm w-32 min-w-0 flex-shrink" [auUse]="[directives.itemInputDirective, {item}]" />
} @else {
<span [auUse]="[directives.itemModifyDirective, {item}]">{{ item.label }}</span>
}
@if (item.children.length > 0) {
<span class="ms-auto">
<svg
Expand Down
9 changes: 9 additions & 0 deletions core/src/components/tree/tree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type TestingTreeState = Omit<TreeState, 'expandedMap'>;
const defaultState: () => TestingTreeState = () => ({
className: '',
normalizedNodes: [],
isEditable: false,
});

describe(`Tree`, () => {
Expand Down Expand Up @@ -62,13 +63,15 @@ describe(`Tree`, () => {
ariaLabel: 'root',
level: 0,
isExpanded: false,
isEdited: false,
children: [
{
label: 'child',
ariaLabel: 'child',
level: 1,
isExpanded: undefined,
children: [],
isEdited: false,
},
],
},
Expand All @@ -87,4 +90,10 @@ describe(`Tree`, () => {
expect(state.normalizedNodes[0].isExpanded).toBe(true);
expect(itemExpands.length).toEqual(1);
});

test(`should return the TreeItem based on the NormalizedTreeItem`, () => {
const newNodes = [{label: 'root', ariaLabel: 'root', children: [{label: 'child', ariaLabel: 'child'}]}];
tree.patch({nodes: newNodes});
expect(tree.api.getOriginalNode(state.normalizedNodes[0])).toEqual(newNodes[0]);
});
});
Loading
Loading