Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Create tree component #999

Merged
Merged
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
12 changes: 12 additions & 0 deletions angular/bootstrap/src/agnos-ui-angular.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ import {SliderComponent, SliderHandleDirective, SliderLabelDirective, SliderStru
import {ProgressbarComponent, ProgressbarBodyDirective, ProgressbarStructureDirective} from './components/progressbar/progressbar.component';
import {ToastBodyDirective, ToastComponent, ToastHeaderDirective, ToastStructureDirective} from './components/toast/toast.component';
import {CollapseDirective} from './components/collapse';
import {
TreeComponent,
TreeItemContentDirective,
TreeItemDirective,
TreeStructureDirective,
TreeItemToggleDirective,
} from './components/tree/tree.component';
/* istanbul ignore next */
const components = [
SlotDirective,
Expand Down Expand Up @@ -78,6 +85,11 @@ const components = [
ToastBodyDirective,
ToastHeaderDirective,
CollapseDirective,
TreeComponent,
TreeStructureDirective,
TreeItemToggleDirective,
TreeItemContentDirective,
TreeItemDirective,
];

@NgModule({
Expand Down
2 changes: 2 additions & 0 deletions angular/bootstrap/src/components/tree/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './tree.component';
export * from './tree.gen';
287 changes: 287 additions & 0 deletions angular/bootstrap/src/components/tree/tree.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
import type {SlotContent} from '@agnos-ui/angular-headless';
import {BaseWidgetDirective, callWidgetFactory, ComponentTemplate, SlotDirective, UseDirective} from '@agnos-ui/angular-headless';
import {
ChangeDetectionStrategy,
Component,
ContentChild,
Directive,
EventEmitter,
inject,
Input,
Output,
TemplateRef,
ViewChild,
} from '@angular/core';
import type {TreeContext, TreeItem, NormalizedTreeItem, TreeSlotItemContext, TreeWidget} from './tree.gen';
import {createTree} from './tree.gen';

/**
* Directive to provide a template reference for tree structure.
*
* This directive uses a template reference to render the {@link TreeContext}.
*/
@Directive({selector: 'ng-template[auTreeStructure]', standalone: true})
export class TreeStructureDirective {
public templateRef = inject(TemplateRef<TreeContext>);
static ngTemplateContextGuard(_dir: TreeStructureDirective, context: unknown): context is TreeContext {
return true;

Check warning on line 27 in angular/bootstrap/src/components/tree/tree.component.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L27 was not covered by tests
}
}

@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [UseDirective, TreeStructureDirective, SlotDirective],
template: `
<ng-template auTreeStructure #structure let-state="state" let-directives="directives" let-api="api">
<ul role="tree" class="au-tree {{ state.className() }}" [auUse]="directives.navigationDirective">
@for (node of state.normalizedNodes(); track trackNode($index, node)) {
<ng-template [auSlot]="state.item()" [auSlotProps]="{state, api, directives, item: node}"></ng-template>
}
</ul>
</ng-template>
`,
})
class TreeDefaultStructureSlotComponent {
@ViewChild('structure', {static: true}) readonly structure!: TemplateRef<TreeContext>;

trackNode(index: number, node: NormalizedTreeItem): string {
return node.label + node.level + index;
}
}

/**
* A constant representing the default slot for tree structure.
*/
export const treeDefaultSlotStructure: SlotContent<TreeContext> = new ComponentTemplate(TreeDefaultStructureSlotComponent, 'structure');

/**
* Directive to provide a template reference for tree item toggle.
*
* This directive uses a template reference to render the {@link TreeSlotItemContext}.
*/
@Directive({selector: 'ng-template[auTreeItemToggle]', standalone: true})
export class TreeItemToggleDirective {
public templateRef = inject(TemplateRef<TreeSlotItemContext>);
static ngTemplateContextGuard(_dir: TreeItemToggleDirective, context: unknown): context is TreeSlotItemContext {
return true;

Check warning on line 67 in angular/bootstrap/src/components/tree/tree.component.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L67 was not covered by tests
}
}

@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [UseDirective, TreeItemToggleDirective],
template: `
<ng-template auTreeItemToggle #toggle let-directives="directives" let-item="item">
@if (item.children.length > 0) {
<button [auUse]="[directives.itemToggleDirective, {item}]"></button>
} @else {
<span class="au-tree-expand-icon-placeholder"></span>
}
</ng-template>
`,
})
class TreeDefaultItemToggleSlotComponent {
@ViewChild('toggle', {static: true}) readonly toggle!: TemplateRef<TreeSlotItemContext>;
}

