Usages Inline Editing

Inline Editing

To activate inline editing, the table first needs to know how to save or delete data. Create a new class extending PanemuTableEditingController. 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:

  1. PanemuTableController.edit()
  2. PanemuTableController.insert()
  3. PanemuTableController.save()
  4. PanemuTableController.deleteSelectedRow()
  5. PanemuTableController.reloadCurrentPage() to cancel editing and return to browse mode.

That's a start to just activate inline-editing. But there is so much more!

Id
Name
Email
Gender
Country
Amount
Date Info
Verified
Updated On
Last Login
1 Abagail Kingscote
2 Nicolina Coit Male Kazakhstan 3,744.95 Wed, 1 Nov 2023 false
3 Sarene Greim Male 4,397.68 Tue, 3 Jan 2023 true
4 Blair Millbank Female Switzerland false
5 Kliment Sprowle Male Indonesia 6,459.93 Mon, 25 Dec 2023 true

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 ColumnType 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()]?.();
  }
Id
Name
Address
1 Abagail Kingscote Jl. Panemu 55652
2 Nicolina Coit Yogyakarta 55672
3 Sarene Greim
4 Blair Millbank
5 Kliment Sprowle Margosari 55651

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 PanemuTableEditingController.onCommitEdit(). Also this table is on edit mode initially.

Id
Description
Country
City
1 Country and city are editable Indonesia Jakarta
2 Only city is editable Indonesia Yogyakarta
3 None is editable Wonderland City 1

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:

Registered
Verified At
Yes 25 Dec 2024 1:10:59
No

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.

Id
Amount (Default Editor)
Amount (No Editor)
Amount (Custom Editor)
1 5
5
5
2 3
3
3
3 4
4
4

A custom cell editor must implement CellEditorComponent. Then override the PanemuTableEditingController.initCellEditorRenderer() 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.

