### File

`src/lib/data-grid.component.ts`

### Implements

`OnInit` `OnDestroy` `AfterContentInit`

### Metadata

changeDetection | `ChangeDetectionStrategy.OnPush`
selector | `rxap-data-grid`
imports | `AsyncPipe` `NgIf` `NgFor` `MatButtonModule` `MatIconModule` `GetFromObjectPipe` `ReplacePipe` `EscapeQuotationMarkPipe` `RxapFormsModule` `DataGridValuePipe` `MatProgressSpinnerModule` `MatDividerModule` `NgTemplateOutlet` `MatFormFieldModule` `NgClass` `IsEmptyPipe`
styleUrls | `./data-grid.component.scss`
templateUrl | `./data-grid.component.html`

### Index

###### Properties
Public data$ Public dataLoading$ Public hasError$ Public isDevMode Public Readonly isEditMode$ Public Readonly isFormMode$ Public Readonly isPlainMode$ Public loading$ Public Readonly mode$ Public rows Public rows$
###### Methods
Public cancel Public disableEditMode Public enableEditMode Public logCurrentFormState Public refresh Public reset retry Public submit
###### Inputs
data dataSource displayProperties header hideEmptyProperties mode viewer
###### Outputs
editModeChange
###### Accessors
mode isFormModeOrHasAnyEditCells isFormMode hasAnyEditCells isEditMode editMode loading

### Constructor

`constructor(cdr: ChangeDetectorRef, router: Router, formDirective?: FormDirective)`
Defined in src/lib/data-grid.component.ts:150
Parameters :
Name | Type | Optional
cdr | `ChangeDetectorRef` | No
router | `Router` | No
formDirective | `FormDirective` | Yes

### Inputs

data
Type : `T`
Defined in src/lib/data-grid.component.ts:124

dataSource
Type : `DataSource<T>`
Defined in src/lib/data-grid.component.ts:116

displayProperties
Type : `string[] | null`
Default value : `null`
Defined in src/lib/data-grid.component.ts:127

header
Type : `boolean`
Default value : `false`
Defined in src/lib/data-grid.component.ts:113

hideEmptyProperties
Type : `boolean`
Default value : `false`
Defined in src/lib/data-grid.component.ts:130

mode
Type : `DataGridMode | string`
Defined in src/lib/data-grid.component.ts:165

viewer
Type : `DataSourceViewer`
Default value : `this`
Defined in src/lib/data-grid.component.ts:121

### Outputs

editModeChange
Type : `EventEmitter`
Defined in src/lib/data-grid.component.ts:135

### Methods

Public cancel
`cancel()`
Defined in src/lib/data-grid.component.ts:374
Returns : `void`

Public disableEditMode
`disableEditMode()`
Defined in src/lib/data-grid.component.ts:294
Returns : `void`

Public enableEditMode
`enableEditMode(skipPatchValue)`
Defined in src/lib/data-grid.component.ts:272
Parameters :
Name | Optional | Default value
skipPatchValue | No | `false`
Returns : `void`

Public logCurrentFormState
`logCurrentFormState()`
Defined in src/lib/data-grid.component.ts:207
Returns : `void`

Public refresh
`refresh()`
Defined in src/lib/data-grid.component.ts:366
Returns : `void`

Public reset
`reset()`
Defined in src/lib/data-grid.component.ts:357
Returns : `void`

retry
`retry()`
Defined in src/lib/data-grid.component.ts:379
Returns : `void`

Public submit
`submit()`
Defined in src/lib/data-grid.component.ts:310
Returns : `void`

### Properties

Public data$
Type : `Observable<T>`
Defined in src/lib/data-grid.component.ts:118

Public dataLoading$
Type : `Observable<boolean>`
Default value : `of(false)`
Defined in src/lib/data-grid.component.ts:142

Public hasError$
Type : `Observable<boolean>`
Default value : `of(false)`
Defined in src/lib/data-grid.component.ts:141

Public isDevMode
Default value : `isDevMode()`
Defined in src/lib/data-grid.component.ts:110

Public Readonly isEditMode$
Type : `Observable<boolean>`
Defined in src/lib/data-grid.component.ts:144

Public Readonly isFormMode$
Type : `Observable<boolean>`
Defined in src/lib/data-grid.component.ts:146

Public Readonly isPlainMode$
Type : `Observable<boolean>`
Defined in src/lib/data-grid.component.ts:147

Public loading$
Default value : `new ToggleSubject()`
Defined in src/lib/data-grid.component.ts:143

Public Readonly mode$
Type : `Observable<DataGridMode>`
Defined in src/lib/data-grid.component.ts:145

