Angular Material Reactive Forms & Node.js API CRUD

Photo by Leon Dewiwje on Unsplash

Overview

This article is an overview of a simple CRUD application to manage users. The application is built with Angular Reactive Forms and Node.js for the API. The example application will show a text fields for name, email, description, and a material drop down list for categories. The edit user form will get the record data and use FormBuilder and FormControl to fill in and gather the form data for submission. The default add user form will also use reactive forms to make sure the required fields are filled out. Node.js API works with a SQLite3 database. For a more in-depth article on how to build a Node.js API with SQLite3 please check out on of the following:

Source Files for the Example

Recommend cloning or downloading the working example just in case something is missing in the article.

Get Source Files

How to run the example

For Angular

cd ng-demp-app
ng s -o

For Node.js

cd API
npm start

Create a New Angular Application

First, create a new application

ng new my-app

Once the application has been created change directory into the new application folder.

cd my-app

Install Material with Angular Schematics – ‘ng add’. This will take care of installing Material and it dependencies and basic configuration. You will still need to take care of the imports which is discussed in the next section.

ng add @angular/material

Create the following components

ng g c components/user-parent-layout
ng g c components/add-user-form
ng g c components/edit-user-form
ng g s services/user
ng g class models/user-class

In the app.module.html remove the default generated content and just place the parent-layout-component.

<app-users-parent-layout></app-users-parent-layout>

Material Module

Because there are a lot of imports to using Material, most examples I have seen use a separate module to handle these imports. Below is a step by step on creating the module and adding it to the root module app.module.ts.

Create a new module labeled material.module.ts. Link to article on how to do this.

Make sure you have the following imports in your apps.module.ts file.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientJsonpModule, HttpClientModule } from '@angular/common/http';
import { MaterialModule } from './material.module';

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    ReactiveFormsModule,
    BrowserAnimationsModule,
    HttpClientModule,
    HttpClientJsonpModule,
    BrowserAnimationsModule,
    FormsModule,    
    MaterialModule
  ],
  providers: [],
  entryComponents: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Add the imports needed to the Root app.module.ts or Feature Module.

Code snippet from app.module.ts