/**
* A constant representing the default slot for tree item toggle.
*/
export const treeDefaultItemToggle: SlotContent<TreeSlotItemContext> = new ComponentTemplate(TreeDefaultItemToggleSlotComponent, 'toggle');

/**
* Directive to provide a template reference for tree item content.
*
* This directive uses a template reference to render the {@link TreeSlotItemContext}.
*/
@Directive({selector: 'ng-template[auTreeItemContent]', standalone: true})
export class TreeItemContentDirective {
public templateRef = inject(TemplateRef<TreeSlotItemContext>);

Check warning on line 101 in angular/bootstrap/src/components/tree/tree.component.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L101 was not covered by tests
static ngTemplateContextGuard(_dir: TreeItemContentDirective, context: unknown): context is TreeSlotItemContext {
return true;

Check warning on line 103 in angular/bootstrap/src/components/tree/tree.component.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L103 was not covered by tests
}
}

@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [UseDirective, SlotDirective, TreeItemContentDirective],
template: `
<ng-template auTreeItem #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}"></ng-template>
{{ item.label }}
</span>
</ng-template>
`,
})
class TreeDefaultItemContentSlotComponent {
@ViewChild('treeItemContent', {static: true}) readonly treeItemContent!: TemplateRef<TreeSlotItemContext>;
}

/**
* A constant representing the default slot for tree item.
*/
export const treeDefaultSlotItemContent: SlotContent<TreeSlotItemContext> = new ComponentTemplate(
TreeDefaultItemContentSlotComponent,
'treeItemContent',
);

/**
* Directive to provide a template reference for tree item.
*
* This directive uses a template reference to render the {@link TreeSlotItemContext}.
*/
@Directive({selector: 'ng-template[auTreeItem]', standalone: true})
export class TreeItemDirective {
public templateRef = inject(TemplateRef<TreeSlotItemContext>);
static ngTemplateContextGuard(_dir: TreeItemDirective, context: unknown): context is TreeSlotItemContext {
return true;

Check warning on line 141 in angular/bootstrap/src/components/tree/tree.component.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L141 was not covered by tests
}
}

@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [UseDirective, SlotDirective, TreeItemDirective],
template: `
<ng-template auTreeItem #treeItem let-state="state" let-directives="directives" let-item="item" let-api="api">
<li [auUse]="[directives.itemAttributesDirective, {item}]">
<ng-template [auSlot]="state.itemContent()" [auSlotProps]="{state, api, directives, item}"></ng-template>
@if (state.expandedMap().get(item)) {
<ul role="group">
@for (child of item.children; track trackNode($index, child)) {
<ng-template [auSlot]="state.item()" [auSlotProps]="{state, api, directives, item: child}"></ng-template>
}
</ul>
}
</li>
</ng-template>
`,
})
class TreeDefaultItemSlotComponent {
@ViewChild('treeItem', {static: true}) readonly treeItem!: TemplateRef<TreeSlotItemContext>;

trackNode(index: number, node: NormalizedTreeItem) {
return node.label + node.level + index;
}
}

/**
* A constant representing the default slot for tree item.
*/
export const treeDefaultSlotItem: SlotContent<TreeSlotItemContext> = new ComponentTemplate(TreeDefaultItemSlotComponent, 'treeItem');

