### File

`src/lib/tree.component.ts`

### Implements

`OnInit` `AfterContentInit`

### Metadata

changeDetection | `ChangeDetectionStrategy.OnPush`
selector | `rxap-tree`
standalone | `true`
imports | `NgStyle` `NgIf` `MatProgressBarModule` `MatTreeModule` `MatIconModule` `IconDirective` `MatCheckboxModule` `MatButtonModule` `ContenteditableDirective` `MatProgressSpinnerModule` `MatDividerModule` `PortalModule` `AsyncPipe` `NgClass` `NgForOf`
styleUrls | `./tree.component.scss`
templateUrl | `./tree.component.html`

### Index

###### Properties

- Public Optional content
- Public getLevel
- Public hasChild
- Public isExpandable
- Public portal
- Public Readonly searchForm
- Public Readonly showTreeNavigation
- Public treeContainer
- Public treeControl

###### Methods

- Public onContentEditableChange
- onMousedown
- onMousemove
- onMouseup
- Public openDetails
- toggleTreeNavigation

###### Inputs

- contentEditableMethod
- dataSource
- dividerOffset
- getIcon
- getStyle
- getType
- hasDetails
- hideLeafIcon
- id
- multiple
- toDisplay

###### Outputs

- details

###### HostListeners

- mousemove
- mouseup

###### Accessors

- nodeDisplayEditable
- cacheId

### Constructor

`constructor(viewContainerRef: ViewContainerRef, cdr: ChangeDetectorRef, contentEditableMethod: Method<any | string | null> | null, renderer: Renderer2, elementRef: ElementRef<HTMLElement>, searchForm: SearchForm | null)`
Defined in src/lib/tree.component.ts:120

Parameters :

Name | Type | Optional
viewContainerRef | `ViewContainerRef` | No
cdr | `ChangeDetectorRef` | No
contentEditableMethod | `Method<any | string | null> | null` | No
renderer | `Renderer2` | No
elementRef | `ElementRef<HTMLElement>` | No
searchForm | `SearchForm | null` | No

### Inputs

contentEditableMethod
Type : `Method<any | string | null> | null`
Defined in src/lib/tree.component.ts:80

dataSource
Type : `TreeDataSource<Data>`
Required : true
Defined in src/lib/tree.component.ts:78

dividerOffset
Type : `string`
Default value : `'256px'`
Defined in src/lib/tree.component.ts:102

getIcon
Type : `NodeGetIconFunction<any>`
Defined in src/lib/tree.component.ts:84

getStyle
Type : `NodeGetStyleFunction<any>`
Defined in src/lib/tree.component.ts:88

getType
Type : `NodeGetTypeFunction<any>`
Defined in src/lib/tree.component.ts:86

hasDetails
Type : `NodeHasDetailsFunction<any>`
Defined in src/lib/tree.component.ts:92

hideLeafIcon
Type : `boolean`
Default value : `false`
Defined in src/lib/tree.component.ts:96

id
Type : `string`
Defined in src/lib/tree.component.ts:98

multiple
Type : `boolean`
Default value : `false`
Defined in src/lib/tree.component.ts:90

toDisplay
Type : `NodeToDisplayFunction<any>`
Defined in src/lib/tree.component.ts:82

### Outputs

details
Type : `EventEmitter`
Defined in src/lib/tree.component.ts:100

### HostListeners

mousemove
Arguments : `'$event'`
`mousemove($event: MouseEvent)`
Defined in src/lib/tree.component.ts:219

mouseup
`mouseup()`
Defined in src/lib/tree.component.ts:214

### Methods

Public onContentEditableChange
`onContentEditableChange(value: string | null, node: Node)`
Defined in src/lib/tree.component.ts:204

Parameters :

Name | Type | Optional
value | `string | null` | No
node | `Node<Data>` | No

Returns : `any`

onMousedown
`onMousedown()`
Defined in src/lib/tree.component.ts:209
Returns : `void`

onMousemove
`onMousemove($event: MouseEvent)`
Decorators : `@HostListener('mousemove', ['$event'])`
Defined in src/lib/tree.component.ts:219

Parameters :

Name | Type | Optional
$event | `MouseEvent` | No

Returns : `void`

onMouseup
`onMouseup()`
Decorators : `@HostListener('mouseup')`
Defined in src/lib/tree.component.ts:214
Returns : `void`

Public openDetails
`openDetails(node: Node)`
Decorators : `@DebounceCall(100)`
Defined in src/lib/tree.component.ts:189

Parameters :

Name | Type | Optional
node | `Node<Data>` | No

Returns : `void`

toggleTreeNavigation
`toggleTreeNavigation()`
Defined in src/lib/tree.component.ts:233
Returns : `void`

### Properties

