UNPKG

ng2-smart-table

Version:

Angular Smart Table

1,468 lines (1,439 loc) 110 kB
import { EventEmitter, Component, Input, Output, ComponentFactoryResolver, ViewChild, ViewContainerRef, ChangeDetectionStrategy, NgModule, ElementRef } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule, FormControl, NgControl, ReactiveFormsModule } from '@angular/forms'; import { CompleterService, Ng2CompleterModule } from 'ng2-completer'; import { Subject } from 'rxjs'; import { cloneDeep } from 'lodash'; import { debounceTime, map, distinctUntilChanged, skip, takeUntil } from 'rxjs/operators'; import { HttpParams } from '@angular/common/http'; /** * Extending object that entered in first argument. * * Returns extended object or false if have no target object or incorrect type. * * If you wish to clone source object (without modify it), just use empty new * object as first argument, like this: * deepExtend({}, yourObj_1, [yourObj_N]); */ const deepExtend = function (...objects) { if (arguments.length < 1 || typeof arguments[0] !== 'object') { return false; } if (arguments.length < 2) { return arguments[0]; } const target = arguments[0]; // convert arguments to array and cut off target object const args = Array.prototype.slice.call(arguments, 1); let val, src; args.forEach((obj) => { // skip argument if it is array or isn't object if (typeof obj !== 'object' || Array.isArray(obj)) { return; } Object.keys(obj).forEach(function (key) { src = target[key]; // source value val = obj[key]; // new value // recursion prevention if (val === target) { return; /** * if new value isn't object then just overwrite by new value * instead of extending. */ } else if (typeof val !== 'object' || val === null) { target[key] = val; return; // just clone arrays (and recursive clone objects inside) } else if (Array.isArray(val)) { target[key] = cloneDeep(val); return; // overwrite by new value if source isn't object or array } else if (typeof src !== 'object' || src === null || Array.isArray(src)) { target[key] = deepExtend({}, val); return; // source value and new value is objects both, extending... } else { target[key] = deepExtend(src, val); return; } }); }); return target; }; class Deferred { constructor() { this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } } // getDeepFromObject({result: {data: 1}}, 'result.data', 2); // returns 1 function getDeepFromObject(object = {}, name, defaultValue) { const keys = name.split('.'); // clone the object let level = deepExtend({}, object); keys.forEach((k) => { if (level && typeof level[k] !== 'undefined') { level = level[k]; } }); return typeof level === 'undefined' ? defaultValue : level; } function getPageForRowIndex(index, perPage) { // we need to add 1 to convert 0-based index to 1-based page number. return Math.floor(index / perPage) + 1; } function prepareValue(value) { return value; } class Cell { constructor(value, row, column, dataSet) { this.value = value; this.row = row; this.column = column; this.dataSet = dataSet; this.newValue = ''; this.newValue = value; } getColumn() { return this.column; } getRow() { return this.row; } getValue() { const valid = this.column.getValuePrepareFunction() instanceof Function; const prepare = valid ? this.column.getValuePrepareFunction() : Cell.PREPARE; return prepare.call(null, this.value, this.row.getData(), this); } setValue(value) { this.newValue = value; } getId() { return this.getColumn().id; } getTitle() { return this.getColumn().title; } isEditable() { if (this.getRow().index === -1) { return this.getColumn().isAddable; } else { return this.getColumn().isEditable; } } } Cell.PREPARE = prepareValue; class Row { constructor(index, data, _dataSet) { this.index = index; this.data = data; this._dataSet = _dataSet; this.isSelected = false; this.isInEditing = false; this.cells = []; this.process(); } getCell(column) { return this.cells.find(el => el.getColumn() === column); } getCells() { return this.cells; } getData() { return this.data; } getIsSelected() { return this.isSelected; } getNewData() { const values = Object.assign({}, this.data); this.getCells().forEach((cell) => values[cell.getColumn().id] = cell.newValue); return values; } setData(data) { this.data = data; this.process(); } process() { this.cells = []; this._dataSet.getColumns().forEach((column) => { const cell = this.createCell(column); this.cells.push(cell); }); } createCell(column) { const defValue = column.settings.defaultValue ? column.settings.defaultValue : ''; const value = typeof this.data[column.id] === 'undefined' ? defValue : this.data[column.id]; return new Cell(value, this, column, this._dataSet); } } class Column { constructor(id, settings, dataSet) { this.id = id; this.settings = settings; this.dataSet = dataSet; this.title = ''; this.type = ''; this.class = ''; this.width = ''; this.hide = false; this.isSortable = false; this.isEditable = true; this.isAddable = true; this.isFilterable = false; this.sortDirection = ''; this.defaultSortDirection = ''; this.editor = { type: '', config: {}, component: null }; this.filter = { type: '', config: {}, component: null }; this.renderComponent = null; this.process(); } getOnComponentInitFunction() { return this.onComponentInitFunction; } getCompareFunction() { return this.compareFunction; } getValuePrepareFunction() { return this.valuePrepareFunction; } getFilterFunction() { return this.filterFunction; } getConfig() { return this.editor && this.editor.config; } getFilterType() { return this.filter && this.filter.type; } getFilterConfig() { return this.filter && this.filter.config; } process() { this.title = this.settings['title']; this.class = this.settings['class']; this.width = this.settings['width']; this.hide = !!this.settings['hide']; this.type = this.prepareType(); this.editor = this.settings['editor']; this.filter = this.settings['filter']; this.renderComponent = this.settings['renderComponent']; this.isFilterable = typeof this.settings['filter'] === 'undefined' ? true : !!this.settings['filter']; this.defaultSortDirection = ['asc', 'desc'] .indexOf(this.settings['sortDirection']) !== -1 ? this.settings['sortDirection'] : ''; this.isSortable = typeof this.settings['sort'] === 'undefined' ? true : !!this.settings['sort']; this.isEditable = typeof this.settings['editable'] === 'undefined' ? true : !!this.settings['editable']; this.isAddable = typeof this.settings['addable'] === 'undefined' ? true : !!this.settings['addable']; this.sortDirection = this.prepareSortDirection(); this.compareFunction = this.settings['compareFunction']; this.valuePrepareFunction = this.settings['valuePrepareFunction']; this.filterFunction = this.settings['filterFunction']; this.onComponentInitFunction = this.settings['onComponentInitFunction']; } prepareType() { return this.settings['type'] || this.determineType(); } prepareSortDirection() { return this.settings['sort'] === 'desc' ? 'desc' : 'asc'; } determineType() { // TODO: determine type by data return 'text'; } } class DataSet { constructor(data = [], columnSettings) { this.columnSettings = columnSettings; this.data = []; this.columns = []; this.rows = []; this.createColumns(columnSettings); this.setData(data); this.createNewRow(); } setData(data) { this.data = data; this.createRows(); } getColumns() { return this.columns; } getRows() { return this.rows; } getFirstRow() { return this.rows[0]; } getLastRow() { return this.rows[this.rows.length - 1]; } findRowByData(data) { return this.rows.find((row) => row.getData() === data); } deselectAll() { this.rows.forEach((row) => { row.isSelected = false; }); // we need to clear selectedRow field because no one row selected this.selectedRow = undefined; } selectRow(row) { const previousIsSelected = row.isSelected; this.deselectAll(); row.isSelected = !previousIsSelected; this.selectedRow = row; return this.selectedRow; } multipleSelectRow(row) { row.isSelected = !row.isSelected; this.selectedRow = row; return this.selectedRow; } selectPreviousRow() { if (this.rows.length > 0) { let index = this.selectedRow ? this.selectedRow.index : 0; if (index > this.rows.length - 1) { index = this.rows.length - 1; } this.selectRow(this.rows[index]); return this.selectedRow; } } selectFirstRow() { if (this.rows.length > 0) { this.selectRow(this.rows[0]); return this.selectedRow; } } selectLastRow() { if (this.rows.length > 0) { this.selectRow(this.rows[this.rows.length - 1]); return this.selectedRow; } } selectRowByIndex(index) { const rowsLength = this.rows.length; if (rowsLength === 0) { return; } if (!index) { this.selectFirstRow(); return this.selectedRow; } if (index > 0 && index < rowsLength) { this.selectRow(this.rows[index]); return this.selectedRow; } // we need to deselect all rows if we got an incorrect index this.deselectAll(); } willSelectFirstRow() { this.willSelect = 'first'; } willSelectLastRow() { this.willSelect = 'last'; } select(selectedRowIndex) { if (this.getRows().length === 0) { return; } if (this.willSelect) { if (this.willSelect === 'first') { this.selectFirstRow(); } if (this.willSelect === 'last') { this.selectLastRow(); } this.willSelect = ''; } else { this.selectRowByIndex(selectedRowIndex); } return this.selectedRow; } createNewRow() { this.newRow = new Row(-1, {}, this); this.newRow.isInEditing = true; } /** * Create columns by mapping from the settings * @param settings * @private */ createColumns(settings) { for (const id in settings) { if (settings.hasOwnProperty(id)) { this.columns.push(new Column(id, settings[id], this)); } } } /** * Create rows based on current data prepared in data source * @private */ createRows() { this.rows = []; this.data.forEach((el, index) => { this.rows.push(new Row(index, el, this)); }); } } class Grid { constructor(source, settings) { this.createFormShown = false; this.onSelectRowSource = new Subject(); this.onDeselectRowSource = new Subject(); this.setSettings(settings); this.setSource(source); } detach() { if (this.sourceOnChangedSubscription) { this.sourceOnChangedSubscription.unsubscribe(); } if (this.sourceOnUpdatedSubscription) { this.sourceOnUpdatedSubscription.unsubscribe(); } } showActionColumn(position) { return this.isCurrentActionsPosition(position) && this.isActionsVisible(); } isCurrentActionsPosition(position) { return position == this.getSetting('actions.position'); } isActionsVisible() { return this.getSetting('actions.add') || this.getSetting('actions.edit') || this.getSetting('actions.delete') || this.getSetting('actions.custom').length; } isMultiSelectVisible() { return this.getSetting('selectMode') === 'multi'; } getNewRow() { return this.dataSet.newRow; } setSettings(settings) { this.settings = settings; this.dataSet = new DataSet([], this.getSetting('columns')); if (this.source) { this.source.refresh(); } } getDataSet() { return this.dataSet; } setSource(source) { this.source = this.prepareSource(source); this.detach(); this.sourceOnChangedSubscription = this.source.onChanged().subscribe((changes) => this.processDataChange(changes)); this.sourceOnUpdatedSubscription = this.source.onUpdated().subscribe((data) => { const changedRow = this.dataSet.findRowByData(data); changedRow.setData(data); }); } getSetting(name, defaultValue) { return getDeepFromObject(this.settings, name, defaultValue); } getColumns() { return this.dataSet.getColumns(); } getRows() { return this.dataSet.getRows(); } selectRow(row) { this.dataSet.selectRow(row); } multipleSelectRow(row) { this.dataSet.multipleSelectRow(row); } onSelectRow() { return this.onSelectRowSource.asObservable(); } onDeselectRow() { return this.onDeselectRowSource.asObservable(); } edit(row) { row.isInEditing = true; } create(row, confirmEmitter) { const deferred = new Deferred(); deferred.promise.then((newData) => { newData = newData ? newData : row.getNewData(); if (deferred.resolve.skipAdd) { this.createFormShown = false; } else { this.source.prepend(newData).then(() => { this.createFormShown = false; this.dataSet.createNewRow(); }); } }).catch((err) => { // doing nothing }); if (this.getSetting('add.confirmCreate')) { confirmEmitter.emit({ newData: row.getNewData(), source: this.source, confirm: deferred, }); } else { deferred.resolve(); } } save(row, confirmEmitter) { const deferred = new Deferred(); deferred.promise.then((newData) => { newData = newData ? newData : row.getNewData(); if (deferred.resolve.skipEdit) { row.isInEditing = false; } else { this.source.update(row.getData(), newData).then(() => { row.isInEditing = false; }); } }).catch((err) => { // doing nothing }); if (this.getSetting('edit.confirmSave')) { confirmEmitter.emit({ data: row.getData(), newData: row.getNewData(), source: this.source, confirm: deferred, }); } else { deferred.resolve(); } } delete(row, confirmEmitter) { const deferred = new Deferred(); deferred.promise.then(() => { this.source.remove(row.getData()); }).catch((err) => { // doing nothing }); if (this.getSetting('delete.confirmDelete')) { confirmEmitter.emit({ data: row.getData(), source: this.source, confirm: deferred, }); } else { deferred.resolve(); } } processDataChange(changes) { if (this.shouldProcessChange(changes)) { this.dataSet.setData(changes['elements']); if (this.getSetting('selectMode') !== 'multi') { const row = this.determineRowToSelect(changes); if (row) { this.onSelectRowSource.next(row); } else { this.onDeselectRowSource.next(null); } } } } shouldProcessChange(changes) { if (['filter', 'sort', 'page', 'remove', 'refresh', 'load', 'paging'].indexOf(changes['action']) !== -1) { return true; } else if (['prepend', 'append'].indexOf(changes['action']) !== -1 && !this.getSetting('pager.display')) { return true; } return false; } /** * @breaking-change 1.8.0 * Need to add `| null` in return type * * TODO: move to selectable? Separate directive */ determineRowToSelect(changes) { if (['load', 'page', 'filter', 'sort', 'refresh'].indexOf(changes['action']) !== -1) { return this.dataSet.select(this.getRowIndexToSelect()); } if (this.shouldSkipSelection()) { return null; } if (changes['action'] === 'remove') { if (changes['elements'].length === 0) { // we have to store which one to select as the data will be reloaded this.dataSet.willSelectLastRow(); } else { return this.dataSet.selectPreviousRow(); } } if (changes['action'] === 'append') { // we have to store which one to select as the data will be reloaded this.dataSet.willSelectLastRow(); } if (changes['action'] === 'add') { return this.dataSet.selectFirstRow(); } if (changes['action'] === 'update') { return this.dataSet.selectFirstRow(); } if (changes['action'] === 'prepend') { // we have to store which one to select as the data will be reloaded this.dataSet.willSelectFirstRow(); } return null; } prepareSource(source) { const initialSource = this.getInitialSort(); if (initialSource && initialSource['field'] && initialSource['direction']) { source.setSort([initialSource], false); } if (this.getSetting('pager.display') === true) { source.setPaging(this.getPageToSelect(source), this.getSetting('pager.perPage'), false); } source.refresh(); return source; } getInitialSort() { const sortConf = {}; this.getColumns().forEach((column) => { if (column.isSortable && column.defaultSortDirection) { sortConf['field'] = column.id; sortConf['direction'] = column.defaultSortDirection; sortConf['compare'] = column.getCompareFunction(); } }); return sortConf; } getSelectedRows() { return this.dataSet.getRows() .filter(r => r.isSelected); } selectAllRows(status) { this.dataSet.getRows() .forEach(r => r.isSelected = status); } getFirstRow() { return this.dataSet.getFirstRow(); } getLastRow() { return this.dataSet.getLastRow(); } getSelectionInfo() { const switchPageToSelectedRowPage = this.getSetting('switchPageToSelectedRowPage'); const selectedRowIndex = Number(this.getSetting('selectedRowIndex', 0)) || 0; const { perPage, page } = this.getSetting('pager'); return { perPage, page, selectedRowIndex, switchPageToSelectedRowPage }; } getRowIndexToSelect() { const { switchPageToSelectedRowPage, selectedRowIndex, perPage } = this.getSelectionInfo(); const dataAmount = this.source.count(); /** * source - contains all table data * dataSet - contains data for current page * selectedRowIndex - contains index for data in all data * * because of that, we need to count index for a specific row in page * if * `switchPageToSelectedRowPage` - we need to change page automatically * `selectedRowIndex < dataAmount && selectedRowIndex >= 0` - index points to existing data * (if index points to non-existing data and we calculate index for current page - we will get wrong selected row. * if we return index witch not points to existing data - no line will be highlighted) */ return (switchPageToSelectedRowPage && selectedRowIndex < dataAmount && selectedRowIndex >= 0) ? selectedRowIndex % perPage : selectedRowIndex; } getPageToSelect(source) { const { switchPageToSelectedRowPage, selectedRowIndex, perPage, page } = this.getSelectionInfo(); let pageToSelect = Math.max(1, page); if (switchPageToSelectedRowPage && selectedRowIndex >= 0) { pageToSelect = getPageForRowIndex(selectedRowIndex, perPage); } const maxPageAmount = Math.ceil(source.count() / perPage); return maxPageAmount ? Math.min(pageToSelect, maxPageAmount) : pageToSelect; } shouldSkipSelection() { /** * For backward compatibility when using `selectedRowIndex` with non-number values - ignored. * * Therefore, in order to select a row after some changes, * the `selectedRowIndex` value must be invalid or >= 0 (< 0 means that no row is selected). * * `Number(value)` returns `NaN` on all invalid cases, and comparisons with `NaN` always return `false`. * * !!! We should skip a row only in cases when `selectedRowIndex` < 0 * because when < 0 all lines must be deselected */ const selectedRowIndex = Number(this.getSetting('selectedRowIndex')); return selectedRowIndex < 0; } } class CellComponent { constructor() { this.inputClass = ''; this.mode = 'inline'; this.isInEditing = false; this.edited = new EventEmitter(); } onEdited(event) { if (this.isNew) { this.grid.create(this.grid.getNewRow(), this.createConfirm); } else { this.grid.save(this.row, this.editConfirm); } } } CellComponent.decorators = [ { type: Component, args: [{ selector: 'ng2-smart-table-cell', template: ` <table-cell-view-mode *ngIf="!isInEditing" [cell]="cell"></table-cell-view-mode> <table-cell-edit-mode *ngIf="isInEditing" [cell]="cell" [inputClass]="inputClass" (edited)="onEdited($event)"> </table-cell-edit-mode> ` },] } ]; CellComponent.propDecorators = { grid: [{ type: Input }], row: [{ type: Input }], editConfirm: [{ type: Input }], createConfirm: [{ type: Input }], isNew: [{ type: Input }], cell: [{ type: Input }], inputClass: [{ type: Input }], mode: [{ type: Input }], isInEditing: [{ type: Input }], edited: [{ type: Output }] }; class EditCellDefault { constructor() { this.inputClass = ''; this.edited = new EventEmitter(); } onEdited(event) { this.edited.next(event); return false; } onStopEditing() { this.cell.getRow().isInEditing = false; return false; } onClick(event) { event.stopPropagation(); } } EditCellDefault.decorators = [ { type: Component, args: [{ template: '' },] } ]; EditCellDefault.propDecorators = { cell: [{ type: Input }], inputClass: [{ type: Input }], edited: [{ type: Output }] }; class CustomEditComponent extends EditCellDefault { constructor(resolver) { super(); this.resolver = resolver; } ngOnChanges(changes) { if (this.cell && !this.customComponent) { const componentFactory = this.resolver.resolveComponentFactory(this.cell.getColumn().editor.component); this.customComponent = this.dynamicTarget.createComponent(componentFactory); // set @Inputs and @Outputs of custom component this.customComponent.instance.cell = this.cell; this.customComponent.instance.inputClass = this.inputClass; this.customComponent.instance.onStopEditing.subscribe(() => this.onStopEditing()); this.customComponent.instance.onEdited.subscribe((event) => this.onEdited(event)); this.customComponent.instance.onClick.subscribe((event) => this.onClick(event)); } } ngOnDestroy() { if (this.customComponent) { this.customComponent.destroy(); } } } CustomEditComponent.decorators = [ { type: Component, args: [{ selector: 'table-cell-custom-editor', template: ` <ng-template #dynamicTarget></ng-template> ` },] } ]; CustomEditComponent.ctorParameters = () => [ { type: ComponentFactoryResolver } ]; CustomEditComponent.propDecorators = { dynamicTarget: [{ type: ViewChild, args: ['dynamicTarget', { read: ViewContainerRef, static: true },] }] }; class DefaultEditComponent extends EditCellDefault { constructor() { super(); } getEditorType() { return this.cell.getColumn().editor && this.cell.getColumn().editor.type; } } DefaultEditComponent.decorators = [ { type: Component, args: [{ selector: 'table-cell-default-editor', template: "<div [ngSwitch]=\"getEditorType()\">\n <select-editor *ngSwitchCase=\"'list'\"\n [cell]=\"cell\"\n [inputClass]=\"inputClass\"\n (onClick)=\"onClick($event)\"\n (onEdited)=\"onEdited($event)\"\n (onStopEditing)=\"onStopEditing()\">\n </select-editor>\n\n <textarea-editor *ngSwitchCase=\"'textarea'\"\n [cell]=\"cell\"\n [inputClass]=\"inputClass\"\n (onClick)=\"onClick($event)\"\n (onEdited)=\"onEdited($event)\"\n (onStopEditing)=\"onStopEditing()\">\n </textarea-editor>\n\n <checkbox-editor *ngSwitchCase=\"'checkbox'\"\n [cell]=\"cell\"\n [inputClass]=\"inputClass\"\n (onClick)=\"onClick($event)\">\n </checkbox-editor>\n\n <completer-editor *ngSwitchCase=\"'completer'\"\n [cell]=\"cell\">\n </completer-editor>\n\n <input-editor *ngSwitchDefault\n [cell]=\"cell\"\n [inputClass]=\"inputClass\"\n (onClick)=\"onClick($event)\"\n (onEdited)=\"onEdited($event)\"\n (onStopEditing)=\"onStopEditing()\">\n </input-editor>\n</div>" },] } ]; DefaultEditComponent.ctorParameters = () => []; class EditCellComponent { constructor() { this.inputClass = ''; this.edited = new EventEmitter(); } onEdited(event) { this.edited.next(event); return false; } getEditorType() { return this.cell.getColumn().editor && this.cell.getColumn().editor.type; } } EditCellComponent.decorators = [ { type: Component, args: [{ selector: 'table-cell-edit-mode', template: ` <div [ngSwitch]="getEditorType()"> <table-cell-custom-editor *ngSwitchCase="'custom'" [cell]="cell" [inputClass]="inputClass" (edited)="onEdited($event)"> </table-cell-custom-editor> <table-cell-default-editor *ngSwitchDefault [cell]="cell" [inputClass]="inputClass" (edited)="onEdited($event)"> </table-cell-default-editor> </div> ` },] } ]; EditCellComponent.propDecorators = { cell: [{ type: Input }], inputClass: [{ type: Input }], edited: [{ type: Output }] }; class DefaultEditor { constructor() { this.onStopEditing = new EventEmitter(); this.onEdited = new EventEmitter(); this.onClick = new EventEmitter(); } } DefaultEditor.decorators = [ { type: Component, args: [{ template: '' },] } ]; DefaultEditor.propDecorators = { cell: [{ type: Input }], inputClass: [{ type: Input }], onStopEditing: [{ type: Output }], onEdited: [{ type: Output }], onClick: [{ type: Output }] }; class CheckboxEditorComponent extends DefaultEditor { constructor() { super(); } onChange(event) { const trueVal = (this.cell.getColumn().getConfig() && this.cell.getColumn().getConfig().true) || true; const falseVal = (this.cell.getColumn().getConfig() && this.cell.getColumn().getConfig().false) || false; this.cell.newValue = event.target.checked ? trueVal : falseVal; } } CheckboxEditorComponent.decorators = [ { type: Component, args: [{ selector: 'checkbox-editor', template: ` <input [ngClass]="inputClass" type="checkbox" class="form-control" [name]="cell.getId()" [disabled]="!cell.isEditable()" [checked]="cell.getValue() == (cell.getColumn().getConfig()?.true || true)" (click)="onClick.emit($event)" (change)="onChange($event)"> `, styles: [":host input,:host textarea{line-height:normal;padding:.375em .75em;width:100%}"] },] } ]; CheckboxEditorComponent.ctorParameters = () => []; class CompleterEditorComponent extends DefaultEditor { constructor(completerService) { super(); this.completerService = completerService; this.completerStr = ''; } ngOnInit() { if (this.cell.getColumn().editor && this.cell.getColumn().editor.type === 'completer') { const config = this.cell.getColumn().getConfig().completer; config.dataService = this.completerService.local(config.data, config.searchFields, config.titleField); config.dataService.descriptionField(config.descriptionField); } } onEditedCompleter(event) { this.cell.newValue = event.title; return false; } } CompleterEditorComponent.decorators = [ { type: Component, args: [{ selector: 'completer-editor', template: ` <ng2-completer [(ngModel)]="completerStr" [dataService]="cell.getColumn().getConfig().completer.dataService" [minSearchLength]="cell.getColumn().getConfig().completer.minSearchLength || 0" [pause]="cell.getColumn().getConfig().completer.pause || 0" [placeholder]="cell.getColumn().getConfig().completer.placeholder || 'Start typing...'" (selected)="onEditedCompleter($event)"> </ng2-completer> ` },] } ]; CompleterEditorComponent.ctorParameters = () => [ { type: CompleterService } ]; class InputEditorComponent extends DefaultEditor { constructor() { super(); } } InputEditorComponent.decorators = [ { type: Component, args: [{ selector: 'input-editor', template: ` <input [ngClass]="inputClass" class="form-control" [(ngModel)]="cell.newValue" [name]="cell.getId()" [placeholder]="cell.getTitle()" [disabled]="!cell.isEditable()" (click)="onClick.emit($event)" (keydown.enter)="onEdited.emit($event)" (keydown.esc)="onStopEditing.emit()"> `, styles: [":host input,:host textarea{line-height:normal;padding:.375em .75em;width:100%}"] },] } ]; InputEditorComponent.ctorParameters = () => []; class SelectEditorComponent extends DefaultEditor { constructor() { super(); } } SelectEditorComponent.decorators = [ { type: Component, args: [{ selector: 'select-editor', template: ` <select [ngClass]="inputClass" class="form-control" [(ngModel)]="cell.newValue" [name]="cell.getId()" [disabled]="!cell.isEditable()" (click)="onClick.emit($event)" (keydown.enter)="onEdited.emit($event)" (keydown.esc)="onStopEditing.emit()"> <option *ngFor="let option of cell.getColumn().getConfig()?.list" [value]="option.value" [selected]="option.value === cell.getValue()">{{ option.title }} </option> </select> ` },] } ]; SelectEditorComponent.ctorParameters = () => []; class TextareaEditorComponent extends DefaultEditor { constructor() { super(); } } TextareaEditorComponent.decorators = [ { type: Component, args: [{ selector: 'textarea-editor', template: ` <textarea [ngClass]="inputClass" class="form-control" [(ngModel)]="cell.newValue" [name]="cell.getId()" [disabled]="!cell.isEditable()" [placeholder]="cell.getTitle()" (click)="onClick.emit($event)" (keydown.enter)="onEdited.emit($event)" (keydown.esc)="onStopEditing.emit()"> </textarea> `, styles: [":host input,:host textarea{line-height:normal;padding:.375em .75em;width:100%}"] },] } ]; TextareaEditorComponent.ctorParameters = () => []; class CustomViewComponent { constructor(resolver) { this.resolver = resolver; } ngOnInit() { if (this.cell && !this.customComponent) { this.createCustomComponent(); this.callOnComponentInit(); this.patchInstance(); } } ngOnDestroy() { if (this.customComponent) { this.customComponent.destroy(); } } createCustomComponent() { const componentFactory = this.resolver.resolveComponentFactory(this.cell.getColumn().renderComponent); this.customComponent = this.dynamicTarget.createComponent(componentFactory); } callOnComponentInit() { const onComponentInitFunction = this.cell.getColumn().getOnComponentInitFunction(); onComponentInitFunction && onComponentInitFunction(this.customComponent.instance); } patchInstance() { Object.assign(this.customComponent.instance, this.getPatch()); } getPatch() { return { value: this.cell.getValue(), rowData: this.cell.getRow().getData() }; } } CustomViewComponent.decorators = [ { type: Component, args: [{ selector: 'custom-view-component', template: ` <ng-template #dynamicTarget></ng-template> ` },] } ]; CustomViewComponent.ctorParameters = () => [ { type: ComponentFactoryResolver } ]; CustomViewComponent.propDecorators = { cell: [{ type: Input }], dynamicTarget: [{ type: ViewChild, args: ['dynamicTarget', { read: ViewContainerRef, static: true },] }] }; class ViewCellComponent { } ViewCellComponent.decorators = [ { type: Component, args: [{ selector: 'table-cell-view-mode', changeDetection: ChangeDetectionStrategy.OnPush, template: ` <div [ngSwitch]="cell.getColumn().type"> <custom-view-component *ngSwitchCase="'custom'" [cell]="cell"></custom-view-component> <div *ngSwitchCase="'html'" [innerHTML]="cell.getValue()"></div> <div *ngSwitchDefault>{{ cell.getValue() }}</div> </div> ` },] } ]; ViewCellComponent.propDecorators = { cell: [{ type: Input }] }; const CELL_COMPONENTS = [ CellComponent, EditCellDefault, DefaultEditor, CustomEditComponent, DefaultEditComponent, EditCellComponent, CheckboxEditorComponent, CompleterEditorComponent, InputEditorComponent, SelectEditorComponent, TextareaEditorComponent, CustomViewComponent, ViewCellComponent, ]; class CellModule { } CellModule.decorators = [ { type: NgModule, args: [{ imports: [ CommonModule, FormsModule, Ng2CompleterModule, ], declarations: [ ...CELL_COMPONENTS, ], exports: [ ...CELL_COMPONENTS, ], },] } ]; class DataSource { constructor() { this.onChangedSource = new Subject(); this.onAddedSource = new Subject(); this.onUpdatedSource = new Subject(); this.onRemovedSource = new Subject(); } refresh() { this.emitOnChanged('refresh'); } load(data) { this.emitOnChanged('load'); return Promise.resolve(); } onChanged() { return this.onChangedSource.asObservable(); } onAdded() { return this.onAddedSource.asObservable(); } onUpdated() { return this.onUpdatedSource.asObservable(); } onRemoved() { return this.onRemovedSource.asObservable(); } prepend(element) { this.emitOnAdded(element); this.emitOnChanged('prepend'); return Promise.resolve(); } append(element) { this.emitOnAdded(element); this.emitOnChanged('append'); return Promise.resolve(); } add(element) { this.emitOnAdded(element); this.emitOnChanged('add'); return Promise.resolve(); } remove(element) { this.emitOnRemoved(element); this.emitOnChanged('remove'); return Promise.resolve(); } update(element, values) { this.emitOnUpdated(element); this.emitOnChanged('update'); return Promise.resolve(); } empty() { this.emitOnChanged('empty'); return Promise.resolve(); } setSort(conf, doEmit) { if (doEmit) { this.emitOnChanged('sort'); } } setFilter(conf, andOperator, doEmit) { if (doEmit) { this.emitOnChanged('filter'); } } addFilter(fieldConf, andOperator, doEmit) { if (doEmit) { this.emitOnChanged('filter'); } } setPaging(page, perPage, doEmit) { if (doEmit) { this.emitOnChanged('paging'); } } setPage(page, doEmit) { if (doEmit) { this.emitOnChanged('page'); } } emitOnRemoved(element) { this.onRemovedSource.next(element); } emitOnUpdated(element) { this.onUpdatedSource.next(element); } emitOnAdded(element) { this.onAddedSource.next(element); } emitOnChanged(action) { this.getElements().then((elements) => this.onChangedSource.next({ action: action, elements: elements, paging: this.getPaging(), filter: this.getFilter(), sort: this.getSort(), })); } } class FilterDefault { constructor() { this.inputClass = ''; this.filter = new EventEmitter(); this.query = ''; } onFilter(query) { this.source.addFilter({ field: this.column.id, search: query, filter: this.column.getFilterFunction(), }); } } FilterDefault.decorators = [ { type: Component, args: [{ template: '' },] } ]; FilterDefault.propDecorators = { column: [{ type: Input }], source: [{ type: Input }], inputClass: [{ type: Input }], filter: [{ type: Output }] }; class FilterComponent extends FilterDefault { constructor() { super(...arguments); this.query = ''; } ngOnChanges(changes) { if (changes.source) { if (!changes.source.firstChange) { this.dataChangedSub.unsubscribe(); } this.dataChangedSub = this.source.onChanged().subscribe((dataChanges) => { const filterConf = this.source.getFilter(); if (filterConf && filterConf.filters && filterConf.filters.length === 0) { this.query = ''; // add a check for existing filters an set the query if one exists for this column // this covers instances where the filter is set by user code while maintaining existing functionality } else if (filterConf && filterConf.filters && filterConf.filters.length > 0) { filterConf.filters.forEach((k, v) => { if (k.field == this.column.id) { this.query = k.search; } }); } }); } } } FilterComponent.decorators = [ { type: Component, args: [{ selector: 'ng2-smart-table-filter', template: ` <div class="ng2-smart-filter" *ngIf="column.isFilterable" [ngSwitch]="column.getFilterType()"> <custom-table-filter *ngSwitchCase="'custom'" [query]="query" [column]="column" [source]="source" [inputClass]="inputClass" (filter)="onFilter($event)"> </custom-table-filter> <default-table-filter *ngSwitchDefault [query]="query" [column]="column" [source]="source" [inputClass]="inputClass" (filter)="onFilter($event)"> </default-table-filter> </div> `, styles: [":host .ng2-smart-filter ::ng-deep input,:host .ng2-smart-filter ::ng-deep select{font-weight:400;line-height:normal;padding:.375em .75em;width:100%}:host .ng2-smart-filter ::ng-deep input[type=search]{box-sizing:inherit}:host .ng2-smart-filter ::ng-deep .completer-dropdown-holder,:host .ng2-smart-filter ::ng-deep a{font-weight:400}"] },] } ]; class DefaultFilterComponent extends FilterDefault { } DefaultFilterComponent.decorators = [ { type: Component, args: [{ selector: 'default-table-filter', template: ` <ng-container [ngSwitch]="column.getFilterType()"> <select-filter *ngSwitchCase="'list'" [query]="query" [ngClass]="inputClass" [column]="column" (filter)="onFilter($event)"> </select-filter> <checkbox-filter *ngSwitchCase="'checkbox'" [query]="query" [ngClass]="inputClass" [column]="column" (filter)="onFilter($event)"> </checkbox-filter> <completer-filter *ngSwitchCase="'completer'" [query]="query" [ngClass]="inputClass" [column]="column" (filter)="onFilter($event)"> </completer-filter> <input-filter *ngSwitchDefault [query]="query" [ngClass]="inputClass" [column]="column" (filter)="onFilter($event)"> </input-filter> </ng-container> ` },] } ]; DefaultFilterComponent.propDecorators = { query: [{ type: Input }] }; class CustomFilterComponent extends FilterDefault { constructor(resolver) { super(); this.resolver = resolver; } ngOnChanges(changes) { if (this.column && !this.customComponent) { const componentFactory = this.resolver.resolveComponentFactory(this.column.filter.component); this.customComponent = this.dynamicTarget.createComponent(componentFactory); // set @Inputs and @Outputs of custom component this.customComponent.instance.query = this.query; this.customComponent.instance.column = this.column; this.customComponent.instance.source = this.source; this.customComponent.instance.inputClass = this.inputClass; this.customComponent.instance.filter.subscribe((event) => this.onFilter(event)); } if (this.customComponent) { this.customComponent.instance.ngOnChanges(changes); } } ngOnDestroy() { if (this.customComponent) { this.customComponent.destroy(); } } } CustomFilterComponent.decorators = [ { type: Component, args: [{ selector: 'custom-table-filter', template: `<ng-template #dynamicTarget></ng-template>` },] } ]; CustomFilterComponent.ctorParameters = () => [ { type: ComponentFactoryResolver } ]; CustomFilterComponent.propDecorators = { query: [{ type: Input }], dynamicTarget: [{ type: ViewChild, args: ['dynamicTarget', { read: ViewContainerRef, static: true },] }] }; class DefaultFilter { constructor() { this.delay = 300; this.filter = new EventEmitter(); } ngOnDestroy() { if (this.changesSubscription) { this.changesSubscription.unsubscribe(); } } setFilter() { this.filter.emit(this.query); } } DefaultFilter.decorators = [ { type: Component, args: [{ template: '' },] } ]; DefaultFilter.propDecorators = { query: [{ type: Input }], inputClass: [{ type: Input }], column: [{ type: Input }], filter: [{ type: Output }] }; class CheckboxFilterComponent extends DefaultFilter { constructor() { super(); this.filterActive = false; this.inputControl = new FormControl(); } ngOnInit() { this.changesSubscription = this.inputControl.valueChanges .pipe(debounceTime(this.delay)) .subscribe((checked) => { this.filterActive = true; const trueVal = (this.column.getFilterConfig() && this.column.getFilterConfig().true) || true; const falseVal = (this.column.getFilterConfig() && this.column.getFilterConfig().false) || false; this.query = checked ? trueVal : falseVal; this.setFilter(); }); } resetFilter(event) { event.preventDefault(); this.query = ''; this.inputControl.setValue(false, { emitEvent: false }); this.filterActive = false; this.setFilter(); } } CheckboxFilterComponent.decorators = [ { type: Component, args: [{ selector: 'checkbox-filter', template: ` <input type="checkbox" [formControl]="inputControl" [ngClass]="inputClass" class="form-control"> <a href="#" *ngIf="filterActive" (click)="resetFilter($event)">{{column.getFilterConfig()?.resetText || 'reset'}}</a> ` },] } ]; CheckboxFilterComponent.ctorParameters = () => []; class CompleterFilterComponent extends DefaultFilter { constructor(completerService) { super(); this.completerService = completerService; this.completerContent = new Subject(); } ngOnInit() { const config = this.column.getFilterConfig().completer; config.dataService = this.completerService.local(config.data, config.searchFields, config.titleField); config.dataService.descriptionField(config.descriptionField); this.changesSubscription = this.completerContent .pipe(map((ev) => (ev && ev.title) || ev || ''), distinctUntilChanged(), debounceTime(this.delay)) .subscribe((search) => { this.query = search; this.setFilter(); }); } inputTextChanged(event) { // workaround to trigger the search event when the home/end buttons are clicked // when this happens the [(ngModel)]="query" is set to "" but the (selected) method is not called // so here it gets called manually if (event === '') { this.completerContent.next(event); } } } CompleterFilterComponent.decorators = [ { type: Component, args: [{ selector: 'completer-filter', template: ` <ng2-completer [(ngModel)]="query" (ngModelChange)="inputTextChanged($event)" [dataService]="column.getFilterConfig().completer.dataService" [minSearchLength]="column.getFilterConfig().completer.minSearchLength || 0" [pause]="column.getFilterConfig().completer.pause || 0" [placeholder]="column.get