Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Angular 2 to Angular 4 with Angular Material UI Components - Part 3

4.90/5 (21 votes)
17 Jun 2017CPOL13 min read 166.5K   2.8K  
In this article, we will convert our Angular 2 application to Angular 4 and replace the traditional HTML & third-party components with Angular Material components.

Articles in the Series

Introduction

In this article, we will continue enhancing the User Management application by upgrading the Angular version from Angular 2 to Angular 4 and using Angular Material 2 components instead of traditional HTML and third-party components (e.g., ng2-bs3-modal modal pop up we used for Add/Update/Delete User).

Angular 4 has couple of new cool features, I don’t want to go over them since you can easily find them online. Check here for quick review. We will use Email Validator and If-else Template conditions in User Management application.

Angular Material is Angular compatible components that we mostly use to design the web application’s UI, e.g., Input, Autocomplete, dropdown, checkbox, etc. Click here to review the Angular Material components. We will replace all current HTML and third-party components with the Angular Material components.

Background

This article is the third part of Angular2 in ASP.NET MVC & Web API - Part 2. In previous articles, we used ng3-bs3-modal third-party modal pop up components and traditional HTML controls. In this article, we will be replacing all controls with Angular Material components. It is highly recommended that you go through Part 1 & Part 2 before reading this article.

Let’s Start

Before starting the actual development, let me show what would be the final output of this article. You can compare it with Angular2 in ASP.NET MVC & Web API - Part 2 to get an idea what we are going to build.

Image 1

Image 2

Image 3

You can see that we added few more form controls on Add User modal pop up and look & feel of each control is very different from Part 1 & Part 2. Let’s start the development:

As this article is a continuation of Angular2 in ASP.NET MVC & Web API - Part 2 article, let’s download the attached project from here.

Restore the Angular 4 Packages

  1. Open the Angular2MVC_p2.sln in Visual Studio 2017 Community Edition (preferably), it is also recommended to rename the solution.
  2. Edit the package.json file and replace the file content with the following packages:
    JavaScript
    {
      "name": "angular-quickstart",
      "version": "1.0.0",
      "description": "QuickStart package.json from the documentation 
                      for Visual Studio 2017 & WebApi",
      "scripts": {
        "start": "tsc && concurrently \"tsc -w\" \"lite-server\" ",
        "lint": "tslint ./app/**/*.ts -t verbose",
        "lite": "lite-server",
        "pree2e": "webdriver-manager update",
        "test": "tsc && concurrently \"tsc -w\" \"karma start karma.conf.js\"",
        "test-once": "tsc && karma start karma.conf.js --single-run",
        "tsc": "tsc",
        "tsc:w": "tsc -w"
      },
      "keywords": [],
      "author": "",
      "license": "MIT",
      "dependencies": {
        "@angular/common": "4.0.2",
        "@angular/compiler": "4.0.2",
        "@angular/core": "4.0.2",
        "@angular/forms": "4.0.2",
        "@angular/http": "4.0.2",
        "@angular/platform-browser": "4.0.2",
        "@angular/platform-browser-dynamic": "4.0.2",
        "@angular/router": "4.0.2",
    
        "angular-in-memory-web-api": "0.2.4",
        "systemjs": "0.19.40",
        "core-js": "2.4.1",
        "rxjs": "5.0.1",
        "zone.js": "0.7.4"
      },
      "devDependencies": {
        "concurrently": "3.2.0",
        "lite-server": "2.2.2",
        "typescript": "2.0.10",
    
        "canonical-path": "0.0.2",
        "tslint": "3.15.1",
        "lodash": "4.16.4",
        "jasmine-core": "2.4.1",
        "karma": "1.3.0",
        "karma-chrome-launcher": "2.0.0",
        "karma-cli": "1.0.1",
        "karma-jasmine": "1.0.2",
        "karma-jasmine-html-reporter": "0.2.2",
        "protractor": "4.0.14",
        "rimraf": "2.5.4",
    
        "@types/node": "6.0.46",
        "@types/jasmine": "2.5.36",
        "@angular/material": "2.0.0-beta.6",
        "@angular/animations": "4.1.3"
      },
      "repository": {}
    }
  3. You can see in the dependencies section that we are upgrading the Angular & other helping packages version to 4.0.2.

    Image 4

  4. In the devDependencies section, you can see that we are importing the Angular Material package:

    Image 5

  5. Right click on package.json file and select option Restore Packages, it will take few minutes to download all the packages. Wait until you get package restore complete message.
  6. Save the file and click on the menu Build -> Rebuild Solution option. It will take few seconds or a minute to download all packages (.NET & Angular). That’s it with Angular 4 packages restore.