import { MaterialModule } from './material.module';
....
@NgModule({
 declarations: [],
 imports: [
    BrowserModule,
    AppRoutingModule,
    BrowserAnimationsModule,
    HttpClientModule,
    HttpClientJsonpModule,
    BrowserAnimationsModule,
    ReactiveFormsModule,
    FormsModule,
    MaterialModule
....

User Class

Create a user class file user-class.ts for edit form to submit both the form builder and control data to the users.service.ts.

Typescript

Code for add-user-form.component.ts

export class UserClass {
  Id: string = '';
  Name: string = '';
  Description: string = '';
  Email: string = '';
  Category: string = '';
}

User Services

The Angular Service file users.service.ts manages the API calls to node.js.

Typescript

Code for add-user-form.component.ts

import { Injectable } from '@angular/core';
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';
import { UserClass } from '../models/user-class';

@Injectable({
  providedIn: 'root'
})

export class UsersService {


  constructor(private http: HttpClient) { }

  getAllUsers(): Observable<any> {
    const URL = `http://localhost:3004/api/users`;

    return this.http.get<any>(URL);
  }

  getSingleUser(id: number): Observable<any> {
    const URL = `http://localhost:3004/api/user/${id}`;

    return this.http.get<any>(URL);
  }

  updateSingleUser(data: UserClass): Observable<any> {
    const URL = `http://localhost:3004/api/user`;
    const throttleConfig = {
      leading: false,
      trailing: true
    }
    const body = new HttpParams()
    .set('Id', data.Id.toString())
    .set('Name', data.Name)
    .set('Email', data.Email)
    .set('Description', data.Description)
    .set('Category', data.Category.toString());
    console.log('body', body.toString())
    return this.http.put<any>(URL, body);

  }

  addSingleUser(data: UserClass): Observable<any> {
    const URL = `http://localhost:3004/api/user`;

    const body = new HttpParams()
    .set('Name', data.Name)
    .set('Email', data.Email)
    .set('Description', data.Description)
    .set('Category', data.Category.toString());

    return this.http.post<any>(URL, body);

  }

  deleteSingleUser(id: number): Observable<any> {
    const URL = `http://localhost:3004/api/user/${id}`;

    return this.http.delete<any>(URL);
  }


}

User Add Form Component

The form component has three Angular Material Text and one Select field controls. Uses form builder to validate input for the text fields and a form control for the select field. Once all the fields have been validated a signature field is enabled which then makes the Submit button available.

Typescript

Code for add-user-form.component.ts

import { Component, OnInit, OnDestroy, EventEmitter, Output, Input  } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { UsersService } from '../../services/users.service';
import { FormBuilder, Validators, FormControl, FormGroup } from '@angular/forms';
import { FormGroupDirective, NgForm} from '@angular/forms';
import { Subscription } from 'rxjs';
import { IUsers, IWebsiteType } from 'src/app/interfaces/iusers';
import { HttpErrorResponse } from '@angular/common/http';
import { UserClass } from 'src/app/models/user-class';


export class MyErrorStateMatcher implements ErrorStateMatcher {
  isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
    const isSubmitted = form && form.submitted;
    return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
  }
}

@Component({
  selector: 'app-add-user-form',
  templateUrl: './add-user-form.component.html',
  styleUrls: ['./add-user-form.component.scss']
})
export class AddUserFormComponent implements OnInit, OnDestroy {
  public data: any;
  @Input() pageDetails: any;
  @Input() tableData: IUsers[] = [];
  @Output() showMessage = new EventEmitter<boolean>();
  @Output() addNewRecord = new EventEmitter<boolean>();

  private subs = new Subscription();
  private newUserCls = new UserClass();

  public emailFormControl = new FormControl('', [Validators.required, Validators.email]);
  public nameArray: any;
  public overlapPageArray: any;
  public toNameArray: any;
  public fromNameArray: any;

  public eventForm = new FormGroup({})
  public isActive = new FormControl('', [Validators.required]);
  public appCategory = new FormControl('', [Validators.required]);

  appTypes: IWebsiteType[] =
    [{ value: "1", viewValue: "Singer" },
    { value: "2", viewValue: "Bassist" },
    { value: "3", viewValue: "Guitarist" },
    { value: "4", viewValue: "Drummer" }];

  constructor(private fb: FormBuilder, private userSVC: UsersService) { }

  ngOnInit() {

    this.toNameArray = [];
    this.fromNameArray = [];
    this.showMessage.emit(false);

    this.eventForm = this.fb.group({
      id: [null],
      name: [null, [Validators.required, Validators.minLength(2), Validators.maxLength(80)]],
      email: [null, [Validators.required, Validators.email]],
      description: [null, [Validators.required, Validators.minLength(2), Validators.maxLength(200)]],
    });

    this.appCategory = new FormControl(0);

  }

  ngOnDestroy() {

    if (this.subs) {
      this.subs.unsubscribe();
    }
  }


  //* Get Application ID for Drop Down / Form Control
  getApplicationID(value: any): number {
    var appVal = 0;
    var counter = 0;
    this.appTypes.forEach(item => {
      if (item.value == value) {
        appVal = counter;
      }
      counter++;
    })
    return appVal;
  }


  //* Submit Form to Parent Layout Component
  onSubmit($event: any) {

    this.addNewRecord.emit(true)
    this.newUserCls.Id = this.eventForm.controls.id.value;
    this.newUserCls.Name = this.eventForm.controls.name.value;
    this.newUserCls.Email = this.eventForm.controls.email.value;
    this.newUserCls.Description = this.eventForm.controls.description.value;
    this.newUserCls.Category = this.appCategory.value;

    this.subs.add(this.userSVC.addSingleUser(this.newUserCls).subscribe((response) => {
      // console.log(response);
      this.addNewRecord.emit(true);
    },
    (err: HttpErrorResponse) => {
      console.log(err);
    }));

  }
}

HTML

The following source is the html markup.

<form [formGroup]="eventForm" novalidate (ngSubmit)="onSubmit($event)"  autocomplete="off" class="example-form">

  <table class="example-full-width" cellspacing="0"><tr>
    <td>
      <mat-form-field class="example-full-width" appearance="fill" >
      <mat-label>Name</mat-label>
      <input matInput formControlName="name">
      <mat-error *ngIf="eventForm.controls.name.touched && eventForm.controls.name.invalid">
        <span *ngIf="eventForm.controls.name.errors">Invalid Title (2 to 80 Characters)</span>
      </mat-error>
      </mat-form-field>
    </td>

    <td>
      <mat-form-field class="example-full-width" appearance="fill">
      <mat-label>Email</mat-label>
      <input matInput  formControlName="email" required>

      <mat-error *ngIf="eventForm.controls.email.touched && eventForm.controls.email.invalid">
        <span >Invalid Email</span>
      </mat-error>
    </mat-form-field>
  </td>
  </tr>
</table>

  <p>
    <mat-form-field class="example-full-width" appearance="fill">
      <mat-label>Description</mat-label>
      <textarea matInput placeholder="" #description formControlName="description"></textarea>
      <mat-hint >{{description.value.length}}/200</mat-hint>
    </mat-form-field>
  </p>

  <table class="example-full-width" cellspacing="0"><tr>
    <td>
      <div class="col-3 text-left">
        <mat-form-field appearance="fill">
          <mat-label>Page Type</mat-label>
          <mat-select   [formControl]="appCategory">
            <mat-option>Please Select</mat-option>
            <mat-option *ngFor="let option of appTypes"  [value]="option.value" >
                {{option.viewValue}}
             </mat-option>
          </mat-select>
        </mat-form-field>
      </div>
    </td>
  </tr>
</table>

<mat-form-field class="example-full-width" appearance="fill" >
  <mat-label>Signature</mat-label>
  <input matInput value="" [disabled]="eventForm.invalid" #signature>
</mat-form-field>


<mat-divider class="mt-5"></mat-divider>

  <div class="mt-5">
        <button mat-raised-button color="primary"

          [disabled]="signature.value.length < 3"
          type="submit">Submit </button>
  </div>

</form>

User Edit Form Component

The form component has 3 Angular Material Text and Select field controls. Uses form builder to validate input for the 3 fields and the form control. The parent component calls the openRecord() function that is passed an id from the users table. The form control is looked up by a hard coded JSON object IWebsiteType[]. Ideally, the values for the select field would come from an API call. Since the 4 fields are already validated just a signature is needed to enabled the Save button.

Typescript

Code for edit-user-form.component.ts

import { Component, OnInit, OnDestroy, EventEmitter, Output, Input  } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { UsersService } from '../../services/users.service';
import { FormBuilder, Validators, FormControl, FormGroup } from '@angular/forms';
import { FormGroupDirective, NgForm} from '@angular/forms';
import { Subscription } from 'rxjs';
import { IUsers, IWebsiteType } from 'src/app/interfaces/iusers';
import { HttpErrorResponse } from '@angular/common/http';
import { UserClass } from 'src/app/models/user-class';

export class MyErrorStateMatcher implements ErrorStateMatcher {
  isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
    const isSubmitted = form && form.submitted;
    return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
  }
}


@Component({
  selector: 'app-edit-user-form',
  templateUrl: './edit-user-form.component.html',
  styleUrls: ['./edit-user-form.component.scss']
})
export class EditUserFormComponent implements OnInit, OnDestroy {
  public data: any;
  @Input() pageDetails: any;
  @Input() tableData: IUsers[] = [];
  @Output() showMessage = new EventEmitter<boolean>();
  @Output() saveChangeToRecord = new EventEmitter<boolean>();

  private subs = new Subscription();
  private newUserCls = new UserClass();

  public emailFormControl = new FormControl('', [Validators.required, Validators.email]);
  public toNameArray: any;
  public fromNameArray: any;

  public eventForm = new FormGroup({})
  public isActive = new FormControl('', [Validators.required]);
  public appCategory = new FormControl('', [Validators.required]);

  appTypes: IWebsiteType[] =
    [{ value: "1", viewValue: "Singer" },
    { value: "2", viewValue: "Bassist" },
    { value: "3", viewValue: "Guitarist" },
    { value: "4", viewValue: "Drummer" }];

  constructor(private fb: FormBuilder, private userSVC: UsersService) { }



  // I N I T
  ngOnInit() {

    this.toNameArray = [];
    this.fromNameArray = [];
    this.showMessage.emit(false);

    this.eventForm = this.fb.group({
      id: [null],
      name: [null, [Validators.required, Validators.minLength(2), Validators.maxLength(80)]],
      email: [null, [Validators.required, Validators.email]],
      description: [null, [Validators.required, Validators.minLength(2), Validators.maxLength(200)]],
    });

    this.appCategory = new FormControl(0);

  }


  // D E S T R O Y
  ngOnDestroy() {

    if (this.subs) {
      this.subs.unsubscribe();
    }
  }


  //* Get Application ID for Drop Down / Form Control

  getApplicationID(value: any): number {
    var appVal = 0;
    var counter = 0;
    this.appTypes.forEach(item => {
      if (item.value == value) {
        appVal = counter;
      }
      counter++;
    })
    return appVal;
  }


  //* Open record called from the Parent Layout Component

  openRecord(id: any): void {
    //* This is where you would use a service call to get a single record
    this.subs.add(this.userSVC.getSingleUser(id).subscribe((response) => {
      console.log(response)
      var record = response.data[0];
      this.eventForm = this.fb.group({
        id: [record.Id],
        name: [record.Name, [Validators.required, Validators.minLength(2), Validators.maxLength(80)]],
        email: [record.Email, [Validators.required, Validators.email]],
        description: [record.Description, [Validators.required, Validators.minLength(2), Validators.maxLength(200)]],
      });
      this.appCategory = new FormControl(this.appTypes[this.getApplicationID(record.Category)].value);
    },
    (err: HttpErrorResponse) => {
      console.log(err);
    }));

  }


  //* Submit Form to Parent Layout Component

  onSubmit($event: any) {


    this.newUserCls.Id = this.eventForm.controls.id.value;
    this.newUserCls.Name = this.eventForm.controls.name.value;
    this.newUserCls.Email = this.eventForm.controls.email.value;
    this.newUserCls.Description = this.eventForm.controls.description.value;
    this.newUserCls.Category = this.appCategory.value;
    console.log(this.newUserCls)
    this.subs.add(this.userSVC.updateSingleUser(this.newUserCls).subscribe((response) => {
      console.log(response)
      this.saveChangeToRecord.emit(true)
    },
    (err: HttpErrorResponse) => {
      console.log(err);
    }));

  }
}

HTML

Code for edit-user-form.component.html

<form [formGroup]="eventForm" novalidate (ngSubmit)="onSubmit($event)"  autocomplete="off" class="example-form">

  <table class="example-full-width" cellspacing="0"><tr>
    <td>
      <mat-form-field class="example-full-width" appearance="fill" >
      <mat-label>Name</mat-label>
      <input matInput formControlName="name">
      <mat-error *ngIf="eventForm.controls.name.touched && eventForm.controls.name.invalid">
        <span *ngIf="eventForm.controls.name.errors">Invalid Title (2 to 80 Characters)</span>        
      </mat-error>
      </mat-form-field>
    </td>

    <td>
      <mat-form-field class="example-full-width" appearance="fill">
      <mat-label>Email</mat-label>
      <input matInput  formControlName="email" required>

      <mat-error *ngIf="eventForm.controls.email.touched && eventForm.controls.email.invalid">
        <span >Invalid Email</span>        
      </mat-error>  
    </mat-form-field>
  </td>
  </tr>
</table>

  <p>
    <mat-form-field class="example-full-width" appearance="fill">
      <mat-label>Description</mat-label>
      <textarea matInput placeholder="" #description formControlName="description"></textarea>
      <mat-hint >{{description.value.length}}/200</mat-hint>        
    </mat-form-field>
  </p>

  <table class="example-full-width" cellspacing="0"><tr>
    <td>
      <div class="col-3 text-left">
        <mat-form-field appearance="fill">
          <mat-label>Page Type</mat-label>
          <mat-select   [formControl]="appCategory">
            <mat-option>Please Select</mat-option>
            <mat-option *ngFor="let option of appTypes"  [value]="option.value" > 
                {{option.viewValue}}
             </mat-option>
          </mat-select>
        </mat-form-field>
      </div>
    </td>
  </tr>
</table>

<mat-form-field class="example-full-width" appearance="fill" >
  <mat-label>Signature</mat-label>
  <input matInput value="" [disabled]="eventForm.invalid" #signature>
</mat-form-field>


<mat-divider class="mt-5"></mat-divider>

  <div class="mt-5">
        <button mat-raised-button color="primary" 

          [disabled]="signature.value.length < 3"
          type="submit">Submit </button>
  </div>

</form>

Users Table Component

The Material Data Table loads initially with an API call to node.js. This gets the user record ID, Name, Email, Description, and Category. This is a plain table with no extra features enabled such as pagination or sorting to keep this example short. The table has actions on both ends of each row. Edit on the left side which fills in the Edit Form and a delete button on the right side to remove a single record. Every time a record is updated or removed a snackbar will appear at the top of the page with a message that closes after 3 seconds.

Typescript

Code for users-table.component.ts

import { HttpErrorResponse } from '@angular/common/http';
import { Component, EventEmitter, OnInit, Input, Output } from '@angular/core';
import { Subscription } from 'rxjs';
import { UsersService } from 'src/app/services/users.service';


@Component({
  selector: 'app-users-table',
  templateUrl: './users-table.component.html',
  styleUrls: ['./users-table.component.scss']
})
export class UsersTableComponent implements OnInit {
  @Input() tableData: any;
  @Output() editRecord = new EventEmitter<number>();
  @Output() deleteRecord = new EventEmitter<boolean>();

  private subs = new Subscription();

  public dataSource: any;
  displayedColumns: string[] = ['Id', 'Name', 'Email', 'Description', 'Category', 'Action'];


  constructor(private userSVC: UsersService) { }


//* Initialize
  ngOnInit(): void {

    this.dataSource = this.tableData;
  }

//* Emits to Parent Layout Component that a record has been chosen
  public onEditForm(id: any) {
    // console.log(id);
    this.editRecord.emit(id);
  }

//* Called on by Parent Layout Component when the User Form Component submits a change
  public onLoadData() {

    this.subs.add(this.userSVC.getAllUsers().subscribe((response) => {
      this.dataSource = response.data;
    },
    (err: HttpErrorResponse) => {
      console.log(err);
    }));

  }

//* Delete a single record
   public onDelete(id: any) {

    this.subs.add(this.userSVC.deleteSingleUser(id).subscribe((response) => {
      console.log(response);
      this.deleteRecord.emit(true);
    },
    (err: HttpErrorResponse) => {
      console.log(err);
    }));
  }
}

HTML

Using mat-table source from https://material.angular.io/components/table/overview.

Code for users-table.component.html

 <div class="table">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z1">

    <!--- Note that these columns can be defined in any order.
          The actual rendered columns are set as a property on the row definition" -->

    <!-- Position Column -->
    <ng-container matColumnDef="Id">
      <th mat-header-cell *matHeaderCellDef> ID </th>
      <td mat-cell *matCellDef="let element"> <button mat-button (click)="onEditForm(element.Id)"> Edit</button>  </td>
    </ng-container>

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


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

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


    <!-- Type Column -->
    <ng-container matColumnDef="Category">
      <th mat-header-cell *matHeaderCellDef> Category </th>
      <td mat-cell *matCellDef="let element"> {{element.Category}} </td>
    </ng-container>

    <!-- Type Column -->
    <ng-container matColumnDef="Action">
      <th mat-header-cell *matHeaderCellDef> Action </th>
      <td mat-cell *matCellDef="let element">
        <button mat-button color="warn" (click)="onDelete(element.Id)">
					  Delete
        </button>
      </td>
    </ng-container>

    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
    <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
  </table>
</div>

Users Parent Layout Component

The Parent Layout Component handles the communication between child components. I wrote it this way versus using a service because it is easier to understand the parent child component of emitting to the parent and the parent calling a function in the child component. An improvement would be to modify the record in the JSON Data instead of an API call to reload the records after an edit.

Important! There is a delay for the Parent to call the child component openRecord(id) function with the @ViewChild() decorator. This delay is necessary for the edit-user-form.component to render after hiding the add-user-form.component. If the openRecord(id) is called before the component is rendered then the record doesn’t get loaded since the parent cannot call a component that hasn’t rendered yet.

Alternative solution is to use a Behavior Subject from a service between the child components.

Typescript

Code for users-parent-layout.component.ts

import { Component, OnInit, ViewChild, OnDestroy } from '@angular/core';
import { EditUserFormComponent } from '../edit-user-form/edit-user-form.component';
import { UsersTableComponent } from '../users-table/users-table.component';
import { IWebsiteType } from 'src/app/interfaces/iusers';
import { Subscription } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';
import { UsersService } from 'src/app/services/users.service';
import { MatSnackBar } from '@angular/material/snack-bar';

@Component({
  selector: 'app-users-parent-layout',
  templateUrl: './users-parent-layout.component.html',
  styleUrls: ['./users-parent-layout.component.scss']
})
export class UsersParentLayoutComponent implements OnInit, OnDestroy {

  @ViewChild(EditUserFormComponent, { static: false }) childFormComp!: EditUserFormComponent;
  @ViewChild(UsersTableComponent, { static: false }) childTableComp!: UsersTableComponent;

  private subs = new Subscription();
  public toggleShowEditForm: boolean = false;
  public tableData: any;
  public isDataLoaded: boolean = false;

  appTypes: IWebsiteType[] =
  [{ value: "1", viewValue: "Singer" },
  { value: "2", viewValue: "Bassist" },
  { value: "3", viewValue: "Guitarist" },
  { value: "4", viewValue: "Drummer" }];

  constructor(private userSVC: UsersService, private _snackBar: MatSnackBar) { }

  //* Initialize
  ngOnInit(): void {    
    this.isDataLoaded = false;
    this.subs.add(this.userSVC.getAllUsers().subscribe((response) => {
      console.log(response)
      this.tableData = response.data;
      this.isDataLoaded = true;
    },
    (err: HttpErrorResponse) => {
      console.log(err);
    }));
  }



  // D E S T R O Y
  ngOnDestroy() {

    if (this.subs) {
      this.subs.unsubscribe();
    }
  }

  //* Called by User Table Component to select a record and sends the record id to the Edit User Form Component
  public onEditRecord(id: any): void {
    //* Render the edit form and hide the add form
    this.toggleShowEditForm = true;

    //* Delay so the edit form can render first before calling the openRecord() function in the component
    setTimeout(() => {
      this.childFormComp.openRecord(id);
    }, 300);

  }


  //* Called by User Form Component to save the changes and reload the User Table Component with the new data
  public onSaveChangeToRecord(record: any) {
    this.childTableComp.onLoadData();
    this._snackBar.open('Record Saved', 'X', {
      duration: 3000,
      verticalPosition: 'top'
    });


    //* Hide the edit form and render the add form
    this.toggleShowEditForm = false;
  }

  //* Called by Add User Form Component to save the changes and reload the User Table Component with the new data
  public onAddNewRecord(record: any) {
    this.childTableComp.onLoadData();
    this._snackBar.open('New Record Added', 'X', {
      duration: 3000,
      verticalPosition: 'top'
    });
  }

    //* Called by Add User Form Component to save the changes and reload the User Table Component with the new data
  public onDeleteRecord($event: any) {
    this.childTableComp.onLoadData();
    this._snackBar.open('Record Deleted', 'X', {
      duration: 3000,
      verticalPosition: 'top'
    });
  }
}

HTML

Using <table mat-table source from https://material.angular.io/components/table/overview.

Code for users-parent-layout.component.html

<div class="grid-container">
    <mat-grid-list cols="12" rowHeight="100px">
        <mat-grid-tile [colspan]="6" [rowspan]="6">

            <app-edit-user-form [tableData]="tableData"                      (saveChangeToRecord)="onSaveChangeToRecord($event)" 
*ngIf="toggleShowEditForm">
            </app-edit-user-form>

            <app-add-user-form (addNewRecord)="onAddNewRecord($event)      *ngIf="!toggleShowEditForm">
            </app-add-user-form>
        </mat-grid-tile>

        <mat-grid-tile [colspan]="6" [rowspan]="6">
            <app-users-table class="table" [tableData]="tableData" (deleteRecord)="onDeleteRecord($event)" (editRecord)="onEditRecord($event)" *ngIf="isDataLoaded">
            </app-users-table>
        </mat-grid-tile>
    </mat-grid-list>
</div>

Node.js API

It’s about time meme

The node.js API uses the following libraries.

Below are the actual working version of each library

"cors": "^2.8.5",
"express": "^4.17.3",
"nodemon": "^2.0.15",
"sqlite3": "^5.0.2"
npm install express, cors, nodemon, sqlite3

Almost Done!

The API

The API will first create a database labeled usersdb.sqlite if one does not exist. Then prefills the users table with 9 records. The API paths are all ‘api/user’ or some variation of it. Using Get, Put, Patch, Delete to differentiate the CRUD operations.

Code for app.js

const express = require('express');
const app = express();
const port = 3004;
var sqlite3 = require('sqlite3').verbose()
const cors = require('cors');

const DBSOURCE = "usersdb.sqlite";

let db = new sqlite3.Database(DBSOURCE, (err) => {
    if (err) {
        // Cannot open database
        console.error(err.message)
        throw err
    }
    else {
        // ** EXAMPLE **
        // ** For a column with unique values **
        // email TEXT UNIQUE, 
        // with CONSTRAINT email_unique UNIQUE (email) 

        db.run(`CREATE TABLE Users (
            Id INTEGER PRIMARY KEY AUTOINCREMENT,                
            Name TEXT,             
            Email TEXT,
            Description TEXT,
            Category INTEGER,
            DateModified DATE,
            DateCreated DATE
            )`,
            (err) => {
                if (err) {
                    // Table already created
                } else {
                    // Table just created, creating some rows
                    var insert = 'INSERT INTO Users (Name,  Description, Email, Category, DateCreated) VALUES (?,?,?,?,?)'
                    db.run(insert, ['Eddie Van Halen', 'Lead Guitar for Van Halen', 'eddiev@email.com', 3, Date('now')])
                    db.run(insert, ['Joe Satriani', 'Lead Guitar for Joe Satriani', 'joe@email.com', 3, Date('now')])
                    db.run(insert, ['Dave Mathews', 'Singer Guitarist for Dave Mathews', 'dave@email.com', 1, Date('now')])
                    db.run(insert, ['Sandy Saraya', 'Lead singer of Saraya', 'saraya@email.com', 1, Date('now')])
                    db.run(insert, ['Rosy Tedjedor', 'Singer of My FIrst Crush', 'rosy@email.com', 1, , Date('now')])
                    db.run(insert, ['Aaron Spears', 'Gospil Drumer', 'aaron@email.com', 3, Date('now')])
                    db.run(insert, ['Neil Piert', 'Drummer for Rush', 'neil@email.com', 4, Date('now')])
                    db.run(insert, ['Geddy Lee', 'Bassist for Rush', 'geddy@email.com', 2, Date('now')])
                    db.run(insert, ['Alex Lifeson', 'Lead Guitarist for Rush', 'alex@email.com', 3, Date('now')])
                    db.run(insert, ['Jeff Pacaro', 'Original Drumer for Totto', 'jeff@email.com', 4, Date('now')])
                }
            });
    }
});


module.exports = db

app.use(
    express.urlencoded(),
    cors()
);

app.get('/', (req, res) => res.send('API Root'));

//* G E T   A L L
app.get("/api/users", (req, res, next) => {
    var sql = "SELECT * FROM Users"
    var params = []
    db.all(sql, params, (err, rows) => {
        if (err) {
            res.status(400).json({ "error": err.message });
            return;
        }
        res.json({
            "message": "success",
            "data": rows
        })
    });
});

//* G E T   S I N G L E   P R O D U C T
app.get("/api/user/:id", (req, res, next) => {
    var sql = "SELECT * FROM Users WHERE Id = ?"
    db.all(sql, req.params.id, (err, rows) => {
        if (err) {
            res.status(400).json({ "error": err.message });
            return;
        }
        res.json({
            "message": "success",
            "data": rows
        })
    });
})

//* C R E A T E 
app.post("/api/user", (req, res) => {
    var errors = []
    console.log('req', req.body)
    if (!req.body.Name) {
        errors.push("Name is missing");
    }
    if (!req.body.Email) {
        errors.push("Email is missing");
    }
    if (!req.body.Description) {
        errors.push("Description is missing");
    }
    if (!req.body.Category) {
        errors.push("Category is missing");
    }
    if (errors.length) {
        res.status(400).json({ "error": errors.join(",") });
        return;
    }
    var data = {
        Name: req.body.Name,
        Email: req.body.Email,
        Description: req.body.Description,
        Category: req.body.Category,        
        DateCreated: Date('now')
    }
    var sql = 'INSERT INTO Users (Name, Email, Description, Category, DateCreated) VALUES (?,?,?,?,?)'
    var params = [data.Name, data.Email, data.Description, data.Category ,Date('now')]
    db.run(sql, params, function (err, result) {
        if (err) {
            res.status(400).json({ "error": err.message })
            return;
        }
        res.json({
            "message": "success",
            "data": data,
            "id": this.lastID
        })
    });
})

//* U P D A T E
app.put("/api/user", (req, res, next) => {
    
    var data = [req.body.Name, req.body.Email, req.body.Description, req.body.Category, Date('now'), req.body.Id];
    
    let sql = `UPDATE Users SET 
               Name = ?, 
               Email = ?, 
               Description = ?, 
               Category = ?, 
               DateModified = ?
               WHERE Id = ?`;

    db.run(sql, data, function (err) {
        if (err) {
            return console.error(err.message);
        }
        console.log(`Row(s) updated: ${this.changes}`);

    });

    res.json({
        message: "success",
        data: data,
        changes: this.changes
    })
})

//* D E L E T E
app.delete("/api/user/:id", (req, res, next) => {
    console.log('req', req)
    db.run(
        'DELETE FROM Users WHERE id = ?',
        req.params.id,
        function (err, result) {
            if (err) {
                res.status(400).json({ "error": res.message })
                return;
            }
            res.json({ "message": "Deleted", changes: this.changes })
        });
})

app.listen(port, () => console.log(`API listening on port ${port}!`));