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 20, 2024
1 parent de19c38 commit dae3cbc
Show file tree
Hide file tree
Showing 43 changed files with 1,749 additions and 1 deletion.
6 changes: 6 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,7 @@ 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, TreeItemDirective, TreeRootDirective, TreeStructureDirective, TreeToggleDirective} from './components/tree/tree.component';
/* istanbul ignore next */
const components = [
SlotDirective,
Expand Down Expand Up @@ -78,6 +79,11 @@ const components = [
ToastBodyDirective,
ToastHeaderDirective,
CollapseDirective,
TreeComponent,
TreeStructureDirective,
TreeToggleDirective,
TreeItemDirective,
TreeRootDirective,
];

@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';
271 changes: 271 additions & 0 deletions angular/bootstrap/src/components/tree/tree.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
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, 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 node) {
<ng-template [auSlot]="state.root()" [auSlotProps]="{state, api, directives, item: node}"></ng-template>
}
</ul>
</ng-template>
`,
})
class TreeDefaultStructureSlotComponent {
@ViewChild('structure', {static: true}) readonly structure!: TemplateRef<TreeContext>;
}

/**
* 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[auTreeToggle]', standalone: true})
export class TreeToggleDirective {
public templateRef = inject(TemplateRef<TreeSlotItemContext>);
static ngTemplateContextGuard(_dir: TreeToggleDirective, context: unknown): context is TreeSlotItemContext {
return true;

Check warning on line 63 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#L63

Added line #L63 was not covered by tests
}
}

@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [UseDirective, TreeToggleDirective],
template: `
<ng-template auTreeToggle #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 TreeDefaultToggleSlotComponent {
@ViewChild('toggle', {static: true}) readonly toggle!: TemplateRef<TreeSlotItemContext>;
}

/**
* A constant representing the default slot for tree item toggle.
*/
export const treeDefaultSlotToggle: SlotContent<TreeSlotItemContext> = new ComponentTemplate(TreeDefaultToggleSlotComponent, '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[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 99 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#L99

Added line #L99 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">
<span class="au-tree-item">
<ng-template [auSlot]="state.toggle()" [auSlotProps]="{state, api, directives, item}"></ng-template>
{{ item.label }}
</span>
</ng-template>
`,
})
class TreeDefaultItemSlotComponent {
@ViewChild('treeItem', {static: true}) readonly treeItem!: TemplateRef<TreeSlotItemContext>;
}

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

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

Check warning on line 134 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#L134

Added line #L134 was not covered by tests
}
}

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

/**
* A constant representing the default slot for tree item.
*/
export const treeDefaultSlotRoot = new ComponentTemplate(TreeDefaultRootSlotComponent, 'treeRoot');

@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,
root: treeDefaultSlotRoot,
item: treeDefaultSlotItem,
toggle: treeDefaultSlotToggle,
},
events: {
onExpandToggle: (item: TreeItem) => this.expandToggle.emit(item),
},
slotTemplates: () => ({
structure: this.slotStructureFromContent?.templateRef,
root: this.slotRootFromContent?.templateRef,
item: this.slotItemFromContent?.templateRef,
toggle: this.slotToggleFromContent?.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
*/
@Input('auItem') item: SlotContent<TreeSlotItemContext>;
@ContentChild(TreeItemDirective, {static: false}) slotItemFromContent: TreeItemDirective | 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(TreeToggleDirective, {static: false}) slotToggleFromContent: TreeToggleDirective | undefined;

/**
* Slot to change the default tree root
*/
@Input('auRoot') root: SlotContent<TreeSlotItemContext>;
@ContentChild(TreeRootDirective, {static: false}) slotRootFromContent: TreeRootDirective | 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} 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
35 changes: 35 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,35 @@
import type {TreeItem} from '@agnos-ui/angular-bootstrap';
import {TreeComponent} 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',
},
],
},
],
},
];
}
1 change: 1 addition & 0 deletions core-bootstrap/src/components/tree/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './tree';
Loading

0 comments on commit dae3cbc

Please sign in to comment.