Upgrade the User Table in Database

  1. In previous articles, we had only three fields in User table, let’s add some more to better understand the Angular Material components.
  2. Go to App_Data folder, right click and select Open or double click on UserDB.mdf file to edit it:

    Image 6

  3. Expand the Tables from Data Connections -> UserDBEntities hierarchy, right click on TblUser and select option Open Table Definition:

    Image 7

  4. You would see only four fields (Id, FirstName, LastName, Gender), let's manually update the table according to the following screen shot:

    Image 8

  5. Once you are done, click on top Update button. It will take few moments and you will end up with the following screen, click on Update Database button:

    Image 9

  6. Next, let’s update the User entity, go to DBContext folder, right click on UserDBEntities.edmx and select option Open or double click to edit it:

    Image 10

  7. Right click anywhere on the screen and select option Update Model from Database

    Image 11

  8. On Update Wizard screen, go to Refresh tab, select Tables and click on Finish button:

    Image 12

  9. After few moments, you would see TblUser would be updated as follows:

    Image 13

We are all set with Database update, let's move to next steps.

Angular 4 & Angular Material Components Update

Let’s update our User Management application to use Angular Material components and few features of Angular 4.

  1. Edit the app -> app.module.ts and add the following import statements for Angular Material. Also add the BrowserAnimationsModule, MaterialModule, MdNativeDateModule modules in import section:
    JavaScript
    import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
    import { MaterialModule, MdNativeDateModule } from '@angular/material';
  2. Next, add the Material and Animation reference in systemjs.config.js file.
    JavaScript
    '@angular/animations': 'npm:@angular/animations/bundles/animations.umd.js',
    '@angular/animations/browser': 'npm:@angular/animations/bundles/animations-browser.umd.js',
    '@angular/platform-browser/animations': 
    'npm:@angular/platform-browser/bundles/platform-browser-animations.umd.js',
    '@angular/material': 'npm:@angular/material/bundles/material.umd.js',
  3. Now that we added the Angular Material module in our AppModule, our application is ready to use Angular Material components. First let’s modify the home page. Edit the app -> Components -> home.component.ts file.
  4. Instead of plain image, let’s use the Card from Angular Material, go to Angular Material Card page to understand what is Card, where and how to use it?
  5. On the Angular Material Card page and for any other component, you can see there are three tabs, OVERVIEW, API REFERENCE and EXAMPLE that give us the following information:
    • OVERVIEW: Component description, architecture and its use. HTML, TS and CSS code and link to working Plunker.
    • API REFERENCE: How to import component, Module and Directive information.
    • EXAMPLE: Running example with almost all features of specific component and Plunker link with HTML, TS and CSS code.
  6. Since we are on card page, click on Example Tab, then click on View Source < > link.

    Image 14

  7. You would land on the following view with three tabs, HTML, TS and CSS.

    Image 15

  8. We will just copy the HTML, Typescript (if required any) and CSS from here to our application for all components where we would need them. Of course, we will modify it according to our requirements but DO NOT shy to copy, this really helps us to take full advantage of Angular Material components without any effort. So, copy the entire md-card HTML and replace it with home.component.ts template’s HTML:

    Image 16

  9. For Angular Material components, we may need CSS for many components, so let’s create stylesheet. Right click on Content folder and select Add -> Style Sheet:

    Image 17

  10. Enter the name style.css and click on OK button:

    Image 18

  11. Replace the style.css content with following CSS:
    CSS
    .example-card {
      width: 400px;
    }
    .example-header-image {
      background-image: url('../images/users.png');
      background-size: cover;
    }
  12. Next, edit the App_Start -> BundleConfig.cs and update the Content/css bundle as follows to add and use the newly added style.css in our application:
    C#
    bundles.Add(new StyleBundle("~/Content/css").Include(
                          "~/Content/style.css",
                          "~/Content/bootstrap.css"));
  13. Now, our style sheet is ready to use, we will keep adding the CSS classes for other components as we move forward in our development.
  14. Let’s go back to our home.component.ts and modify it. Change the md-card-title, e.g., "Users", md-card-subtitle as "Sample Image", etc. Update the image URL in md-card-image to src="../../images/users.png". The final home.component.ts template should look like the following:

    Image 19

  15. Compile and run the project, your home page should be as follows:

    Image 20

  16. Great, so we successfully used our first Angular Material component.
  17. Next, let’s move to user.component.ts and update it with the Angular Material components. To make it simple and use Dialog component, we need to break it in two components. The UserComponent will only have User list whereas Add, Update and Delete functionality will be moved to ManageUser component that we are going to create in next steps.
  18. In previous articles, we were using ng2-bs3-modal modal pop up control for add, update and delete screen that is not needed any more, so remove all of its references from app.module.ts, systemjs.config.js and user.component.ts files.
  19. Next, let’s create the ManageUser component and then, we will come back to UserComponent to clean it. Right click on app -> Components and select Add -> TypeScript file:

    Image 21

  20. Enter the name manageuser.component.ts and click on OK button. Also create the manageuser.component.html file:

    Image 22

  21. In the previous steps, we updated the User table with few additional columns, let’s update our user.ts accordingly. Edit the app-> Model -> user.ts and update it as follows:
    JavaScript
    export interface IUser {
        Id: number,
        FirstName: string,
        LastName: string,
        Email:string,
        Gender: string,
        DOB: string,
        City: string,
        State: string,
        Zip: string,
        Country:string
    }
  22. In manageuser.component.html, we will add Angular Material Dialog. We will simply go to Dialog Plunker and copy the dialog-result-example-dialog.html file content to manageuser.component.html. From this file, you can see that there are three main sections of Dialog, title, content and action buttons that are quite self-explanatory. The other Angular Material components correspond to user.ts model fields that will go inside md-dialog-content div are as follows:
    • mdInput: For FirstName, LastName, Email, City and Zip
    • md-radio-button: For Gender (Male/Female)
    • md-datepicker: Date picker control for Date of Birth (DOB)
    • md-autocomplete: Auto complete dropdown for states, it filters state as soon we start typing. (Same as our Search User functionality we developed through pipe)
    • md-select: Dropdown for Country.
    • md-raised-button: Cancel and Add/Update/Delete buttons.
  23. You can go to Plunker link for each component to see running example, I personally prefer Plunker to get the code for each component.
  24. Copy the following HTML in manageuser.component.html file:
    HTML
    <form novalidate (ngSubmit)="onSubmit(userFrm)" [formGroup]="userFrm">
        <div>
            <h1 md-dialog-title><span>
            <md-icon>create</md-icon>{{modalTitle}}</span></h1>
        </div>
        <div style="padding-bottom:1px;background-color:#d8d8d8"></div>
        <div md-dialog-content class="md-dialog-container">
            <div class="frm-ctrl">
                <md-input-container>
                    <input mdInput placeholder="First Name" 
                    formControlName="FirstName">
                </md-input-container>
                <div *ngIf="formErrors.FirstName" class="text-danger">
                    {{ formErrors.FirstName }}
                </div>
            </div>
            <div class="frm-ctrl">
                <md-input-container>
                    <input mdInput placeholder="Last Name" 
                    formControlName="LastName">
                </md-input-container>
                <div *ngIf="formErrors.LastName" class="text-danger">
                    {{ formErrors.LastName }}
                </div>
            </div>
            <div class="frm-ctrl">
                <md-input-container>
                    <input type="email" 
                    mdInput placeholder="Email" formControlName="Email">
                </md-input-container>
                <div *ngIf="formErrors.Email" class="text-danger">
                    {{ formErrors.Email }}
                </div>
            </div>
            <div class="frm-ctrl">
                <md-radio-group formControlName="Gender">
                    <md-radio-button *ngFor="let gndr of gender" [value]="gndr">
                        {{gndr}}
                    </md-radio-button>
                </md-radio-group>
                <div *ngIf="formErrors.Gender" class="text-danger">
                    {{ formErrors.Gender }}
                </div>
            </div>
            <div class="frm-ctrl">
                <md-input-container style="width:50%">
                    <input mdInput [mdDatepicker]="picker" 
                    placeholder="Date of Birth" formControlName="DOB">
                    <button mdSuffix [mdDatepickerToggle]="picker"></button>
                </md-input-container>
                <md-datepicker #picker></md-datepicker>
                <div *ngIf="formErrors.DOB" class="text-danger">
                    {{ formErrors.DOB }}
                </div>
            </div>
            <div class="frm-ctrl">
                    <div class="line_ctrl">
                        <md-input-container class="example-full-width">
                            <input mdInput placeholder="City" formControlName="City">
                        </md-input-container>
                        <div *ngIf="formErrors.City" class="text-danger">
                            {{ formErrors.City }}
                        </div>
                    </div>
                    <div class="line_ctrl">
                        <md-input-container>
                            <input mdInput placeholder="State" 
                            [mdAutocomplete]="auto" formControlName="State">
                        </md-input-container>
                        <md-autocomplete #auto="mdAutocomplete">
                            <md-option *ngFor="let state of filteredStates | 
                            async" [value]="state">
                                {{ state }}
                            </md-option>
                        </md-autocomplete>
                        <div *ngIf="formErrors.State" class="text-danger">
                            {{ formErrors.State }}
                        </div>
                    </div>
                    <div class="line_ctrl">
                        <md-input-container class="example-full-width">
                            <input mdInput #postalCode maxlength="5" 
                            placeholder="Zip" formControlName="Zip">
                            <md-hint align="end">{{postalCode.value.length}} / 5</md-hint>
                        </md-input-container>
                        <div *ngIf="formErrors.Zip" class="text-danger">
                            {{ formErrors.Zip }}
                        </div>
                    </div>
            </div>
            <div class="frm-ctrl">
                <md-select placeholder="Country" 
                style="width:50%" formControlName="Country">
                    <md-option *ngFor="let ctry of country" [value]="ctry.value">
                        {{ ctry.viewValue }}
                    </md-option>
                </md-select>
                <div *ngIf="formErrors.Country" class="text-danger">
                    {{ formErrors.Country }}
                </div>
            </div>
        </div>
        <md-dialog-actions class="md-dialog-footer" align="end" >
            <button color="warn" type="button" 
            md-raised-button (click)="dialogRef.close()">Cancel</button>&nbsp;
            <button type="submit" color="primary" 
            [disabled]="userFrm.invalid" md-raised-button>{{modalBtnTitle}}</button>
        </ md-dialog-actions>
    </form>
  25. This is the same Reactive (Model Driven) form we were using in the previous article but with additional fields and also with Angular Material components that I briefly explained in previous step. This form will be loaded in Dialog box as you can see this is in the Dialog content tags. We will see in UserComponent how to display the Dialog box. Add the following CSS in style.css file. You are welcome to change the CSS according to your choice:
    CSS
    .md-dialog-container {
        width: 550px;
        height: 480px;
        padding-bottom: 20px;
        padding-top: 20px;
    }
    .md-dialog-footer {
        padding: 20px;
        width: 100%;
    }
    md-input-container {
        width: 100%;
    }
    .frm-ctrl {
        padding-top: 20px;
        width: 100%;
    }
    .line_ctrl {
        float: left;
        width: 145px;
        text-align: right;
        margin: 2px;
        display: inline;
    }
  26. Next, copy the following code in manageuser.component.ts and let’s understand it:
    JavaScript
    import { Component, OnInit, ViewChild } from '@angular/core';
    import { UserService } from '../Service/user.service';
    import { FormBuilder, FormGroup, Validators } from '@angular/forms';
    import { ModalComponent } from 'ng2-bs3-modal/ng2-bs3-modal';
    import { IUser } from '../Model/user';
    import { DBOperation } from '../Shared/enum';
    import { Observable } from 'rxjs/Rx';
    import { Global } from '../Shared/global';
    import { MdDialog, MdDialogRef } from '@angular/material';
    import { FormControl } from '@angular/forms';
    @Component({
        templateUrl: 'app/Components/manageuser.component.html',
    })
    export class ManageUser implements OnInit {
        msg: string;
        indLoading: boolean = false;
        userFrm: FormGroup;
        dbops: DBOperation;
        modalTitle: string;
        modalBtnTitle: string;
        listFilter: string;
        selectedOption: string;
        user: IUser;
        country = [
            { value: 'USA', viewValue: 'USA' },
            { value: 'Canada', viewValue: 'Canada' }
        ];
        gender = [
            'Male',
            'Female'
        ];
        states = ['Alabama','Alaska','Arizona','Arkansas',
        'California','Colorado','Connecticut','Delaware',
        'Florida','Georgia','Hawaii',
                  'Idaho','Illinois','Indiana','Iowa',
                  'Kansas','Kentucky','Louisiana','Maine',
                  'Maryland','Massachusetts','Michigan','Minnesota',
                  'Mississippi','Missouri','Montana','Nebraska',
                  'Nevada','New Hampshire','New Jersey',
                  'New Mexico','New York','North Carolina',
                  'North Dakota','Ohio','Oklahoma','Oregon',
                  'Pennsylvania','Rhode Island','South Carolina',
                  'South Dakota','Tennessee','Texas',
                  'Utah','Vermont','Virginia','Washington',
                  'West Virginia','Wisconsin','Wyoming'
                 ];
        stateCtrl: FormControl;
        filteredStates: any;
        constructor(private fb: FormBuilder, 
        private _userService: UserService, public dialogRef: MdDialogRef<ManageUser>) { }
        filterStates(val: string) {
            return val ? this.states.filter(s => new RegExp(`^${val}`, 'gi').test(s))
                : this.states;
        }
        ngOnInit(): void {
            this.userFrm = this.fb.group({
                Id: [''],
                FirstName: ['', [Validators.required, Validators.maxLength(50)]],
                LastName: ['', [Validators.required, Validators.maxLength(50)]],
                Email: ['', [Validators.required, Validators.email]],
                Gender: ['', Validators.required],
                DOB: ['', Validators.required],
                City: ['', Validators.required],
                State: ['', Validators.required],
                Zip: ['', Validators.required],
                Country: ['', Validators.required]
            });
            this.filteredStates = this.userFrm.controls["State"].
            valueChanges.startWith(null).map(name => this.filterStates(name));
            this.userFrm.valueChanges.subscribe(data => this.onValueChanged(data));
            this.onValueChanged();
            if (this.dbops == DBOperation.create)
                this.userFrm.reset();
            else
                this.userFrm.setValue(this.user);
            this.SetControlsState(this.dbops == DBOperation.delete ? false : true);
        }
        onValueChanged(data?: any) {
            if (!this.userFrm) { return; }
            const form = this.userFrm;
            for (const field in this.formErrors) {
                // clear previous error message (if any)
                this.formErrors[field] = '';
                const control = form.get(field);
                if (control && control.dirty && !control.valid) {
                    const messages = this.validationMessages[field];
                    for (const key in control.errors) {
                        this.formErrors[field] += messages[key] + ' ';
                    }
                }
            }
        }
        formErrors = {
            'FirstName': '',
            'LastName': '',
            'Email': '',
            'Gender': '',
            'DOB': '',
            'City': '',
            'State': '',
            'Zip': '',
            'Country': ''
        };
        validationMessages = {
            'FirstName': {
                'maxlength': 'First Name cannot be more than 50 characters long.',
                'required': 'First Name is required.'
            },
            'LastName': {
                'maxlength': 'Last Name cannot be more than 50 characters long.',
                'required': 'Last Name is required.'
            },
            'Email': {
                'email': 'Invalid email format.',
                'required': 'Email is required.'
            },
            'Gender': {
                'required': 'Gender is required.'
            }
            ,
            'DOB': {
                'required': 'DOB is required.'
            }
            ,
            'City': {
                'required': 'City is required.'
            }
            ,
            'State': {
                'required': 'State is required.'
            }
            ,
            'Zip': {
                'required': 'Zip is required.'
            }
            ,
            'Country': {
                'required': 'Country is required.'
            }
        };
        onSubmit(formData: any) {
            switch (this.dbops) {
                case DBOperation.create:
                    this._userService.post(Global.BASE_USER_ENDPOINT, 
                                           formData.value).subscribe(
                        data => {
                            if (data == 1) //Success
                            {
                                this.dialogRef.close("success");
                            }
                            else {
                                this.dialogRef.close("error");
                            }
                        },
                        error => {
                            this.dialogRef.close("error");
                        }
                    );
                    break;
                case DBOperation.update:
                    this._userService.put(Global.BASE_USER_ENDPOINT, 
                    formData._value.Id, formData._value).subscribe(
                        data => {
                            if (data == 1) //Success
                            {
                                this.dialogRef.close("success");
                            }
                            else {
                                this.dialogRef.close("error");
                            }
                        },
                        error => {
                            this.dialogRef.close("error");
                        }
                    );
                    break;
                case DBOperation.delete:
                    this._userService.delete(Global.BASE_USER_ENDPOINT, 
                                             formData._value.Id).subscribe(
                        data => {
                            if (data == 1) //Success
                            {
                                this.dialogRef.close("success");
                            }
                            else {
                                this.dialogRef.close("error");
                            }
                        },
                        error => {
                            this.dialogRef.close("error");
                        }
                    );
                   break;
            }
        }
        SetControlsState(isEnable: boolean) {
            isEnable ? this.userFrm.enable() : this.userFrm.disable();
        }
    }
  27. Just for readability purposes, you can shrink the above code to have scroll bar.
  28. Most of the code I copied from user.component.ts from previous article, e.g., User Form creation, initialization and error message. Only difference is few more fields and validation rules including Email Validator that is Angular 4 feature. There are local variables for Country, Gender and States that populate md-select, md-radio-button and md-autocomplete controls respectively. One interesting thing about States md-autocomplete control is how it works? If you see in ngOnInit() event, after creating and initializing the userFrm reactive form, we are adding the following line:
    JavaScript
    this.filteredStates = this.userFrm.controls["State"].
    valueChanges.startWith(null).map(name => this.filterStates(name));
  29. In the above line, every time when user starts typing any character in States md-autocomplete component, valueChanges event occurs that calls filterStates method that compares the user entered string with array of states and returns the matching result on runtime. The body of filterStates method is as follows:
    JavaScript
    filterStates(val: string) {
               return val ? this.states.filter
               (s => new RegExp(`^${val}`, 'gi').test(s)): this.states;
       }
  30. The rest of the code is already explained in previous articles.
  31. Now let's go to user.component.ts file and replace the existing code with the following:
    JavaScript
    import { Component, OnInit, ViewChild } from '@angular/core';
    import { UserService } from '../Service/user.service';
    import { IUser } from '../Model/user';
    import { DBOperation } from '../Shared/enum';
    import { Observable } from 'rxjs/Rx';
    import { Global } from '../Shared/global';
    import { ManageUser} from './manageuser.component';
    import { MdDialog, MdDialogRef } from '@angular/material';
    @Component({
        templateUrl: 'app/Components/user.component.html'
    })
    export class UserComponent implements OnInit {
        users: IUser[];
        user: IUser;
        msg: string;
        dbops: DBOperation;
        modalTitle: string;
        modalBtnTitle: string;
        listFilter: string;
        searchTitle: string = "Search: ";
        selectedOption: string;
        constructor(private _userService: UserService, private dialog: MdDialog) { }
        openDialog() {
            let dialogRef = this.dialog.open(ManageUser);
            dialogRef.componentInstance.dbops = this.dbops;
            dialogRef.componentInstance.modalTitle = this.modalTitle;
            dialogRef.componentInstance.modalBtnTitle = this.modalBtnTitle;
            dialogRef.componentInstance.user = this.user;
            dialogRef.afterClosed().subscribe(result => {
                if (result == "success") {
                    this.LoadUsers();
                    switch (this.dbops) {
                        case DBOperation.create:
                            this.msg = "Data successfully added.";
                            break;
                        case DBOperation.update:
                            this.msg = "Data successfully updated.";
                            break;
                        case DBOperation.delete:
                            this.msg = "Data successfully deleted.";
                            break;
                    }
                }
                else if (result == "error")
                    this.msg = "There is some issue in saving records, 
                    please contact to system administrator!"
                else
                    this.msg = result;
            });
        }
        ngOnInit(): void {
             this.LoadUsers();
        }
        LoadUsers(): void {
            this._userService.get(Global.BASE_USER_ENDPOINT)
                .subscribe(users => { this.users = users; }
                //,error => this.msg = <any>error
                );
        }
        addUser() {
            this.dbops = DBOperation.create;
            this.modalTitle = "Add New User";
            this.modalBtnTitle = "Add";
            this.openDialog();
        }
        editUser(id: number) {
            this.dbops = DBOperation.update;
            this.modalTitle = "Edit User";
            this.modalBtnTitle = "Update";
            this.user = this.users.filter(x => x.Id == id)[0];
            this.openDialog();
        }
        deleteUser(id: number) {
            this.dbops = DBOperation.delete;
            this.modalTitle = "Confirm to Delete?";
            this.modalBtnTitle = "Delete";
            this.user = this.users.filter(x => x.Id == id)[0];
            this.openDialog();
        }
        criteriaChange(value: string): void {
            if (value != '[object Event]')
                this.listFilter = value;
        }
    }
  32. The UserComponent is now quite slim because lot of code is being moved to ManageUser component. One new method is openDialog that opens the Dialog box and sends the parameters. Let’s understand it step by step:
    • let dialogRef = this.dialog.open(ManageUser): dialog.open takes the ManageUser component as a parameter that we just created in previous steps.
    • dialogRef.componentInstance.dbops = this.dbops; is used to send the parameter to Dialog component (ManageUser component). We are sending dpops (what kind of operation we want to do, Add/Update/Delete to change the view accordingly). The modalTitle and modalBtnTitle are labels for each DB operation view. user parameter is the single user record that we are sending for Edit and Delete view. After parameters statement, we are subscribing the Dialog’s afterClosed event to check the result sent from ManageUser component. Check the ManageUser component’s onSubmit function, we are explicitly sending the “success” and “error” string. Based on success and failure of corresponding DB operation, we are showing message under the User list.
  33. In addUser, editUser and deleteUser methods, we are only setting the dbops, modalTitle and modalBtnTitle variables values and calling the openDialog function where these variables are sent to ManageUser component as I explained in previous steps. Rest of the code is self-explanatory.
  34. Next, edit the user.component.html and replace the code with the following:
    HTML
    <div class='panel panel-primary'>
        <div class='panel-heading'>
            User Management
        </div>
        <div class='panel-body'>
            <div>
                <search-list [title]='searchTitle' 
                (change)="criteriaChange($event)"></search-list>
            </div>
            <div class='table-responsive'>
                <div style="padding-bottom:10px">
                <button class="btn btn-primary" 
                (click)="addUser()">Add</button></div>
                <div *ngIf='users && users.length==0' 
                class="alert alert-info" role="alert">No record found!</div>
                <table class='table table-striped' *ngIf='users; else loadingScreen;'>
                    <thead>
                        <tr>
                            <th>First Name</th>
                            <th>Last Name</th>
                            <th>Gender</th>
                            <th></th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr *ngFor="let user of users | userFilter:listFilter">
                            <td>{{user.FirstName}}</td>
                            <td>{{user.LastName}}</td>
                            <td>{{user.Gender}}</td>
                            <td>
                                <button title="Edit" class="btn btn-primary" 
                                (click)="editUser(user.Id)">Edit</button>
                                <button title="Delete" class="btn btn-danger" 
                                (click)="deleteUser(user.Id)">Delete</button>
                            </td>
                        </tr>
                    </tbody>
                </table>
                <ng-template #loadingScreen><div class="alert alert-info" 
                role="alert"><md-progress-spinner mode="indeterminate" 
                style="width:50px; 
                height:50px"></md-progress-spinner>loading...</div></ng-template>
            </div>
            <div *ngIf="msg" role="alert" 
            class="alert alert-info alert-dismissible">
                <button type="button" class="close" 
                data-dismiss="alert" aria-label="Close">
                <span aria-hidden="true">&times;</span></button>
                <span class="glyphicon glyphicon-exclamation-sign" 
                aria-hidden="true"></span>
                <span class="sr-only">Error:</span>
                {{msg}}
            </div>
        </div>
    </div>
  35. We removed the ng2-bs3-modal from HTML source since its functionality is being moved to ManageUser component where we are using Angular Material's Dialog component. Here, we are using one more Angular 4 feature, ngIf – else. Find the <table class='table table-striped' *ngIf='users; else loadingScreen;'> statement. *ngIf='users; else loadingScreen;' that says until users list is loaded, show the loadingScreen template. Where loadingScreen template is as follows in subsequent statements:
    HTML
    <ng-template #loadingScreen><div class="alert alert-info" 
    role="alert"><md-progress-spinner mode="indeterminate" 
    style="width:50px; height:50px"></md-progress-spinner>loading...</div></ng-template>
  36. md-progress-spinner is Angular Material spinner that I replaced with loading image. ngIf-else is a great feature that helps in code optimization. Due to this, I removed the isLoading variable that is not required anymore.
  37. Now that our ManageUser component is completed, let’s add it in AppModule.ts, edit the app-> app.module.ts, import the ManageUser component and also add it in declarations section:
    JavaScript
    import { ManageUser } from './components/manageuser.component';
    //Declaration Statement
    declarations: [AppComponent, UserComponent, 
                   HomeComponent, UserFilterPipe, SearchComponent, ManageUser],
  38. One more step is to add the ManageUser in entryComponents section as follows:
    JavaScript
    entryComponents: [ManageUser]
  39. Angular Material also comes with pre-built themes, browse the themes folder from node_modules\@angular\material\prebuilt-themes. You can use any theme, I would use indigo-pink.css. Add the theme reference in Views -> Shared -> _Layout.cshtml page:
    HTML
    <link href="/node_modules/%40angular/material/prebuilt-themes/indigo-pink.css" 
     rel="stylesheet" />
    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" 
     rel="stylesheet">
  40. In the second statement, I am importing Material icons from google CDN, that we will use in AppComponent while redesigning the menu. Spinner, buttons and other components color scheme, visibility and layout depends on the theme you select. Remove the theme and run the application, you would see, the application UI would be messed up.
  41. Rebuild, clear your browser cache (browser history) and run the application. Test the application by Adding, Updating and Deleting the user.
  42. In the final step, I want to use the Angular Material Menu component. Edit the app -> app.component.ts and replace its content with the following:
    JavaScript
    import { Component } from "@angular/core"
    import {
        Router,
        // import as RouterEvent to avoid confusion with the DOM Event
        Event as RouterEvent,
        NavigationStart,
        NavigationEnd,
        NavigationCancel,
        NavigationError
    } from '@angular/router';
    @Component({
        selector: "user-app",
        template: `
                    <div>
                      <nav class='navbar navbar-default'>
                           <div class='container-fluid'>
                          <button md-icon-button [mdMenuTriggerFor]="menu">
                             <md-icon>more_vert</md-icon> Menu
                           </button>
                           <md-menu #menu="mdMenu">
                             <button md-menu-item [routerLink]="['home']">
                               <md-icon>home</md-icon>
                               <span>Home</span>
                             </button>
                             <button md-menu-item [routerLink]="['user']">
                               <md-icon>group</md-icon>
                               <span>Users Management</span>
                             </button>
                           </md-menu>
                        </div>
                       </nav>
                       <div class='container'>
                           <router-outlet><div class="loading-overlay" 
                           *ngIf="loading">
                        <!-- show something fancy here, 
                        here with Angular 2 Material's loading bar or circle -->
                        <md-progress-bar mode="indeterminate"></md-progress-bar>
                         </div></router-outlet>
                       </div>
                    </div>
                    `
    })
    export class AppComponent {
        loading: boolean = true;
        constructor(private router: Router) {
            router.events.subscribe((event: RouterEvent) => {
                this.navigationInterceptor(event);
            });
        }
        // Shows and hides the loading spinner during RouterEvent changes
        navigationInterceptor(event: RouterEvent): void {
            if (event instanceof NavigationStart) {
               this.loading = true;
            }
            if (event instanceof NavigationEnd) {
                setTimeout(() => { this.loading = false; }, 1000)
               // this.loading = false;
            }
            // Set loading state to false in both of the 
            // below events to hide the spinner in case a request fails
            if (event instanceof NavigationCancel) {
                this.loading = false;
            }
            if (event instanceof NavigationError) {
                this.loading = false;
            }
        }
    }
  43. In template’ HTML, we are using md-menu which has md-menu-item buttons with routerLink to corresponding views (home & user). Another thing is md-icon that would display before the menu item, we already have added the icons reference in _Layout.cshtml page. For complete list of icons, click here.
  44. Another thing that we updated in AppComponent is the loading progress bar between the views. This is possible by intercepting the router change and showing the md-progress-bar based on loading variable's boolean value. I purposely added setTimeout(() => { this.loading = false; }, 1000) delay to show you the spinner when switching between the views.
  45. Rebuild, clear the browser cache and run the application, you should end up with the following screen:

    Image 23

  46. You can click on User Management button from top menu and see blue progress bar under the top menu.

Points of Interest

I like the Angular Material components because it saves my time and effort from using several third-party components, all components are both template and model driven (Reactive form) compatible.

Angular 4 has few more useful features, two of them ngIf-else and Email Validator are used in this article. Rest you can use wherever they are applicable.

Things You Can Improve

There are few features you can implement to improve this application. For example, the Date of Birth cannot be in future, try adding the validation rule for DOB, Show more fields in User list other than First Name, Last Name and Gender, do not show Search filter until User list is fully loaded and add the snack-bar component for successful Add/Update/Delete message instead of current Bootstrap message. Try it!

History

  • 12th June, 2017: Article created

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)