UNPKG

@lordkriegan/mat-data-table

Version:

[![npm version](https://badge.fury.io/js/%40lordkriegan%2Fmat-data-table.svg)](https://badge.fury.io/js/%40lordkriegan%2Fmat-data-table) [![npm license](https://img.shields.io/npm/l/%40lordkriegan%2Fmat-data-table)](https://www.npmjs.com/package/@lordkri

217 lines (212 loc) 24.1 kB
import { NgStyle, NgComponentOutlet } from '@angular/common'; import * as i0 from '@angular/core'; import { ViewChild, Input, Component } from '@angular/core'; import * as i7 from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button'; import * as i9 from '@angular/material/divider'; import { MatDividerModule } from '@angular/material/divider'; import * as i1 from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field'; import * as i6 from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon'; import * as i2 from '@angular/material/input'; import { MatInputModule } from '@angular/material/input'; import * as i8 from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu'; import * as i5 from '@angular/material/paginator'; import { MatPaginatorModule, MatPaginator } from '@angular/material/paginator'; import * as i4 from '@angular/material/sort'; import { MatSortModule, MatSort } from '@angular/material/sort'; import * as i3 from '@angular/material/table'; import { MatTableDataSource, MatTableModule } from '@angular/material/table'; import * as i10 from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip'; import { Observable } from 'rxjs'; class MaterialDataTableComponent { /** * The data to be displayed in the table. * Can be provided as a static array of objects (`T[]`) or an `Observable<T[]>`. * If an Observable is provided, the table will update automatically when the Observable emits new data. */ tableData = null; /** * An array of column definitions that map data properties to table columns. * Each object in the array must conform to the `IColumnMap<T>` type. * @see IColumnMap */ columnMappings = []; /** * Optional configuration object to customize the table's features and behavior. * If not provided, default options will be used. * @see ITableOptions */ tableOptions = {}; mergedOptions = {}; paginator; sort; _defaultTableOptions = { showFilter: true, filterOptions: { label: 'Filter' }, showPaginator: true, paginatorOptions: { pageSizeOptions: [5, 10, 25, 100] }, showSorter: true, sorterOptions: { defaultSortDirection: 'asc' }, showActions: false, actionOptions: {}, showTableActions: false, tableActionOptions: {}, }; dataSource = new MatTableDataSource([]); dataSubscription = null; isViewInitialized = false; constructor() { //empty constructor } ngOnInit() { this._mergeOptions(); } ngOnChanges(changes) { if (changes['tableOptions']) { this._mergeOptions(); } // When a new Observable is passed in... if (changes['tableData'] && this.isViewInitialized) { // If the view is ready, we can safely re-subscribe to the new observable. this.subscribeToData(); } } _mergeOptions() { const defaults = this._defaultTableOptions; const user = this.tableOptions || {}; // Deep merge the options, with user options taking precedence. this.mergedOptions = { ...defaults, ...user, filterOptions: { ...defaults.filterOptions, ...user.filterOptions }, paginatorOptions: { ...defaults.paginatorOptions, ...user.paginatorOptions }, sorterOptions: { ...defaults.sorterOptions, ...user.sorterOptions }, actionOptions: { ...defaults.actionOptions, ...user.actionOptions }, tableActionOptions: { ...defaults.tableActionOptions, ...user.tableActionOptions }, }; } ngAfterViewInit() { this.isViewInitialized = true; // Connect the paginator and filter to the data source ONCE. // The MatTableDataSource will handle updates automatically from here. if (this.mergedOptions.showPaginator) { this.dataSource.paginator = this.paginator; } if (this.mergedOptions.showFilter && this.mergedOptions.filterOptions?.filterPredicate) { this.dataSource.filterPredicate = this.mergedOptions.filterOptions.filterPredicate; } this.subscribeToData(); // Now subscribe to data changes. } subscribeToData() { this.dataSubscription?.unsubscribe(); if (this.tableData) { if (this.tableData instanceof Observable) { this.dataSubscription = this.tableData.subscribe(data => { // Only update the data. The paginator and filter are already connected. this.dataSource.data = data ?? []; this.connectSort(); }); } else { this.dataSource.data = this.tableData ?? []; this.connectSort(); } } } connectSort() { // Connect sort only once, after the first data load, to avoid race conditions. if (this.mergedOptions.showSorter && this.sort && !this.dataSource.sort) { this.dataSource.sort = this.sort; if (this.mergedOptions.sorterOptions?.sortData) { this.dataSource.sortData = this.mergedOptions.sorterOptions.sortData; } if (this.mergedOptions.sorterOptions?.sortingDataAccessor) { this.dataSource.sortingDataAccessor = this.mergedOptions.sorterOptions?.sortingDataAccessor; } } } generateDisplayColumns() { const displayColumns = this.columnMappings.map(col => col.key); if (this.mergedOptions.showActions || this.mergedOptions.showTableActions) { displayColumns.push('actions'); } return displayColumns; } /** * Returns the tooltip text for a cell. * It handles both static string tooltips and function-based tooltips. * @param column The column definition, which may contain the tooltip. * @param row The data for the current row. * @returns The tooltip string, or an empty string if no tooltip is defined. */ getTooltipText(column, row) { if (column.tooltip === undefined) { return ''; } if (typeof column.tooltip === 'function') { return (column.tooltip)(row[column.key]); } return column.tooltip; } getComponentInputs(column, row) { return { data: row[column.key], ...column.componentInputs }; } ngOnDestroy() { // Clean up the subscription to prevent memory leaks when the component is destroyed. this.dataSubscription?.unsubscribe(); } applyFilter(event) { const filterValue = event.target.value; this.dataSource.filter = filterValue.trim().toLowerCase(); if (this.dataSource.paginator) { this.dataSource.paginator.firstPage(); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.6", ngImport: i0, type: MaterialDataTableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.6", type: MaterialDataTableComponent, isStandalone: true, selector: "mat-data-table", inputs: { tableData: "tableData", columnMappings: "columnMappings", tableOptions: "tableOptions" }, viewQueries: [{ propertyName: "paginator", first: true, predicate: MatPaginator, descendants: true }, { propertyName: "sort", first: true, predicate: MatSort, descendants: true }], usesOnChanges: true, ngImport: i0, template: "@if (mergedOptions.showFilter) {\r\n <mat-form-field>\r\n <mat-label>{{mergedOptions.filterOptions?.label || 'Filter'}}</mat-label>\r\n <input matInput (keyup)=\"applyFilter($event)\" [placeholder]=\"mergedOptions.filterOptions?.placeholder || ''\" #input>\r\n </mat-form-field>\r\n }\r\n \r\n \r\n <div class=\"mat-elevation-z8\">\r\n <table mat-table \r\n [dataSource]=\"dataSource\" matSort \r\n [matSortActive]=\"mergedOptions.sorterOptions?.defaultSortColumn || ''\" \r\n [matSortDirection]=\"mergedOptions.sorterOptions?.defaultSortDirection || 'asc'\" \r\n [matSortDisabled]=\"!mergedOptions.showSorter\"\r\n [ngStyle]=\"mergedOptions.tableStyles || {}\"\r\n >\r\n \r\n @for (column of columnMappings; track $index) {\r\n <ng-container [matColumnDef]=\"column.key\">\r\n <th [ngStyle]=\"column?.headerCellStyles || {}\" mat-header-cell *matHeaderCellDef mat-sort-header>{{column.label}}</th>\r\n <td [ngStyle]=\"column?.dataCellStyles || {}\" mat-cell *matCellDef=\"let row\" \r\n [matTooltip]=\"getTooltipText(column, row)\"\r\n [matTooltipDisabled]=\"getTooltipText(column, row) === ''\"\r\n >\r\n @if (column?.component; as component) {\r\n <ng-container *ngComponentOutlet=\"component; inputs: getComponentInputs(column, row)\"></ng-container>\r\n } @else if (column?.transformer) {\r\n {{column.transformer!(row[column.key])}}\r\n } @else {\r\n {{row[column.key]}}\r\n }\r\n </td>\r\n </ng-container>\r\n }\r\n \r\n @if (mergedOptions.showActions || mergedOptions.showTableActions) {\r\n <ng-container matColumnDef=\"actions\">\r\n <th mat-header-cell *matHeaderCellDef class=\"action-column\">\r\n @if (mergedOptions.showTableActions) {\r\n <button mat-icon-button [matMenuTriggerFor]=\"tableMenu\" [ngStyle]=\"mergedOptions.tableActionOptions?.buttonStyles || {}\"><mat-icon>more_vert</mat-icon></button>\r\n <mat-menu #tableMenu=\"matMenu\">\r\n @for (action of mergedOptions.tableActionOptions?.actions; track $index) {\r\n @if (action.divider) {\r\n <mat-divider></mat-divider>\r\n } @else {\r\n <button mat-menu-item (click)=\"action.fxn!()\">\r\n @if (action.icon) {\r\n <mat-icon>{{action.icon}}</mat-icon>\r\n }\r\n <span>{{action.label}}</span>\r\n </button>\r\n }\r\n }\r\n </mat-menu>\r\n }\r\n </th>\r\n <td mat-cell *matCellDef=\"let row\" class=\"action-column\">\r\n @if (mergedOptions.showActions) {\r\n <button mat-icon-button [matMenuTriggerFor]=\"menu\" [ngStyle]=\"mergedOptions.actionOptions?.buttonStyles || {}\"><mat-icon>more_vert</mat-icon></button>\r\n <mat-menu #menu=\"matMenu\">\r\n @for (action of mergedOptions.actionOptions?.actions; track $index) {\r\n @if (action.divider) {\r\n <mat-divider></mat-divider>\r\n } @else {\r\n <button mat-menu-item (click)=\"action.fxn!(row)\">\r\n @if (action.icon) {\r\n <mat-icon>{{action.icon}}</mat-icon>\r\n }\r\n <span>{{action.label}}</span>\r\n </button>\r\n }\r\n }\r\n </mat-menu>\r\n }\r\n </td>\r\n </ng-container>\r\n }\r\n \r\n <tr mat-header-row *matHeaderRowDef=\"generateDisplayColumns()\"></tr>\r\n <tr mat-row *matRowDef=\"let row; columns: generateDisplayColumns();\"></tr>\r\n \r\n <!-- Row shown when there is no matching data. -->\r\n <tr class=\"mat-row\" *matNoDataRow>\r\n <td class=\"mat-cell\" [attr.colspan]=\"generateDisplayColumns().length\">\r\n @if (mergedOptions.noTableRow && !dataSource.filter) {\r\n <ng-container *ngComponentOutlet=\"mergedOptions.noTableRow\"></ng-container>\r\n } @else if (mergedOptions.showFilter) {\r\n <p style=\"text-align: center;\">No data matching the filter {{dataSource.filter ? '\"' + dataSource.filter+ '\"' : ''}}</p>\r\n } @else {\r\n <p style=\"text-align: center;\">No data found</p>\r\n }\r\n </td>\r\n </tr>\r\n </table>\r\n </div>\r\n @if (mergedOptions.showPaginator) {\r\n <mat-paginator [pageSizeOptions]=\"mergedOptions.paginatorOptions?.pageSizeOptions || [5, 10, 25, 100]\" aria-label=\"Select page of users\"></mat-paginator>\r\n }", styles: ["table{width:100%;table-layout:fixed}.mat-mdc-form-field{font-size:14px;width:100%}.action-column{white-space:nowrap;text-align:right;width:10%;min-width:80px}.action-column button{margin-left:8px}\n"], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"], exportAs: ["ngComponentOutlet"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i1.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i2.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatTableModule }, { kind: "component", type: i3.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i3.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i3.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i3.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i3.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i3.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i3.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i3.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i3.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i3.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "directive", type: i3.MatNoDataRow, selector: "ng-template[matNoDataRow]" }, { kind: "ngmodule", type: MatSortModule }, { kind: "directive", type: i4.MatSort, selector: "[matSort]", inputs: ["matSortActive", "matSortStart", "matSortDirection", "matSortDisableClear", "matSortDisabled"], outputs: ["matSortChange"], exportAs: ["matSort"] }, { kind: "component", type: i4.MatSortHeader, selector: "[mat-sort-header]", inputs: ["mat-sort-header", "arrowPosition", "start", "disabled", "sortActionDescription", "disableClear"], exportAs: ["matSortHeader"] }, { kind: "ngmodule", type: MatPaginatorModule }, { kind: "component", type: i5.MatPaginator, selector: "mat-paginator", inputs: ["color", "pageIndex", "length", "pageSize", "pageSizeOptions", "hidePageSize", "showFirstLastButtons", "selectConfig", "disabled"], outputs: ["page"], exportAs: ["matPaginator"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i6.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i7.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i8.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i8.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i8.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i9.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i10.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.6", ngImport: i0, type: MaterialDataTableComponent, decorators: [{ type: Component, args: [{ selector: 'mat-data-table', imports: [ NgStyle, NgComponentOutlet, MatFormFieldModule, MatInputModule, MatTableModule, MatSortModule, MatPaginatorModule, MatIconModule, MatButtonModule, MatMenuModule, MatDividerModule, MatTooltipModule ], standalone: true, template: "@if (mergedOptions.showFilter) {\r\n <mat-form-field>\r\n <mat-label>{{mergedOptions.filterOptions?.label || 'Filter'}}</mat-label>\r\n <input matInput (keyup)=\"applyFilter($event)\" [placeholder]=\"mergedOptions.filterOptions?.placeholder || ''\" #input>\r\n </mat-form-field>\r\n }\r\n \r\n \r\n <div class=\"mat-elevation-z8\">\r\n <table mat-table \r\n [dataSource]=\"dataSource\" matSort \r\n [matSortActive]=\"mergedOptions.sorterOptions?.defaultSortColumn || ''\" \r\n [matSortDirection]=\"mergedOptions.sorterOptions?.defaultSortDirection || 'asc'\" \r\n [matSortDisabled]=\"!mergedOptions.showSorter\"\r\n [ngStyle]=\"mergedOptions.tableStyles || {}\"\r\n >\r\n \r\n @for (column of columnMappings; track $index) {\r\n <ng-container [matColumnDef]=\"column.key\">\r\n <th [ngStyle]=\"column?.headerCellStyles || {}\" mat-header-cell *matHeaderCellDef mat-sort-header>{{column.label}}</th>\r\n <td [ngStyle]=\"column?.dataCellStyles || {}\" mat-cell *matCellDef=\"let row\" \r\n [matTooltip]=\"getTooltipText(column, row)\"\r\n [matTooltipDisabled]=\"getTooltipText(column, row) === ''\"\r\n >\r\n @if (column?.component; as component) {\r\n <ng-container *ngComponentOutlet=\"component; inputs: getComponentInputs(column, row)\"></ng-container>\r\n } @else if (column?.transformer) {\r\n {{column.transformer!(row[column.key])}}\r\n } @else {\r\n {{row[column.key]}}\r\n }\r\n </td>\r\n </ng-container>\r\n }\r\n \r\n @if (mergedOptions.showActions || mergedOptions.showTableActions) {\r\n <ng-container matColumnDef=\"actions\">\r\n <th mat-header-cell *matHeaderCellDef class=\"action-column\">\r\n @if (mergedOptions.showTableActions) {\r\n <button mat-icon-button [matMenuTriggerFor]=\"tableMenu\" [ngStyle]=\"mergedOptions.tableActionOptions?.buttonStyles || {}\"><mat-icon>more_vert</mat-icon></button>\r\n <mat-menu #tableMenu=\"matMenu\">\r\n @for (action of mergedOptions.tableActionOptions?.actions; track $index) {\r\n @if (action.divider) {\r\n <mat-divider></mat-divider>\r\n } @else {\r\n <button mat-menu-item (click)=\"action.fxn!()\">\r\n @if (action.icon) {\r\n <mat-icon>{{action.icon}}</mat-icon>\r\n }\r\n <span>{{action.label}}</span>\r\n </button>\r\n }\r\n }\r\n </mat-menu>\r\n }\r\n </th>\r\n <td mat-cell *matCellDef=\"let row\" class=\"action-column\">\r\n @if (mergedOptions.showActions) {\r\n <button mat-icon-button [matMenuTriggerFor]=\"menu\" [ngStyle]=\"mergedOptions.actionOptions?.buttonStyles || {}\"><mat-icon>more_vert</mat-icon></button>\r\n <mat-menu #menu=\"matMenu\">\r\n @for (action of mergedOptions.actionOptions?.actions; track $index) {\r\n @if (action.divider) {\r\n <mat-divider></mat-divider>\r\n } @else {\r\n <button mat-menu-item (click)=\"action.fxn!(row)\">\r\n @if (action.icon) {\r\n <mat-icon>{{action.icon}}</mat-icon>\r\n }\r\n <span>{{action.label}}</span>\r\n </button>\r\n }\r\n }\r\n </mat-menu>\r\n }\r\n </td>\r\n </ng-container>\r\n }\r\n \r\n <tr mat-header-row *matHeaderRowDef=\"generateDisplayColumns()\"></tr>\r\n <tr mat-row *matRowDef=\"let row; columns: generateDisplayColumns();\"></tr>\r\n \r\n <!-- Row shown when there is no matching data. -->\r\n <tr class=\"mat-row\" *matNoDataRow>\r\n <td class=\"mat-cell\" [attr.colspan]=\"generateDisplayColumns().length\">\r\n @if (mergedOptions.noTableRow && !dataSource.filter) {\r\n <ng-container *ngComponentOutlet=\"mergedOptions.noTableRow\"></ng-container>\r\n } @else if (mergedOptions.showFilter) {\r\n <p style=\"text-align: center;\">No data matching the filter {{dataSource.filter ? '\"' + dataSource.filter+ '\"' : ''}}</p>\r\n } @else {\r\n <p style=\"text-align: center;\">No data found</p>\r\n }\r\n </td>\r\n </tr>\r\n </table>\r\n </div>\r\n @if (mergedOptions.showPaginator) {\r\n <mat-paginator [pageSizeOptions]=\"mergedOptions.paginatorOptions?.pageSizeOptions || [5, 10, 25, 100]\" aria-label=\"Select page of users\"></mat-paginator>\r\n }", styles: ["table{width:100%;table-layout:fixed}.mat-mdc-form-field{font-size:14px;width:100%}.action-column{white-space:nowrap;text-align:right;width:10%;min-width:80px}.action-column button{margin-left:8px}\n"] }] }], ctorParameters: () => [], propDecorators: { tableData: [{ type: Input, args: [{ required: true }] }], columnMappings: [{ type: Input, args: [{ required: true }] }], tableOptions: [{ type: Input }], paginator: [{ type: ViewChild, args: [MatPaginator] }], sort: [{ type: ViewChild, args: [MatSort] }] } }); /* * Public API Surface of material-data-table */ /** * Generated bundle index. Do not edit. */ export { MaterialDataTableComponent }; //# sourceMappingURL=lordkriegan-mat-data-table.mjs.map