Editing Tabular Data in Angular
Tags: angular, typescript, dataengineering
Published: 2021-07-06 18:06:13

Jim Armstrong | ng-conf | May 2019

- A fun dive into everything from custom Directives to advanced _ViewChildren_

#### Introduction

This article targets beginning to intermediate-level Angular developers and covers a wide variety of topics that arise in production applications. While centered around the concept of editing tabular data, these techniques may be used in a variety of other Angular applications.

For anyone who has read at least one of my articles, you should understand that my background is applied mathematics and scientific computing. So, this article continues the trend of exploring the use of Angular in scientific and business (analytics) applications.

Working with time-series data is a fundamental concept in numerous business and engineering sectors. In this context, front-end development is largely concerned with minor transformations and display of data. Concepts such as data grids, tabular display, and visualization with charts are quite familiar to front-end devs. What is likely to be less familiar is the need to edit one or more values in a time series.

Data often comes from physical instruments that have some degree of fallibility and/or manual entry that is subject to typical human error. So, at some point during your FE career, it may be necessary to develop components that facilitate both the display **and** editing of tabular data. Only the latter is discussed in this article.

#### The Project

Before continuing, point your friendly, neighborhood browser to this Github, so that you can follow along with the project deconstruction.

[theAlgorithmist/Angular-Table-Editing](https://github.com/theAlgorithmist/Angular-Table-Editing) on github.com

Techniques covered in the remainder of the article include

- Using Angular Material from a feature module

- Custom Directives (including @HostListener and @HostBinding)

- @ViewChild vs @ViewChildren and subscribing to changes on the latter

- Validate while typing

- Custom Events

The project is organized into a few simple folders,

— _src/app_

— — _features_

— — — _table-edit_ (table edit component, custom directive, and feature module

— — libs (because we always need some custom libraries — otherwise, I would be out of business :)

— — _models_ (all data models)

— — _services_ (because data has to come from somewhere)

The data used in this sample project comes from an actual, historical dataset on used-car sales from the book, “Machine Learning in R” by Lantz. For tutorial purposes, suppose that all data in the table comes from reliable sources except mileage, which is hand-entered in another application. The code provided with this article simulates a use-case where someone with edit and/or approval authority visually examines a series of data to search for outliers. That data is displayed in a table which contains an _Input_ field in one column to support editing that particular item. To make the demo more realistic, the original data was hand-edited to insert a number of outliers.

And, it would not be a project if we did not have some requirements! Every one of the following requirements was taken from an actual client application I’ve worked on in the past.

1 — Display the data in a table with headers and data returned from a service.

2 — One and only one column is editable, the car mileage. This is hardcoded into the application and will not change.

3 — The table should be paged. The number of initial rows and allowable rows for padding will be provided. Allow sorting by date of manufacture as older cars should generally have more mileage.

4 — A user may tab between rows, but indication of an edited value is via pressing ‘Return’. I’ve also been required to add a small button to the side of the input in actual projects, but that’s not required for this demo.

5 — User inputs are validated while typing. Only numerical, integer inputs (with no minus sign) are allowed. If the user enters an incorrect character, reset the input field value to its value when the user first focused on the field (or the most recently edited and valid value).

6 — Inputs fields have a small, grey border by default (color to be provided and not changeable). When the user successfully edits a mileage value, replace the border with a green color (to be provided and not changeable).

7 — Whenever the user navigates to a new page, the input borders should be reset to the default value.

8 — Whenever a user clicks on a row, whether they edit a value or not, record that click and store the number of clicks on each car id to be returned to the server. I actually had a client who wanted to do this to capture ‘interest’ in a particular row of data, i.e. they believed the click was indicative of interest in the data whether the user actually edited the data or not. Okay, well, as long as the money’s there … I don’t care :)

9 — Capture whenever the user moves from one page to another so that we can potentially take action in the future. Yes, folks, that’s a common one … people want to do something, but they won’t know what it is until well into the future.

10 — Add a ‘Save’ button. Clicking on this button will send a record of all edited data to the server. For tutorial purposes, the button will be implemented, but the handler only logs the edited data to the console.

In an actual application, a person with _edit_ authority would perform the data editing and then after saving the data, a person with approval authority would be responsible for viewing all data and approving the modifications. This article is only concerned with the edit portion of the process.

#### Material Table

Enough has been written on the use of _Material_ and _Material Table_ in particular, that there is little benefit from adding a lot of explanation in this article. Suffice to say that I personally prefer using _ngContainer_ to create templates for each column. The most important column in the layout provided below is _mileage_, and there is a _Material Input_ field that allows editing of the mileage values.

```
<div class="mat-elevation-z8">

  <table mat-table matSort [dataSource]="dataSource">

    <tr mat-header-row *matHeaderRowDef="displayOrder"></tr>

    <tr mat-row *matRowDef="let row; columns: displayOrder" (click)="onTouched(row)"></tr>

    <ng-container matColumnDef="year">
      <th mat-header-cell *matHeaderCellDef mat-sort-header="year"> Year </th>
      <td mat-cell *matCellDef="let element"> {{element.year}} </td>
    </ng-container>

    <ng-container matColumnDef="model">
      <th mat-header-cell *matHeaderCellDef> Model </th>
      <td mat-cell *matCellDef="let element"> {{element.model}} </td>
    </ng-container>

    <ng-container matColumnDef="price">
      <th mat-header-cell *matHeaderCellDef> Price </th>
      <td mat-cell *matCellDef="let element"> {{element.price}} </td>
    </ng-container>

    <ng-container matColumnDef="mileage">
      <th mat-header-cell *matHeaderCellDef> Mileage </th>
      <td mat-cell *matCellDef="let element">
        <mat-form-field>
          <input matInput class="bordered editable" type="text" min="0" max="1000000" value="{{element.mileage}}" id="{{element.carid}}"
                 (keyup)="__checkNumber($event)" (inputChanged)="onEdited($event)" >
          <mat-hint><strong>Mileage</strong></mat-hint>
        </mat-form-field>
      </td>
    </ng-container>

    <ng-container matColumnDef="color">
      <th mat-header-cell *matHeaderCellDef> Color </th>
      <td mat-cell *matCellDef="let element"> {{element.color}} </td>
    </ng-container>

    <ng-container matColumnDef="transmission">
      <th mat-header-cell *matHeaderCellDef> Transmission </th>
      <td mat-cell *matCellDef="let element"> {{element.transmission}} </td>
    </ng-container>

  </table>

  <!-- options should always be Fibonacci :) -->
  <mat-paginator [length]="150" [pageSize]="5" [pageSizeOptions]="[5, 8, 13]" showFirstLastButtons (page)="onPage($event)"></mat-paginator>

  <div align="right">
    <button mat-button color="primary" (click)="onSave()">Save</button>
  </div>
</div>
```
###### [table-edit.component.html](https://gist.github.com/theAlgorithmist/441519c3e4e018094b48906f4896247d#file-table-edit-component-html) hosted on GitHub

Note the inclusion of the _Material_ paginator near the end of the layout.

The necessary _Material_ modules are separated into a feature module as shown in the file

/src/app/features/material.module.ts

```
import { NgModule } from '@angular/core';

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import {
  MatTableModule,
  MatPaginatorModule,
  MatInputModule,
  MatSortModule,
  MatButtonModule
} from '@angular/material';

const PLATFORM_IMPORTS: Array<any> = [BrowserAnimationsModule];

const MATERIAL_IMPORTS: Array<any> = [MatTableModule, MatPaginatorModule, MatInputModule, MatSortModule, MatButtonModule];

@NgModule({
  imports: [PLATFORM_IMPORTS, MATERIAL_IMPORTS],

  exports: MATERIAL_IMPORTS,

  declarations: []
})
export class MaterialModule { }
```

which is imported into the table edit module

/src/app/features/table-edit/table-edit.module.ts

```
import { NgModule     } from '@angular/core';
import { CommonModule } from '@angular/common';

import { MaterialModule } from '../material.module';

import { TableEditComponent     } from '../table-edit/table-edit/table-edit.component';
import { InputSelectorDirective } from './directives/input-selector.directive';

export const TABLE_COMPONENTS: Array<any> = [TableEditComponent, InputSelectorDirective];

@NgModule({
  imports: [MaterialModule, CommonModule],

  exports: TABLE_COMPONENTS,

  declarations: TABLE_COMPONENTS
})
export class TableEditModule { }
```

This allows the table-edit functionality to be easily imported into any project, including ours in _/src/app/app.module.ts_

```
import { BrowserModule    } from '@angular/platform-browser';
import { NgModule         } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';

// feature module
import { TableEditModule  } from './features/table-edit/table-edit.module';

// app-level components
import { AppComponent } from './app.component';

const APP_DECLARATIONS: Array<any> = [AppComponent];

@NgModule({
  declarations: APP_DECLARATIONS,
  imports: [
    BrowserModule, HttpClientModule, TableEditModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
```

#### Data Models

All data models (interfaces) for the application are in the _/src/app/models/models.ts_ file. A single record of car data is modeled as

```
export interface ICarData
{
  carid: number;
  year: number,
  model: string,
  price: number
  mileage: number;
  color: string;
  transmission: TransmissionEnum;
}
```

and the entire model (including headers) is

```
export interface ICarDataModel
{
  header: Array<string>;

  data: Array<ICarData>;
}
```

When the user edits car mileage, it is necessary to record the _id_ of the edited vehicle and the new mileage value, which are stored in an _IEditedData_ instance.

```
export interface IEditedData
{
  id: number;

  value: number;
}
```

#### Main App Component

The main app component, _/src/app/app.component.ts_ simply loads a car data model from a JSON file and then separates the header and car data into two bound variables in the ___onModelLoaded_ method,

```
protected __onModelLoaded(result: ICarDataModel): void
{
  // this handler could be used to check the integrity of returned data
  this.header = result.header.slice();

  // assign a copy of the returned model to the bound data
  this.data = result.data.map( (car: ICarData): ICarData => {return JSON.parse(JSON.stringify(car))} );
}
```

From this point, the remainder of the application is handled by the table edit component.

#### Table Edit Component

The table edit component (_/src/app/features/table-edit/table-edit/table-edit.component.ts_) employs an _InputSelectorDirective_ to select individual Input fields. This is accomplished by using a class selector in the _Directive_,

```
@Directive({
  selector: '.editable'
})
export class InputSelectorDirective implements OnInit
```

and then applying that class in the _Input_ field in the template,

_/src/app/features/table-edit/table-edit/table-edit.component.html_

```
<input matInput class="bordered editable" type="text" min="0" max="1000000" value="{{element.mileage}}" id="{{element.carid}}"
       (keyup)="__checkNumber($event)" (inputChanged)="onEdited($event)" >
```

A _ViewChild_ of this _Directive_ provides a direct reference to a single instance of that _Directive_, applied to an _Input_ field with the class ‘editable.’ This application, however, requires references to **all** such input fields in the current table page. This is where _ViewChildren_ and _QueryList_ are used.

_/src/app/features/table-edit/table-edit/table-edit.component.ts_

```
@ViewChildren(InputSelectorDirective)
protected _inputs: QueryList<InputSelectorDirective>;   // reference to QueryList returned by Angular
protected _inputsArr: Array<InputSelectorDirective>;    // Array of Directive references
```

The _QueryList_ provides a reference to the _InputSelectorDirective_ for all Input fields in the current page.

Two Typescript _Records_ are used to store edited data and record ‘row touches’,

```
protected _edited: Record<string, number>;
protected _touches: Record<string, number>;
```

Some programmatic support is required to interface with the _Material_ table, specifically a data source, reference to the _MatPaginator_ (paginator), and _MatSort_ (sorting). This is accomplished with two _ViewChild_ instances and a public variable (for binding)

```
@ViewChild(MatPaginator)
protected _paginator: MatPaginator;

@ViewChild(MatSort)
protected _sort: MatSort;

// (Material) Datasource for the table display
public dataSource: MatTableDataSource<ICarData>;
```

That concludes the basic setup for this component. In terms of logic, a summary of relevant class methods follows to aid in your deconstruction of the application.

Method: _onEdited(evt: IEditedData): void_

This method is called whenever the mileage data is edited. It first checks the argument and event id and then stores the edited data in the class edited-data _Record_.

Method: _onTouched(row: ICarData): void_

This method is called whenever a user clicks on a table row, which is taken as an indication of interest in that data, whether it is edited or not. Yes, I’ve actually had to implement this for a client in a real application. As long as the check clears the bank, it works for me :)