/**
* TreeComponent is an Angular component that extends the BaseWidgetDirective
* to provide a customizable tree widget. This component allows for various
* configurations and customizations through its inputs and outputs.
*/
@Component({
selector: '[auTree]',
standalone: true,
imports: [SlotDirective],
template: ` <ng-template [auSlot]="state.structure()" [auSlotProps]="{state, api, directives}"></ng-template> `,
})
export class TreeComponent extends BaseWidgetDirective<TreeWidget> {
quentinderoubaix marked this conversation as resolved.
Show resolved Hide resolved
constructor() {
super(
callWidgetFactory({
factory: createTree,
widgetName: 'tree',
defaultConfig: {
structure: treeDefaultSlotStructure,
item: treeDefaultSlotItem,
itemContent: treeDefaultSlotItemContent,
itemToggle: treeDefaultItemToggle,
},
events: {
onExpandToggle: (item: NormalizedTreeItem) => this.expandToggle.emit(item),
},
slotTemplates: () => ({
structure: this.slotStructureFromContent?.templateRef,
item: this.slotItemFromContent?.templateRef,
itemContent: this.slotItemContentFromContent?.templateRef,
itemToggle: this.slotItemToggleFromContent?.templateRef,
}),
}),
);
}
/**
* Optional accessibility label for the tree if there is no explicit label
*
* @defaultValue `''`
*/
@Input('auAriaLabel') ariaLabel: string | undefined;
/**
* Array of the tree nodes to display
*
* @defaultValue `[]`
*/
@Input('auNodes') nodes: TreeItem[] | undefined;
/**
* CSS classes to be applied on the widget main container
*
* @defaultValue `''`
*/
@Input('auClassName') className: string | undefined;
/**
* Retrieves expand items of the TreeItem
quentinderoubaix marked this conversation as resolved.
Show resolved Hide resolved
*
* @param node - HTML element that is representing the expand item
*
* @defaultValue
* ```ts
* (node: HTMLElement) => node.querySelectorAll('button')
* ```
*/
@Input('auNavSelector') navSelector: ((node: HTMLElement) => NodeListOf<HTMLButtonElement>) | undefined;
/**
* Return the value for the 'aria-label' attribute of the toggle
* @param label - tree item label
*
* @defaultValue
* ```ts
* (label: string) => `Toggle ${label}`
* ```
*/
@Input('auAriaLabelToggleFn') ariaLabelToggleFn: ((label: string) => string) | undefined;

/**
* An event emitted when the user toggles the expand of the TreeItem.
*
* Event payload is equal to the TreeItem clicked.
*
* @defaultValue
* ```ts
* () => {}
* ```
*/
@Output('auExpandToggle') expandToggle = new EventEmitter<TreeItem>();

/**
* Slot to change the default tree item content
*/
@Input('auItemContent') item: SlotContent<TreeSlotItemContext>;
@ContentChild(TreeItemContentDirective, {static: false}) slotItemContentFromContent: TreeItemContentDirective | undefined;

/**
* Slot to change the default display of the tree
*/
@Input('auStructure') structure: SlotContent<TreeContext>;
@ContentChild(TreeStructureDirective, {static: false}) slotStructureFromContent: TreeStructureDirective | undefined;

/**
* Slot to change the default tree item toggle
*/
@Input('auToggle') toggle: SlotContent<TreeSlotItemContext>;
@ContentChild(TreeItemToggleDirective, {static: false}) slotItemToggleFromContent: TreeItemToggleDirective | undefined;

/**
* Slot to change the default tree item
*/
@Input('auItem') root: SlotContent<TreeSlotItemContext>;
@ContentChild(TreeItemDirective, {static: false}) slotItemFromContent: TreeItemDirective | undefined;
}
4 changes: 4 additions & 0 deletions angular/bootstrap/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ export type {ToastContext, ToastProps, ToastState, ToastWidget, ToastApi, ToastD
export {createToast, getToastDefaultConfig} from './components/toast';
export * from './components/toast';

export type {TreeProps, TreeState, TreeWidget, TreeApi, TreeDirectives, TreeItem, NormalizedTreeItem} from './components/tree';
export {createTree, getTreeDefaultConfig} from './components/tree';
export * from './components/tree';

export * from '@agnos-ui/core-bootstrap/services/transitions';
export * from '@agnos-ui/core-bootstrap/types';

Expand Down
34 changes: 34 additions & 0 deletions angular/demo/bootstrap/src/app/samples/tree/basic.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {TreeComponent, type TreeItem} from '@agnos-ui/angular-bootstrap';
import {Component} from '@angular/core';

@Component({
standalone: true,
template: ` <au-component auTree [auNodes]="nodes"></au-component> `,
imports: [TreeComponent],
})
export default class BasicTreeComponent {
readonly nodes: TreeItem[] = [
{
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',
},
],
},
],
},
];
}
4 changes: 4 additions & 0 deletions angular/ssr-app/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,8 @@ <h2>Toast</h2>
<div class="my-3">
<au-component auToast>This is a toast!</au-component>
</div>
<h2>Tree</h2>
<div class="my-3">
<au-component auTree [auNodes]="nodes"></au-component>
</div>
</div>
Loading
Loading