Public Optional content
Type : `TreeContentDirective`
Decorators : `@ContentChild(TreeContentDirective, {static: true})`
Defined in src/lib/tree.component.ts:94

Public getLevel
Default value : `() => {...}`
Defined in src/lib/tree.component.ts:148

Public hasChild
Default value : `() => {...}`
Defined in src/lib/tree.component.ts:152

Public isExpandable
Default value : `() => {...}`
Defined in src/lib/tree.component.ts:150

Public portal
Type : `TemplatePortal | null`
Default value : `null`
Defined in src/lib/tree.component.ts:103

Public Readonly searchForm
Type : `SearchForm | null`
Decorators : `@Optional() @Inject(RXAP_FORM_DEFINITION)`
Defined in src/lib/tree.component.ts:134

Public Readonly showTreeNavigation
Default value : `signal(true)`
Defined in src/lib/tree.component.ts:107

Public treeContainer
Type : `ElementRef`
Decorators : `@ViewChild('treeContainer', {static: true})`
Defined in src/lib/tree.component.ts:105

Public treeControl
Type : `FlatTreeControl<Node<Data>>`
Defined in src/lib/tree.component.ts:76

### Accessors

nodeDisplayEditable
get `nodeDisplayEditable()`
Defined in src/lib/tree.component.ts:140