Method: _onSave(): void_

This is a placeholder for you to implement a service call to store the edited data should you wish to modify the code for use in a production environment. The edited data is logged to the console to help visualize formatting of the edited-data Record.

Method: _onPage(evt: PageEvent): void_

This is another placeholder method in case you want to modify the application to perform some function whenever the user pages to another set of table data.

Method: ___checkNumber(evt: any): boolean_

This method is called to validate a number while typing. It defers the validation to the library method, _Validation.checkNumber()_, which is useful for numeric entry of physical properties that must be greater than or equal to zero.

Method: ___onInputsChanged(): void_

This method is executed whenever the _QueryList_ of _Input_ fields changes (i.e. on page change). The method’s primary action is to reset the border color on all new fields. Modify for additional functionality as you see fit.

Since the _QueryList_ of _InputSelectorDirective instances_ changes every time the user navigates to a new page of the table, it is necessary to subscribe to changes in that list. The subscription is made in the _ngAfterViewInit_ lifecycle method,

```
public ngAfterViewInit(): void
{
  // subscribe to changes in the query list
  this._inputs.changes.subscribe( () => this.__onInputsChanged() );
}
```
and here is the handler,

```
protected __onInputsChanged(): void
{
  // input query list changed (which happens on profile selection)
  this._inputsArr = this._inputs.toArray();

  // set default border color on everything
  if (this._inputsArr && this._inputsArr.length > 0) {
    this._inputsArr.forEach( (input: InputSelectorDirective): void => {input.borderColor = '#cccccc'});
  }
}
```

