feat: Implementieren und integrieren von wiederverwendbaren Angular Material-Tabellenkomponenten

- Erstellen der `DDTable`-Komponente für dynamische Angular Material-Tabellen mit Unterstützung für erweiterbare Zeilen, Filterung, Sortierung und Pagination.
- Ersetzen der bestehenden Tabellenimplementierung in `EnvelopeTableComponent` durch die `DDTable`-Komponente für verbesserte Funktionalität und Wartbarkeit.
- Aktualisieren der `EnvelopeTableComponent`, um `DDTable` für die Anzeige von Umschlagdaten zu verwenden, einschließlich Filter- und Paginierungsoptionen.
- Implementieren von `ClearableInputComponent` für eine verbesserte Filtererfahrung.
- Anpassen des Datenabrufs und -handlings, um die Integration von `DDTable` zu berücksichtigen.
- Verbessern der `EnvelopeTableComponent` mit ordnungsgemäßen Animationstriggern für erweiterbare Zeilen.
This commit is contained in:
Developer 02 2024-09-04 14:58:48 +02:00
parent 7444eeba2a
commit 363358aaa1
7 changed files with 311 additions and 96 deletions

View File

@ -0,0 +1,59 @@
@if(isFilterable) {
<mat-form-field>
<mat-label>{{filter.label}}</mat-label>
<input matInput (keyup)="applyFilter($event)" [placeholder]="filter.placeholder" #input>
</mat-form-field>
}
<table mat-table [dataSource]="dataSource" multiTemplateDataRows class="mat-elevation-z8" matSort>
@for (column of __columnsToDisplay; track column) {
<ng-container matColumnDef="{{column}}">
@if(isSortable) {
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{schema[column].header}} </th>
}
@else {
<th mat-header-cell *matHeaderCellDef> {{schema[column].header}} </th>
}
<td mat-cell *matCellDef="let element"> {{schema[column].field(element)}} </td>
</ng-container>
}
<!-- Expanded Content Column - The detail row is made up of this one column that spans across all columns -->
@if(isExpandable) {
<ng-container matColumnDef="expand">
<th mat-header-cell *matHeaderCellDef aria-label="row actions">&nbsp;</th>
<td mat-cell *matCellDef="let element">
<button mat-icon-button aria-label="expand row" (click)="toggleExpandedRow(element, $event)">
@if (__expandedElement === element) {
<mat-icon>keyboard_arrow_up</mat-icon>
} @else {
<mat-icon>keyboard_arrow_down</mat-icon>
}
</button>
</td>
</ng-container>
<ng-container matColumnDef="expandedDetail">
<td mat-cell *matCellDef="let element" [attr.colspan]="__columnsToDisplayWithExpand.length">
<div class="example-element-detail" [@detailExpand]="element == __expandedElement ? 'expanded' : 'collapsed'">
@if(__expandedElement === element){
<ng-content select="[expanded]" detailed></ng-content>
}
</div>
</td>
</ng-container>
}
@if(isExpandable) {
<tr mat-header-row *matHeaderRowDef="__columnsToDisplayWithExpand"></tr>
<tr mat-row *matRowDef="let element; columns: __columnsToDisplayWithExpand;" class="example-element-row"
[class.example-expanded-row]="__expandedElement === element" (click)="toggleExpandedRow(element, $event)">
</tr>
<tr mat-row *matRowDef="let row; columns: ['expandedDetail']" class="example-detail-row"></tr>
}
@else {
<tr mat-header-row *matHeaderRowDef="__columnsToDisplay"></tr>
<tr mat-row *matRowDef="let row; columns: __columnsToDisplay;"></tr>
}
</table>
@if(paginatorSizeOptions && paginatorSizeOptions.length > 0) {
<mat-paginator [pageSizeOptions]="paginatorSizeOptions" aria-label="Select page of users"></mat-paginator>
}

View File

