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 19, 2024
1 parent dd0c588 commit 1802fac
Show file tree
Hide file tree
Showing 41 changed files with 1,694 additions and 0 deletions.
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';
239 changes: 239 additions & 0 deletions angular/bootstrap/src/components/tree/tree.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
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({selector: 'ng-template[auTreeStructure]', standalone: true})
export class TreeStructureDirective {
public templateRef = inject(TemplateRef<TreeContext>);

Check warning on line 20 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#L20

Added line #L20 was not covered by tests
static ngTemplateContextGuard(_dir: TreeStructureDirective, context: unknown): context is TreeContext {
return true;

Check warning on line 22 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#L22

Added line #L22 was not covered by tests
}
}

@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [UseDirective, TreeStructureDirective, SlotDirective],
template: `
<ng-template autTreeStructure #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>
`,
})
export class TreeDefaultStructureSlotComponent {
@ViewChild('structure', {static: true}) readonly structure!: TemplateRef<TreeContext>;
}

export const treeDefaultSlotStructure = new ComponentTemplate(TreeDefaultStructureSlotComponent, 'structure');

@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 50 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#L50

Added line #L50 was not covered by tests
}
}

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

export const treeDefaultSlotToggle = new ComponentTemplate(TreeDefaultToggleSlotComponent, 'toggle');

@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 78 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#L78

Added line #L78 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>
`,
})
export class TreeDefaultItemSlotComponent {
@ViewChild('treeItem', {static: true}) readonly treeItem!: TemplateRef<TreeSlotItemContext>;
}

export const treeDefaultSlotItem = new ComponentTemplate(TreeDefaultItemSlotComponent, 'treeItem');

@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 105 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#L105

Added line #L105 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>
`,
})
export class TreeDefaultRootSlotComponent {
@ViewChild('treeRoot', {static: true}) readonly treeRoot!: TemplateRef<TreeSlotItemContext>;
}

export const treeDefaultSlotRoot = new ComponentTemplate(TreeDefaultRootSlotComponent, 'treeRoot');

@Component({
selector: '[auTree]',
standalone: true,
imports: [UseDirective, 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 {AgnosUIAngularModule} from '@agnos-ui/angular-bootstrap';
import {Component} from '@angular/core';

@Component({
standalone: true,
template: ` <au-component auTree [auNodes]="nodes"></au-component> `,
imports: [AgnosUIAngularModule],
})
export default class BasicTreeComponent {
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';
76 changes: 76 additions & 0 deletions core-bootstrap/src/components/tree/tree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type {TreeProps as CoreProps, TreeState as CoreState, TreeApi, TreeDirectives, TreeItem} from '@agnos-ui/core/components/tree';
import {createTree as createCoreTree, getTreeDefaultConfig as getCoreDefaultConfig} from '@agnos-ui/core/components/tree';
import {extendWidgetProps} from '@agnos-ui/core/services/extendWidget';
import type {SlotContent, Widget, WidgetFactory, WidgetSlotContext} from '@agnos-ui/core/types';

export * from '@agnos-ui/core/components/tree';

/**
* Represents the context for a Tree widget.
* This interface is an alias for `WidgetSlotContext<TreeWidget>`.
*/
export type TreeContext = WidgetSlotContext<TreeWidget>;
/**
* Represents the context for a tree item, extending the base `TreeContext`
* with an additional `item` property.
*/
export type TreeSlotItemContext = TreeContext & {item: TreeItem};

interface TreeExtraProps {
/**
* Slot to change the default display of the tree
*/
structure: SlotContent<TreeContext>;
/**
* Slot to change the default tree root
*/
root: SlotContent<TreeSlotItemContext>;
/**
* Slot to change the default tree item
*/
item: SlotContent<TreeSlotItemContext>;
/**
* Slot to change the default tree item toggle
*/
toggle: SlotContent<TreeSlotItemContext>;
}

/**
* Represents the state of a Tree component.
*/
export interface TreeState extends CoreState, TreeExtraProps {}
/**
* Represents the properties for the Tree component.
*/
export interface TreeProps extends CoreProps, TreeExtraProps {}
/**
* Represents a Tree widget component.
*/
export type TreeWidget = Widget<TreeProps, TreeState, TreeApi, TreeDirectives>;

const defaultConfigExtraProps: TreeExtraProps = {
structure: undefined,
root: undefined,
item: undefined,
toggle: undefined,
};

/**
* Retrieve a shallow copy of the default Tree config
* @returns the default Tree config
*/
export function getTreeDefaultConfig(): TreeProps {
return {...getCoreDefaultConfig(), ...defaultConfigExtraProps};

Check warning on line 63 in core-bootstrap/src/components/tree/tree.ts

View check run for this annotation

Codecov / codecov/patch

core-bootstrap/src/components/tree/tree.ts#L63

Added line #L63 was not covered by tests
}

/**
* Create a Tree with given config props
* @param config - an optional tree config
* @returns a TreeWidget
*/
export const createTree: WidgetFactory<TreeWidget> = extendWidgetProps(createCoreTree, defaultConfigExtraProps, {
structure: undefined,
root: undefined,
item: undefined,
toggle: undefined,
});
Loading

0 comments on commit 1802fac

Please sign in to comment.