The use of this method and _onPage()_ provides a natural separation of the primary focus on _Input_ field changes with any other activities that may be requested on page change. The result is better focus on single responsibility between methods.

#### Input Selector Directive

This _Directive_ provides a collection of _Output_ and event handlers to facilitate editing the mileage data in the table.

The single _Output_ is ‘inputChanged’ and is emitted whenever a mileage value is changed,

/src/app/features/table-edit/directives/input-selector.directive.ts

```
@Output('inputChanged')
protected _changed: EventEmitter<IEditedData>;
```

A single _HostBinding_ to the _border-color_ style facilitates changing the border color of each _Input_ field based on whether that element is initially displayed or in an edited state.

```
@HostBinding('style.border-color')
public borderColor: string = '#cccccc';
```

There are two host listeners, one for the ‘focus’ event and the other for ‘keyup.’ When an _Input_ field receives focus, it is necessary to capture the current value and the id associated with that mileage value. The former is used to re-populate the field with the initial value in the event a typing error is detected. The _id_ must be emitted along with the edited value in order to associate the edited value with a specific record of car data.

The ‘keyup’ listener performs basic validation on the current numerical input for the mileage value. A valid value on clicking ‘Return’ causes the _Input_ field to be colored green. Input errors while typing cause the field to be repopulated with the last-known-good value.

