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 4, 2024
1 parent ebfbdac commit 451b793
Show file tree
Hide file tree
Showing 40 changed files with 1,134 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';
222 changes: 222 additions & 0 deletions angular/bootstrap/src/components/tree/tree.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
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>);
static ngTemplateContextGuard(_dir: TreeStructureDirective, context: unknown): context is TreeContext {
return true;
}
}

@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() }}">
@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;
}
}

@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
class="au-tree-expand-button"
[class.au-tree-expand-button-expanded]="state.expandedMap().get(item)"
[auUse]="[directives.itemToggleDirective, {item}]"
></button>
} @else {
<span class="au-tree-expand-button-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;
}
}

@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;
}
}

@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 accessiblity 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;

/**
* 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,
ariaLabel: 'Node 1',
children: [
{
label: 'Node 1.1',
isExpanded: false,
ariaLabel: 'Node 1.1',
children: [
{
label: 'Node 1.1.1',
ariaLabel: 'Node 1.1.1',
},
],
},
{
label: 'Node 1.2',
ariaLabel: 'Node 1.2',
},
],
},
];
}
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';
60 changes: 60 additions & 0 deletions core-bootstrap/src/components/tree/tree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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';

export type TreeContext = WidgetSlotContext<TreeWidget>;
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>;
}

export interface TreeState extends CoreState, TreeExtraProps {}
export interface TreeProps extends CoreProps, TreeExtraProps {}

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};
}

/**
* 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,
});
5 changes: 5 additions & 0 deletions core-bootstrap/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {RatingProps} from './components/rating';
import type {SelectProps} from './components/select';
import type {SliderProps} from './components/slider';
import type {ToastProps} from './components/toast';
import type {TreeProps} from './components/tree';

/**
* Configuration interface for various Bootstrap widgets.
Expand Down Expand Up @@ -53,4 +54,8 @@ export interface BootstrapWidgetsConfig {
* collapse widget config
*/
collapse: CollapseProps;
/**
* tree widget config
*/
tree: TreeProps;
}
1 change: 1 addition & 0 deletions core-bootstrap/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from './components/select';
export * from './components/slider';
export * from './components/toast';
export * from './components/collapse';
export * from './components/tree';

export * from './services/transitions';
export * from './config';
Expand Down
13 changes: 13 additions & 0 deletions core-bootstrap/src/scss/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,16 @@ $au-slider-handle-size-lg: 1.5rem !default;
$au-slider-font-size-lg: 1.125rem !default;
$au-slider-offset-lg: 0rem !default;
// scss-docs-end slider-vars

//tree variables
// scss-docs-start tree-vars
$au-tree-item-padding-start: 2.25rem !default;
$au-tree-expand-button-margin-inline-end: 0.5rem !default;
$au-tree-expand-button-border-radius: 0.375rem !default;
$au-tree-expand-button-background-color: transparent !default;
$au-tree-expand-button-background-color-hover: #c5d5f9 !default;
$au-tree-expand-icon-color-default: blue !default; // TODO change to a proper color
$au-tree-expand-icon-color-hover: darkblue !default; // TODO change to a proper color
$au-tree-expand-icon-default: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 10 10"><path d="M3 1 L7 5 L3 9" stroke="#{$au-tree-expand-icon-color-default}" stroke-width="1" fill="none"/></svg>') !default;
$au-tree-expand-icon-hover: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 10 10"><path d="M3 1 L7 5 L3 9" stroke="#{$au-tree-expand-icon-color-hover}" stroke-width="1" fill="none"/></svg>') !default;
// scss-docs-end slider-vars
Loading

0 comments on commit 451b793

Please sign in to comment.