/ 266
Id
Name
Email
Gender
Country
Amount
Enrolled
Last Login
Verified
1 Abagail Kingscote Female
Philippines
9,339.72 Wed, 26 Apr 2023 Yes
2 Nicolina Coit Male
Kazakhstan
3,744.95 Wed, 1 Nov 2023 No
3 Sarene Greim Male
United States
4,397.68 Tue, 3 Jan 2023 Yes
4 Blair Millbank Female
Philippines
3,334.58 Sat, 1 Jul 2023 No
5 Kliment Sprowle Male
Indonesia
6,459.93 Mon, 25 Dec 2023 Yes
6 Winifred Dikle Female
Kazakhstan
9,833.93 Sun, 6 Aug 2023 No
7 Chadd Nacci Male
China
4,383.54 Tue, 3 Jan 2023 Yes
8 Matthiew Morland Female
Greece
9,727.90 Sun, 22 Oct 2023 No
9 Perceval Glasheen Male
Netherlands
7,201.29 Sun, 23 Jul 2023 Yes
10 Fenelia Oblein Male
Sweden
3,654.03 Fri, 13 Oct 2023 No
11 Lisette Ornells Female
Cuba
6,775.49 Fri, 27 Oct 2023 Yes
12 Mellie Anthill Male
Philippines
7,474.30 Tue, 31 Jan 2023 No
13 Charles Simkovitz Male
Brazil
7,871.40 Sun, 15 Oct 2023 Yes
14 Brett Hew Female
Macedonia
6,925.89 Thu, 2 Mar 2023 No
15 Netti Treend Female
Peru
6,569.53 Sun, 12 Mar 2023 Yes
16 Donetta Scotland Female
Russia
7,647.54 Sat, 17 Jun 2023 No
17 Larry Candish Female
Mongolia
8,287.03 Mon, 18 Sep 2023 Yes
18 Ianthe Nijssen Male
Georgia
3,230.41 Mon, 3 Jul 2023 No
19 Rivalee Heavyside Male
Indonesia
5,557.91 Thu, 18 May 2023 Yes
20 Ekaterina Heyburn Female
Indonesia
5,916.96 Mon, 30 Oct 2023 No
21 Cynthia Garrard Female
Jamaica
3,155.08 Fri, 15 Sep 2023 Yes
22 Bruis Henryson Male
Russia
4,079.36 Tue, 27 Jun 2023 No
23 Brittani Swaby Female
Ukraine
7,766.85 Sat, 2 Sep 2023 Yes
24 Johnna von Hagt Male
Ukraine
7,231.27 Tue, 3 Jan 2023 No
25 Brooks Kalberer Female
Sweden
3,130.28 Thu, 12 Oct 2023 Yes
26 Osmond Beevens Female
China
5,242.18 Mon, 14 Aug 2023 No
27 Stephi Forsdicke Male
China
3,027.02 Sat, 7 Jan 2023 Yes
28 Kristan Collingworth Male
China
6,649.44 Fri, 31 Mar 2023 No
29 Bord Petkov Male
Sweden
6,362.19 Mon, 10 Apr 2023 Yes
30 Nara Boorne Female
Argentina
6,527.11 Mon, 24 Jul 2023 No
31 Ransell Bullingham Male
Philippines
8,855.43 Tue, 5 Sep 2023 Yes
32 Kerr Eastcott Female
China
5,105.78 Sat, 28 Jan 2023 No
33 Victor Hallgalley Male
China
3,361.66 Mon, 24 Jul 2023 Yes
34 Delia Rosendale Female
France
4,381.41 Wed, 21 Jun 2023 No
35 Mickey Belding Female
China
8,716.32 Mon, 24 Apr 2023 Yes
36 Pippo MacColl Female
France
6,428.34 Tue, 12 Dec 2023 No
37 Radcliffe Castellet Male
China
6,133.15 Tue, 28 Mar 2023 Yes
38 Arvie Maxweell Female
Thailand
7,053.02 Sun, 21 May 2023 No
39 Darnell Valler Male
Poland
4,982.23 Fri, 23 Jun 2023 Yes
40 Mirilla Ziems Female
China
6,263.73 Sun, 8 Jan 2023 No
41 Werner Dorin Female
Philippines
3,215.49 Wed, 1 Feb 2023 Yes
42 Dael Lesek Male
China
4,099.27 Sun, 13 Aug 2023 No
43 Diarmid Dmytryk Male
Brazil
3,144.37 Sat, 15 Jul 2023 Yes
44 Eunice Reily Male
Indonesia
3,477.99 Wed, 15 Nov 2023 No
45 Matty Duddle Male
Macedonia
5,296.22 Sat, 25 Mar 2023 Yes
46 Luciano Godilington Male
France
4,139.65 Mon, 7 Aug 2023 No
47 Stacie Vassie Male
Ukraine
8,122.35 Sun, 29 Oct 2023 Yes
48 Aharon Samter Female
Oman
4,361.33 Fri, 10 Mar 2023 No
49 Hillery Canedo Female
Iran
9,719.70 Sun, 9 Jul 2023 Yes
50 Jacquelin Crowcombe Female
Morocco
7,359.53 Thu, 15 Jun 2023 No
51 Allayne Bachelor Male
Poland
3,332.50 Wed, 19 Apr 2023 Yes
52 Lindie Bodley Male
Armenia
3,413.69 Fri, 14 Apr 2023 No
53 Sharia Roskelly Female
Bulgaria
8,705.67 Fri, 24 Feb 2023 Yes
54 Saul Ubsdale Male
France
7,604.27 Thu, 8 Jun 2023 No
55 Nate Churms Female
Colombia
7,457.68 Tue, 11 Jul 2023 Yes
56 Tracey Bucknall Male
Afghanistan
9,351.93 Mon, 13 Feb 2023 No
57 Carole Sparrow Female
Russia
6,366.73 Tue, 3 Jan 2023 Yes
58 Jennica Pashba Female
Vietnam
4,825.08 Sun, 5 Mar 2023 No
59 Carlotta Turvey Female
Philippines
6,191.06 Tue, 7 Mar 2023 Yes
60 Drucill Roaf Female
China
4,394.57 Thu, 26 Jan 2023 No
61 Lucien Steuart Female
Japan
5,225.22 Fri, 10 Nov 2023 Yes
62 Daryl Slimon Male
Portugal
6,995.50 Sun, 17 Dec 2023 No
63 Sheelah Laydon Male
Poland
7,313.91 Tue, 18 Jul 2023 Yes
64 Bertie Brewood Male
Ivory Coast
9,912.11 Mon, 27 Nov 2023 No
65 Louise Rosin Male
Hungary
8,218.10 Sun, 11 Jun 2023 Yes
66 Ara Gaskins Female
Bulgaria
5,960.34 Mon, 6 Feb 2023 No
67 Roshelle Tollemache Female
Mexico
5,319.58 Wed, 10 May 2023 Yes
68 Mabelle Schwanden Female
Vietnam
6,830.94 Mon, 10 Jul 2023 No
69 Fayina Parsonson Male
China
5,845.86 Sat, 2 Sep 2023 Yes
70 Chancey Blackway Female
France
6,252.95 Thu, 28 Sep 2023 No
71 Josee Dasent Male
China
7,682.65 Fri, 3 Mar 2023 Yes
72 Larisa Dillway Female
China
5,202.31 Mon, 23 Oct 2023 No
73 Oby Woolard Female
Russia
5,111.98 Sat, 12 Aug 2023 Yes
74 Marylou Lebang Male
Spain
7,076.70 Sun, 27 Aug 2023 No
75 Nollie Rillett Female
Ivory Coast
9,177.99 Sun, 10 Dec 2023 Yes
76 Jillayne Geal Female
China
8,314.71 Thu, 22 Jun 2023 No
77 Andreas Marling Female
Senegal
8,927.05 Sat, 1 Apr 2023 Yes
78 Edwin Kilmister Female
Ukraine
4,888.32 Fri, 17 Mar 2023 No
79 Hershel Cornelis Male
Indonesia
4,712.07 Sat, 25 Mar 2023 Yes
80 Nancy Tomanek Female
Russia
8,979.17 Sat, 13 May 2023 No
81 Kikelia Hawgood Male
Albania
8,688.32 Sat, 1 Apr 2023 Yes
82 Cele Thridgould Male
Poland
4,559.98 Fri, 30 Jun 2023 No
83 Petronella Fairbank Male
Czech Republic
7,322.40 Tue, 13 Jun 2023 Yes
84 Thorn Maruszewski Male
Syria
5,074.47 Fri, 2 Jun 2023 No
85 Jodi Tall Female
Kyrgyzstan
8,097.92 Sat, 20 May 2023 Yes
86 Bail Ehlerding Female
China
8,739.09 Fri, 3 Feb 2023 No
87 Natal Thorald Male
Croatia
7,632.75 Fri, 29 Sep 2023 Yes
88 Krishna Burwood Male
Poland
4,722.89 Mon, 29 May 2023 No
89 Zarah Mousdall Female
China
8,195.89 Thu, 1 Jun 2023 Yes
90 Torre Moss Female
China
3,262.70 Wed, 10 May 2023 No
91 Merry Paridge Male
China
8,407.38 Mon, 13 Mar 2023 Yes
92 Laurene Lucius Female
Philippines
4,604.98 Wed, 6 Dec 2023 No
93 Isa Braine Male
Panama
4,844.65 Fri, 8 Sep 2023 Yes
94 Leisha Tompsett Male
China
6,575.16 Tue, 8 Aug 2023 No
95 Florry O'Doireidh Male
Denmark
4,294.56 Wed, 4 Jan 2023 Yes
96 Sophi Roberti Male
China
3,185.59 Fri, 9 Jun 2023 No
97 Leanora Nester Male
China
7,958.93 Sat, 10 Jun 2023 Yes
98 Giacopo Stallion Male
China
8,455.91 Fri, 13 Oct 2023 No
99 Euell Yanshonok Female
Czech Republic
5,455.26 Thu, 27 Jul 2023 Yes
100 Cyrillus Gilstoun Female
China
5,418.84 Sun, 5 Nov 2023 No

Summary

There are 3 TABLE_MODEs: 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 PanemuTableController.reloadCurrentPage() to cancel editing and return to browse mode.

INSERT MODE:

  • Call PanemuTableController.insert() to enter 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 PanemuTableController.edit() to enter 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

toolbar.component.ts
toolbar.component.html
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

address-cell-editor.ts
address-cell-editor.html
address-cell-editor.scss
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

message-dialog.component.ts
documentation.service.ts
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);
  }
}