cacheId
get `cacheId()`
Defined in src/lib/tree.component.ts:144

    import { PortalModule, TemplatePortal } from '@angular/cdk/portal';
    import { FlatTreeControl } from '@angular/cdk/tree';
    import { AsyncPipe, NgClass, NgForOf, NgIf, NgStyle } from '@angular/common';
    import {
      AfterContentInit,
      ChangeDetectionStrategy,
      ChangeDetectorRef,
      Component,
      ContentChild,
      ElementRef,
      EventEmitter,
      HostListener,
      Inject,
      Input,
      isDevMode,
      OnInit,
      Optional,
      Output,
      Renderer2,
      signal,
      ViewChild,
      ViewContainerRef,
    } from '@angular/core';
    import { MatButtonModule } from '@angular/material/button';
    import { MatCheckboxModule } from '@angular/material/checkbox';
    import { MatDividerModule } from '@angular/material/divider';
    import { MatIconModule } from '@angular/material/icon';
    import { MatProgressBarModule } from '@angular/material/progress-bar';
    import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
    import { MatTreeModule } from '@angular/material/tree';
    import { ContenteditableDirective } from '@rxap/contenteditable';
    import {
      Node,
      NodeGetIconFunction,
      NodeGetStyleFunction,
      NodeGetTypeFunction,
      NodeHasDetailsFunction,
      NodeToDisplayFunction,
    } from '@rxap/data-structure-tree';
    import { IconDirective } from '@rxap/material-directives/icon';
    import { Method } from '@rxap/pattern';
    import { DebounceCall, WithChildren, WithIdentifier } from '@rxap/utilities';
    import { map, startWith, tap } from 'rxjs/operators';
    import { RXAP_TREE_CONTENT_EDITABLE_METHOD } from './tokens';
    import { TreeContentDirective } from './tree-content.directive';
    import { TreeDataSource } from './tree.data-source';
    import { SearchForm } from './search.form';
    import { RXAP_FORM_DEFINITION } from '@rxap/forms';

    @Component({
      selector: 'rxap-tree',
      templateUrl: './tree.component.html',
      styleUrls: [ './tree.component.scss' ],
      changeDetection: ChangeDetectionStrategy.OnPush,
      standalone: true,
      imports: [
        NgStyle,
        NgIf,
        MatProgressBarModule,
        MatTreeModule,
        MatIconModule,
        IconDirective,
        MatCheckboxModule,
        MatButtonModule,
        ContenteditableDirective,
        MatProgressSpinnerModule,
        MatDividerModule,
        PortalModule,
        AsyncPipe,
        NgClass,
        NgForOf,
      ],
    })
    export class TreeComponent<Data extends WithIdentifier & WithChildren = any>
      implements OnInit, AfterContentInit {
      public treeControl: FlatTreeControl<Node<Data>>;
      @Input({required: true})
      public dataSource!: TreeDataSource<Data>;
      @Input()
      public contentEditableMethod?: Method<any, string | null> | null;
      @Input()
      public toDisplay?: NodeToDisplayFunction<any>;
      @Input()
      public getIcon?: NodeGetIconFunction<any>;
      @Input()
      public getType?: NodeGetTypeFunction<any>;
      @Input()
      public getStyle?: NodeGetStyleFunction<any>;
      @Input()
      public multiple                      = false;
      @Input()
      public hasDetails?: NodeHasDetailsFunction<any>;
      @ContentChild(TreeContentDirective, {static: true})
      public content?: TreeContentDirective;
      @Input()
      public hideLeafIcon                  = false;
      @Input()
      public id?: string;
      @Output()
      public details                       = new EventEmitter();
      @Input()
      public dividerOffset                 = '256px';
      public portal: TemplatePortal | null = null;
      @ViewChild('treeContainer', {static: true})
      public treeContainer!: ElementRef;

      public readonly showTreeNavigation = signal(true);

      /**
       * Indicates that the divider is moved with mouse down
       * @private
       */
      private _moveDivider                       = false;
      /**
       * Holds the current tree container width.
       * If null the move divider feature was not yet used and the initial
       * container width is not calculated
       * @private
       */
      private _treeContainerWidth: number | null = null;

      constructor(
        @Inject(ViewContainerRef)
        private readonly viewContainerRef: ViewContainerRef,
        @Inject(ChangeDetectorRef)
        private readonly cdr: ChangeDetectorRef,
        @Optional()
        @Inject(RXAP_TREE_CONTENT_EDITABLE_METHOD)
          contentEditableMethod: Method<any, string | null> | null,
        private readonly renderer: Renderer2,
        private readonly elementRef: ElementRef<HTMLElement>,
        @Optional()
        @Inject(RXAP_FORM_DEFINITION)
        public readonly searchForm: SearchForm | null,
      ) {
        this.treeControl           = new FlatTreeControl(this.getLevel, this.isExpandable);
        this.contentEditableMethod = contentEditableMethod;
      }

      public get nodeDisplayEditable(): boolean {
        return !!this.contentEditableMethod;
      }

      public get cacheId() {
        return [ 'rxap', 'tree', this.id ].join('/');
      }

      public getLevel = (node: Node<Data>) => node.depth;

      public isExpandable = (node: Node<Data>) => node.hasChildren;

      public hasChild = (_: number, nodeData: Node<Data>) => nodeData.hasChildren;

      public ngOnInit() {
        this.dataSource.searchForm = this.searchForm;
        this.dataSource.setTreeControl(this.treeControl);
        this.dataSource.setToDisplay(this.toDisplay);
        this.dataSource.setGetIcon(this.getIcon);
        this.dataSource.setHasDetails(this.hasDetails);
        this.dataSource.setGetStyle(this.getStyle);
        this.dataSource.setGetType(this.getType);
        this.multiple = this.dataSource.metadata.selectMultiple ?? this.multiple;

        if (this.dataSource.selected.hasValue()) {
          this.dataSource.selected.selected.forEach((node) =>
            this.openDetails(node),
          );
        }

        const cachedOffset = localStorage.getItem(this.cacheId);
        if (cachedOffset && cachedOffset.match(/^(\d+\.)?\d+px$/)) {
          this.setDividerOffset(cachedOffset);
        } else if (isDevMode()) {
          console.log('Divider offset cache is not available or invalid: ' + cachedOffset);
        }
      }

      public ngAfterContentInit(): void {
        this.dataSource.selected.changed
        .pipe(
          map(($event) => $event.source.selected),
          startWith(this.dataSource.selected.selected),
          tap((selected) => selected.forEach((node) => this.openDetails(node))),
        )
        .subscribe();
      }

      @DebounceCall(100)
      public openDetails(node: Node<Data>): void {
        if (this.content) {
          this.portal = new TemplatePortal<any>(
            this.content.template,
            this.viewContainerRef,
            {
              $implicit: node.item,
              node,
            },
          );
          this.cdr.markForCheck();
        }
        this.details.emit(node.item);
      }

      public onContentEditableChange(value: string | null, node: Node<Data>) {
        return this.contentEditableMethod?.call(value, node.item, node);
      }

      onMousedown() {
        this._moveDivider = true;
      }

      @HostListener('mouseup')
      onMouseup() {
        this._moveDivider = false;
      }

      @HostListener('mousemove', [ '$event' ])
      onMousemove($event: MouseEvent) {
        if (this._moveDivider) {
          if (!this._treeContainerWidth) {
            this._treeContainerWidth = this.treeContainer.nativeElement.clientWidth as number;
          }
          const rect               = this.elementRef.nativeElement.getBoundingClientRect();
          this._treeContainerWidth = Math.min(Math.max($event.clientX - (
            rect.left + 12
          ), 128), rect.right - rect.left - 128);
          const offset             = this._treeContainerWidth + 'px';
          this.setDividerOffset(offset);
        }
      }

      toggleTreeNavigation() {
        this.showTreeNavigation.update((value) => !value);
      }

      private setDividerOffset(offset: string) {
        this.dividerOffset = offset;
        this.renderer.setStyle(this.treeContainer.nativeElement, 'max-width', offset);
        this.renderer.setStyle(this.treeContainer.nativeElement, 'min-width', offset);
        this.renderer.setStyle(this.treeContainer.nativeElement, 'flex-basis', offset);
        localStorage.setItem(this.cacheId, offset);
      }
    }

    <div class="grow flex flex-col gap-2 justify-start items-start">
      <button (click)="toggleTreeNavigation()" class="!justify-start" mat-button type="button">
        <span class="flex flex-row justify-start items-center gap-6">
          <ng-template [ngIf]="showTreeNavigation()">
            <mat-icon>arrow_back</mat-icon>
            <span i18n>Hide tree navigation</span>
          </ng-template>
          <ng-template [ngIf]="!showTreeNavigation()">
            <mat-icon>arrow_forward</mat-icon>
            <span i18n>Show tree navigation</span>
          </ng-template>
        </span>
      </button>
      <div class="flex flex-row grow w-full">
        <div #treeContainer [ngClass]="{ 'hidden': !showTreeNavigation() }"
             [ngStyle]="{ maxWidth: dividerOffset, minWidth: dividerOffset, flexBasis: dividerOffset }"
             class="w-fit grow-0 overflow-y-auto">
          <mat-progress-bar *ngIf="!dataSource || (dataSource.loading$ | async)" mode="indeterminate"></mat-progress-bar>
          <ng-content select="[searchHeader]"></ng-content>
          <mat-tree *ngIf="dataSource; else loading" [dataSource]="dataSource" [treeControl]="treeControl">

            <!-- Node without children -->
            <mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding>
              <div class="flex flex-row justify-start items-center gap-2">
                <mat-icon [ngClass]="{ 'hidden': hideLeafIcon }">subdirectory_arrow_right</mat-icon>
                <ng-container *ngIf="node.icon?.length">
                  <mat-icon *ngFor="let icon of node.icon" [rxapIcon]="$any(icon)"></mat-icon>
                </ng-container>
                <ng-template [ngIf]="multiple">
                  <mat-checkbox
                    (change)="node.toggleSelect()"
                    [checked]="node.selected"
                    [disabled]="!node.hasDetails"
                    [ngStyle]="node.style"
                    class="grow-0 truncate">
                    {{ node.display }}
                  </mat-checkbox>
                </ng-template>
                <ng-template [ngIf]="!multiple">
                  <button
                    (click)="node.select()"
                    [color]="node.selected ? 'primary' : undefined"
                    [disabled]="!node.hasDetails"
                    [ngStyle]="node.style"
                    class="grow-0 truncate"
                    mat-button
                    type="button"
                  >
                    {{ node.display }}
                  </button>
                </ng-template>
              </div>
            </mat-tree-node>

            <!-- Node with children -->
            <mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding>
              <button [attr.aria-label]="'toggle ' + node.filename" mat-icon-button matTreeNodeToggle type="button">
                <mat-icon class="mat-icon-rtl-mirror">
                  {{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}
                </mat-icon>
              </button>
              <div class="flex flex-row justify-start items-center gap-2">
                <ng-container *ngIf="node.icon?.length else withoutIcon">
                  <mat-icon *ngFor="let icon of node.icon" [rxapIcon]="$any(icon)"></mat-icon>
                </ng-container>
                <ng-template #withoutIcon>
                  <div></div>
                </ng-template>
                <ng-template [ngIfElse]="withoutDetails" [ngIf]="node.hasDetails">
                  <button
                    (click)="node.select()"
                    [color]="node.selected ? 'primary' : undefined"
                    [ngStyle]="node.style"
                    class="grow-0 truncate"
                    mat-button
                    type="button"
                  >
                    {{ node.display }}
                  </button>
                </ng-template>
                <ng-template #withoutDetails>
                  <span
                    (change)="onContentEditableChange($event, node)"
                    [disabled]="!nodeDisplayEditable"
                    [ngStyle]="node.style"
                    class="grow-0 truncate"
                    rxapContenteditable>
                    {{ node.display }}
                  </span>
                </ng-template>
                <mat-progress-spinner
                  *ngIf="node.isLoading$ | async"
                  [diameter]="16"
                  class="grow-0 pl-4"
                  mode="indeterminate"
                ></mat-progress-spinner>
              </div>
            </mat-tree-node>

          </mat-tree>
        </div>

        <div (mousedown)="onMousedown()" *ngIf="showTreeNavigation()"
             class="divider cursor-ew-resize px-3 grow-0">
          <mat-divider [vertical]="true" class="h-full"></mat-divider>
        </div>

        <div class="grow">
          <ng-container *ngIf="portal">
            <ng-template [cdkPortalOutlet]="portal"></ng-template>
          </ng-container>
          <ng-content></ng-content>
        </div>
      </div>
    </div>

    <ng-template #loading>Load data source</ng-template>

`./tree.component.scss`

Legend
Html element
Component
Html element with directive

# results matching ""

# No results matching ""