Professional Application Development in MEAN Stack - Part 7
Date Published: 09/17/2018
In this part, we will mostly work on the Admin section. We will develop Menu Mangement page where we can add, update, delete and view the website's menu items. We will also develop our own Data Grid control that would be used in all future Admin pages e.g. manage contacts, pages etc. PS: The source code for this part is private and only available if you subscribe and create the account with Full Stack Hub!


Introduction

In this part, we will mostly work on the Admin section, we will create the Menu Mangement page where we would be able to see all Menu items and functionality to add, update and delete them. We will also create our own Data Grid Component by using multiple Angular Material UI components, this component would be used in all Admin pages to manage the website's content.

Let's Start

It is strongly recommended to read all previous parts before moving to this one, can't promise you would understand all the steps I am going to list down without knowledge of previous parts. 

  1. Get the Part 6 from Fullstack GitHub, clone or download it locally. Run the mazharncoweb project by ng serve -o command and server project by pm2 start environment.json --env development command. Make sure there is no error, if you find any, please comment or send me an email.
  2. In the previous part, we already have created the POST, PUT and DELETE APIs in Node.js server application so we will only work on client side.
  3. Right click on the mazharncoweb folder and select the option Open in Terminal, enter the command: ng g c admin/admin to create the Admin page for Admin section, this would contain links to other management pages e.g. Menu, User Messages, and Page management. Verify that app folder has admin component created in the admin folder. 
  4. Edit the app -> admin -> admin.component.css and add following CSS in it:  
    mat-card
    {
      margin: 0px;
      padding: 0px;
    }
    
    mat-card-noshadow{ 
        background: #ECECF4;
        box-shadow: none !important;
    }
    
    mat-card-content{
      margin: 0px;
      padding: 0px;
    }
    
    .header
    {
    background: #E90C0C;
    border-bottom: 5px solid #790606;
    height: 50px;
    padding-left: 5px;
    }
    
    .subheader
    {
    height: 40px;
    background: #5B5C60;
    }
    
    .subheader_title{
        vertical-align:baseline;
        padding-top: 5px;
        padding-bottom: 0px;
        padding-left: 5px;
        font-size: 13pt;
        font-family: Arial, Helvetica, sans-serif;
        color: #C8CCD2;
    }
    
    .header_title{
        vertical-align:baseline;
        padding-top: 10px;
        padding-bottom: 0px;
        padding-left: 5px;
        font-size: 16pt;
        font-family: Arial, Helvetica, sans-serif;
        color: #FFFFFF;
    }
    
    .card_cntnt
    {
        padding: 15px;
        padding-bottom: 15px;
    }
    
    .card_footer
    {
        text-align: left;
        padding-left: 40px;
    }
    
    .frm-ctrl {
        width: 90%;
    }
    
    .icon_align
    {
        vertical-align: middle;
    }
    .phone_cntnt
    {
        font-weight: bold;
    }
    ​
  5. In above CSS, we are mostly fixing the Angular Material UI Card component header and title, I am purposely keeping the header background color red to highlight the Admin screen, feel free to play with CSS and change the font style and color.  
  6. Edit the app -> admin -> admin.component.html and replace the existing content with the following HTML:   
    <div>
      <mat-card>
        <mat-card-header class="header">
          <mat-card-title class="header_title"><i class="material-icons">security</i> Manage Site Content</mat-card-title>
        </mat-card-header>
        <mat-card-content class="card_cntnt">
          <div>
            <div>
              <button [routerLink]="['managemenu']" mat-button color="primary"><i class="material-icons">menu</i>    Menus Management</button>
            </div>
            <hr>
            <div>
              <button [routerLink]="['usermsg']" mat-button color="primary"><i class="material-icons">message</i>    User Messages</button>
            </div>
            <hr>
            <div>
                <button [routerLink]="['managepage']" mat-button color="primary"><i class="material-icons">pages</i>    Pages Management</button>
            </div>
          </div>
        </mat-card-content>
      </mat-card>
    </div>​
  7. The above HTML is quite self-explanatory, we have Material Card with three buttons; Menu Management, User Messages, and Pages Management. The [routerLink] directive is holding the page route information, we will define these route in the app-routing.module.ts routing table.
  8. Don't worry about admin.component.ts since we are not adding any functionality here.
  9. Next, as discussed in step 7, let's create the routes in the app-routing.module.ts file, edit the app -> app-routing.module.ts file and replace it content with following, you would see at the end, we added routes for admin and maangemenu for now, we will add rest of the routes later:   
    import { NgModule } from '@angular/core';
    import { Routes, RouterModule } from '@angular/router';
    import { HomeComponent } from "./client/home/home.component";
    import { ContactComponent } from "./client/contact/contact.component";
    import { ViewPageComponent } from "./client/view-page/view-page.component";
    import { AdminComponent } from './admin/admin/admin.component';
    import { MenuListComponent } from './admin/menu/menu-list/menu-list.component';
    
    const routes: Routes = [
      {
        path: '',
        redirectTo: 'home',
        pathMatch: 'full'
      },
      {
        path: 'home',
        component: HomeComponent,
      },
      {
        path: 'contact',
        component: ContactComponent,
      },
      {
        path: 'page/:id',
        component: ViewPageComponent,
      },
      {
        path:'admin',
        component: AdminComponent
      },
      {
        path:'admin/managemenu',
        component: MenuListComponent,
      }
    ];
    
    @NgModule({
      imports: [RouterModule.forRoot(routes)],
      exports: [RouterModule]
    })
    export class AppRoutingModule { }
    ​
  10. So far good, run the application and browse the http://localhost:4200/admin URL, you should land to Admin Index page with three links and a cool red color header. 
  11. Next step is the important one, we will create our custom grid that would take the input parameters and create the data grid with Add, Update and Delete operation. I am not going to explain the inner detail of the data grid since it needs a separate article to explain it in detail that I would write in future after converting it to NPM package. 
  12. Go ahead and in maharncoweb terminal, run the commandng g c shared/datagrid , you would see a new datagrid folder in a shared folder with a new component. Edit the datagrid.component.html and replace its content with the following:   
    <div>
      <div class="row">
        <div class="col-md-10">
          <mat-form-field floatPlaceholder="never">
            <input matInput #filter placeholder="{{filterPlaceholder}}">
          </mat-form-field>
        </div>
        <div class="col-md-2 text-right">
          <button (click)="click(1,null)" *ngFor="let hb of hdrBtn" mat-raised-button color="primary">{{hb.title}}</button>
        </div>
      </div>
      <div></div>
      <div>
        <mat-table #table [dataSource]="dataSource" matSort>
    
          <ng-container *ngFor="let clmn of displayedColumns" matColumnDef="{{clmn.variable}}">
            <mat-header-cell *matHeaderCellDef mat-sort-header>{{clmn.display}}</mat-header-cell>
            <mat-cell *matCellDef="let element">
              <span *ngIf="clmn.type=='text'">{{element[clmn.variable]}}</span>
              <span *ngIf="clmn.type=='btn'" style="text-align: right"><button mat-raised-button color="warn" (click)="click(clmn.action,element)" >{{clmn.variable}}</button></span>
            </mat-cell>
          </ng-container>
    
          <mat-header-row *matHeaderRowDef="displayedColumnsVar"></mat-header-row>
          <mat-row *matRowDef="let row; columns: displayedColumnsVar;"></mat-row>
        </mat-table>
      </div>
      <div *ngIf="menudataObj && menudataObj.data.length==0">
        There is no recod available!
      </div>
      <mat-paginator #paginator [length]="menudataObj.data.length" [pageIndex]="0" [pageSize]="10" [pageSizeOptions]="[10,20,50,100]">
      </mat-paginator>
    </div>​
  13. We are using the built-in Material UI Table and Paginator components that really help us to create the awesome grid with the minimum HTML code.
  14. Next, edit the datagrid.component.ts and replace its content with the following:    
    import { Component, OnInit, ViewChild, ElementRef, Input, Output, EventEmitter } from '@angular/core';
    import { DataService } from "../../service/data/data.service";
    import { Observable } from 'rxjs/Observable';
    import 'rxjs/add/operator/debounceTime';
    import 'rxjs/add/operator/distinctUntilChanged';
    import 'rxjs/add/observable/fromEvent';
    import { MatPaginator } from "@angular/material/paginator";
    import { MatSort } from "@angular/material/sort";
    import { GridData, GridDataSource } from "./grid-ops";
    import { MatDialog } from '@angular/material';
    
    @Component({
      selector: 'app-datagrid',
      templateUrl: './datagrid.component.html',
      styleUrls: ['./datagrid.component.css']
    })
    
    export class DatagridComponent implements OnInit {
    
      @Input() filterPlaceholder: string;
      @Input() displayedColumns;
      @Input() gridBtn;
      @Input() hdrBtn;
      @Input() data;
    
      @Output() btnclick: EventEmitter<any> = new EventEmitter<any>();
    
      @ViewChild(MatPaginator) paginator: MatPaginator;
      @ViewChild('filter') filter: ElementRef;
      @ViewChild(MatSort) sort: MatSort;
    
      displayedColumnsVar: string[] = [];
    
      menudataObj = null;
      dataSource = null;
    
      constructor(private _dataService: DataService, private dialog: MatDialog) { }
    
      ngOnInit() {
        this.displayedColumns.forEach(element => this.displayedColumnsVar.push(element.variable));
        this.loadGridData();
        Observable.fromEvent(this.filter.nativeElement, 'keyup')
          .debounceTime(150)
          .distinctUntilChanged()
          .subscribe(() => {
            if (!this.dataSource) { return; }
            this.dataSource.filter = this.filter.nativeElement.value;
          });
      }
    
      ngOnChanges(changes: any) {
        if (JSON.stringify(changes).indexOf("data") != -1 && this.dataSource)
          this.dataSource.updateData();
      }
    
      public loadGridData() {
        this.menudataObj = new GridData(this.data);
        this.dataSource = new GridDataSource(this.paginator, this.menudataObj,
          this.sort, this.displayedColumnsVar);
    
      }
    
      click(actn: string, row: any): void {
        let rtn = {
          action: actn,
          row: row
        };
        this.btnclick.emit(rtn);
      }
    
    }
    ​
  15. As promised, I would explain this code some other time because that will make this article too long and boring. Feel free to dig into it yourself.    
  16. We need to create one more class to facilitate the data grid, in the same mazharncoweb terminal, run the command:  ng g class shared/datagrid/grid-ops
  17. Edit the app -> shared -> datagrid -> grid-ops and replace its content with the following:   
    import { BehaviorSubject } from "rxjs/BehaviorSubject";
    import { DataSource } from "@angular/cdk/collections";
    import { MatPaginator, MatSort } from "@angular/material/material";
    import { Observable } from "rxjs/Observable";
    import 'rxjs/add/observable/merge';
    
    export class GridData {
        outputData: any[];
        dataChange: BehaviorSubject<any[]> = new BehaviorSubject<any[]>([]);
        get data(): any[] { return this.dataChange.value; }
    
        constructor(private inptData: any) {
            this.loadData();
        }
    
        loadData() {
            this.inptData.subscribe(data => {
                this.outputData = data;
                this.dataChange.next(data);
            });
        }
    }
    
    export class GridDataSource extends DataSource<any> {
    
        _filterChange = new BehaviorSubject('');
        get filter(): string { return this._filterChange.value; }
        set filter(filter: string) { this._filterChange.next(filter); }
    
        constructor(private _paginator: MatPaginator,
            private outputData: GridData,
            private _sort: MatSort,
            private displayedColumns: string[]) { super() }
    
        connect(): Observable<any[]> {
            const displayDataChanges = [
                this.outputData.dataChange,
                this._filterChange,
                this._paginator.page,
                this._sort.sortChange,
            ];
    
            return Observable.merge(...displayDataChanges).map(() => {
    
                const startIndex = this._paginator.pageIndex * this._paginator.pageSize;
                return this.getSortedData().slice().filter((item: any) => {
                    let itemarray: string;
                    this.displayedColumns.forEach(x => { itemarray += item[x] });
                    let searchStr = (itemarray).toLowerCase();
                    return searchStr.indexOf(this.filter.toLowerCase()) != -1;
                }).splice(startIndex, this._paginator.pageSize);
    
            });
        }
    
        updateData() {
            if (this.outputData)
                this.outputData.loadData();
        }
    
        getMainData(): any[] {
            return this.outputData.outputData;
        }
    
        getSortedData(): any[] {
            const data = this.outputData.data.slice();
            if (!this._sort.active || this._sort.direction == '') { return data; }
    
            return data.sort((a, b) => {
                let propertyA: number | string = '';
                let propertyB: number | string = '';
    
                [propertyA, propertyB] = [a[this._sort.active], b[this._sort.active]];
    
                let valueA = isNaN(+propertyA) ? propertyA : +propertyA;
                let valueB = isNaN(+propertyB) ? propertyB : +propertyB;
    
                return (valueA < valueB ? -1 : 1) * (this._sort.direction == 'asc' ? 1 : -1);
            });
        }
    
        disconnect() { }
    }
    ​
  18. Most of the code, I took and modified from Angular Material UI Paginator and Sort components, you can also take a look there and try to make a sense what is being modified in this class. 
  19. Next, let's modify the enum.ts and add the CRUD operation enumeration, I also modified it to avoid hard-coded strings in the rest of components. Edit the app -> shared -> enum.ts and replace its content with the following:    
    export enum DBOperation {
        create = 1,
        update = 2,
        delete =3
    }
    
    export enum ResponseSnackbar {
        Sucess = 'Sucess',
        Error = 'Error',
        Pending ='Pending'
    }
    
    export enum IMenuType {
        Main = 'Main',
        Sub = 'Sub'
    }
    export enum IYesNo {
        Yes = 'Yes',
        No = 'No'
    }
    export enum IActiveDead {
        Active = 'Active',
        Dead = 'Dead'
    }
    export enum IPageStatus {
        Published = 'Published',
        Draft = 'Draft',
        Initiated = 'Initiated',
        Dead = 'Dead'
    }​
  20. You can see in the above file, we are introducing DBOperation enum that would be used to pass the parameters to the grid component, we won't need to pass a hard-coded string to identify the operation. Same goes to other enumerations, you will see these enumerations in action in upcoming steps. 
  21. For all delete operation, e.g. delete a menu item, we want to create an extra layer of confirmation after viewing the record, a user wants to delete. Let's create a delete confirmation component that would be used in all management page. In mazharncoweb terminal, enter the command: ng g c shared/messages/confirm-delete
  22. Edit the app -> shared -> messages -> confirm-delete.component.css and replace its content with the following:  
    .header{
        background: #E90C0C;
        border-bottom: 5px solid #790606;
        height: 50px;
        padding-left: 5px;
        }
      
        .header_title{
            vertical-align:baseline;
            padding-top: 10px;
            padding-bottom: 0px;
            padding-left: 5px;
            font-size: 16pt;
            font-family: Arial, Helvetica, sans-serif;
            color: rgba(255, 255, 255, 0.85);
        }
        mat-card{
          margin: 0px;
          padding: 0px;
      
        }
        mat-card-content{
          margin: 0px;
          padding: 0px;
       
          height: 170px;
          overflow:hidden;
        }
        
      .mat-dialog-container {
          padding: 0 !important;
          margin: 0 !important;
          }
      
      .frm-ctrl {
          width: 100%;
       }
      
       .card_cntnt
       {
           padding: 20px;
       }
       .msg
       {
           font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
           font-size: large;
        padding-top: 10px;
       }
       .footer
       {
           padding-top: 10px;
           text-align: center;
       }​
  23. Pretty much same CSS we created for Admin component, feel free to change it and make it more attractive.
  24. Edit the app -> shared -> messages -> confirm-delete.component.html and replace its content with the following:  
    <mat-card>
      <mat-card-header class="header">
        <mat-card-title class="header_title"><i class="material-icons">delete</i> Verify one more time!</mat-card-title>
      </mat-card-header>
      <mat-card-content class="card_cntnt">
        <div class="msg">
          <p><i class="material-icons">live_help</i> Are you sure to delete the selected record?</p>
        </div>
        <hr>
        <div class="footer">
          <button mat-raised-button color="primary" [mat-dialog-close]="true" tabindex="2">Yes</button>
          <button mat-raised-button color="warn" (click)="onNoClick()" tabindex="-1">No</button>
        </div>
      </mat-card-content>
    </mat-card>​
  25. There are one verification message and two buttons at the bottom, this component would appear in the dialog box like traditional JavaScript confirm but with our custom style.
  26. Edit the app -> shared -> messages -> confirm-delete.component.ts and replace its content with the following:   
    import { Component } from "@angular/core";
    import { MatDialogRef } from "@angular/material";
    
    @Component({
      selector: 'confirm-delete-modal',
      templateUrl: './confirm-delete.component.html',
      styleUrls: ['./confirm-delete.component.css']
    })
    export class ConfirmDeleteComponent {
    
      constructor(
        public dialogRef: MatDialogRef<ConfirmDeleteComponent>) { }
    
      onNoClick(): void {
        this.dialogRef.close();
      }
    
    }
    ​
  27. You can see we are using Angular Material Dialog Ref component that is taking the Confirm Delete component reference which will open this component in the dialog box, that's very simple, any component you want to open in the dialog box, you can use the same approach. 
  28. The onNoClick function is attached to the Cancel button and it simply hides the dialog box.
  29. Now we have Confirm Delete component ready to be used, let's update the util.ts to create a function to open the dialog box. In management components e.g. Manu Management we will call a method from util.ts. Edit the app -> shared -> util.ts and replace its content with the following:  
    import { Injectable } from "@angular/core";
    import { MatSnackBar, MatDialog } from "@angular/material";
    import { ResponseSnackbar } from "./enum";
    import { Observable } from "rxjs/Observable";
    import { ConfirmDeleteComponent } from "./messages/confirm-delete/confirm-delete.component";
    
    @Injectable()
    export class Util {
        constructor(private snackBar: MatSnackBar,public dialog: MatDialog) { }
    
        openSnackBar(message: string, action: ResponseSnackbar): void {
            let dur = action == (ResponseSnackbar.Error || ResponseSnackbar.Pending) ? -1 : 2000;
            this.snackBar.open(message, action.toString(), {
                duration: dur,
            });
        }
    
        confirmDelete(): Observable<any> {
            let dialogRef = this.dialog.open(ConfirmDeleteComponent, {
              width: '500px'
            });
            return dialogRef.afterClosed();
          }
    
          getEnumArray(enumObj: any) {
            return Object.keys(enumObj).map(function (type) {
              return enumObj[type];
            });
          }
          
    }
    ​
  30. You can see, we added the confirmDelete function that would return true or false based on the user selection and then we will decide accordingly to delete the record or not. 
  31. If you remember our APIs client, we only had GET method, since we are creating a Menu Mangement component with CRUD operation, we need the POST, PUT and DELETE methods. Edit the app -> service -> data -> data.service.ts and replace its content as following:   
    import { Injectable } from '@angular/core';
    import { Observable } from "rxjs/Observable";
    import 'rxjs/add/operator/map';
    import 'rxjs/add/operator/do';
    import 'rxjs/add/operator/catch';
    import { environment } from "../../../environments/environment";
    import { HttpClient, HttpHeaders } from '@angular/common/http';
    
    @Injectable()
    export class DataService {
    
      SERVER_URL = environment.api_url;
    
      constructor(public _http: HttpClient) { }
    
      get(url: string): Observable<any> {
        let headers = new HttpHeaders({ 'Content-Type': 'application/json' });
        return this._http.get(this.SERVER_URL + url, { headers: headers });
      }
    
      post(url: string, model: any): Observable<any> {
        let body = JSON.stringify(model);
        let headers = new HttpHeaders({ 'Content-Type': 'application/json' });
        return this._http.post(this.SERVER_URL + url, body, { headers: headers });
      }
    
      put(url: string, model: any): Observable<any> {
        let body = JSON.stringify(model);
        let headers = new HttpHeaders({ 'Content-Type': 'application/json' });
        return this._http.put(this.SERVER_URL + url, body, { headers: headers });
      }
    
      delete(url: string): Observable<any> {
        let headers = new HttpHeaders({ 'Content-Type': 'application/json' });
        return this._http.delete(this.SERVER_URL + url, { headers: headers });
      }
    
    }
    ​
  32. Cool, so this is pretty clean APIs client with all required methods. It is always wise to capture the error, that's an assignment for you to read the latest documentation from the Angular official website and implement it. Though, this code would work absolutely fine with no error.
  33. In above service, we are using the HTTPClientModule that was not imported earlier so let's add it in AppModule. Edit the app -> app.module.ts and replace its content with the following:   
    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
    import { FormsModule, ReactiveFormsModule } from '@angular/forms';
    //Http Module For GET, POST, PUT and DELETE RESTful APIs
    import { HttpModule } from '@angular/http';
    
    import {
      MatAutocompleteModule,
      MatButtonModule,
      MatButtonToggleModule,
      MatCardModule,
      MatCheckboxModule,
      MatChipsModule,
      MatDatepickerModule,
      MatDialogModule,
      MatExpansionModule,
      MatGridListModule,
      MatIconModule,
      MatInputModule,
      MatListModule,
      MatMenuModule,
      MatNativeDateModule,
      MatPaginatorModule,
      MatProgressBarModule,
      MatProgressSpinnerModule,
      MatRadioModule,
      MatRippleModule,
      MatSelectModule,
      MatSidenavModule,
      MatSliderModule,
      MatSlideToggleModule,
      MatSnackBarModule,
      MatSortModule,
      MatTableModule,
      MatTabsModule,
      MatToolbarModule,
      MatTooltipModule,
      MatStepperModule
    } from '@angular/material';
    
    import { HttpClientModule } from '@angular/common/http';
    
    import { AppComponent } from './app.component';
    import 'hammerjs';
    
    import { AgmCoreModule } from '@agm/core';
    import { NgxCarouselModule } from 'ngx-carousel';
    
    import { HomeComponent } from './client/home/home.component';
    import { AppRoutingModule } from "./app-routing.module";
    import { ContactComponent } from './client/contact/contact.component';
    import { DataService } from './service/data/data.service';
    import { Util } from "./shared/util";
    import { ViewPageComponent } from './client/view-page/view-page.component';
    import { FooterComponent } from './footer.component';
    import { AdminComponent } from './admin/admin/admin.component';
    import { DatagridComponent } from './shared/datagrid/datagrid.component';
    import { ConfirmDeleteComponent } from './shared/messages/confirm-delete/confirm-delete.component';
    import { ManageMenuComponent } from './admin/menu/manage-menu/manage-menu.component';
    import { MenuListComponent } from './admin/menu/menu-list/menu-list.component';
    
    @NgModule({
      declarations: [
        AppComponent,
        HomeComponent,
        ContactComponent,
        ViewPageComponent,
        FooterComponent,
        AdminComponent,
        DatagridComponent,
        ConfirmDeleteComponent,
        ManageMenuComponent,
        MenuListComponent
      ],
      imports: [
        HttpClientModule,
        HttpModule,
        FormsModule,
        ReactiveFormsModule,
        AgmCoreModule.forRoot({
          apiKey: 'AIzaSyCKHGctDoGx1_YdAbRsPlJYQqlQeC6kR2E'
        }),
        NgxCarouselModule,
        BrowserModule,
        AppRoutingModule,
        BrowserAnimationsModule,
        MatButtonModule,
        MatCheckboxModule,
        MatCardModule,
        MatInputModule,
        MatRadioModule,
        MatSelectModule,
        MatTabsModule,
        MatSortModule,
        MatPaginatorModule,
        MatTableModule,
        MatSnackBarModule,
        MatIconModule,
        MatDialogModule,
        MatAutocompleteModule,
        MatGridListModule,
        MatMenuModule,
        MatProgressBarModule,
        MatExpansionModule,
        MatTooltipModule,
        MatSlideToggleModule
      ],
      providers: [DataService,Util],
      bootstrap: [AppComponent],
      entryComponents: [ManageMenuComponent,ConfirmDeleteComponent]
    })
    export class AppModule { }
    ​
  34. OK, so far we created the baseline for Menu Mangement components development, we need a data grid, Confirm Delete and updated APIs client with all CRUD HTTP verbs (PUT, POST and DELETE) that we created in previous steps, we are ready to create a Menu Management components. There would be two components for all manage components, one would have a data grid with all records and Add, Update and Delete buttons. Second would be displayed in the dialog box where you can Add new, Update and Delete existing record. 
  35. In mazharncoweb terminal, run the command: ng g c admin/menu/menuList , this component would have data grid we created in previous steps. Edit the app -> admin -> menu -> menu-list.component.css and replace its content (if any) with following:  
    mat-card
    {
      margin: 0px;
      padding: 0px;
    }
    
    mat-card-noshadow{ 
        background: #ECECF4;
        box-shadow: none !important;
    }
    
    mat-card-content{
      margin: 0px;
      padding: 0px;
    }
    
    .header
    {
    background: #E90C0C;
    border-bottom: 5px solid #790606;
    height: 50px;
    padding-left: 5px;
    }
    
    .subheader
    {
    height: 40px;
    background: #5B5C60;
    }
    
    .subheader_title{
        vertical-align:baseline;
        padding-top: 5px;
        padding-bottom: 0px;
        padding-left: 5px;
        font-size: 13pt;
        font-family: Arial, Helvetica, sans-serif;
        color: #C8CCD2;
    }
    
    .header_title{
        vertical-align:baseline;
        padding-top: 10px;
        padding-bottom: 0px;
        padding-left: 5px;
        font-size: 16pt;
        font-family: Arial, Helvetica, sans-serif;
        color: #FFFFFF;
    }
    
    .card_cntnt
    {
        padding: 15px;
        padding-bottom: 15px;
    }
    
    .card_footer
    {
        text-align: left;
        padding-left: 40px;
    }
    
    .frm-ctrl {
        width: 90%;
    }
    
    .icon_align
    {
        vertical-align: middle;
    }
    .phone_cntnt
    {
        font-weight: bold;
    }
    ​
  36. This is again our famous CSS, no need to explain it again.
  37. Next, let's edit the app -> admin -> menu -> menu-list.component.html and replace its contents with the following:   
    <div>
      <mat-card>
        <mat-card-header class="header">
          <mat-card-title class="header_title"><i class="material-icons">menu</i> Menus Management</mat-card-title>
        </mat-card-header>
        <mat-card-content class="card_cntnt">
          <div>
            <app-datagrid
            [displayedColumns]="displayedColumns"
            [filterPlaceholder]="filterPlaceholder"
            [hdrBtn]="hdrBtn"
            [data]="data"
            (btnclick)="gridaction($event)"
            ></app-datagrid>
          </div>
        </mat-card-content>
      </mat-card>
    </div>
    <button [routerLink]="['../../admin']" mat-button color="primary">Back to Menu</button>​
  38. In above HTML, we are using our newly created data grid and passing the values to its properties that are defined in a menu-list.component.ts file. It looks pretty clean to me, the properties name somehow give information about their purpose but it would be more clear in component's typescript. 
  39. Edit the app -> admin -> menu -> menu-list.component.ts and replace its content with the following:   
    import { Component, OnInit } from '@angular/core';
    import { DBOperation } from "../../../shared/enum";
    import { DataService } from "../../../service/data/data.service";
    import { Util } from "../../../shared/util";
    import { IMenu } from "../../../model/menu";
    import { MatDialog } from "@angular/material/dialog";
    import { ManageMenuComponent } from '../manage-menu/manage-menu.component';
    
    @Component({
      selector: 'app-menulist',
      templateUrl: './menu-list.component.html',
      styleUrls: ['./menu-list.component.css']
    })
    export class MenuListComponent implements OnInit {
    
      dbops: DBOperation;
      modalTitle: string;
      modalBtnTitle: string;
      menu: IMenu;
      data: any;
      url: string = "/api/menu";
    
      displayedColumns: any[] = [
        {
          display: 'Menu Name',
          variable: 'MenuName',
          type: 'text'
        },
        {
          display: 'Menu Code',
          variable: 'MenuCode',
          type: 'text'
        },
        {
          display: 'Menu Order',
          variable: 'MenuOrder',
          type: 'text'
        },
        {
          display: 'Parent Menu Code',
          variable: 'ParentMenuCode',
          type: 'text'
        },
        {
          display: 'Status',
          variable: 'Status',
          type: 'text'
        },
        {
          display: 'Message',
          variable: 'MenuType',
          type: 'text'
        },
        {
          display: '',
          variable: 'Edit',
          action: DBOperation.update,
          type: 'btn'
        },
        {
          display: '',
          variable: 'Delete',
          action: DBOperation.delete,
          type: 'btn'
        }
      ];
    
      hdrBtn: any[] = [
        {
          title: 'Add New Menu',
          keys: [],
          action: DBOperation.create
        }];
    
      filterPlaceholder: string = "Search Menu";
      constructor(private _dataService: DataService, private dialog: MatDialog, private _util: Util) {
       
      }
    
      ngOnInit() {
        this.loadData();
      }
    
      loadData() {
        this.data = this._dataService.get(this.url).map(data => data.data);
      }
    
      openDialog() {
        let dialogRef = this.dialog.open(ManageMenuComponent);
        dialogRef.componentInstance.dbops = this.dbops;
        dialogRef.componentInstance.modalTitle = this.modalTitle;
        dialogRef.componentInstance.modalBtnTitle = this.modalBtnTitle;
        dialogRef.componentInstance.menu = this.menu;
    
        dialogRef.afterClosed().subscribe(result => {
          this.loadData();
        });
      }
    
      gridaction(gridaction: any): void {
        switch (gridaction.action) {
          case DBOperation.create:
            this.addMenu();
            break;
          case DBOperation.update:
            this.editMenu(gridaction.row);
            break;
          case DBOperation.delete:
            this.deleteMenu(gridaction.row);
            break;
        }
      }
    
      addMenu() {
        this.dbops = DBOperation.create;
        this.modalTitle = "Add New Menu";
        this.modalBtnTitle = "Add";
        this.openDialog();
      }
      editMenu(menu: IMenu) {
        this.dbops = DBOperation.update;
        this.modalTitle = "Edit Menu";
        this.modalBtnTitle = "Update";
        delete menu["__v"];
        this.menu = <IMenu>menu;
        this.openDialog();
      }
      deleteMenu(menu: IMenu) {
        this.dbops = DBOperation.delete;
        this.modalTitle = "Confirm to Delete?";
        this.modalBtnTitle = "Delete";
        delete menu["__v"];
        this.menu = <IMenu>menu;
        this.openDialog();
      }
    
    }​
  40. This is an important code to understand so let's dig into it a little bit:
    1. dbops: DBOperation: Here we are creating DBOpertaion enumeration type variable dbops so that we don't use hard-code string to represent CRUD operation.
    2. menu: IMenu: I am a big fan of MVC architecture so always like to declare the interface or class for Model. We created the IMenu in the previous article to represent the menu records returned from the database and match the data schema.
    3. displayedColumns: This is one of input variable to the data grid, it is an object containing grid column title, corresponding variable (the JSON response object's key name), data type and for last two button type items, we are also specifying the action i.e. update and delete. 
    4. hdrBtn: Also an input variable to the data grid, it contains a list of all buttons that would be displayed on top of the data grid. Currently, it has only one Add New Menu button.
    5. loadData(): This method will load all menu records from the database and store it in data variable, this method is being called from ngOnInit() event that triggers as soon page is being initiated.   
    6. gridaction(): This is an output method of the data grid, in a displayedColumns object, you can see we are passing the action for button type items. In data grid when user would click on Add, Edit or Update button, this action would be returned back to gridaction()'s event argument along the complete data row from where we can get data ID to identify which record needs to be updated or deleted, of course for add operation, we only need action and not a data row. In the function body, you can see, we have switch statement on action and calling the corresponding method based on action and passing the data row in case of the update and delete.
    7. CRUD FUnctions: addMenu(), editMenu() and deleteMenu() are initializing the dialog box header and button title, edit and delete menu are also getting the data row return from data grid and assigning it to IMenu interface casted menu variable that would be used to show the data to user before updating or deleting. All function calls one common method openDialog at the end.
    8. openDialog(): This method will open the CRUD screens in the dialog box, we will create the CRUD component in the next step. It also passes the data to Manage Menu (CRUD) component that we initialize in Add, Edit and Delete Menu functions. In the end, we are attaching the afterClosed event to update the data in the data grid as soon user performs any CRUD operation. This would avoid any refresh button. 
  41. Now let's create a Manage Menu component where the user can actually perform CRUD operation on Menu, in mazharncoweb terminal run the command: ng g c admin/menu/manageMenu 
  42. Edit the app -> admin -> menu -> manage-menu -> manage-menu.component.css and replace (if any) text with following:  
    .header{
        background: #E90C0C;
        border-bottom: 5px solid #790606;
        height: 50px;
        padding-left: 5px;
        }
      
        .header_title{
            vertical-align:baseline;
            padding-top: 10px;
            padding-bottom: 0px;
            padding-left: 5px;
            font-size: 16pt;
            font-family: Arial, Helvetica, sans-serif;
            color: rgba(255, 255, 255, 0.85);
        }
        mat-card{
          margin: 0px;
          padding: 0px;
      
        }
        mat-card-content{
          margin: 0px;
          padding: 0px;
          width: 600px;
          height: 500px;
        }
        
      .mat-dialog-container {
          padding: 0 !important;
          margin: 0 !important;
          }
      
      .frm-ctrl {
          width: 100%;
       }
      
       .card_cntnt
       {
           padding: 20px;
       }
       .footer
       {
           padding-top: 20px;
           text-align: right;
       }​
  43. Next edit the app -> admin -> menu -> manage-menu -> manage-menu.component.html and replace its content with the following:   
    <mat-card>
      <mat-card-header class="header">
        <mat-card-title class="header_title"><i class="material-icons">menu</i> {{modalTitle}}</mat-card-title>
      </mat-card-header>
      <mat-card-content class="card_cntnt">
        <div>
          <form novalidate (ngSubmit)="onSubmit(menuFrm)" [formGroup]="menuFrm">
            <div>
              <mat-form-field class="frm-ctrl">
                <input matInput placeholder="Menu Name" formControlName="MenuName">
                <mat-error *ngIf="menuFrm.controls['MenuName'].errors?.required">
                    Menu Name is required!
                </mat-error>
              </mat-form-field>
            </div>
            <div>
              <mat-form-field class="frm-ctrl">
                <input matInput placeholder="Menu Code" formControlName="MenuCode">
                <mat-error *ngIf="menuFrm.controls['MenuCode'].errors?.required">
                    Menu Code is required!
                </mat-error>
              </mat-form-field>
            </div>
            <div>
              <mat-form-field class="frm-ctrl">
                <input matInput placeholder="Menu URL" formControlName="MenuUrl">
                <mat-error *ngIf="menuFrm.controls['MenuUrl'].errors?.required">
                    Menu Url is required!
                </mat-error>
              </mat-form-field>
            </div>
            <div>
              <mat-form-field class="frm-ctrl">
                <input matInput placeholder="Menu Order" formControlName="MenuOrder">
                <mat-error *ngIf="menuFrm.controls['MenuOrder'].errors?.required">
                    Menu Order is required!
                </mat-error>
              </mat-form-field>
            </div>
            <div class="frm-ctrl">
              <mat-form-field class="frm-ctrl">
                <input matInput placeholder="Group Name" formControlName="GroupName">
                <mat-error *ngIf="menuFrm.controls['GroupName'].errors?.required">
                    Group Name is required!
                </mat-error>
              </mat-form-field>
            </div>
    
            <div>
              <mat-radio-group formControlName="MenuType" class="frm-ctrl">
                <mat-radio-button *ngFor="let mtype of menuType" [value]="mtype">
                  {{mtype}}
                </mat-radio-button>
              </mat-radio-group>
            </div>
    
            <div class="frm-ctrl">
              <mat-radio-group formControlName="Status" class="frm-ctrl">
                <mat-radio-button *ngFor="let sts of status" [value]="sts">
                  {{sts}}
                </mat-radio-button>
              </mat-radio-group>
            </div>
    
            <div>
              <mat-form-field class="frm-ctrl">
                <mat-select placeholder="Parent Menu Code" formControlName="ParentMenuCode">
                  <mat-option *ngFor="let menu of menuDDL" [value]="menu.key">
                    {{ menu.value }}
                  </mat-option>
                </mat-select>
              </mat-form-field>
            </div>
            <div class="footer">
              <button color="warn" type="button" mat-raised-button (click)="dialogRef.close()">Cancel</button>&nbsp;
              <button type="submit" color="primary" [disabled]="menuFrm.invalid" mat-raised-button>{{modalBtnTitle}}</button></div>
          </form>
        </div>
      </mat-card-content>
    </mat-card>​
  44. You can go through this code, quite easy to understand, we are using the Model Driven Form and specifying all the input fields including, text boxes, radio button, and dropdown list. The model header and button title are coming from MenuList component as described in previous steps, just to remind you, this component would be opened in the dialog box. 
  45. Edit the app -> admin -> menu -> manage-menu -> manage-menu.component.ts and replace its content with the following:    
    import { Component, OnInit } from '@angular/core';
    import { IMenu } from "../../../model/menu";
    import { DBOperation, ResponseSnackbar, IMenuType, IActiveDead } from "../../../shared/enum";
    import { FormBuilder, FormGroup, Validators } from "@angular/forms";
    import { Util } from "../../../shared/util";
    import { DataService } from "../../../service/data/data.service";
    import { MatDialogRef } from "@angular/material/dialog";
    
    @Component({
      selector: 'app-managemenu',
      templateUrl: './manage-menu.component.html',
      styleUrls: ['./manage-menu.component.css']
    })
    export class ManageMenuComponent implements OnInit {
      POST_URL: string = "/api/menu";
      RST_URL: string = "/api/menu/id";
      GET_ALL_URL: string = "/api/menu"
    
      menuDDL: any;
      menu: IMenu;
      menus: IMenu[];
    
      dbops: DBOperation;
      modalTitle: string;
      modalBtnTitle: string;
      selectedOption: string;
    
      menuFrm: FormGroup;
    
      menuType = this._util.getEnumArray(IMenuType);// this._ddlList.menuType;
      status = this._util.getEnumArray(IActiveDead);// this._ddlList.activeDead;
    
      constructor(private _fb: FormBuilder, private _dataService: DataService,
        private _util: Util, public dialogRef: MatDialogRef<ManageMenuComponent>) { }
    
      ngOnInit() {
        this.menuDDL = this.getMenuddl();
        this.menuFrm = this._fb.group({
          _id: [''],
          MenuName: ['', [Validators.required, Validators.maxLength(50)]],
          MenuCode: ['', [Validators.required, Validators.maxLength(50)]],
          MenuUrl: [''],
          MenuOrder: ['', [Validators.required]],
          MenuType: ['', [Validators.required]],
          Status: ['', [Validators.required]],
          DateAdded: [''],
          DateUpdated: [''],
          ParentMenuCode: [''],
          GroupName: ['']
        });
        if (this.dbops != DBOperation.create)
          this.menuFrm.setValue(this.menu);
    
        if (this.dbops == DBOperation.delete)
          this.menuFrm.disable();
    
        if (this.dbops == DBOperation.update)
          this.menuFrm.controls["MenuCode"].disable();
      }
    
    
      getMenuddl(): any[] {
        let menuURL = "/api/menu";
        let dataMenu: { key: string, value: string }[] = [];
        this._dataService.get(menuURL).subscribe(
          data => {
            if (data.success == true) //Success
            {
              data.data.forEach((element: any) => {
                dataMenu.push({ key: element.MenuCode, value: element.MenuName });
              });
            }
            else {
              alert(JSON.stringify(data.msg));
            }
          },
          error => {
          }
        );
        return dataMenu;
      }
    
    
      onSubmit(formData: any) {
        switch (this.dbops) {
          case DBOperation.create:
            delete formData.value._id;
            this._dataService.post(this.POST_URL, formData.value).subscribe(
              data => {
                if (data.success == true) //Success
                {
                  this._util.openSnackBar(data.msg, ResponseSnackbar.Sucess);
                  this.dialogRef.close();
                }
                else {
                  this._util.openSnackBar(JSON.stringify(data.msg), ResponseSnackbar.Error);
                }
              },
              error => {
              });
            break;
          case DBOperation.update:
            this._dataService.put(this.POST_URL, formData.value).subscribe(
              data => {
                if (data.success == true) //Success
                {
                  this._util.openSnackBar(data.msg,  ResponseSnackbar.Sucess);
                  this.dialogRef.close();
                }
                else {
                  this._util.openSnackBar(JSON.stringify(data.msg), ResponseSnackbar.Error);
                }
              },
              error => {
              });
            break;
          case DBOperation.delete:
            this._util.confirmDelete().subscribe(result => {
              if (<boolean>result == true) {
                this._dataService.delete(this.RST_URL.replace("id", formData.value._id)).subscribe(
                  data => {
                    if (data.success == true) //Success
                    {
                      this._util.openSnackBar(data.msg, ResponseSnackbar.Sucess);
                      this.dialogRef.close();
                    }
                    else {
                      this._util.openSnackBar(JSON.stringify(data.msg), ResponseSnackbar.Error);
                    }
                  },
                  error => {
                  });
              }
            });
            break;
        }
    
      }
    }
    ​
  46. Let's briefly go through the code:
    1. In the start of the component, there are a couple of API URLs to load menu items from the database. 
    2. ngOnInit(): As I told you, I am a big fan of the Angular Model Driven form, here we are creating the menuFrm and specifying the form elements, initial value, and validation rules. Also, we are initializing the form elements values sent from menu-list items in case of the update and delete operations. 
    3. onSubmit(): As soon as the user will submit the form, the control will call this method where we are calling the POST, PUT or DELETE services from a data.service.ts file that we extended in previous steps. One interesting step is in delete operation where we are first calling the confirmDelete method from a util.ts class that will open another confirmation dialog box and would return yes, no based on user action. 
    4. Rest of methods are to load the drop down values form database or perform any helping functionality. 
  47. Let's fix one cosmetic thing, I don't like the extra spacing and margin in Angular Material UI dialog box, so let's edit the app -> app.component.ts and add following CSS class on top of the file:   
    .mat-dialog-container {
      padding: 0 !important;
      margin: 0 !important;
      }​
  48. Since we used the dialog box, so components in dialog box should be added in AppModule entryComponents sections since they are not dynamically part of DOM like other components and we explicitly have to load them. Other components get loaded through route etc. Edit the app -> app.module.ts file and replace its content with the following:   
    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
    import { FormsModule, ReactiveFormsModule } from '@angular/forms';
    //Http Module For GET, POST, PUT and DELETE RESTful APIs
    import { HttpModule } from '@angular/http';
    
    import {
      MatAutocompleteModule,
      MatButtonModule,
      MatButtonToggleModule,
      MatCardModule,
      MatCheckboxModule,
      MatChipsModule,
      MatDatepickerModule,
      MatDialogModule,
      MatExpansionModule,
      MatGridListModule,
      MatIconModule,
      MatInputModule,
      MatListModule,
      MatMenuModule,
      MatNativeDateModule,
      MatPaginatorModule,
      MatProgressBarModule,
      MatProgressSpinnerModule,
      MatRadioModule,
      MatRippleModule,
      MatSelectModule,
      MatSidenavModule,
      MatSliderModule,
      MatSlideToggleModule,
      MatSnackBarModule,
      MatSortModule,
      MatTableModule,
      MatTabsModule,
      MatToolbarModule,
      MatTooltipModule,
      MatStepperModule
    } from '@angular/material';
    
    import { HttpClientModule } from '@angular/common/http';
    
    import { AppComponent } from './app.component';
    import 'hammerjs';
    
    import { AgmCoreModule } from '@agm/core';
    import { NgxCarouselModule } from 'ngx-carousel';
    
    import { HomeComponent } from './client/home/home.component';
    import { AppRoutingModule } from "./app-routing.module";
    import { ContactComponent } from './client/contact/contact.component';
    import { DataService } from './service/data/data.service';
    import { Util } from "./shared/util";
    import { ViewPageComponent } from './client/view-page/view-page.component';
    import { FooterComponent } from './footer.component';
    import { AdminComponent } from './admin/admin/admin.component';
    import { DatagridComponent } from './shared/datagrid/datagrid.component';
    import { ConfirmDeleteComponent } from './shared/messages/confirm-delete/confirm-delete.component';
    import { ManageMenuComponent } from './admin/menu/manage-menu/manage-menu.component';
    import { MenuListComponent } from './admin/menu/menu-list/menu-list.component';
    
    @NgModule({
      declarations: [
        AppComponent,
        HomeComponent,
        ContactComponent,
        ViewPageComponent,
        FooterComponent,
        AdminComponent,
        DatagridComponent,
        ConfirmDeleteComponent,
        ManageMenuComponent,
        MenuListComponent
      ],
      imports: [
        HttpClientModule,
        HttpModule,
        FormsModule,
        ReactiveFormsModule,
        AgmCoreModule.forRoot({
          apiKey: 'AIzaSyCKHGctDoGx1_YdAbRsPlJYQqlQeC6kR2E'
        }),
        NgxCarouselModule,
        BrowserModule,
        AppRoutingModule,
        BrowserAnimationsModule,
        MatButtonModule,
        MatCheckboxModule,
        MatCardModule,
        MatInputModule,
        MatRadioModule,
        MatSelectModule,
        MatTabsModule,
        MatSortModule,
        MatPaginatorModule,
        MatTableModule,
        MatSnackBarModule,
        MatIconModule,
        MatDialogModule,
        MatAutocompleteModule,
        MatGridListModule,
        MatMenuModule,
        MatProgressBarModule,
        MatExpansionModule,
        MatTooltipModule,
        MatSlideToggleModule
      ],
      providers: [DataService,Util],
      bootstrap: [AppComponent],
      entryComponents: [ManageMenuComponent,ConfirmDeleteComponent]
    })
    export class AppModule { }
    ​
  49. One last thing we have to fix is the Contact component because we introduced enumeration instead of a hard-coded string, edit the app -> client -> contact -> contact.component.ts:    
    import { Component, OnInit } from '@angular/core';
    import { FormGroup, FormBuilder, Validators } from "@angular/forms";
    import { DataService } from "../../service/data/data.service";
    import { Util } from "../../shared/util";
    import { ResponseSnackbar } from "../../shared/enum";
    
    @Component({
      selector: 'app-contact',
      templateUrl: './contact.component.html',
      styleUrls: ['./contact.component.css']
    })
    export class ContactComponent implements OnInit {
    
      POST_CONTACT: string = "/api/contact";
    
      contactFrm: FormGroup;
    
      constructor(private _fb: FormBuilder, private _dataService: DataService, private _util: Util) { }
    
      ngOnInit() {
    
        this.contactFrm = this._fb.group({
          _id: [''],
          Name: ['', [Validators.required, Validators.maxLength(150)]],
          Phone: ['', [Validators.required]],
          EmailAddress: ['', [Validators.required, Validators.email, Validators.maxLength(250)]],
          Message: ['', [Validators.required, Validators.maxLength(1000)]]
        });
      }
    
      onSubmit(formData: any) {
        delete formData.value._id;
        if (this.contactFrm.invalid) {
          this._util.openSnackBar("Please enter the valid values!", ResponseSnackbar.Error);
        }
        else {
          this._dataService.post(this.POST_CONTACT, formData.value).subscribe(
            data => {
              if (data.success == true) //Success
              {
                this._util.openSnackBar(data.msg, ResponseSnackbar.Sucess);
              }
              else {
                this._util.openSnackBar(data.msg,ResponseSnackbar.Error);
              }
            },
            error => {
            });
        }
      }
    
      resetFrm() {
        this.contactFrm.reset();
      }
    
    }
    ​
  50. That's all for now, run the application and browse the URL http://localhost:4200/admin , then click on Menu Management, you would land the http://localhost:4200/admin/managemenu page. You should have at least one record there if you correctly follow my last article properly, otherwise go ahead, click on Add New Menu button, fill up all information and save it, try to edit and delete it after. 
  51. Let me know if you have any issue in any step.



Keywords: Angular Reactive Form tutorial, Node.js nodeemailer, MEAN stack tutorial, Angular 5 tutorial for beginners, MEAN Stack tutorial for beginners, Rxjs tutorial or beginner, Rxjs vs Promise,nodemailer,pm2, Node.js MongoDB, Node.js mongoose configuration, Angular Data Grid Control, Free Angular Data Grid Control