@ -0,0 +1,52 @@
table {
width: 100%;
}
tr.example-detail-row {
height: 0;
}
tr.example-element-row:not(.example-expanded-row):hover {
background: whitesmoke;
}
tr.example-element-row:not(.example-expanded-row):active {
background: #efefef;
}
.example-element-row td {
border-bottom-width: 0;
}
.example-element-detail {
overflow: hidden;
display: flex;
}
.example-element-diagram {
min-width: 80px;
border: 2px solid black;
padding: 8px;
font-weight: lighter;
margin: 8px 0;
height: 104px;
}
.example-element-symbol {
font-weight: bold;
font-size: 40px;
line-height: normal;
}
.example-element-description {
padding: 16px;
}
.example-element-description-attribution {
opacity: 0.5;
}
.mat-mdc-form-field {
font-size: 14px;
width: 100%;
}

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DDTable } from './dd-table.component';
describe('TableExpandableRowsExampleComponent', () => {
let component: DDTable;
let fixture: ComponentFixture<DDTable>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DDTable]
})
.compileComponents();
fixture = TestBed.createComponent(DDTable);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,103 @@
import { AfterViewInit, Component, Input, ViewChild, inject } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatTable, MatTableDataSource, MatTableModule } from '@angular/material/table';
import { ConfigurationService } from '../../services/configuration.service';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
/**
* @title Table with expandable rows
*/
@Component({
selector: 'dd-table',
styleUrl: 'dd-table.component.scss',
templateUrl: 'dd-table.component.html',
animations: [
trigger('detailExpand', [
state('collapsed,void', style({ height: '0px', minHeight: '0' })),
state('expanded', style({ height: '*' })),
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
]),
],
standalone: true,
imports: [
MatTableModule,
MatButtonModule,
MatIconModule,
MatFormFieldModule,
MatInputModule,
MatTableModule,
MatSort,
MatSortModule,
MatPaginator,
MatPaginatorModule
],
})
export class DDTable implements AfterViewInit {
public readonly dataSource: any = new MatTableDataSource();
@Input() public set columnsToDisplay(value: string[]) {
this.__columnsToDisplay = value;
this.__columnsToDisplayWithExpand = [...value, 'expand'];
}
@Input() public set data(value: any[]) {
this.dataSource.data = value;
}
@Input() schema: Record<string, { header: string; field: (element: any) => any; }> = {}
@Input() paginatorSizeOptions?: number[];
@Input() filter: { label: string, placeholder: string } = { label: '', placeholder: '' }
@Input() isFilterable: boolean = false;
@Input() isExpandable: boolean = false;
@Input() isSortable: boolean = false;
@Input() onToggleExpandedRow: (element: any, event: Event) => void = (element: any, event: Event) => { }
public get data(): any[] {
return this.dataSource.data;
}
__columnsToDisplay: string[] = [];
__columnsToDisplayWithExpand: string[] = [];
__expandedElement!: any;
config: ConfigurationService = inject(ConfigurationService);
@ViewChild(MatSort) sort!: MatSort;
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatTable) table!: MatTable<any>;
ngAfterViewInit(): void {
if (this.isSortable)
this.dataSource.sort = this.sort;
if (this.paginatorSizeOptions && this.paginatorSizeOptions.length > 0)
this.dataSource.paginator = this.paginator;
}
applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value;
this.dataSource.filter = filterValue.trim().toLowerCase();
}
update() {
this.table.renderRows();
}
toggleExpandedRow(element: any, event: Event): void {
// first determine the new expanded element, thus it would be possible to use up-to-date
const newExpandedElement = this.__expandedElement === element ? null : element;
// before update the expanded element call the call-back method to show up-to-date component
this.onToggleExpandedRow(newExpandedElement, event);
// assign expanded element
this.__expandedElement = newExpandedElement;
event.stopPropagation();
}
}

View File

@ -1,39 +1,4 @@
<mat-form-field> <dd-table [data]="data" [columnsToDisplay]="displayedColumns" [schema]="schema"
<mat-label>Filter</mat-label> [paginatorSizeOptions]="[5, 10, 25, 100]" [filter]="{label: 'Filter', placeholder: ''}"
<input matInput (keyup)="applyFilter($event)" placeholder="Ex. ium" #input> [onToggleExpandedRow]="onToggleExpandedRow" [isSortable]="true" [isExpandable]="false" [isFilterable]="true">
</mat-form-field> </dd-table>
<table #table mat-table [dataSource]="dataSource" class="mat-elevation-z8" matSort>
@for (colId of displayedColumns; track colId) {
<ng-container matColumnDef="{{colId}}">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{schema[colId].header}} </th>
<td mat-cell *matCellDef="let element"> {{schema[colId].field(element)}} </td>
</ng-container>
}
<ng-container matColumnDef="expand">
<th mat-header-cell *matHeaderCellDef aria-label="row actions">&nbsp;</th>
<td mat-cell *matCellDef="let element">
<button mat-icon-button aria-label="expand row"
(click)="(expandedElement = expandedElement === element ? null : element); $event.stopPropagation()">
@if (expandedElement === element) {
<mat-icon>keyboard_arrow_up</mat-icon>
} @else {
<mat-icon>keyboard_arrow_down</mat-icon>
}
</button>
</td>
</ng-container>
<!-- Expanded Content Column - The detail row is made up of this one column that spans across all columns -->
<tr mat-header-row *matHeaderRowDef="columnsToDisplayWithExpand"></tr>
<tr mat-row *matRowDef="let element; columns: columnsToDisplayWithExpand;" class="example-element-row"
[class.example-expanded-row]="expandedElement === element"
(click)="expandedElement = expandedElement === element ? null : element">
</tr>
<!--<tr mat-row *matRowDef="let row; columns: ['expandedDetail']; when: isExpandedRow" class="example-detail-row"></tr>-->
</table>
<mat-paginator [pageSizeOptions]="[5, 10, 25, 100]" aria-label="Select page of users"></mat-paginator>

View File

