Skip to content

Commit

Permalink
feat: Create tree component
Browse files Browse the repository at this point in the history
  • Loading branch information
MarkoOleksiyenko committed Nov 25, 2024
1 parent de19c38 commit ed5a197
Show file tree
Hide file tree
Showing 48 changed files with 1,998 additions and 3 deletions.
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> {
constructor() {
super(
callWidgetFactory({
factory: createTree,
widgetName: 'tree',
defaultConfig: {
structure: treeDefaultSlotStructure,
item: treeDefaultSlotItem,
itemContent: treeDefaultSlotItemContent,
itemToggle: treeDefaultItemToggle,
},
events: {
onExpandToggle: (item: TreeItem) => 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
*
* @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

0 comments on commit ed5a197

Please sign in to comment.