Inline Editing
To activate inline editing, the table first needs to know how to save or delete data. Create a new class extending
. Override the saveData
and deleteData
methods and put your logic there.
class SampleEditingController extends PanemuTableEditingController <DataModel> {
override saveData(data: DataModel[], tableMode: TABLE_MODE ): Observable<DataModel[]> {
// Call your API to save here. Check the value of tableMode beforehand.
// It could be either 'insert' or 'edit'.
// You need to return Observable<DataModel[]>, which could be from the API you call
// or just something like of(data). The returned data will be displayed
// on the table. So server-side-generated values like id can be displayed after save.
}
override deleteData(data: DataModel): Observable<any> {
// Call delete API here. Return any observable so that the table knows when
// the process finishes.
}
}
After that, set the editing controller to the table controller:
this.controller.editingController = new SampleEditingController();
The controller has some methods regarding this feature:
PanemuTableController.edit() PanemuTableController.insert() PanemuTableController.save() PanemuTableController.deleteSelectedRow()
to cancel editing and return toPanemuTableController.reloadCurrentPage() browse
mode.
That's a start to just activate inline-editing. But there is so much more!
The toolbar in the examples is not part of the library. It is up to you to create the necessary UI to support the editing.
The component used as a cell editor depends on the
of the column. It is customizable.
Angular Reactive Form
Angular Reactive Form is the backbone of inline-editing. It supports FormControl, FormGroup, and FormArray. In the example below, the Address column is a combination of Street and Zip Code. So we are going to use FormGroup to handle Address editing.
form: { [f in keyof Required<any>]: () => AbstractControl | undefined } = {
id: () => undefined,
name: () => new FormControl('', { updateOn: 'blur', validators: [Validators.required, Validators.maxLength(15), Validators.minLength(5)] }),
address: () => new FormGroup(
{
street: new FormControl('', { validators: [Validators.maxLength(50)] }),
zipCode: new FormControl('', { validators: [Validators.maxLength(5), Validators.minLength(5)] })
},
{ validators: this.addressValidator() }
)
}
override createFormControl(field: keyof any, rowData: any, tableMode: TABLE_MODE ): AbstractControl | null | undefined {
return this.form[field.toString()]?.();
}
You've noticed that we override the createFormControl
method to specify AbstractControl for each column. We can also specify the validations here. Always return a new AbstractControl every time because each row will have its own set of FormControls. If a cell doesn't have a FormControl, then it is not editable.
That example also shows how to create a custom cell editor, in this case is AddressCellEditor
. The source code is at the bottom of this page.
Listening To Cell Edit
In the example below, the Country and City cells are logically connected. If the user selects a country, the options in City are updated. It is achieved by overriding
. Also this table is on edit mode initially.
Managing Editable Cells
If we want to make a cell permanently not editable, we can return undefined
in createFormControl
for that column. But what if we want to make a cell not editable based on the value of another cell? Here is the example:
The Verified At column is editable if the Registered value is Yes. We put logic in onCommitEdit
and onStartEdit
to make it happen.
Custom Editor
This example displays the amount
field in 3 columns. One with the default editor, the second without an editor, and the third with a custom editor. There is a logic in initCellEditorRenderer
that inspects the __key of the column and acts accordingly. We can't use field
here because all 3 columns have the same field
.
5 | |||
3 | |||
4 |
A custom cell editor must implement
. Then override the
method and use it in renderer.component
.
override initCellEditorRenderer(renderer: CellEditorRenderer <CustomData>, column: PropertyColumn <CustomData>): CellEditorRenderer <CustomData> | null {
if (column.__key == 'amount_1') {
return null;
} else if (column.__key == 'amount_2') {
renderer.component = CustomAmountEditor;
}
return renderer;
}
Disabled sorting, filtering, grouping and pagination
The included pagination and query component listen to tables' mode. If table is not in browse
mode, they are disabled.
Summary
There are 3
s: browse, edit, insert.
INSERT and EDIT mode:
- Cell editors are only displayed when the respective row is selected.
- Developers can specify which columns are editable for each row.
- It is possible to put validators at the cell level using Angular FormControl or at the row level by overriding the
canSave
method. - Cells with invalid values will have a red bottom border.
- Upon successful save, the mode changes to 'browse'.
- Save is rejected if there are invalid values.
- Changed rows have a light-yellow background. Changed cells have a darker-yellow background.
- The editing is powered by Angular Reactive Form. Each edited row has a FormGroup.
- Pagination and Query components are disabled during editing. Sorting functionality too. This is to avoid unsaved data loss.
- Call
to cancel editing and return toPanemuTableController.reloadCurrentPage() browse
mode.
INSERT MODE:
- Call
to enterPanemuTableController.insert() insert
mode and add a new row. - The new row is always put at the top.
- Only newly inserted rows can be selected. So users can't edit persisted data.
- Users can delete newly inserted rows. The persisted rows can't be deleted because they aren't selectable.
- If all new rows are deleted, the mode automatically changes to browse.
- All newly inserted rows are included in the save method.
EDIT MODE:
- Call
to enterPanemuTableController.edit() edit
mode, then users can select a row to edit. - Only rows with changed cells will be included in the save method.
Additional Source Code
Toolbar
import { Component, computed, input, OnInit } from '@angular/core';
import { PanemuTableController } from 'ngx-panemu-table';
@Component({
selector: 'toolbar-component',
templateUrl: 'toolbar.component.html',
standalone: true,
})
export class ToolbarComponent {
controller = input<PanemuTableController <any>>();
allowInsert = input<boolean>(true);
allowDelete = input<boolean>(true);
canDelete = computed(() => this.controller()?.mode() != 'edit' && this.controller()?.selectedRowSignal());
browse = computed(() => this.controller()?.mode() == 'browse')
canInsert = computed(() => this.controller()?.mode() != 'edit')
canSave = computed(() => this.controller()?.mode() != 'browse')
reload() {
this.controller()?.reloadCurrentPage();
}
insert() {
this.controller()?.insert();
}
edit() {
this.controller()?.edit();
}
deleteRow() {
this.controller()?.deleteSelectedRow();
}
export() {
this.controller()?.exportToCsv();
}
save() {
this.controller()?.save();
}
}
Address (FormGroup) Cell Editor
import { Component, computed, effect, WritableSignal } from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { CellEditorComponent , CellValidationError } from 'ngx-panemu-table';
@Component({
selector: 'string-cell-editor',
standalone: true,
imports: [ReactiveFormsModule],
templateUrl: 'address-cell-editor.html',
styleUrl: 'address-cell-editor.scss'
})
export class AddressCellEditor implements CellEditorComponent {
formControl!: FormGroup;
errorMessage!: WritableSignal<string | CellValidationError >;
streetError = computed(() => {
if (this.errorMessage() && typeof this.errorMessage() == 'object' && Object.keys(this.errorMessage()).includes('street')) {
return (this.errorMessage() as CellValidationError )['street']
}
return ''
})
zipError = computed(() => {
if (this.errorMessage() && typeof this.errorMessage() == 'object' && Object.keys(this.errorMessage()).includes('zipCode')) {
return (this.errorMessage() as CellValidationError )['zipCode']
}
return ''
})
formGroupError = computed(() => {
return typeof this.errorMessage() == 'string' ? this.errorMessage() : ''
})
constructor() {
effect(() => console.log(JSON.stringify(this.errorMessage())))
}
}
Custom Amount Editor using Slider
import { Component, WritableSignal } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { MatSliderModule } from '@angular/material/slider';
import { CellEditorComponent , CellValidationError } from 'ngx-panemu-table';
@Component({
standalone: true,
imports: [MatSliderModule, ReactiveFormsModule],
template: `
<mat-slider min="0" max="10" step="1" [title]="errorMessage() || ''">
<input matSliderThumb [formControl]="formControl" [value]="formControl.value">
</mat-slider>
`
})
export class CustomAmountEditor implements CellEditorComponent {
formControl!: FormControl;
parameter?: any;
errorMessage!: WritableSignal<string | CellValidationError | null>;
ngOnInit() { }
}
Message Dialog
import { Component, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { MessageDialogObject } from './message-dialog.model';
import { CommonModule } from '@angular/common';
@Component({
selector: 'cmp-message-dialog',
templateUrl: './message-dialog.component.html',
standalone: true,
imports: [CommonModule, MatDialogModule],
styleUrls: ['./message-dialog.component.scss']
})
export class MessageDialogComponent {
titleClass = '';
titleIcon = '';
constructor(public dialogRef: MatDialogRef<MessageDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: MessageDialogObject) {
if (data.type === 'info') {
this.titleClass = 'mat-dialog-title bg-blue-700 text-white';
this.titleIcon = 'icon-info-circled';
} else if (data.type === 'confirm') {
this.titleClass = 'mat-dialog-title bg-yellow-500 text-yellow-900';
this.titleIcon = 'icon-attention';
} else if (data.type === 'error') {
this.titleClass = 'mat-dialog-title bg-red-700 text-white';
this.titleIcon = 'icon-block';
}
if (data.noLabel || data.cancelLabel) {
this.dialogRef.disableClose = true;
}
}
close(answer: any) {
this.dialogRef.close(answer);
}
}