@ -7,4 +7,53 @@ table {
font-size: 14px; font-size: 14px;
width: 100%; width: 100%;
margin-top: 10px; margin-top: 10px;
}
/* For expanding table */
table {
width: 100%;
}
tr.example-detail-row {
height: 0;
}
tr.example-element-row:not(.example-expanded-row):hover {
background: whitesmoke;
}
tr.example-element-row:not(.example-expanded-row):active {
background: #efefef;
}
.example-element-row td {
border-bottom-width: 0;
}
.example-element-detail {
overflow: hidden;
display: flex;
}
.example-element-diagram {
min-width: 80px;
border: 2px solid black;
padding: 8px;
font-weight: lighter;
margin: 8px 0;
height: 104px;
}
.example-element-symbol {
font-weight: bold;
font-size: 40px;
line-height: normal;
}
.example-element-description {
padding: 16px;
}
.example-element-description-attribution {
opacity: 0.5;
} }

View File

@ -1,20 +1,14 @@
import { AfterViewInit, Component, Input, ViewChild } from '@angular/core'; import { AfterViewInit, Component, Input, ViewChild, inject, viewChild } from '@angular/core';
import { EnvelopeReceiverService } from '../../services/envelope-receiver.service'; import { EnvelopeReceiverService } from '../../services/envelope-receiver.service';
import { MatTable, MatTableModule, MatTableDataSource } from '@angular/material/table';
import { CommonModule } from '@angular/common'
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { animate, state, style, transition, trigger } from '@angular/animations'; import { animate, state, style, transition, trigger } from '@angular/animations';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import {MatPaginator, MatPaginatorModule} from '@angular/material/paginator';
import {MatSort, MatSortModule} from '@angular/material/sort';
import { ConfigurationService } from '../../services/configuration.service'; import { ConfigurationService } from '../../services/configuration.service';
import { DDTable } from "../dd-table/dd-table.component";
import { ClearableInputComponent } from '../clearable-input/clearable-input.component'
@Component({ @Component({
selector: 'app-envelope-table', selector: 'app-envelope-table',
standalone: true, standalone: true,
imports: [MatTableModule, CommonModule, MatTableModule, MatButtonModule, MatIconModule, MatFormFieldModule, MatInputModule, MatTableModule, MatPaginator, MatPaginatorModule, MatSort, MatSortModule], imports: [DDTable, ClearableInputComponent],
templateUrl: './envelope-table.component.html', templateUrl: './envelope-table.component.html',
animations: [ animations: [
trigger('detailExpand', [ trigger('detailExpand', [
@ -25,13 +19,13 @@ import { ConfigurationService } from '../../services/configuration.service';
], ],
styleUrl: './envelope-table.component.scss' styleUrl: './envelope-table.component.scss'
}) })
export class EnvelopeTableComponent implements AfterViewInit { export class EnvelopeTableComponent implements AfterViewInit {
@Input() options?: { min_status?: number; max_status?: number; ignore_status?: number[] } @Input() options?: { min_status?: number; max_status?: number; ignore_status?: number[] }
@Input() displayedColumns: string[] = ['title', 'status', 'type', 'privateMessage', 'addedWhen']; displayedColumns: string[] = ['title', 'status', 'type', 'privateMessage', 'addedWhen'];
@Input() schema: Record<string, { header: string; field: (element: any) => any; }> = { schema: Record<string, { header: string; field: (element: any) => any; }> = {
'title': { 'title': {
header: 'Title', header: 'Title',
field: (element: any) => element.envelope.title field: (element: any) => element.envelope.title
@ -51,50 +45,20 @@ export class EnvelopeTableComponent implements AfterViewInit {
'addedWhen': { 'addedWhen': {
header: 'Added When', header: 'Added When',
field: (element: any) => element.addedWhen field: (element: any) => element.addedWhen
},
}
columnsToDisplayWithExpand = [...this.displayedColumns, 'expand'];
expandedElement: any | null;
@ViewChild(MatTable) table!: MatTable<any>;
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
dataSource = new MatTableDataSource();
constructor(private erService: EnvelopeReceiverService, private config: ConfigurationService) {
this.dataSource.filterPredicate = (data: any, filter: string): boolean => {
const dataStr = Object.values(data as { [key: string]: any }).reduce((currentTerm, key) => {
return currentTerm + (key ? key.toString().toLowerCase() : '');
}, '').trim().toLowerCase();
return dataStr.indexOf(filter) !== -1;
};
}
async ngAfterViewInit() {
this.dataSource.paginator = this.paginator;
this.dataSource.data = await this.erService.getEnvelopeReceiverAsync(this.options);
this.dataSource.sort = this.sort;
}
public updateTable() {
this.table.renderRows();
}
isExpandedRow(index: number, row: any): boolean {
return (row?.envelopeId === this.expandedElement?.envelopeId) && (row?.receiverId === this.expandedElement?.receiverId);
}
applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value;
this.dataSource.filter = filterValue.trim().toLowerCase();
if (this.dataSource.paginator) {
this.dataSource.paginator.firstPage();
} }
} }
data: any[] = [];
@ViewChild(ClearableInputComponent) input!: ClearableInputComponent
onToggleExpandedRow(element: any, event: Event) { }
private erService: EnvelopeReceiverService = inject(EnvelopeReceiverService);
private config: ConfigurationService = inject(ConfigurationService);
async ngAfterViewInit() {
this.data = await this.erService.getEnvelopeReceiverAsync(this.options);
}
} }