Public rows
Type : `QueryList<DataGridRowDefDirective<T>>`
Decorators : `@ContentChildren(DataGridRowDefDirective)`
Defined in src/lib/data-grid.component.ts:133

Public rows$
Type : `Observable<QueryList<DataGridRowDefDirective<T>>>`
Default value : `EMPTY`
Defined in src/lib/data-grid.component.ts:140

### Accessors

mode
set `mode(value: DataGridMode | string)`
Defined in src/lib/data-grid.component.ts:165
Parameters :
Name | Type | Optional
value | `DataGridMode | string` | No
Returns : `void`

isFormModeOrHasAnyEditCells
get `isFormModeOrHasAnyEditCells()`
Defined in src/lib/data-grid.component.ts:173

isFormMode
get `isFormMode()`
Defined in src/lib/data-grid.component.ts:177

hasAnyEditCells
get `hasAnyEditCells()`
Defined in src/lib/data-grid.component.ts:181

isEditMode
get `isEditMode()`
Defined in src/lib/data-grid.component.ts:185

editMode
set `editMode(value: boolean)`
Defined in src/lib/data-grid.component.ts:189
Parameters :
Name | Type | Optional
value | `boolean` | No
Returns : `void`

loading
get `loading()`
Defined in src/lib/data-grid.component.ts:196

    import {
      AsyncPipe,
      NgClass,
      NgFor,
      NgIf,
      NgTemplateOutlet,
    } from '@angular/common';
    import {
      AfterContentInit,
      ChangeDetectionStrategy,
      ChangeDetectorRef,
      Component,
      ContentChildren,
      EventEmitter,
      Input,
      isDevMode,
      OnDestroy,
      OnInit,
      Optional,
      Output,
      QueryList,
    } from '@angular/core';
    import { MatButtonModule } from '@angular/material/button';
    import { MatDividerModule } from '@angular/material/divider';
    import { MatFormFieldModule } from '@angular/material/form-field';
    import { MatIconModule } from '@angular/material/icon';
    import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
    import {
      ActivationEnd,
      Router,
    } from '@angular/router';
    import {
      FormDirective,
      RxapFormsModule,
    } from '@rxap/forms';
    import {
      DataSource,
      DataSourceViewer,
    } from '@rxap/pattern';
    import {
      EscapeQuotationMarkPipe,
      GetFromObjectPipe,
      ReplacePipe,
    } from '@rxap/pipes';
    import { ToggleSubject } from '@rxap/rxjs';
    import { clone } from '@rxap/utilities';
    import {
      BehaviorSubject,
      combineLatest,
      debounceTime,
      EMPTY,
      merge,
      Observable,
      of,
      Subscription,
    } from 'rxjs';
    import {
      filter,
      map,
      shareReplay,
      take,
      tap,
    } from 'rxjs/operators';
    import { DataGridRowDefDirective } from './data-grid-row-def.directive';
    import { DataGridValuePipe } from './data-grid-value.pipe';
    import { IsEmptyPipe } from './is-empty.pipe';

    export enum DataGridMode {
      /**
       * The view cell template is used to display the property value
       */
      PLAIN = 'plain',
      /**
       * The edit cell template is used to display the property value, but the form and all controls are marked as disabled
       */
      FORM  = 'form',
    }

    function IsDataGridMode(value: string): value is DataGridMode {
      return [ DataGridMode.PLAIN, DataGridMode.FORM ].includes(value as any);
    }

    @Component({
        // eslint-disable-next-line @angular-eslint/component-selector
        selector: 'rxap-data-grid',
        templateUrl: './data-grid.component.html',
        styleUrls: ['./data-grid.component.scss'],
        changeDetection: ChangeDetectionStrategy.OnPush,
        imports: [
            AsyncPipe,
            NgIf,
            NgFor,
            MatButtonModule,
            MatIconModule,
            GetFromObjectPipe,
            ReplacePipe,
            EscapeQuotationMarkPipe,
            RxapFormsModule,
            DataGridValuePipe,
            MatProgressSpinnerModule,
            MatDividerModule,
            NgTemplateOutlet,
            MatFormFieldModule,
            NgClass,
            IsEmptyPipe,
        ]
    })
    export class DataGridComponent<T extends Record<string, any>> implements OnInit, OnDestroy, AfterContentInit {

      public isDevMode = isDevMode();

      @Input()
      public header = false;

      @Input()
      public dataSource?: DataSource<T>;

      public data$!: Observable<T>;

      @Input()
      public viewer: DataSourceViewer = this;

      @Input()
      public data?: T;

      @Input()
      public displayProperties: string[] | null = null;

      @Input()
      public hideEmptyProperties = false;

      @ContentChildren(DataGridRowDefDirective)
      public rows!: QueryList<DataGridRowDefDirective<T>>;
      @Output()
      public editModeChange                                           = new EventEmitter<{
        mode: boolean,
        data?: T,
        done: () => void
      }>();
      public rows$: Observable<QueryList<DataGridRowDefDirective<T>>> = EMPTY;
      public hasError$: Observable<boolean>                           = of(false);
      public dataLoading$: Observable<boolean>                        = of(false);
      public loading$                                                 = new ToggleSubject();
      public readonly isEditMode$: Observable<boolean>;
      public readonly mode$: Observable<DataGridMode>;
      public readonly isFormMode$: Observable<boolean>;
      public readonly isPlainMode$: Observable<boolean>;
      private _editMode$                                              = new BehaviorSubject<boolean>(false);
      private _mode$                                                  = new BehaviorSubject<DataGridMode>(DataGridMode.PLAIN);
      private _routerEventSubscription: Subscription | null           = null;

      constructor(
        private readonly cdr: ChangeDetectorRef,
        private readonly router: Router,
        @Optional()
        private readonly formDirective?: FormDirective,
      ) {
        this.isEditMode$  = this._editMode$.asObservable();
        this.mode$        = this._mode$.asObservable();
        this.isFormMode$  = this.mode$.pipe(map(mode => mode === DataGridMode.FORM));
        this.isPlainMode$ = this.mode$.pipe(map(mode => mode === DataGridMode.PLAIN));
      }

      @Input()
      public set mode(value: DataGridMode | string) {
        if (IsDataGridMode(value)) {
          this._mode$.next(value);
        } else {
          throw new Error(`The data grid mode only support 'plain' and 'form' - given '${ value }'`);
        }
      }

      public get isFormModeOrHasAnyEditCells() {
        return this._mode$.value === DataGridMode.FORM || this.hasAnyEditCells;
      }

      public get isFormMode() {
        return this._mode$.value === DataGridMode.FORM;
      }

      public get hasAnyEditCells() {
        return this.rows?.some(row => !!row.editCell) ?? false;
      }

      public get isEditMode() {
        return this._editMode$.value;
      }

      public set editMode(value: boolean) {
        this._editMode$.next(value);
      }

      /**
       * @deprecated use the loading$ property instead
       */
      public get loading() {
        return this.loading$.value;
      }

      public ngAfterContentInit() {
        this.rows$ = merge(
          of(this.rows),
          this.rows.changes,
        );
      }

      public logCurrentFormState() {
        console.log(clone(this.formDirective?.form.value));
      }

      public ngOnInit() {
        // resets the edit mode if this component is used in a sibling router path
        // if not reset the edit mode is president after the route changes
        this._routerEventSubscription = this.router.events.pipe(
          filter(event => event instanceof ActivationEnd),
          tap(() => this.disableEditMode()),
        ).subscribe();

        if (this.dataSource && this.data) {
          throw new Error('You can not use both dataSource and data input');
        }

        let data$: Observable<T> = EMPTY;

        if (this.dataSource) {
          data$ = this.dataSource.connect(this.viewer);
        }

        if (this.data) {
          data$ = of(this.data);
        }

        if (data$ === EMPTY && isDevMode()) {
          console.warn('No data source or data input provided');
        }

        this.data$ = data$.pipe(
          debounceTime(100),
          tap(data => this.data = data),
          tap(data => {
            if (this.formDirective && this.isFormMode) {
              this.formDirective.form.patchValue(data, {
                coerce: true,
                strict: true,
              });
              if (this.isFormMode) {
                this.formDirective.form.disable();
              }
            }
          }),
          shareReplay(1),
        );
        if (this.dataSource) {
          this.hasError$    = this.dataSource.hasError$ ?? this.hasError$;
          this.dataLoading$ = this.dataSource.loading$ ?? this.dataLoading$;
        }
        if (this.formDirective && this.isFormMode) {
          this.formDirective.form.disabledWhile(combineLatest([
            this._editMode$,
            this._mode$,
          ]).pipe(
            map(([ editMode, mode ]) => !editMode && mode === 'form'),
          ), {onlySelf: false});
        }
      }

      public ngOnDestroy() {
        this.dataSource?.disconnect(this.viewer);
        this._routerEventSubscription?.unsubscribe();
      }

      public enableEditMode(skipPatchValue = false) {
        if (!this.isFormModeOrHasAnyEditCells) {
          if (isDevMode()) {
            console.warn('Can not enable edit mode if the mode is not form');
          }
          return;
        }
        if (!this.formDirective) {
          if (isDevMode()) {
            console.warn('Can not enable edit mode without a form directive');
          }
          return;
        }
        this.editMode = true;
        if (!skipPatchValue && this.data) {
          this.formDirective.form.patchValue(this.data, {
            coerce: true,
            strict: true,
          });
        }
      }

      public disableEditMode() {
        if (!this.isFormModeOrHasAnyEditCells) {
          if (isDevMode()) {
            console.warn('Can not disable edit mode if the mode is not form');
          }
          return;
        }
        if (!this.formDirective) {
          if (isDevMode()) {
            console.warn('Can not enable edit mode without a form directive');
          }
          return;
        }
        this.editMode = false;
      }

      public submit() {
        if (!this.isFormModeOrHasAnyEditCells) {
          if (isDevMode()) {
            console.warn('Can not submit if the mode is not form');
          }
          return;
        }
        if (!this.formDirective) {
          if (isDevMode()) {
            console.warn('Can not support without a form directive');
          }
          return;
        }
        this.loading$.enable();
        this.formDirective.form.markAllAsDirty();
        this.formDirective.form.markAllAsTouched();
        this.formDirective.cdr.markForCheck();
        this.formDirective.rxapSubmit.pipe(
          take(1),
          tap(() => {
            this.disableEditMode();
          }),
        ).subscribe();
        this.formDirective.invalidSubmit.pipe(
          take(1),
          tap(() => {
            this.loading$.disable();
          }),
        ).subscribe();
        this.formDirective.submitError$.pipe(
          filter(Boolean), // if the error is undefined we do not want to disable the loading
          take(1),
          tap(() => {
            this.loading$.disable();
            this.enableEditMode(true);
          }),
        ).subscribe();
        this.formDirective.submitSuccessful$.pipe(
          take(1),
          tap(() => {
            this.loading$.disable();
            this.refresh();
          }),
        ).subscribe();
        this.formDirective.onSubmit(new Event('submit'));
      }

      public reset() {
        if (this.formDirective && this.data && this.isFormModeOrHasAnyEditCells) {
          this.formDirective.form.patchValue(this.data, {
            coerce: true,
            strict: true,
          });
        }
      }

      public refresh() {
        if (this.dataSource) {
          this.dataSource.refresh();
        } else if (isDevMode()) {
          console.warn('can not refresh the data. data source is not defined');
        }
      }

      public cancel() {
        this.reset();
        this.disableEditMode();
      }

      retry() {
        this.refresh();
      }
    }

    <div *ngIf="(hasError$ | async)" class="flex flex-col items-center justify-center gap-y-8">
      <span i18n>Something has gone wrong!</span>
      <button (click)="retry()" i18n mat-stroked-button type="button">Retry</button>
    </div>
    <div *ngIf="(dataLoading$ | async)" class="flex flex-col items-center justify-center gap-y-8">
      <mat-spinner></mat-spinner>
    </div>
    <!-- if the dataSource input is used. It is possible the the hasError$ or loading$ property is === EMPTY. this would
    results in an null as output. If the === false check is used this can result into the behavior that the data grid is
    never shown. -->
    <!-- eslint-disable-next-line @angular-eslint/template/no-negated-async -->
    <table *ngIf="!(hasError$ | async) && !(dataLoading$ | async)" class="w-full table-auto">
      <thead *ngIf="header">
      <tr>
        <th class="py-2" i18n>Label</th>
        <th class="py-2" i18n>Value</th>
      </tr>
      </thead>
      <tbody>
      <ng-template [ngForOf]="rows$ | async" let-first="first" let-row ngFor>
        <ng-template
          [ngIf]="(!displayProperties || row.isSubHeader || (row.name && displayProperties.includes(row.name))) && (!hideEmptyProperties || false === (data$ | isEmpty: row.name | async))">
          <!-- region horizontal view -->
          <ng-template [ngIfElse]="normal" [ngIf]="row.flip">
            <tr>
              <td class="py-2" colspan="2">
                <mat-divider></mat-divider>
              </td>
            </tr>
            <tr [attr.data-name]="row.name + '-header'">
              <ng-container
                *ngTemplateOutlet="row.headerCell?.template ?? defaultHeaderCell; context: { $implicit: row.name }"></ng-container>
            </tr>
            <tr [attr.data-name]="row.name + '-value'">
              <ng-template [ngIfThen]="content" [ngIf]="row.name"></ng-template>
            </tr>
            <tr>
              <td class="py-4" colspan="2">
                <mat-divider></mat-divider>
              </td>
            </tr>
          </ng-template>
          <!-- endregion -->
          <!-- region normal view -->
          <ng-template #normal>
            <tr *ngIf="!first && row.isSubHeader">
              <td class="py-4" colspan="2">
                <mat-divider></mat-divider>
              </td>
            </tr>
            <tr [attr.data-name]="row.name" [ngClass]="{ 'sub-header text-2xl': row.isSubHeader }">
              <ng-container
                *ngTemplateOutlet="row.headerCell?.template ?? defaultHeaderCell; context: { $implicit: row.name }"></ng-container>
              <ng-template [ngIfThen]="content" [ngIf]="row.name"></ng-template>
            </tr>
          </ng-template>
          <!-- endregion -->
        </ng-template>
        <ng-template #content>
          <td [attr.colspan]="row.flip ? 2 : 1" [ngClass]="{
          'h-20': row.editCell?.template && (isEditMode || isFormMode) && false,
          }" class="pl-8 pr-4 w-full py-2">
            <ng-template [ngIfThen]="editMode" [ngIf]="row.editCell?.template && (isEditMode || isFormMode)"></ng-template>
            <ng-template [ngIfThen]="viewMode"
                         [ngIf]="!row.editCell?.template || (!isEditMode && !isFormMode)"></ng-template>
          </td>
          <ng-template #viewMode>
            <ng-container
              *ngTemplateOutlet="row.cell?.template ?? defaultCell;context: { $implicit: data$ | dataGridValue: row.name | async, data: data$ | async }"></ng-container>
          </ng-template>
          <ng-template #editMode>
            <ng-container
              *ngTemplateOutlet="row.editCell?.template ?? row.cell?.template ?? defaultCell;context: { $implicit: data$ | dataGridValue: row.name | async, data: data$ | async }"></ng-container>
          </ng-template>
        </ng-template>
      </ng-template>
      </tbody>
      <tfoot *ngIf="hasAnyEditCells">
      <tr>
        <td class="py-2" colspan="2">
          <ng-template [ngIfElse]="viewModeButton" [ngIf]="isEditMode">
            <div class="pt-8 flex flex-col gap-y-6">
              <div class="flex flex-row gap-x-6 items-center justify-start">
                <button (click)="submit()" [disabled]="loading$ | async" color="primary" mat-raised-button type="button">
                  <span class="flex flex-row gap-x-6 items-center justify-center">
                    <span i18n>Save</span>
                    <mat-spinner *ngIf="loading$ | async" color="accent" diameter="15"></mat-spinner>
                  </span>
                </button>
                <button (click)="reset()" [disabled]="loading$ | async" mat-stroked-button type="button">
                  <ng-container i18n>Reset</ng-container>
                </button>
                <button (click)="cancel()" [disabled]="loading$ | async" color="warn" mat-stroked-button type="button">
                  <ng-container i18n>Cancel</ng-container>
                </button>
                <button (click)="logCurrentFormState()" *ngIf="isDevMode" mat-button type="button">
                  <ng-container i18n>Current Form State</ng-container>
                </button>
              </div>

              <ng-template rxapFormSubmitInvalid>
                <mat-error i18n>Ensure all formula fields are valid.</mat-error>
              </ng-template>

              <ng-template let-error rxapFormSubmitFailed>
                <mat-error>{{ error.error?.message ?? error.message }}</mat-error>
              </ng-template>

              <ng-template rxapFormSubmitSuccessful>
                <span i18n>Submit successfully.</span>
              </ng-template>

            </div>
          </ng-template>
          <ng-template #viewModeButton>
            <div class="pt-8">
              <button (click)="enableEditMode()" color="primary" mat-raised-button type="button">
                <span class="flex flex-row gap-x-6 items-center justify-center">
                  <span i18n>Edit</span>
                  <mat-spinner *ngIf="loading$ | async" color="accent" diameter="15"></mat-spinner>
                </span>
              </button>
            </div>
          </ng-template>
        </td>
      </tr>
      </tfoot>
    </table>

    <ng-template #defaultCell let-value>
      <span>{{ value }}</span>
    </ng-template>
    <ng-template #defaultHeaderCell let-name>
      <th class="py-2 whitespace-nowrap">{{ name }}</th>
    </ng-template>

`./data-grid.component.scss`

    :host {
      ::ng-deep th {
        text-align: right;
        width: auto;
      }
    }

Legend
Html element
Component
Html element with directive

# results matching ""

# No results matching ""