14 } from '@angular/core';
21 } from '@swimlane/ngx-datatable';
22 import * as _ from 'lodash';
23 import { Observable, timer as observableTimer } from 'rxjs';
25 import { CellTemplate } from '../../enum/cell-template.enum';
26 import { CdTableColumn } from '../../models/cd-table-column';
27 import { CdTableFetchDataContext } from '../../models/cd-table-fetch-data-context';
28 import { CdTableSelection } from '../../models/cd-table-selection';
29 import { CdUserConfig } from '../../models/cd-user-config';
33 templateUrl: './table.component.html',
34 styleUrls: ['./table.component.scss']
36 export class TableComponent implements AfterContentChecked, OnInit, OnChanges, OnDestroy {
37 @ViewChild(DatatableComponent)
38 table: DatatableComponent;
39 @ViewChild('tableCellBoldTpl')
40 tableCellBoldTpl: TemplateRef<any>;
41 @ViewChild('sparklineTpl')
42 sparklineTpl: TemplateRef<any>;
43 @ViewChild('routerLinkTpl')
44 routerLinkTpl: TemplateRef<any>;
45 @ViewChild('checkIconTpl')
46 checkIconTpl: TemplateRef<any>;
47 @ViewChild('perSecondTpl')
48 perSecondTpl: TemplateRef<any>;
49 @ViewChild('executingTpl')
50 executingTpl: TemplateRef<any>;
51 @ViewChild('classAddingTpl')
52 classAddingTpl: TemplateRef<any>;
54 // This is the array with the items to be shown.
57 // Each item -> { prop: 'attribute name', name: 'display name' }
59 columns: CdTableColumn[];
60 // Each item -> { prop: 'attribute name', dir: 'asc'||'desc'}
62 sorts?: SortPropDir[];
63 // Method used for setting column widths.
66 // Display the tool header, including reload button, pagination and search fields?
69 // Display the table header?
72 // Display the table footer?
75 // Page size to show. Set to 0 to show unlimited number of rows.
80 * Auto reload time in ms - per default every 5s
81 * You can set it to 0, undefined or false to disable the auto reload feature in order to
82 * trigger 'fetchData' if the reload button is clicked.
85 autoReload: any = 5000;
87 // Which row property is unique for a row. If the identifier is not specified in any
88 // column, then the property name of the first column is used. Defaults to 'id'.
91 // If 'true', then the specified identifier is used anyway, although it is not specified
92 // in any column. Defaults to 'false'.
94 forceIdentifier = false;
95 // Allows other components to specify which type of selection they want,
96 // e.g. 'single' or 'multi'.
98 selectionType: string = undefined;
99 // By default selected item details will be updated on table refresh, if data has changed
101 updateSelectionOnRefresh: 'always' | 'never' | 'onChange' = 'onChange';
106 // Only needed to set if the classAddingTpl is used
108 customCss?: { [css: string]: number | string | ((any) => boolean) };
111 * Should be a function to update the input data if undefined nothing will be triggered
113 * Sometimes it's useful to only define fetchData once.
115 * Usage of multiple tables with data which is updated by the same function
117 * The function is triggered through one table and all tables will update
120 fetchData = new EventEmitter();
123 * This should be defined if you need access to the selection object.
125 * Each time the table selection changes, this will be triggered and
126 * the new selection object will be sent.
128 * @memberof TableComponent
131 updateSelection = new EventEmitter();
134 * Use this variable to access the selected row(s).
136 selection = new CdTableSelection();
138 tableColumns: CdTableColumn[];
140 [key: string]: TemplateRef<any>;
144 loadingIndicator = true;
145 loadingError = false;
146 paginationClasses = {
147 pagerLeftArrow: 'i fa fa-angle-double-left',
148 pagerRightArrow: 'i fa fa-angle-double-right',
149 pagerPrevious: 'i fa fa-angle-left',
150 pagerNext: 'i fa fa-angle-right'
152 userConfig: CdUserConfig = {};
154 localStorage = window.localStorage;
155 private saveSubscriber;
156 private reloadSubscriber;
157 private updating = false;
159 // Internal variable to check if it is necessary to recalculate the
160 // table columns after the browser window has been resized.
161 private currentWidth: number;
163 constructor(private ngZone: NgZone, private cdRef: ChangeDetectorRef) {}
166 this._addTemplates();
168 // Check whether the specified identifier exists.
169 const exists = _.findIndex(this.columns, ['prop', this.identifier]) !== -1;
170 // Auto-build the sorting configuration. If the specified identifier doesn't exist,
171 // then use the property of the first column.
172 this.sorts = this.createSortingDefinition(
173 exists ? this.identifier : this.columns[0].prop + ''
175 // If the specified identifier doesn't exist and it is not forced to use it anyway,
176 // then use the property of the first column.
177 if (!exists && !this.forceIdentifier) {
178 this.identifier = this.columns[0].prop + '';
181 this.initUserConfig();
182 this.columns.forEach((c) => {
183 if (c.cellTransformation) {
184 c.cellTemplate = this.cellTemplates[c.cellTransformation];
187 c.flexGrow = c.prop + '' === this.identifier ? 1 : 2;
190 c.resizeable = false;
193 this.filterHiddenColumns();
194 // Load the data table content every N ms or at least once.
195 // Force showing the loading indicator if there are subscribers to the fetchData
196 // event. This is necessary because it has been set to False in useData() when
197 // this method was triggered by ngOnChanges().
198 if (this.fetchData.observers.length > 0) {
199 this.loadingIndicator = true;
201 if (_.isInteger(this.autoReload) && this.autoReload > 0) {
202 this.ngZone.runOutsideAngular(() => {
203 this.reloadSubscriber = observableTimer(0, this.autoReload).subscribe(() => {
204 this.ngZone.run(() => {
205 return this.reloadData();
216 this.tableName = this._calculateUniqueTableName(this.columns);
217 this._loadUserConfig();
218 this._initUserConfigAutoSave();
220 if (!this.userConfig.limit) {
221 this.userConfig.limit = this.limit;
223 if (!this.userConfig.sorts) {
224 this.userConfig.sorts = this.sorts;
226 if (!this.userConfig.columns) {
227 this.updateUserColumns();
229 this.columns.forEach((c, i) => {
230 c.isHidden = this.userConfig.columns[i].isHidden;
235 _calculateUniqueTableName(columns) {
236 const stringToNumber = (s) => {
237 if (!_.isString(s)) {
241 for (let i = 0; i < s.length; i++) {
242 result += s.charCodeAt(i) * i;
248 (result, value, index) =>
249 (stringToNumber(value.prop) + stringToNumber(value.name)) * (index + 1) + result,
256 const loaded = this.localStorage.getItem(this.tableName);
258 this.userConfig = JSON.parse(loaded);
262 _initUserConfigAutoSave() {
263 const source = Observable.create(this._initUserConfigProxy.bind(this));
264 this.saveSubscriber = source.subscribe(this._saveUserConfig.bind(this));
267 _initUserConfigProxy(observer) {
268 this.userConfig = new Proxy(this.userConfig, {
269 set(config, prop, value) {
270 config[prop] = value;
271 observer.next(config);
277 _saveUserConfig(config) {
278 this.localStorage.setItem(this.tableName, JSON.stringify(config));
281 updateUserColumns() {
282 this.userConfig.columns = this.columns.map((c) => ({
285 isHidden: !!c.isHidden
289 filterHiddenColumns() {
290 this.tableColumns = this.columns.filter((c) => !c.isHidden);
294 if (this.reloadSubscriber) {
295 this.reloadSubscriber.unsubscribe();
297 if (this.saveSubscriber) {
298 this.saveSubscriber.unsubscribe();
302 ngAfterContentChecked() {
303 // If the data table is not visible, e.g. another tab is active, and the
304 // browser window gets resized, the table and its columns won't get resized
305 // automatically if the tab gets visible again.
306 // https://github.com/swimlane/ngx-datatable/issues/193
307 // https://github.com/swimlane/ngx-datatable/issues/193#issuecomment-329144543
308 if (this.table && this.table.element.clientWidth !== this.currentWidth) {
309 this.currentWidth = this.table.element.clientWidth;
310 this.table.recalculate();
315 this.cellTemplates.bold = this.tableCellBoldTpl;
316 this.cellTemplates.checkIcon = this.checkIconTpl;
317 this.cellTemplates.sparkline = this.sparklineTpl;
318 this.cellTemplates.routerLink = this.routerLinkTpl;
319 this.cellTemplates.perSecond = this.perSecondTpl;
320 this.cellTemplates.executing = this.executingTpl;
321 this.cellTemplates.classAdding = this.classAddingTpl;
324 useCustomClass(value: any): string {
325 if (!this.customCss) {
326 throw new Error('Custom classes are not set!');
328 const classes = Object.keys(this.customCss);
329 const css = Object.values(this.customCss)
330 .map((v, i) => ((_.isFunction(v) && v(value)) || v === value) && classes[i])
333 return _.isEmpty(css) ? undefined : css;
341 const value = parseInt(e.target.value, 10);
343 this.userConfig.limit = value;
348 if (!this.updating) {
349 this.loadingError = false;
350 const context = new CdTableFetchDataContext(() => {
351 // Do we have to display the error panel?
352 this.loadingError = context.errorConfig.displayError;
353 // Force data table to show no data?
354 if (context.errorConfig.resetData) {
357 // Stop the loading indicator and reset the data table
358 // to the correct state.
361 this.fetchData.emit(context);
362 this.updating = true;
367 this.loadingIndicator = true;
373 const id = row[this.identifier];
374 if (_.isUndefined(id)) {
375 throw new Error(`Wrong identifier "${this.identifier}" -> "${id}"`);
383 return; // Wait for data
385 this.rows = [...this.data];
386 if (this.search.length > 0) {
390 this.updateSelected();
394 * Reset the data table to correct state. This includes:
395 * - Disable loading indicator
396 * - Reset 'Updating' flag
399 this.loadingIndicator = false;
400 this.updating = false;
404 * After updating the data, we have to update the selected items
405 * because details may have changed,
406 * or some selected items may have been removed.
409 if (this.updateSelectionOnRefresh === 'never') {
412 const newSelected = [];
413 this.selection.selected.forEach((selectedItem) => {
414 for (const row of this.data) {
415 if (selectedItem[this.identifier] === row[this.identifier]) {
416 newSelected.push(row);
421 this.updateSelectionOnRefresh === 'onChange' &&
422 _.isEqual(this.selection.selected, newSelected)
426 this.selection.selected = newSelected;
431 this.selection.update();
432 this.updateSelection.emit(_.clone(this.selection));
435 toggleColumn($event: any) {
436 const prop: TableColumnProp = $event.target.name;
437 const hide = !$event.target.checked;
438 if (hide && this.tableColumns.length === 1) {
439 $event.target.checked = true;
442 _.find(this.columns, (c: CdTableColumn) => c.prop === prop).isHidden = hide;
443 this.updateColumns();
447 this.updateUserColumns();
448 this.filterHiddenColumns();
449 const sortProp = this.userConfig.sorts[0].prop;
450 if (!_.find(this.tableColumns, (c: CdTableColumn) => c.prop === sortProp)) {
451 this.userConfig.sorts = this.createSortingDefinition(this.tableColumns[0].prop);
452 this.table.onColumnSort({ sorts: this.userConfig.sorts });
454 this.table.recalculate();
455 this.cdRef.detectChanges();
458 createSortingDefinition(prop: TableColumnProp): SortPropDir[] {
462 dir: SortDirection.asc
467 changeSorting({ sorts }) {
468 this.userConfig.sorts = sorts;
471 updateFilter(clearSearch = false) {
475 // prepare search strings
476 let search = this.search.toLowerCase().replace(/,/g, '');
477 const columns = this.columns.filter((c) => c.cellTransformation !== CellTemplate.sparkline);
478 if (search.match(/['"][^'"]+['"]/)) {
479 search = search.replace(/['"][^'"]+['"]/g, (match: string) => {
480 return match.replace(/(['"])([^'"]+)(['"])/g, '$2').replace(/ /g, '+');
484 this.rows = this.subSearch(this.data, search.split(' ').filter((s) => s.length > 0), columns);
485 // Whenever the filter changes, always go back to the first page
486 this.table.offset = 0;
489 subSearch(data: any[], currentSearch: string[], columns: CdTableColumn[]) {
490 if (currentSearch.length === 0 || data.length === 0) {
493 const searchTerms: string[] = currentSearch
497 const columnsClone = [...columns];
498 const dataClone = [...data];
499 const filterColumns = (columnName: string) =>
500 columnsClone.filter((c) => c.name.toLowerCase().indexOf(columnName) !== -1);
501 if (searchTerms.length === 2) {
502 columns = filterColumns(searchTerms[0]);
504 const searchTerm: string = _.last(searchTerms);
505 data = this.basicDataSearch(searchTerm, data, columns);
506 // Checks if user searches for column but he is still typing
507 if (data.length === 0 && searchTerms.length === 1 && filterColumns(searchTerm).length > 0) {
510 return this.subSearch(data, currentSearch, columnsClone);
513 basicDataSearch(searchTerm: string, rows: any[], columns: CdTableColumn[]) {
514 if (searchTerm.length === 0) {
517 return rows.filter((row) => {
519 columns.filter((col) => {
520 let cellValue: any = _.get(row, col.prop);
522 if (!_.isUndefined(col.pipe)) {
523 cellValue = col.pipe.transform(cellValue);
525 if (_.isUndefined(cellValue) || _.isNull(cellValue)) {
529 if (_.isArray(cellValue)) {
530 cellValue = cellValue.join(' ');
531 } else if (_.isNumber(cellValue) || _.isBoolean(cellValue)) {
532 cellValue = cellValue.toString();
534 return cellValue.toLowerCase().indexOf(searchTerm) !== -1;
541 // Return the function used to populate a row's CSS classes.
544 clickable: !_.isUndefined(this.selectionType)