src/lib/data-grid.component.ts
OnInit
OnDestroy
AfterContentInit
changeDetection | ChangeDetectionStrategy.OnPush |
selector | rxap-data-grid |
standalone | true |
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 |
Properties |
|
Methods |
|
Inputs |
Outputs |
Accessors |
constructor(cdr: ChangeDetectorRef, router: Router, formDirective?: FormDirective)
|
||||||||||||
Defined in src/lib/data-grid.component.ts:151
|
||||||||||||
Parameters :
|
data | |
Type : T
|
|
Defined in src/lib/data-grid.component.ts:125
|
dataSource | |
Type : DataSource<T>
|
|
Defined in src/lib/data-grid.component.ts:117
|
displayProperties | |
Type : string[] | null
|
|
Default value : null
|
|
Defined in src/lib/data-grid.component.ts:128
|
header | |
Type : boolean
|
|
Default value : false
|
|
Defined in src/lib/data-grid.component.ts:114
|
hideEmptyProperties | |
Type : boolean
|
|
Default value : false
|
|
Defined in src/lib/data-grid.component.ts:131
|
mode | |
Type : DataGridMode | string
|
|
Defined in src/lib/data-grid.component.ts:166
|
viewer | |
Type : DataSourceViewer
|
|
Default value : this
|
|
Defined in src/lib/data-grid.component.ts:122
|
editModeChange | |
Type : EventEmitter
|
|
Defined in src/lib/data-grid.component.ts:136
|
Public cancel |
cancel()
|
Defined in src/lib/data-grid.component.ts:375
|
Returns :
void
|
Public disableEditMode |
disableEditMode()
|
Defined in src/lib/data-grid.component.ts:295
|
Returns :
void
|
Public enableEditMode | ||||||
enableEditMode(skipPatchValue)
|
||||||
Defined in src/lib/data-grid.component.ts:273
|
||||||
Parameters :
Returns :
void
|
Public logCurrentFormState |
logCurrentFormState()
|
Defined in src/lib/data-grid.component.ts:208
|
Returns :
void
|
Public refresh |
refresh()
|
Defined in src/lib/data-grid.component.ts:367
|
Returns :
void
|
Public reset |
reset()
|
Defined in src/lib/data-grid.component.ts:358
|
Returns :
void
|
retry |
retry()
|
Defined in src/lib/data-grid.component.ts:380
|
Returns :
void
|
Public submit |
submit()
|
Defined in src/lib/data-grid.component.ts:311
|
Returns :
void
|
Public data$ |
Type : Observable<T>
|
Defined in src/lib/data-grid.component.ts:119
|
Public dataLoading$ |
Type : Observable<boolean>
|
Default value : of(false)
|
Defined in src/lib/data-grid.component.ts:143
|
Public hasError$ |
Type : Observable<boolean>
|
Default value : of(false)
|
Defined in src/lib/data-grid.component.ts:142
|
Public isDevMode |
Default value : isDevMode()
|
Defined in src/lib/data-grid.component.ts:111
|
Public Readonly isEditMode$ |
Type : Observable<boolean>
|
Defined in src/lib/data-grid.component.ts:145
|
Public Readonly isFormMode$ |
Type : Observable<boolean>
|
Defined in src/lib/data-grid.component.ts:147
|
Public Readonly isPlainMode$ |
Type : Observable<boolean>
|
Defined in src/lib/data-grid.component.ts:148
|
Public loading$ |
Default value : new ToggleSubject()
|
Defined in src/lib/data-grid.component.ts:144
|
Public Readonly mode$ |
Type : Observable<DataGridMode>
|
Defined in src/lib/data-grid.component.ts:146
|
Public rows |
Type : QueryList<DataGridRowDefDirective<T>>
|
Decorators :
@ContentChildren(DataGridRowDefDirective)
|
Defined in src/lib/data-grid.component.ts:134
|
Public rows$ |
Type : Observable<QueryList<DataGridRowDefDirective<T>>>
|
Default value : EMPTY
|
Defined in src/lib/data-grid.component.ts:141
|
mode | ||||||
setmode(value: DataGridMode | string)
|
||||||
Defined in src/lib/data-grid.component.ts:166
|
||||||
Parameters :
Returns :
void
|
isFormModeOrHasAnyEditCells |
getisFormModeOrHasAnyEditCells()
|
Defined in src/lib/data-grid.component.ts:174
|
isFormMode |
getisFormMode()
|
Defined in src/lib/data-grid.component.ts:178
|
hasAnyEditCells |
gethasAnyEditCells()
|
Defined in src/lib/data-grid.component.ts:182
|
isEditMode |
getisEditMode()
|
Defined in src/lib/data-grid.component.ts:186
|
editMode | ||||||
seteditMode(value: boolean)
|
||||||
Defined in src/lib/data-grid.component.ts:190
|
||||||
Parameters :
Returns :
void
|
loading |
getloading()
|
Defined in src/lib/data-grid.component.ts:197
|
|
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,
standalone: true,
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;
}
}