DialogModule,
SelectModule,
TagModule,
- LayerModule
+ LayerModule,
+ InputModule,
+ GridModule
} from 'carbon-components-angular';
import AddIcon from '@carbon/icons/es/add/16';
import FilterIcon from '@carbon/icons/es/filter/16';
import MaximizeIcon from '@carbon/icons/es/maximize/16';
import ArrowDown from '@carbon/icons/es/caret--down/16';
import ChevronDwon from '@carbon/icons/es/chevron--down/16';
+import CheckMarkIcon from '@carbon/icons/es/checkmark/32';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { FormlyModule } from '@ngx-formly/core';
ThemeModule,
SelectModule,
TagModule,
- LayerModule
+ LayerModule,
+ InputModule,
+ GridModule
],
declarations: [
TableComponent,
CloseIcon,
MaximizeIcon,
ArrowDown,
- ChevronDwon
+ ChevronDwon,
+ CheckMarkIcon
]);
}
}
[text]="value">
</cd-copy-2-clipboard-button>
</ng-template>
+
+<ng-template #editingTpl
+ let-value="data.value"
+ let-row="data.row"
+ let-column="data.column">
+ @if (isCellEditing(row?.id, column?.prop)) {
+ <form [formGroup]="formGroup"
+ #formDir="ngForm">
+ <div cdsRow>
+ <div cdsCol>
+ <cds-text-label [invalid]="formGroup.controls[row?.id + '-' + column?.prop]?.invalid && formGroup.controls[row?.id + '-' + column?.prop]?.dirty"
+ [invalidText]="errorTpl">
+ <input type="text"
+ cdsText
+ size="sm"
+ [id]="row?.id + '-' + column?.prop"
+ [formControlName]="row?.id + '-' + column?.prop"
+ (input)="valueChange(row?.id, column?.prop, $event.target.value)"
+ [invalid]="formGroup.controls[row?.id + '-' + column?.prop]?.invalid && formGroup.controls[row?.id + '-' + column?.prop]?.dirty">
+ </cds-text-label>
+ <ng-template #errorTpl>
+ <span *ngIf="formGroup?.showError(row?.id + '-' + column?.prop, formDir, 'required')">
+ <ng-container i18n>This field is required.</ng-container>
+ </span>
+ <span *ngIf="column?.customTemplateConfig?.formGroup?.showError(row?.id + '-' + column?.prop, formDir, 'pattern')">
+ <ng-container i18n>The field format is invalid.</ng-container>
+ </span>
+ </ng-template>
+ </div>
+ <div cdsCol
+ [columnNumbers]="{sm:1}"
+ class="cds-p-0 cds-pt-3">
+ <button cdsButton="ghost"
+ size="sm"
+ id="cell-inline-save-btn"
+ type="button"
+ (click)="saveCellItem(row?.id, column?.prop)">
+ <cd-icon type="check"></cd-icon>
+ </button>
+ </div>
+ </div>
+ </form>
+ } @else {
+ <div cdsRow>
+ <div cdsCol
+ class="cds-pt-3">
+ {{ value }}
+ </div>
+ <div cdsCol
+ [columnNumbers]="{lg: 5}"
+ class="edit-btn">
+ <button cdsButton="ghost"
+ size="sm"
+ id="cell-inline-edit-btn"
+ (click)="editCellItem(row?.id, column, value)">
+ <cd-icon type="edit"></cd-icon>
+ </button>
+ </div>
+ </div>
+ }
+</ng-template>
.scrollable-expanded-row::-webkit-scrollbar-track {
background: transparent;
}
+
+tr .edit-btn {
+ opacity: 0;
+}
+
+tr:hover .edit-btn {
+ opacity: 1;
+}
import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { FormsModule } from '@angular/forms';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterTestingModule } from '@angular/router/testing';
imports: [
BrowserAnimationsModule,
FormsModule,
+ ReactiveFormsModule,
ComponentsModule,
RouterTestingModule,
NgbDropdownModule,
expect(executingElement.nativeElement.textContent.trim()).toBe(`(${state})`);
};
+ const testEditingTemplate = (editing = false) => {
+ component.autoReload = -1;
+
+ const data = createFakeData(10);
+ // add id to every row so that we can use it to save the state.
+ component.data = data.map((item, i) => ({
+ ...item,
+ id: `id-${i}`
+ }));
+ component.localColumns = component.columns = [
+ {
+ prop: 'a',
+ name: 'Name',
+ cellTransformation: CellTemplate.editing,
+ customTemplateConfig: {
+ validators: []
+ }
+ }
+ ];
+
+ // trigger an editing by setting the edit state.
+ if (editing) {
+ component.editCellItem('id-0', component.localColumns[0], '0');
+ }
+
+ component.ngOnInit();
+ component.ngAfterViewInit();
+ fixture.detectChanges();
+
+ if (editing) {
+ const inputElement = fixture.debugElement
+ .query(By.css('[cdstablerow] [cdstabledata]'))
+ .query(By.css('input'));
+ expect(inputElement).not.toBeNull();
+
+ const saveButton = fixture.debugElement
+ .query(By.css('[cdstablerow] [cdstabledata]'))
+ .query(By.css('#cell-inline-save-btn'));
+ expect(saveButton).not.toBeNull();
+
+ const svgElement = saveButton.nativeElement.querySelector('svg');
+ expect(svgElement).not.toBeNull();
+ expect(svgElement.classList.contains('check-icon')).toBeTruthy();
+ } else {
+ const editButton = fixture.debugElement
+ .query(By.css('[cdstablerow] [cdstabledata]'))
+ .query(By.css('#cell-inline-edit-btn'));
+ expect(editButton).not.toBeNull();
+
+ const svgElement = editButton.nativeElement.querySelector('svg');
+ expect(svgElement).not.toBeNull();
+ expect(svgElement.classList.contains('edit-icon')).toBeTruthy();
+ }
+ };
+
it('should display executing template', () => {
testExecutingTemplate();
});
it('should display executing template with custom classes', () => {
testExecutingTemplate({ valueClass: 'a b', executingClass: 'c d' });
});
+
+ it('should display an edit icon on the cell', () => {
+ testEditingTemplate();
+ });
+
+ it('should display input element and save button if editing', () => {
+ testEditingTemplate(true);
+ });
});
describe('reload data', () => {
import { filter, map } from 'rxjs/operators';
import { CdSortDirection } from '../../enum/cd-sort-direction';
import { CdSortPropDir } from '../../models/cd-sort-prop-dir';
+import { EditState } from '../../models/cd-table-editing';
+import { CdFormGroup } from '../../forms/cd-form-group';
+import { FormControl } from '@angular/forms';
const TABLE_LIST_LIMIT = 10;
type TPaginationInput = { page: number; size: number; filteredData: any[] };
rowDetailTpl: TemplateRef<any>;
@ViewChild('tableActionTpl', { static: true })
tableActionTpl: TemplateRef<any>;
+ @ViewChild('editingTpl', { static: true })
+ editingTpl: TemplateRef<any>;
@ContentChild(TableDetailDirective) rowDetail!: TableDetailDirective;
@ContentChild(TableActionsComponent) tableActions!: TableActionsComponent;
*/
@Output() columnFiltersChanged = new EventEmitter<CdTableColumnFiltersChange>();
+ @Output()
+ editSubmitAction = new EventEmitter<{ [field: string]: string }>();
+
/**
* Use this variable to access the selected row(s).
*/
}
private previousRows = new Map<string | number, TableItem[]>();
+ editingCells = new Set<string>();
+ editStates: EditState = {};
+ formGroup: CdFormGroup = new CdFormGroup({});
+
constructor(
// private ngZone: NgZone,
private cdRef: ChangeDetectorRef,
this.cellTemplates.path = this.pathTpl;
this.cellTemplates.tooltip = this.tooltipTpl;
this.cellTemplates.copy = this.copyTpl;
+ this.cellTemplates.editing = this.editingTpl;
}
useCustomClass(value: any): string {
this.selectAllCheckboxSomeSelected = false;
}
}
+
+ editCellItem(rowId: string, column: CdTableColumn, value: string) {
+ const key = `${rowId}-${column.prop}`;
+ this.formGroup.addControl(key, new FormControl('', column.customTemplateConfig?.validators));
+ this.editingCells.add(key);
+ if (!this.editStates[rowId]) {
+ this.editStates[rowId] = {};
+ }
+ this.formGroup?.get(key).setValue(value);
+ this.editStates[rowId][column.prop] = value;
+ }
+
+ saveCellItem(rowId: string, colProp: string) {
+ if (this.formGroup?.invalid) {
+ this.formGroup.setErrors({ cdSubmitButton: true });
+ return;
+ }
+ this.editSubmitAction.emit(this.editStates[rowId]);
+ this.editingCells.delete(`${rowId}-${colProp}`);
+ delete this.editStates[rowId][colProp];
+ }
+
+ isCellEditing(rowId: string, colProp: string): boolean {
+ return this.editingCells.has(`${rowId}-${colProp}`);
+ }
+
+ valueChange(rowId: string, colProp: string, value: string) {
+ this.editStates[rowId][colProp] = value;
+ }
}
// ...
// cellTransformation: CellTemplate.copy,
*/
- copy = 'copy'
+ copy = 'copy',
+ /*
+ This template will let you edit the cell value inline. You can pass the validators in the
+ customTemplateConfig.
+ // {
+ // ...
+ // cellTransformation: CellTemplate.editing,
+ // customTemplateConfig: {
+ // validators: [Validators.required]
+ // }
+ // ...
+ // }
+ */
+ editing = 'editing'
}
danger: 'danger',
infoCircle: 'info-circle',
success: 'success',
- warning: 'warning'
+ warning: 'warning',
+ edit: 'edit',
+ check: 'check'
} as const;
--- /dev/null
+export interface EditState {
+ [rowId: string]: {
+ [field: string]: string;
+ };
+}
@import './src/styles/ceph-custom/icons';
@import './src/styles/ceph-custom/navs';
@import './src/styles/ceph-custom/toast';
+@import './src/styles/ceph-custom/spacings';
/* If javascript is disabled. */
.noscript {
@forward 'dropdown';
@forward 'forms';
@forward 'icons';
+@forward 'spacings';
--- /dev/null
+@use '@carbon/layout';
+
+.cds-p-0 {
+ padding: 0;
+}
+
+.cds-pt-3 {
+ padding-top: layout.$spacing-03;
+}