OnInit,
Output,
PipeTransform,
+ SimpleChanges,
TemplateRef,
ViewChild
} from '@angular/core';
import {
DatatableComponent,
+ getterForProp,
SortDirection,
SortPropDir,
TableColumnProp
} from '@swimlane/ngx-datatable';
-import { getterForProp } from '@swimlane/ngx-datatable/release/utils';
-import * as _ from 'lodash';
+import _ from 'lodash';
import { Observable, Subject, Subscription, timer as observableTimer } from 'rxjs';
-import { Icons } from '../../../shared/enum/icons.enum';
-import { CellTemplate } from '../../enum/cell-template.enum';
-import { CdTableColumn } from '../../models/cd-table-column';
-import { CdTableColumnFilter } from '../../models/cd-table-column-filter';
-import { CdTableColumnFiltersChange } from '../../models/cd-table-column-filters-change';
-import { CdTableFetchDataContext } from '../../models/cd-table-fetch-data-context';
-import { CdTableSelection } from '../../models/cd-table-selection';
-import { CdUserConfig } from '../../models/cd-user-config';
+import { TableStatus } from '~/app/shared/classes/table-status';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableColumnFilter } from '~/app/shared/models/cd-table-column-filter';
+import { CdTableColumnFiltersChange } from '~/app/shared/models/cd-table-column-filters-change';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { CdUserConfig } from '~/app/shared/models/cd-user-config';
@Component({
selector: 'cd-table',
mapTpl: TemplateRef<any>;
@ViewChild('truncateTpl', { static: true })
truncateTpl: TemplateRef<any>;
+ @ViewChild('rowDetailsTpl', { static: true })
+ rowDetailsTpl: TemplateRef<any>;
// This is the array with the items to be shown.
@Input()
// Page size to show. Set to 0 to show unlimited number of rows.
@Input()
limit? = 10;
+ // Has the row details?
+ @Input()
+ hasDetails = false;
/**
* Auto reload time in ms - per default every 5s
* You can set it to 0, undefined or false to disable the auto reload feature in order to
* trigger 'fetchData' if the reload button is clicked.
+ * You can set it to a negative number to, on top of disabling the auto reload,
+ * prevent triggering fetchData when initializing the table.
*/
@Input()
autoReload: any = 5000;
// By default selected item details will be updated on table refresh, if data has changed
@Input()
updateSelectionOnRefresh: 'always' | 'never' | 'onChange' = 'onChange';
+ // By default expanded item details will be updated on table refresh, if data has changed
+ @Input()
+ updateExpandedOnRefresh: 'always' | 'never' | 'onChange' = 'onChange';
@Input()
autoSave = true;
@Input()
extraFilterableColumns: CdTableColumn[] = [];
+ @Input()
+ status = new TableStatus();
+
/**
* Should be a function to update the input data if undefined nothing will be triggered
*
@Output()
updateSelection = new EventEmitter();
+ @Output()
+ setExpandedRow = new EventEmitter();
+
/**
* This should be defined if you need access to the applied column filters.
*
*/
selection = new CdTableSelection();
+ /**
+ * Use this variable to access the expanded row
+ */
+ expanded: any = undefined;
+
+ /**
+ * To prevent making changes to the original columns list, that might change
+ * how the table is renderer a second time, we now clone that list into a
+ * local variable and only use the clone.
+ */
+ localColumns: CdTableColumn[];
tableColumns: CdTableColumn[];
icons = Icons;
cellTemplates: {
search = '';
rows: any[] = [];
loadingIndicator = true;
- loadingError = false;
paginationClasses = {
pagerLeftArrow: Icons.leftArrowDouble,
pagerRightArrow: Icons.rightArrowDouble,
}
ngOnInit() {
+ this.localColumns = _.clone(this.columns);
+
// ngx-datatable triggers calculations each time mouse enters a row,
// this will prevent that.
- this.table.element.addEventListener('mouseenter', (e) => e.stopPropagation(), true);
+ this.table.element.addEventListener('mouseenter', (e) => e.stopPropagation());
this._addTemplates();
if (!this.sorts) {
// Check whether the specified identifier exists.
- const exists = _.findIndex(this.columns, ['prop', this.identifier]) !== -1;
+ const exists = _.findIndex(this.localColumns, ['prop', this.identifier]) !== -1;
// Auto-build the sorting configuration. If the specified identifier doesn't exist,
// then use the property of the first column.
this.sorts = this.createSortingDefinition(
- exists ? this.identifier : this.columns[0].prop + ''
+ exists ? this.identifier : this.localColumns[0].prop + ''
);
// If the specified identifier doesn't exist and it is not forced to use it anyway,
// then use the property of the first column.
if (!exists && !this.forceIdentifier) {
- this.identifier = this.columns[0].prop + '';
+ this.identifier = this.localColumns[0].prop + '';
}
}
+
this.initUserConfig();
- this.columns.forEach((c) => {
+ this.localColumns.forEach((c) => {
if (c.cellTransformation) {
c.cellTemplate = this.cellTemplates[c.cellTransformation];
}
}
});
+ this.initExpandCollapseColumn(); // If rows have details, add a column to expand or collapse the rows
this.initCheckboxColumn();
this.filterHiddenColumns();
this.initColumnFilters();
this.updateColumnFilterOptions();
+ // Notify all subscribers to reset their current selection.
+ this.updateSelection.emit(new CdTableSelection());
// Load the data table content every N ms or at least once.
// Force showing the loading indicator if there are subscribers to the fetchData
// event. This is necessary because it has been set to False in useData() when
});
});
});
- } else {
+ } else if (!this.autoReload) {
this.reloadData();
+ } else {
+ this.useData();
+ }
+
+ if (this.selectionType === 'single') {
+ this.table.selectCheck = this.singleSelectCheck.bind(this);
}
}
initUserConfig() {
if (this.autoSave) {
- this.tableName = this._calculateUniqueTableName(this.columns);
+ this.tableName = this._calculateUniqueTableName(this.localColumns);
this._loadUserConfig();
this._initUserConfigAutoSave();
}
if (!this.userConfig.columns) {
this.updateUserColumns();
} else {
- this.columns.forEach((c, i) => {
- c.isHidden = this.userConfig.columns[i].isHidden;
+ this.userConfig.columns.forEach((col) => {
+ for (let i = 0; i < this.localColumns.length; i++) {
+ if (this.localColumns[i].prop === col.prop) {
+ this.localColumns[i].isHidden = col.isHidden;
+ }
+ }
});
}
}
}
_initUserConfigAutoSave() {
- const source: Observable<any> = Observable.create(this._initUserConfigProxy.bind(this));
+ const source: Observable<any> = new Observable(this._initUserConfigProxy.bind(this));
this.saveSubscriber = source.subscribe(this._saveUserConfig.bind(this));
}
}
updateUserColumns() {
- this.userConfig.columns = this.columns.map((c) => ({
+ this.userConfig.columns = this.localColumns.map((c) => ({
prop: c.prop,
name: c.name,
isHidden: !!c.isHidden
*/
initCheckboxColumn() {
if (this.selectionType === 'multiClick') {
- this.columns.unshift({
+ this.localColumns.unshift({
prop: undefined,
resizeable: false,
sortable: false,
}
}
+ /**
+ * Add a column to expand and collapse the table row if it 'hasDetails'
+ */
+ initExpandCollapseColumn() {
+ if (this.hasDetails) {
+ this.localColumns.unshift({
+ prop: undefined,
+ resizeable: false,
+ sortable: false,
+ draggable: false,
+ isHidden: false,
+ canAutoResize: false,
+ cellClass: 'cd-datatable-expand-collapse',
+ width: 40,
+ cellTemplate: this.rowDetailsTpl
+ });
+ }
+ }
+
filterHiddenColumns() {
- this.tableColumns = this.columns.filter((c) => !c.isHidden);
+ this.tableColumns = this.localColumns.filter((c) => !c.isHidden);
}
initColumnFilters() {
- let filterableColumns = _.filter(this.columns, { filterable: true });
+ let filterableColumns = _.filter(this.localColumns, { filterable: true });
filterableColumns = [...filterableColumns, ...this.extraFilterableColumns];
this.columnFilters = filterableColumns.map((col: CdTableColumn) => {
return {
return _.isEmpty(css) ? undefined : css;
}
- ngOnChanges() {
- this.useData();
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes.data && changes.data.currentValue) {
+ this.useData();
+ }
}
setLimit(e: any) {
reloadData() {
if (!this.updating) {
- this.loadingError = false;
+ this.status = new TableStatus();
const context = new CdTableFetchDataContext(() => {
// Do we have to display the error panel?
- this.loadingError = context.errorConfig.displayError;
+ if (!!context.errorConfig.displayError) {
+ this.status = new TableStatus('danger', $localize`Failed to load data.`);
+ }
// Force data table to show no data?
if (context.errorConfig.resetData) {
this.data = [];
this.updateFilter();
this.reset();
this.updateSelected();
+ this.updateExpanded();
}
/**
if (this.updateSelectionOnRefresh === 'never') {
return;
}
- const newSelected: any[] = [];
+ const newSelected = new Set();
this.selection.selected.forEach((selectedItem) => {
for (const row of this.data) {
if (selectedItem[this.identifier] === row[this.identifier]) {
- newSelected.push(row);
+ newSelected.add(row);
}
}
});
+ const newSelectedArray = Array.from(newSelected.values());
if (
this.updateSelectionOnRefresh === 'onChange' &&
- _.isEqual(this.selection.selected, newSelected)
+ _.isEqual(this.selection.selected, newSelectedArray)
) {
return;
}
- this.selection.selected = newSelected;
+ this.selection.selected = newSelectedArray;
this.onSelect(this.selection);
}
+ updateExpanded() {
+ if (_.isUndefined(this.expanded) || this.updateExpandedOnRefresh === 'never') {
+ return;
+ }
+
+ const expandedId = this.expanded[this.identifier];
+ const newExpanded = _.find(this.data, (row) => expandedId === row[this.identifier]);
+
+ if (this.updateExpandedOnRefresh === 'onChange' && _.isEqual(this.expanded, newExpanded)) {
+ return;
+ }
+
+ this.expanded = newExpanded;
+ this.setExpandedRow.emit(newExpanded);
+ }
+
onSelect($event: any) {
- this.selection.selected = $event['selected'];
+ // Ensure we do not process DOM 'select' events.
+ // https://github.com/swimlane/ngx-datatable/issues/899
+ if (_.has($event, 'selected')) {
+ this.selection.selected = $event['selected'];
+ }
this.updateSelection.emit(_.clone(this.selection));
}
- toggleColumn($event: any) {
- const prop: TableColumnProp = $event.target.name;
- const hide = !$event.target.checked;
+ private singleSelectCheck(row: any) {
+ return this.selection.selected.indexOf(row) === -1;
+ }
+
+ toggleColumn(column: CdTableColumn) {
+ const prop: TableColumnProp = column.prop;
+ const hide = !column.isHidden;
if (hide && this.tableColumns.length === 1) {
- $event.target.checked = true;
+ column.isHidden = true;
return;
}
- _.find(this.columns, (c: CdTableColumn) => c.prop === prop).isHidden = hide;
+ _.find(this.localColumns, (c: CdTableColumn) => c.prop === prop).isHidden = hide;
this.updateColumns();
}
updateFilter() {
let rows = this.columnFilters.length !== 0 ? this.doColumnFiltering() : this.data;
- if (this.search.length > 0) {
- const columns = this.columns.filter((c) => c.cellTransformation !== CellTemplate.sparkline);
+ if (this.search.length > 0 && rows) {
+ const columns = this.localColumns.filter(
+ (c) => c.cellTransformation !== CellTemplate.sparkline
+ );
// update the rows
rows = this.subSearch(rows, TableComponent.prepareSearch(this.search), columns);
// Whenever the filter changes, always go back to the first page
if (currentSearch.length === 0 || data.length === 0) {
return data;
}
- const searchTerms: string[] = currentSearch
- .pop()
- .replace(/\+/g, ' ')
- .split(':');
+ const searchTerms: string[] = currentSearch.pop().replace(/\+/g, ' ').split(':');
const columnsClone = [...columns];
if (searchTerms.length === 2) {
columns = columnsClone.filter((c) => c.name.toLowerCase().indexOf(searchTerms[0]) !== -1);
};
};
}
+
+ toggleExpandRow(row: any, isExpanded: boolean, event: any) {
+ event.stopPropagation();
+ if (!isExpanded) {
+ // If current row isn't expanded, collapse others
+ this.expanded = row;
+ this.table.rowDetail.collapseAllRows();
+ this.setExpandedRow.emit(row);
+ } else {
+ // If all rows are closed, emit undefined
+ this.expanded = undefined;
+ this.setExpandedRow.emit(undefined);
+ }
+ this.table.rowDetail.toggleExpandRow(row);
+ }
}