```
@HostListener('keyup', ['$event']) onKeyUp(evt: KeyboardEvent): boolean
{
  // test for singleton leading negative sign as first character
  const v: string = this._input.value;
  const n: number = v.length;

  // for now, allow a blank field as it is possible that the entire number could be deleted by backspace before
  // entering a new number
  if (n == 0) {
    return true;
  }

  // physical quantities may not be negative and a decimal is currently not allowed
  if ( (n == 1 && v == "-") || (evt.key == ".") )
  {
    this.hasError     = true;
    this._input.value = this._currentValue.toString();

    return true;
  }

  // check for most recent keystroke being an enter, which is currently the only way to indicate an edit
  const code: string = evt.code.toLowerCase();
  if (code == 'enter' || code == 'return')
  {
    if (!isNaN(+v) && isFinite(+v))
    {
      this.hasError      = false;
      this._currentValue = +v;

      // set 'edited' border color and emit the changed event
      this.borderColor = '#66CD00';

      this._changed.emit({id: this._currentID, value: +v});
    }
    else
    {
      this.hasError     = true;
      this._input.value = this._currentValue.toString();
    }

    return true;
  }

  this.hasError = !Validation.checkNumber(evt);

  if (this.hasError)
  {
    console.log( "error: ", this._currentValue );
    // indicate an error by replacing the bad input with the 'current' or last-known good value
    // this may be altered in a future release
    this._input.value = this._currentValue.toString();
  }

  return true;
}
```
###### [input-selector.directive.ts](https://gist.github.com/theAlgorithmist/a2ce0b6da2e43cc2f6f63befc5710eae#file-input-selector-directive-ts) hosted by GitHub

#### Summary

This has been a long and somewhat involved deconstruction. Scientific, engineering, and business-analytic applications often expose a much higher degree of interactivity to FE devs. I hope this article and the supporting code has helped beginning- and intermediate-level Angular developers with their understanding of the platform.

---

## ng-conf: Join us for the Reliable Web Summit

Come learn from community members and leaders the best ways to build reliable web applications, write quality code, choose scalable architectures, and create effective automated tests. Powered by ng-conf, join us for the Reliable Web Summit this August 26th & 27th, 2021.
https://reliablewebsummit.com/