ngx-mat-dynamic-table
Version:
Dynamic material table table.
798 lines (784 loc) • 45 kB
JavaScript
import { ɵɵdefineInjectable, Injectable, Component, EventEmitter, ChangeDetectionStrategy, Inject, LOCALE_ID, Input, Output, ViewChild, Pipe, NgModule } from '@angular/core';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
import { SelectionModel } from '@angular/cdk/collections';
import { formatDate, CommonModule } from '@angular/common';
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { saveAs } from 'file-saver';
import { utils, write } from 'xlsx';
import { values, map, partialRight, pick, get, cloneDeep, set } from 'lodash';
import { Observable, BehaviorSubject, Subject, interval } from 'rxjs';
import { debounce } from 'rxjs/operators';
import { FlexLayoutModule } from '@angular/flex-layout';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatSelectModule } from '@angular/material/select';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { ENTER, COMMA } from '@angular/cdk/keycodes';
import { moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';
import { MatChipsModule } from '@angular/material/chips';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
class NgxMatDynamicTableService {
constructor() { }
}
NgxMatDynamicTableService.ɵprov = ɵɵdefineInjectable({ factory: function NgxMatDynamicTableService_Factory() { return new NgxMatDynamicTableService(); }, token: NgxMatDynamicTableService, providedIn: "root" });
NgxMatDynamicTableService.decorators = [
{ type: Injectable, args: [{
providedIn: 'root'
},] }
];
NgxMatDynamicTableService.ctorParameters = () => [];
class NgxMatDynamicTableComponent {
constructor() { }
ngOnInit() {
}
}
NgxMatDynamicTableComponent.decorators = [
{ type: Component, args: [{
selector: 'ngx-mat-ngx-mat-dynamic-table',
template: `
<p>
ngx-mat-dynamic-table works!
</p>
`
},] }
];
NgxMatDynamicTableComponent.ctorParameters = () => [];
class XlsxExportService {
constructor() {
this.fileType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8';
this.fileExtension = '.xlsx';
}
exportExcel(jsonData, headers, fileName) {
const columnHeaders = values(headers);
const ws = utils.json_to_sheet(map(this.renameKeys(jsonData, headers), partialRight(pick, columnHeaders)), { header: columnHeaders });
const wb = { Sheets: { 'data': ws }, SheetNames: ['data'] };
const excelBuffer = write(wb, { bookType: 'xlsx', type: 'array' });
this.saveExcelFile(excelBuffer, fileName);
}
saveExcelFile(buffer, fileName) {
const data = new Blob([buffer], { type: this.fileType });
saveAs(data, fileName + this.fileExtension);
}
renameKeys(jsonData, headers) {
return jsonData.map(j => {
let renamed = {};
// Get all the keys of the headers
Object.keys(headers).forEach(key => {
// make the header value the new key of the renamed object.
// use the key of the header with lodash get function to get nested objects.
renamed[headers[key]] = get(j, key);
});
return renamed;
});
}
}
XlsxExportService.decorators = [
{ type: Injectable }
];
XlsxExportService.ctorParameters = () => [];
class ColumnStorageService {
constructor() {
this.dbName = 'ngx-mat-dynamic-table';
this.storeName = 'columns';
this.version = 1; //versions start at 1
var request = indexedDB.open(this.dbName, this.version);
request.onerror = (event) => {
console.warn('IndexedDB not enabled.');
};
request.onupgradeneeded = (event) => {
this.db = request.result;
this.objectStore = this.db.createObjectStore(this.storeName, { keyPath: 'tableId' });
console.debug('ngx-mat-dynamic-table: create object store 👍');
};
request.onsuccess = (event) => {
this.db = request.result;
console.debug('ngx-mat-dynamic-table: db opened 👍' + this.db);
};
}
isDbOpen() {
return !!this.db;
}
save(doc) {
// Dont try to save if there is no db
if (!this.db)
return;
var request = this.db.transaction([this.storeName], 'readwrite')
.objectStore(this.storeName)
.put(doc);
request.onsuccess = (event) => {
// console.debug('Saved columns...');
};
}
read(tableId) {
return new Observable((observer) => {
var transaction = this.db.transaction([this.storeName]);
if (!transaction) {
observer.next(null);
observer.complete();
return;
}
var objectStore = transaction.objectStore(this.storeName);
var request = objectStore.get(tableId);
request.onerror = (event) => {
observer.error(new Error);
observer.complete();
};
request.onsuccess = (event) => {
if (request.result) {
observer.next(request.result);
observer.complete();
}
else {
// observer.error((e) => console.log(e));
observer.next(null);
observer.complete();
}
};
});
}
}
ColumnStorageService.ɵprov = ɵɵdefineInjectable({ factory: function ColumnStorageService_Factory() { return new ColumnStorageService(); }, token: ColumnStorageService, providedIn: "root" });
ColumnStorageService.decorators = [
{ type: Injectable, args: [{
providedIn: 'root'
},] }
];
ColumnStorageService.ctorParameters = () => [];
class DynamicTableComponent {
constructor(locale, xlsxExportService, columnStorageService) {
this.locale = locale;
this.xlsxExportService = xlsxExportService;
this.columnStorageService = columnStorageService;
this.export = false; // Enable export for this table
this.filter = false; // Enable filter for this table
this.multiple = false; // Allow multiple select for this table
this.optionalColumns = false; // Show or hide columns
this.emitFilteredData = false; // Show or hide columns
this.rowClicked = new EventEmitter();
this.cellClicked = new EventEmitter();
this.selectedRows = new EventEmitter();
this.filterResult = new EventEmitter();
this.selection = new SelectionModel(true, []);
this.displayedColumns = [];
this.dataSource = new MatTableDataSource();
this.totalsRowVisible = false;
// The hidden columns filtered out
this.columnsToShow = new FormControl();
this.filterKeyUp = new BehaviorSubject('');
this.columnSearch$ = new Subject();
}
ngOnChanges(changes) {
// Add headers to of data to display in table
if (changes.columns && changes.columns.currentValue) {
this.setColumns();
}
if (changes.tableData.currentValue) {
// TODO: Test this default sort change
// if (this.sortActive) {
// this.dataSource.data = this.tableData.sort((a, b) => (_get(a, this.sortActive) > _get(b, this.sortActive)) ? this.sortDirection === 'asc' ? 1 : -1 : ((_get(b, this.sortActive) > _get(a, this.sortActive)) ? this.sortDirection === 'asc' ? -1 : 1 : 0));
// } else {
// this.dataSource.data = this.tableData;
// }
// Clear selection if new data is comming in
this.selection.clear();
this.dataSource.data = this.tableData;
this.updateColumnTotals();
}
}
ngAfterViewInit() {
this.filterKeyUp.pipe(debounce(() => interval(2000))).subscribe(filterValue => {
this.searchLoading = false;
// apply the filter after 2 seconds
this.dataSource.filter = filterValue;
if (this.dataSource.paginator) {
this.dataSource.paginator.firstPage();
}
// update totals
this.updateColumnTotals();
// If the flag is set, emit the filtered data
if (this.emitFilteredData) {
this.getFilteredData();
}
});
this.dataSource.sort = this.sort;
this.dataSource.paginator = this.paginator;
}
ngOnInit() {
this.dataSource.sortingDataAccessor = (data, attribute) => get(data, attribute);
// Changed this sort function from the source to compare alpha numerics https://stackoverflow.com/a/66157044/6044269
// https://github.com/angular/components/blob/master/src/material/table/table-data-source.ts#L165
this.dataSource.sortData = (data, sort) => {
const active = sort.active;
const direction = sort.direction;
if (!active || direction == '') {
return data;
}
return data.sort((a, b) => {
let valueA = this.dataSource.sortingDataAccessor(a, active);
let valueB = this.dataSource.sortingDataAccessor(b, active);
const valueAType = typeof valueA;
const valueBType = typeof valueB;
if (valueAType !== valueBType) {
if (valueAType === 'number') {
valueA += '';
}
if (valueBType === 'number') {
valueB += '';
}
}
let comparatorResult = 0;
if (valueA != null && valueB != null) {
// ! This is the If that I changed https://stackoverflow.com/a/66157044/6044269
if (valueAType === 'string' && valueBType === 'string') {
comparatorResult = valueA.localeCompare(valueB, 'en', { numeric: true });
}
else if (valueA > valueB) {
comparatorResult = 1;
}
else if (valueA < valueB) {
comparatorResult = -1;
}
}
else if (valueA != null) {
comparatorResult = 1;
}
else if (valueB != null) {
comparatorResult = -1;
}
return comparatorResult * (direction == 'asc' ? 1 : -1);
});
};
// Search for nested objects
this.dataSource.filterPredicate = (data, filter) => {
let validRow = true;
if (!filter)
return validRow;
// type|searchString:column#searchType,type|searchString:column#searchType...
const terms = filter.split(','); // Split multiple search
terms.some(term => {
let [type, criteria] = term.split('|'); // Split global / column search
let dataString = '';
let searchType; // type of search in a column
// let criteria;
if (type === 'global') {
// Search All
this.columns.filter(c => c.search).map(c => c.columnDef).forEach(column => {
const cellData = get(data, column);
if (cellData)
dataString += cellData;
});
// String contains/equals in all columns
if (!dataString || dataString.toString().toLowerCase().indexOf(criteria.toLowerCase()) === -1) {
validRow = false;
return true;
}
}
else {
const [searchString, columnAndType] = criteria.split(':'); // Split search string and criteria
let column;
[column, searchType] = columnAndType.split('#'); // Split column and type of search
criteria = searchString;
dataString = get(data, column); // get the data
if (searchType === '!=') {
// String contains/equals in all columns
if (!dataString || dataString.toString().toLowerCase().indexOf(criteria.toLowerCase()) !== -1) {
validRow = false;
return true;
}
}
else if (searchType === 'empty') {
if (dataString) {
validRow = false;
return true;
}
}
else {
// String contains/equals in all columns
if (!dataString || dataString.toString().toLowerCase().indexOf(criteria.toLowerCase()) === -1) {
validRow = false;
return true;
}
}
}
});
return validRow;
};
}
updateXLSXHeaders() {
this.xlsxHeaders = {};
this.columnsToShow.value.forEach(c => {
this.xlsxHeaders[c.columnDef] = c.columnTitle;
});
}
updateColumnTotals() {
if (!this.dataSource) {
return;
}
this.columns.map(col => {
if ((col.total || col.average)) {
this.totalsRowVisible = true;
if (this.dataSource.filteredData.length > 0) {
col.totalValue = this.dataSource.filteredData.map(t => get(t, col.columnDef)).reduce((acc, value) => acc + (value ? value : 0), 0);
if (isNaN(col.totalValue)) {
col.totalValue = 0;
col.averageValue = 0;
}
else {
col.averageValue = col.totalValue / this.dataSource.filteredData.length;
}
}
else {
col.totalValue = 0;
col.averageValue = 0;
}
}
return col;
});
}
applyFilter(searchTerms) {
this.searchLoading = true;
let filterString;
searchTerms.forEach(term => {
// type: Global or in a column
// |searchString: the text/number searching for
// :column: when type is column, this field is set
// #searchType = != >= <= empty
// type|searchString:column#searchType,type|searchString:column#searchType...
const newTerm = `${term.type}|${term.search}${term.type === 'column' ? ':' + term.column + '#' + term.searchType : ''}`;
if (filterString)
filterString += `,${newTerm}`;
else
filterString = newTerm;
});
this.filterKeyUp.next(filterString);
}
applyColumnFilter(element, column, columnTitle, searchType) {
//
if (searchType && searchType !== 'empty' && !element.value)
return;
this.searchLoading = true;
// Send data to table-search-input and add a chip
this.columnSearch$.next({ type: 'column', column: column, columnTitle: columnTitle, inputReference: element, search: element.value, searchType: searchType });
}
_rowClicked(row) {
if (!this.rowClick)
return;
this.rowClicked.emit(row);
}
onMatCellClick(row, column) {
if (!this.cellClick)
return;
this.cellClicked.emit({ row: row, column: column.columnDef, data: get(row, column.columnDef) });
}
/** Whether the number of selected elements matches the total number of rows. */
isAllSelected() {
const numSelected = this.selection.selected.length;
const numRows = this.dataSource.data.length;
return numSelected === numRows;
}
/** Selects all rows if they are not all selected; otherwise clear selection. */
masterToggle() {
this.isAllSelected() ?
this.selection.clear() :
this.dataSource.filteredData.forEach(row => this.selection.select(row));
this.emitSelected();
}
toggle(event, row) {
if (event)
this.selection.toggle(row);
this.emitSelected();
}
emitSelected() {
this.selectedRows.emit(this.selection.selected);
}
setColumns() {
this.setDefaultSorting();
// Check for saved columns in indexedDB
if (this.tableId) {
this.savedColumnsLoading = true;
// TODO: wait here for the db
if (this.columnStorageService.isDbOpen()) {
this.columnStorageService.read(this.tableId).subscribe(res => {
this.setSavedColumnSelection(res);
});
}
else {
// Wait for the DB to wake up and then query
setTimeout(() => {
this.columnStorageService.read(this.tableId).subscribe(res => {
this.setSavedColumnSelection(res);
});
}, 500);
}
}
else {
this.setStaticColumns();
}
}
setStaticColumns() {
// List of all columns to show in the view
this.displayedColumns = [];
// Totals row starts as false untill inspection of column definitions
this.totalsRowVisible = false;
// add the rest of non hidden columns to the list
this.displayedColumns = this.columns.filter(c => !c.hidden).map(c => c.columnDef);
// If multiple select is enabled, add select to the from of the array
if (this.multiple)
this.displayedColumns.unshift('select');
// set visible columns as checked in the selector
this.columnsToShow.setValue(this.columns.filter(c => !c.hidden));
this.updateColumnTotals();
}
displayColumnsChanged(event) {
this.displayedColumns = event.value.map(c => c.columnDef);
if (this.multiple)
this.displayedColumns.unshift('select');
// save the current visible columns
this.saveCurrentColumnSelection(this.displayedColumns);
}
eportToExcell() {
this.updateXLSXHeaders();
this.formatExportData();
this.xlsxExportService.exportExcel(this.exportData, this.xlsxHeaders, this.fileName ? this.fileName : 'Export');
}
getFilteredData() {
this.cloneData();
this.filterResult.emit(this.exportData);
}
cloneData() {
// Format dates for exporting, temp
if (this.selection.hasValue()) {
// Export selected data
this.exportData = this.selection.selected.map(a => cloneDeep(a));
}
else {
// Export filtered data and sorted
this.exportData = this.dataSource.sortData(this.dataSource.filteredData, this.dataSource.sort).map(a => cloneDeep(a));
}
}
formatExportData() {
this.cloneData();
// If the columns contain a date
let dateColumns = this.columnsToShow.value.filter(e => e.type === 'date');
if (dateColumns && dateColumns.length > 0) {
this.exportData = this.exportData.map(td => {
dateColumns.forEach(col => {
if (get(td, col.columnDef))
set(td, col.columnDef, formatDate(get(td, col.columnDef), col.dateFormat ? col.dateFormat : 'dd/MM/yyyy, HH:mm', this.locale));
});
return td;
});
}
}
nestedFilterCheck(search, data, key) {
if (typeof data[key] === 'object') {
for (const k in data[key]) {
if (data[key][k] !== null) {
search = this.nestedFilterCheck(search, data[key], k);
}
}
}
else {
search += data[key];
}
return search;
}
setDefaultSorting() {
this.columns.some(col => {
if (col.sort) {
this.sortActive = col.columnDef;
this.sortDirection = col.sort;
return true;
}
});
}
setSavedColumnSelection(saved) {
this.savedColumnsLoading = false;
if (!saved || saved.visibleColumns.length === 0) {
this.setStaticColumns();
return;
}
// set the column selector with the values stored in the indexeddb
this.columnsToShow.setValue(this.columns.filter(c => saved.visibleColumns.some(item => item === c.columnDef)));
// If multiple is enabled but was not saved in the saved array, add it
if (this.multiple && saved.visibleColumns[0] !== 'select')
saved.visibleColumns.unshift('select');
this.displayedColumns = saved.visibleColumns;
}
saveCurrentColumnSelection(displayedColumns) {
if (!this.tableId)
return;
let toSave = this.displayedColumns.slice();
// Do not save the selection column
if (this.multiple && toSave[0] === 'select')
toSave.shift();
this.columnStorageService.save({
tableId: this.tableId,
visibleColumns: toSave
});
}
}
DynamicTableComponent.decorators = [
{ type: Component, args: [{
selector: 'ngx-mat-dynamic-table',
template: "<div fxLayout=\"row wrap\" fxLayoutAlign=\"start center\" fxLayoutGap=\"10px\">\r\n <!-- Search input with chips -->\r\n <ngx-mat-table-search-input [columnSearch$]=\"columnSearch$\" (searchChange)=\"applyFilter($event)\">\r\n </ngx-mat-table-search-input>\r\n <mat-spinner *ngIf=\"searchLoading\" [diameter]=\"20\"></mat-spinner>\r\n <span fxFlex.gt-xs></span>\r\n <!-- Select enabled columns -->\r\n <mat-spinner *ngIf=\"savedColumnsLoading\" [diameter]=\"20\"></mat-spinner>\r\n <mat-form-field *ngIf=\"optionalColumns\" appearance=\"outline\">\r\n <mat-label>Columns</mat-label>\r\n <mat-select [formControl]=\"columnsToShow\" multiple (selectionChange)=\"displayColumnsChanged($event)\">\r\n <mat-option *ngFor=\"let column of columns\" [value]=\"column\">{{column.columnTitle}}</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n <!-- Export selected or filtered data to excel -->\r\n <button *ngIf=\"export\" mat-stroked-button color=\"primary\" (click)=\"eportToExcell()\">\r\n <mat-icon>import_export</mat-icon> Export\r\n </button>\r\n <!-- Indicate number of selected items -->\r\n <span *ngIf=\"selection.selected.length > 0\" class=\"mat-caption\">{{selection.selected.length}} values selected</span>\r\n</div>\r\n<!-- Table wrapping container -->\r\n<div class=\"table-container\">\r\n <table mat-table #table [dataSource]=\"dataSource\" matSort [matSortActive]=\"sortActive\"\r\n [matSortDirection]=\"sortDirection\" style=\"width: 100%;\">\r\n <!-- Checkbox Column -->\r\n <ng-container *ngIf=\"multiple\" matColumnDef=\"select\">\r\n <th mat-header-cell *matHeaderCellDef>\r\n <mat-checkbox (change)=\"$event ? masterToggle() : null\"\r\n [checked]=\"selection.hasValue() && isAllSelected()\"\r\n [indeterminate]=\"selection.hasValue() && !isAllSelected()\">\r\n </mat-checkbox>\r\n </th>\r\n <td mat-cell *matCellDef=\"let row\">\r\n <mat-checkbox (click)=\"$event.stopPropagation()\" (change)=\"toggle($event, row)\"\r\n [checked]=\"selection.isSelected(row)\">\r\n </mat-checkbox>\r\n </td>\r\n <ng-container *ngIf=\"totalsRowVisible\">\r\n <td mat-footer-cell *matFooterCellDef></td>\r\n </ng-container>\r\n </ng-container>\r\n <!-- Table columns -->\r\n <ng-container *ngFor=\"let column of columns\" [matColumnDef]=\"column.columnDef\" [sticky]=\"column.sticky\"\r\n [stickyEnd]=\"column.stickyEnd\">\r\n <!-- Table header -->\r\n <th mat-header-cell *matHeaderCellDef>\r\n <!-- Header and search icon -->\r\n <div fxLayout=\"row\" fxLayoutAlign=\"start center\">\r\n <span mat-sort-header>\r\n {{ column.columnTitle }}\r\n </span>\r\n <button *ngIf=\"column.search\" class=\"search-button\" mat-icon-button [matMenuTriggerFor]=\"menu\"\r\n (menuOpened)=\"searchInput.focus()\">\r\n <mat-icon [ngClass]=\"{'search-active': searchInput.value}\">search</mat-icon>\r\n </button>\r\n </div>\r\n <!-- Search Menu -->\r\n <mat-menu #menu=\"matMenu\">\r\n <div (click)=\"$event.stopPropagation()\" fxLayout=\"column\"\r\n style=\"padding-right: 8px; padding-left: 8px;\">\r\n <!-- Search input -->\r\n <mat-form-field>\r\n <input matInput #searchInput\r\n (keyup)=\"applyColumnFilter($event.target, column.columnDef, column.columnTitle, searchType.value);\"\r\n [placeholder]=\"column.columnTitle\" [disabled]=\"searchType.value === 'empty'\">\r\n <mat-hint>Search</mat-hint>\r\n <button mat-button *ngIf=\"searchInput.value\" matSuffix mat-icon-button aria-label=\"Clear\"\r\n (click)=\"searchInput.value=''; applyColumnFilter(searchInput, column.columnDef, column.columnTitle, searchType.value)\">\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n </mat-form-field>\r\n <!-- Search options -->\r\n <div fxLayout=\"row\" fxLayoutAlign=\"end end\">\r\n <mat-button-toggle-group name=\"search type\" aria-label=\"Search Type\"\r\n #searchType=\"matButtonToggleGroup\" value=\"=\"\r\n (change)=\"applyColumnFilter(searchInput, column.columnDef, column.columnTitle, searchType.value)\">\r\n <mat-button-toggle value=\"=\" matTooltip=\"Equals\">=</mat-button-toggle>\r\n <mat-button-toggle value=\"!=\" matTooltip=\"Not Equals\">\u2260</mat-button-toggle>\r\n <!-- <mat-button-toggle value=\">=\">>=</mat-button-toggle>\r\n <mat-button-toggle value=\"<=\"><=</mat-button-toggle> -->\r\n <mat-button-toggle value=\"empty\" matTooltip=\"Empty Cell\">\" \"</mat-button-toggle>\r\n </mat-button-toggle-group>\r\n </div>\r\n </div>\r\n </mat-menu>\r\n </th>\r\n <!-- Table Cell -->\r\n <td mat-cell *matCellDef=\"let row\" [class]=\"row[column.cellClassKey]\"\r\n [attr.data-label]=\"column.columnTitle\" (click)=\"onMatCellClick(row, column);\">\r\n <ng-container [ngSwitch]=\"column.type\">\r\n <!-- Number type columns -->\r\n <span *ngSwitchCase=\"'number'\"\r\n [ngClass]=\"column.unit?.position === 'before' ? 'unit-before' : 'unit-after'\"\r\n [attr.data-unit]=\"row | lodashGet: column.unit?.key\">\r\n {{ row | lodashGet: column.columnDef | number }}\r\n </span>\r\n <!-- Date Type columns -->\r\n <span *ngSwitchCase=\"'date'\">\r\n {{ row | lodashGet: column.columnDef | date: column.dateFormat ? column.dateFormat :\r\n 'dd/MM/yyyy, HH:mm' }}\r\n </span>\r\n <!-- Icon Columns -->\r\n <span *ngSwitchCase=\"'icon'\">\r\n <ng-container *ngFor=\"let icon of column.icons\">\r\n <mat-icon *ngIf=\"icon.value === (row | lodashGet: column.columnDef)\"\r\n [style.color]=\"icon.color\" [matTooltip]=\"icon.matTooltip\">{{icon.matIcon}}\r\n </mat-icon>\r\n </ng-container>\r\n </span>\r\n <!-- Text Columns -->\r\n <span *ngSwitchDefault>\r\n {{ row | lodashGet: column.columnDef }}\r\n </span>\r\n </ng-container>\r\n </td>\r\n\r\n <ng-container *ngIf=\"totalsRowVisible\">\r\n <td mat-footer-cell *matFooterCellDef [attr.data-label]=\"column.columnTitle\">\r\n <ng-container *ngIf=\"column.total\">\r\n {{column.totalValue | number}}\r\n </ng-container>\r\n <ng-container *ngIf=\"column.total && column.average\">\r\n | \r\n </ng-container>\r\n <ng-container *ngIf=\"column.average\">\r\n μ {{column.averageValue | number}}\r\n </ng-container>\r\n </td>\r\n </ng-container>\r\n\r\n </ng-container>\r\n\r\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns; sticky: true;\"></tr>\r\n <tr mat-row *matRowDef=\"let row; columns: displayedColumns;\" (click)=\"_rowClicked(row)\"\r\n [ngClass]=\"{'row-click': rowClick}\"></tr>\r\n\r\n <tr class=\"mat-row\" *matNoDataRow>\r\n <td class=\"mat-cell\" [attr.colspan]=\"displayedColumns.length\">No data</td>\r\n </tr>\r\n\r\n <ng-container *ngIf=\"totalsRowVisible\">\r\n <tr mat-footer-row *matFooterRowDef=\"displayedColumns; sticky: true;\"></tr>\r\n </ng-container>\r\n\r\n </table>\r\n</div>\r\n<mat-paginator class=\"mat-paginator-sticky\" [pageSizeOptions]=\"pageSizeOptions ? pageSizeOptions: [50, 100, 200]\">\r\n</mat-paginator>",
changeDetection: ChangeDetectionStrategy.OnPush,
styles: [".row-click{cursor:pointer}.search-button{height:24px!important;line-height:24px!important;width:24px!important}.search-button .mat-icon{height:18px!important;line-height:18px!important;width:18px!important}.search-button .material-icons{font-size:18px!important}.search-active{color:#00f}.unit-after:after,.unit-before:before{content:attr(data-unit)}.mat-cell{padding-left:5px;padding-right:5px}.mat-cell,.mat-header-row{text-overflow:ellipsis;white-space:nowrap}@media screen and (min-width:550px){.table-container{max-height:calc(100vh - 150px);overflow:auto}}@media screen and (max-width:550px){.mat-table .mat-row{border-bottom:5px solid #ddd;padding-bottom:5px}.mat-table .mat-header-row{display:flex;flex-wrap:wrap;height:auto;margin-bottom:4%;position:-webkit-sticky;position:sticky;top:0;z-index:2}.mat-table .mat-header-row th.mat-header-cell{align-items:center;display:flex;flex:1;justify-content:space-around;padding:1rem .5rem}.mat-table .mat-header-row th.mat-header-cell>.mat-sort-header-container{padding-left:15px}.mat-table .mat-cell{display:block;font-size:1em;font-weight:700;height:30px;margin-bottom:4%;padding-left:8px;padding-right:8px;text-align:right}.mat-table .mat-cell:before{content:attr(data-label);float:left;font-size:.85em;font-weight:400;text-transform:uppercase}.mat-table .mat-footer-cell{display:block;font-size:1em;font-weight:700;height:30px;margin-bottom:4%;padding-left:8px;padding-right:8px;text-align:right}.mat-table .mat-footer-cell:before{content:attr(data-label);float:left;font-size:.85em;font-weight:400;text-transform:uppercase}.mat-table .mat-cell:last-child{border-bottom:5px solid #ddd}}.mat-button-toggle-label-content{line-height:32px!important}"]
},] }
];
DynamicTableComponent.ctorParameters = () => [
{ type: String, decorators: [{ type: Inject, args: [LOCALE_ID,] }] },
{ type: XlsxExportService },
{ type: ColumnStorageService }
];
DynamicTableComponent.propDecorators = {
tableId: [{ type: Input }],
tableData: [{ type: Input }],
rowClick: [{ type: Input }],
cellClick: [{ type: Input }],
fileName: [{ type: Input }],
columns: [{ type: Input }],
pageSizeOptions: [{ type: Input }],
export: [{ type: Input }],
filter: [{ type: Input }],
multiple: [{ type: Input }],
optionalColumns: [{ type: Input }],
emitFilteredData: [{ type: Input }],
rowClicked: [{ type: Output }],
cellClicked: [{ type: Output }],
selectedRows: [{ type: Output }],
filterResult: [{ type: Output }],
sort: [{ type: ViewChild, args: [MatSort,] }],
paginator: [{ type: ViewChild, args: [MatPaginator,] }]
};
class DynamicTableContainerComponent {
constructor() {
this.export = false; // Enable export for this table
this.filter = false; // Enable filter for this table
this.multiple = false; // Allow multiple select for this table
this.optionalColumns = false; // Show or hide columns
this.emitFilteredData = false; // Show or hide columns
this.rowClicked = new EventEmitter();
this.cellClicked = new EventEmitter();
this.selectedRows = new EventEmitter();
this.filterResult = new EventEmitter();
}
ngOnInit() {
}
}
DynamicTableContainerComponent.decorators = [
{ type: Component, args: [{
selector: 'ngx-mat-dynamic-table-container',
template: "<ng-container *ngIf=\"tableData$\">\n <div *ngIf=\"(tableData$ | async) as tableData; else loading\">\n <ngx-mat-dynamic-table [tableId]=\"tableId\" [columns]=\"columns\" [tableData]=\"tableData\" [rowClick]=\"rowClick\"\n (rowClicked)=\"rowClicked.emit($event)\" [cellClick]=\"cellClick\" (cellClicked)=\"cellClicked.emit($event)\"\n [fileName]=\"fileName\" [export]=\"export\" [filter]=\"filter\" [multiple]=\"multiple\"\n (selectedRows)=\"selectedRows.emit($event)\" [optionalColumns]=\"optionalColumns\"\n [pageSizeOptions]=\"pageSizeOptions\" [emitFilteredData]=\"emitFilteredData\"\n (filterResult)=\"filterResult.emit($event)\">\n </ngx-mat-dynamic-table>\n </div>\n <ng-template #loading>\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n </ng-template>\n</ng-container>",
changeDetection: ChangeDetectionStrategy.OnPush,
styles: [""]
},] }
];
DynamicTableContainerComponent.ctorParameters = () => [];
DynamicTableContainerComponent.propDecorators = {
tableId: [{ type: Input }],
tableData$: [{ type: Input }],
columns: [{ type: Input }],
rowClick: [{ type: Input }],
cellClick: [{ type: Input }],
fileName: [{ type: Input }],
export: [{ type: Input }],
filter: [{ type: Input }],
multiple: [{ type: Input }],
optionalColumns: [{ type: Input }],
pageSizeOptions: [{ type: Input }],
emitFilteredData: [{ type: Input }],
rowClicked: [{ type: Output }],
cellClicked: [{ type: Output }],
selectedRows: [{ type: Output }],
filterResult: [{ type: Output }]
};
class XlsxTableExportComponent {
constructor(xlsxExportService, matSnackBar) {
this.xlsxExportService = xlsxExportService;
this.matSnackBar = matSnackBar;
}
ngOnInit() {
}
eportToExcell() {
if (!this.data || !this.headers) {
this.matSnackBar.open('Provide data and headers.', 'Ok', { duration: 4000 });
return;
}
this.xlsxExportService.exportExcel(this.data, this.headers, this.fileName ? this.fileName : 'Export');
}
}
XlsxTableExportComponent.decorators = [
{ type: Component, args: [{
selector: 'ngx-mat-xlsx-table-export',
template: "<button mat-stroked-button color=\"primary\" (click)=\"eportToExcell()\"><mat-icon>import_export</mat-icon> Export</button>",
styles: [""]
},] }
];
XlsxTableExportComponent.ctorParameters = () => [
{ type: XlsxExportService },
{ type: MatSnackBar }
];
XlsxTableExportComponent.propDecorators = {
data: [{ type: Input }],
fileName: [{ type: Input }],
headers: [{ type: Input }]
};
class LodashGetPipe {
transform(value, key) {
if (!key)
return '';
return get(value, key);
}
}
LodashGetPipe.decorators = [
{ type: Pipe, args: [{
name: 'lodashGet'
},] }
];
class TableSearchInputComponent {
constructor() {
this.visible = true;
this.selectable = true;
this.separatorKeysCodes = [ENTER, COMMA];
this.searchCtrl = new FormControl();
this.searchTerms = [];
this.searchChange = new EventEmitter();
}
ngOnInit() {
this.columnSearchSubscription = this.columnSearch$.subscribe(searchTerm => {
if ((searchTerm.search || '').trim() || searchTerm.searchType === 'empty') {
// See if there are existing global search
let columnSearch = this.searchTerms.find(st => st.column === searchTerm.column);
// If there are, update
if (columnSearch) {
columnSearch.search = searchTerm.search.trim();
columnSearch.searchType = searchTerm.searchType;
}
else // Else add
this.searchTerms.push(searchTerm);
this.emitNewSearch();
}
else {
this.remove(searchTerm);
}
});
}
add(event) {
const value = event.target.value;
// See if there are existing global search
let globalSearch = this.searchTerms.find(st => st.type === 'global');
// Add global search term
if ((value || '').trim()) {
// If there are, update
if (globalSearch)
globalSearch.search = value.trim();
else // Else add
this.searchTerms.push({ type: 'global', search: value.trim() });
this.emitNewSearch();
}
else {
this.remove(globalSearch);
}
}
remove(searchTerm) {
let index;
// if column is cleared
if (searchTerm.type === 'column') {
index = this.searchTerms.findIndex(i => i.column === searchTerm.column);
}
else {
index = this.searchTerms.findIndex(i => i.search === searchTerm.search && i.type === searchTerm.type);
if (index >= 0)
this.searchInput.nativeElement.value = null;
}
if (searchTerm.inputReference)
searchTerm.inputReference.value = '';
if (index >= 0) {
this.searchTerms.splice(index, 1);
this.emitNewSearch();
}
}
orderChanged(event) {
moveItemInArray(this.searchTerms, event.previousIndex, event.currentIndex);
this.emitNewSearch();
}
emitNewSearch() {
this.searchChange.emit(this.searchTerms);
}
ngOnDestroy() {
this.columnSearchSubscription.unsubscribe();
}
}
TableSearchInputComponent.decorators = [
{ type: Component, args: [{
selector: 'ngx-mat-table-search-input',
template: "<div fxLayout=\"column\" fxLayout.gt-xs=\"row wrap\" fxLayoutAlign=\"start start\" fxLayoutGap.gt-xs=\"15px\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Search</mat-label>\n <input matInput placeholder=\"All Columns\" #searchInput (keyup)=\"add($event)\">\n </mat-form-field>\n <div fxLayout=\"column\">\n <span *ngIf=\"searchTerms.length > 0\" class=\"mat-caption\">Filters</span>\n <mat-chip-list cdkDropList cdkDropListOrientation=\"horizontal\" (cdkDropListDropped)=\"orderChanged($event)\">\n <mat-chip class=\"example-box\" cdkDrag *ngFor=\"let term of searchTerms\"\n [color]=\"term.type === 'global' ? 'primary' : 'accent'\" selected\n [matTooltip]=\"term.type === 'global' ? 'All':term.columnTitle\">\n <span *ngIf=\"term.searchType === 'empty'; else not_empty\">\n {{term.columnTitle}} (empty)\n </span>\n <ng-template #not_empty>\n {{term.search}} {{term.searchType && term.searchType === '!=' ?\n '(\u2260)' : ''}}\n </ng-template>\n <mat-icon (click)=\"remove(term)\" matChipRemove>cancel</mat-icon>\n </mat-chip>\n </mat-chip-list>\n </div>\n</div>",
styles: [""]
},] }
];
TableSearchInputComponent.ctorParameters = () => [];
TableSearchInputComponent.propDecorators = {
columnSearch$: [{ type: Input }],
searchInput: [{ type: ViewChild, args: ['searchInput',] }],
searchChange: [{ type: Output }]
};
class NgxMatDynamicTableModule {
}
NgxMatDynamicTableModule.decorators = [
{ type: NgModule, args: [{
declarations: [
NgxMatDynamicTableComponent,
DynamicTableComponent,
DynamicTableContainerComponent,
XlsxTableExportComponent,
LodashGetPipe,
TableSearchInputComponent
],
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
FlexLayoutModule,
MatTableModule,
MatButtonModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
MatCheckboxModule,
MatPaginatorModule,
MatSortModule,
MatProgressBarModule,
MatTooltipModule,
MatSelectModule,
MatMenuModule,
MatProgressSpinnerModule,
MatChipsModule,
DragDropModule,
MatButtonToggleModule
],
providers: [XlsxExportService],
exports: [
DynamicTableComponent,
DynamicTableContainerComponent,
XlsxTableExportComponent
]
},] }
];
/*
* Public API Surface of ngx-mat-dynamic-table
*/
/**
* Generated bundle index. Do not edit.
*/
export { DynamicTableComponent, DynamicTableContainerComponent, NgxMatDynamicTableComponent, NgxMatDynamicTableModule, NgxMatDynamicTableService, XlsxTableExportComponent, XlsxExportService as ɵa, ColumnStorageService as ɵb, LodashGetPipe as ɵc, TableSearchInputComponent as ɵd };
//# sourceMappingURL=ngx-mat-dynamic-table.js.map