Angular is a platform that makes it easy to build applications with the web. Angular combines declarative templates, dependency injection, end to end tooling, and integrated best practices to solve development challenges. Angular empowers developers to build applications that live on the web, mobile, or the desktop

Introduction

In this post we will create a user interface to interract with the REST API we created in the previous post of this series.

Project Structure

At the end of this guide our folder structure will look similar to the following:

.
|__e2e/
|__node_modules/
|__src/
|  |__app/
|  |  |__app.component.scss
|  |  |__app.component.html
|  |  |__app.component.spec.ts
|  |  |__app.component.ts
|  |  |__app.module.ts
|  |  |__job-descriptor.ts
|  |  |__quartz.service.spec.ts
|  |  |__quartz.service.ts
|  |  |__modal/
|  |  |  |__modal.component.css
|  |  |  |__modal.component.html
|  |  |  |__modal.component.spec.ts
|  |  |  |__modal.component.ts
|  |__assets/
|  |__environments/
|  |  |__environment.prod.ts
|  |  |__environment.ts
|  |__favicon.ico
|  |__index.html
|  |__main.ts
|  |__polyfills.ts
|  |__styles.css
|  |__test.ts
|  |__tsconfig.app.json
|  |__tsconfig.spec.json
|  |__typings.d.ts
|__.angular-cli.json
|__.editorconfig
|__.gitignore
|__karma.conf.js
|__package.json
|__protractor.conf.js
|__README.md
|__tsconfig.json
|__tslint.json

Prerequisites

To follow along this guide, you should have the following:

Install and Setup Angular project

To ease developing an Angular project, we will use the Angular CLI. First install the CLI:

C:\> npm install -g @angular/cli

Test the installation succeeded:

C:\> ng help
ng build <options...>
  Builds your app and places it into the output path (dist/ by default).
  aliases: b
  --target (String) (Default: development) Defines the build target.
    aliases: -t <value>, -dev (--target=development), -prod (--target=production), --target <value>
  --environment (String) Defines the build environment.
    aliases: -e <value>, --environment <value>
  --output-path (Path) Path where output will be placed.
    aliases: -op <value>, --outputPath <value>
  --aot (Boolean) Build using Ahead of Time compilation.
    aliases: -aot
...
ng xi18n <options...>
  Extracts i18n messages from source code.
  --i18n-format (String) (Default: xlf) Output format for the generated file.
    aliases: -f <value>, -xmb (--i18n-format=xmb), -xlf (--i18n-format=xlf), --xliff (--i18n-format=xlf), --i18nFormat <value>
  --output-path (Path) (Default: null) Path where output will be placed.
    aliases: -op <value>, --outputPath <value>
  --verbose (Boolean) (Default: false) Adds more details to output logging.
    aliases: --verbose
  --progress (Boolean) (Default: true) Log progress to the console while running.
    aliases: --progress
  --app (String) Specifies app name to use.
    aliases: -a <value>, -app <value>
  --locale (String) Specifies the source language of the application.
    aliases: -l <value>, --locale <value>
  --out-file (String) Name of the file to output.
    aliases: -of <value>, --outFile <value>

Now we create the Angular project:

C:\> ng new quartz-manager-ui
C:\> cd quartz-manager-ui
C:\> ng serve -o

Navigate to http://localhost:4200/. The app will automatically reload if you change any of the source files.

Angular Initial

Initial Angular Project

Create the Layout

We will be using Material Design for this project. Add the material dependencies to the package.json:

file: package.json

...
 "dependencies": {
    "@angular/animations": "^5.2.1",
    "@angular/cdk": "^5.1.0",
    "@angular/common": "^5.2.1",
    "@angular/compiler": "^5.2.1",
    "@angular/core": "^5.2.1",
    "@angular/forms": "^5.2.1",
    "@angular/http": "^5.2.1",
    "@angular/material": "^5.1.0",
    "@angular/platform-browser": "^5.2.1",
    "@angular/platform-browser-dynamic": "^5.2.1",
    "@angular/router": "^5.2.1",
    "core-js": "^2.4.1",
    "hammerjs": "^2.0.8",
    "rxjs": "^5.5.2",
    "zone.js": "^0.8.14"
  },
  "devDependencies": {
    "@angular/cli": "^1.6.6",
    "@angular/compiler-cli": "^5.2.1",
    "@angular/language-service": "^5.2.1",
    "@types/jasmine": "~2.5.53",
    "@types/jasminewd2": "~2.0.2",
    "@types/node": "~6.0.60",
    "codelyzer": "^4.0.1",
    "jasmine-core": "~2.6.2",
    "jasmine-spec-reporter": "~4.1.0",
    "karma": "~1.7.0",
    "karma-chrome-launcher": "~2.1.1",
    "karma-cli": "~1.0.1",
    "karma-coverage-istanbul-reporter": "^1.2.1",
    "karma-jasmine": "~1.1.0",
    "karma-jasmine-html-reporter": "^0.2.2",
    "protractor": "~5.1.2",
    "ts-node": "~3.2.0",
    "tslint": "~5.7.0",
    "typescript": "~2.4.2"
  }
...

Run npm update @angular/cli and npm update from the terminal to install all packages. When all packages are installed add the material icons:

file: src/index.html

<!doctype html>
<html lang="en">
<head>
  ...
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

</head>
  ...
</html>

Replace the contents of app.component.html:

file: src/app/app.component.html

<div class="quartz-container">
  <div class="menu-section">

    <mat-toolbar>
      <button mat-icon-button aria-label="Open basic menu">
        <mat-icon>dehaze</mat-icon>
      </button> Quartz Manager
    </mat-toolbar>

    <mat-drawer-container class="content">
      <mat-drawer mode="side" opened="true">
        <mat-nav-list>
          <a mat-list-item>
            <mat-icon>home</mat-icon>Manager</a>
          <mat-divider></mat-divider>

          <a mat-list-item>
            <mat-icon>work</mat-icon>Jobs</a>
          <!-- </ng-template> -->
        </mat-nav-list>
      </mat-drawer>
      <mat-drawer-content>
        <div class="drawer-content">
          <h1>Dashboard</h1>
          <mat-divider></mat-divider>

        </div>
      </mat-drawer-content>
    </mat-drawer-container>
  </div>
</div>

Add some CSS:

file: src/app/app.component.scss

.quartz-container {
  display: flex;
  flex-flow: row wrap;
  .menu-section {
    width: 100%;
    margin: 20px;
    .content {
      height: 500px;
      flex: 1 1 auto;
    }
    .drawer-content {
      margin: 20px;
    }
    .mat-table {
      overflow: auto;
      max-height: 500px;
    }
  }
  .end-icon {
    justify-content: flex-end;
  }
  .sidebar-container {
    width: 400px;
    height: 200px;
  }
}

We will add one of the prebuilt Material themes by adding it to the styles.css:

file: src/styles.css

@import "~@angular/material/prebuilt-themes/deeppurple-amber.css";
html,
body {
  height: 100%;
  margin: 0
}

body {
  font-family: Roboto, Arial, sans-serif;
  margin: 0;
}

We need to add the Material modules to the project:

file: src/app/app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import {
  MatToolbarModule,
  MatIconModule,
  MatSidenavModule,
  MatListModule,
  MatButtonModule,
  MatDividerModule
} from '@angular/material';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

@NgModule({
  ...
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    MatToolbarModule,
    MatIconModule,
    MatSidenavModule,
    MatListModule,
    MatButtonModule,
    MatDividerModule
  ]
  ...
})
export class AppModule { }

Finally replace the contents of app.component.ts:

file: src/app/app.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {

}

Run the project again and you would something similar to the image below.

Angular Dashboard Initial

Initial Quartz Dashboard

Add Template to List Jobs

We will list all jobs created in a table. Use the CLI to generate a class to represent the attributes of the job:

C:\> ng g class job-descriptor

Open the generated class and add the following to it:

file: src/app/job-descriptor.ts

export class JobDescriptor {
  public name?: string;
  public group?: string;
  public subject?: string;
  public messageBody?: string;
  public to?: string[];
  public cc?: string[];
  public bcc?: string[];

  constructor(
    name?: string,
    group?: string,
    subject?: string,
    messageBody?: string,
    to?: string[],
    cc?: string[],
    bcc?: string[]
  ) {
    this.name = name ? name : null;
    this.group = group ? group : null;
    this.subject = subject ? subject : null;
    this.messageBody = messageBody ? messageBody : null;
    this.to = to ? to : null;
    this.cc = cc ? cc : null;
    this.bcc = bcc ? bcc : null;
  }
}

Add the markup for the table just below the <mat-divider> tag:

file: src/app/app.component.html

...
<h1>Dashboard</h1>
<mat-divider></mat-divider>
<div class="mat-elevation-z8">
  <mat-table #table [dataSource]="dataSource">

    <ng-container matColumnDef="group">
      <mat-header-cell *matHeaderCellDef> Group </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.group}} </mat-cell>
    </ng-container>

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

    <ng-container matColumnDef="subject">
      <mat-header-cell *matHeaderCellDef> Subject </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.subject}} </mat-cell>
    </ng-container>

    <ng-container matColumnDef="to">
      <mat-header-cell *matHeaderCellDef> Receiver </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.to}} </mat-cell>
    </ng-container>
    <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
    <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
  </mat-table>
</div>
...

To enable the use of material table we have to register a MatTableModule in the Application Module:

file: src/app/app.module.ts

import { NgModule } from '@angular/core';
import {
  ...
  MatTableModule
} from '@angular/material';

@NgModule({
  ...
  imports: [
    ...
    MatTableModule
  ]
  ...
})
export class AppModule { }

Create a service class to take care of loading the data from the backend. In the initial implementation of this class we will load mock data and in the next section we will do the actual wiring to a backend. Enough talking:

C:\> ng g service quartz

And add the following to the file:

file: src/app/quartz.service.ts

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { JobDescriptor } from './job-descriptor';
import 'rxjs/add/observable/of';

const jobs: JobDescriptor[] = [
  new JobDescriptor('dishes', 'tut', 'wash the dishes', 'Do not forget to wash the dishes', ['son@house-duty.com']),
  new JobDescriptor('exam', 'tut', 'prepare for exam', 'Study for end of term exam',
    ['serious-student@school.com'], ['teacher@school.com']),
  new JobDescriptor('invest', 'work', 'invest in stock', 'get stock options from work', ['employee@company.com'])
];

@Injectable()
export class QuartzService {
  constructor() { }

  findAllJobs(): Observable<JobDescriptor[]> {
    return Observable.of(jobs);
  }
}

Register this class as a provider in the Application Module:

file: src/app/app.module.ts

import { NgModule } from '@angular/core';
import { QuartzService } from './quartz.service';
...

@NgModule({
  ...
  imports: [
    ...
  ],
  providers: [
    QuartzService
  ]
  ...
})
export class AppModule { }

The last piece to this is the Component to load the table. Modify the app.component.ts to look like the following:

file: src/app/app.component.ts

import { MatTableDataSource } from '@angular/material';
import { Component, OnInit } from '@angular/core';
import { QuartzService } from './quartz.service';
import { JobDescriptor } from './job-descriptor';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  jobs: JobDescriptor[];
  displayedColumns = ['name', 'group', 'subject', 'to'];
  dataSource: MatTableDataSource<JobDescriptor>;

  constructor(private quartz: QuartzService) {
  }

  ngOnInit() {
    this.quartz.findAllJobs().subscribe(
      (jobs) => {
        this.jobs = jobs;
        this.dataSource = new MatTableDataSource<JobDescriptor>(this.jobs);
      }
    );
  }
}

The OnInit interface above is a life-cycle callback for Angular. In it we can initialize whatever code we want to run when the component is initialized. It is invoked only once when the directive is instantiated.

The private quartz: QuartzService is a shorthand for intializing class fields. Consult the TypeScript reference for details.

Start the application if it is not already running:

C:\> ng serve -o

After doing all this you should see something similar to this:

Angular Mock Job List

Mock Job List

Get the List of Jobs over HTTP

In the previous section we listed a mock list of Jobs. In this section we will connect to the Quartz API over HTTP using the Angular HttpClient abstraction.
We need to setup the backend for Angular to communicate with. We have two options here, either option 1 or 2:

  1. Download the source and run locally:- To run the source locally you must have JDK 8 or newer installed. Get the tarball/zip and extract. Setup the following SMTP environmental properties:
    • MAIL_HOST = smtp.example.com
    • MAIL_PORT = 1111
    • MAIL_USERNAME = person@example.com
    • MAIL_PASSWORD = secret

    Replace the values with valid SMTP settings

    Run the following command from the source directory: mvnw clean spring-boot:run. This will start an HTTP server at http://localhost:8080

  2. Deploy your own to Heroku:- To deploy the source to Heroku you must hava a Heroku account. Once you have an account setup, click on the heroku button below.

    Deploy

    Fill the required information and your app will be available at https://<appname>.herokuapp.com/.

    We will refer to https://<appname>.herokuapp.com/ from this point on as HEROKU_BASE_URL.

Add the following to the environment.ts:

file: src/environments/environment.ts

export const environment = {
  production: false,
  apiBaseUrl: 'http://localhost:8080/api/v1.0'
  // apiBaseUrl: '${HEROKU_BASE_URL}/api/v1.0' -- when using Heroku
};

Inject the HttpClient:

file: src/app/quartz.service.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { JobDescriptor } from './job-descriptor';
import { environment } from '../environments/environment';

@Injectable()
export class QuartzService {
  private resourceUrl = environment.apiBaseUrl;
  constructor(private http: HttpClient) { }

  findAllJobs(): Observable<JobDescriptor[]> {
    const url = `${this.resourceUrl}/jobs`;
    return this.http.get<JobDescriptor[]>(url);
  }
}

On line 10 we are injecting the HttpClient. For the injection to take effect we need to register the HttpClientModule as a provider in the app.module.ts:

file: src/app/app.module.ts

import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
...

@NgModule({
  ...
  imports: [
    HttpClientModule
    ...
  ],
  ...
})
export class AppModule { }

That’s all you require to setup Angular to communicate with a backend HTTP service.

Creating Jobs

The next step is to actually create some jobs. We will create a modal where the job details will be added. Add the following markup just below the Dashboard of the app.component.html:

file: src/app/app.component.html

...
<h1>Dashboard</h1>
<mat-divider></mat-divider>

<br/>
<button mat-raised-button color="accent" (click)="openDialog()">Add Job!</button>
<br/>
<br/>
...

Replace the contents of app.component.ts with this:

file: src/app/app.component.ts

import { MatTableDataSource, MatDialog } from '@angular/material';
import { Component, OnInit } from '@angular/core';
import { QuartzService } from './quartz.service';
import { ModalComponent } from './modal/modal.component';
import { JobDescriptor } from './job-descriptor';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {

  jobs: JobDescriptor[];
  displayedColumns = ['name', 'group', 'subject', 'to'];
  dataSource: MatTableDataSource<JobDescriptor>;

  constructor(public dialog: MatDialog, private quartz: QuartzService) {
  }

  ngOnInit() {
    this.quartz.findAllJobs().subscribe(
      data => {
        this.jobs = data;
        this.dataSource = new MatTableDataSource<JobDescriptor>(this.jobs);
      },
      err => {
        console.log('Something went wrong!');
      }
    );
  }

  openDialog() {
    const dialogRef = this.dialog.open(ModalComponent, {
      width: '100%'
    });

    dialogRef.componentInstance.addJob.subscribe((data) => {
      data.to = [data.to];
      this.quartz.createJobForGroup(data.group, data).subscribe(() => {
        this.jobs.push(data);
        this.dataSource.data = this.jobs;
      }
      );
    });

    dialogRef.afterClosed().subscribe(result => {
      console.log(`Dialog result: ${result}`);
    });
  }
}

Create a Modal Component:

C:\> ng g component modal

In the generated files add the following:

file: src/app/modal/modal.component.ts

import { Component, EventEmitter, Output } from '@angular/core';
import { JobDescriptor } from '../job-descriptor';

@Component({
  moduleId: module.id.toString(),
  selector: 'app-modal',
  templateUrl: './modal.component.html',
  styleUrls: ['./modal.component.css']
})
export class ModalComponent {
  job: JobDescriptor = new JobDescriptor();

  @Output()
  addJob: EventEmitter<JobDescriptor> = new EventEmitter();

  onCreateJob(job: JobDescriptor) {
    this.addJob.emit(job);
  }
}

file: src/app/modal/modal.component.html

<h2 mat-dialog-title>Create Job</h2>
<mat-dialog-content>
  <mat-grid-list cols="2" rowHeight="6:1">
    <mat-grid-tile>
      <mat-form-field>
        <input matInput placeholder="name" [(ngModel)]="job.name">
      </mat-form-field>
    </mat-grid-tile>
    <mat-grid-tile>
      <mat-form-field>
        <input matInput placeholder="group" [(ngModel)]="job.group">
      </mat-form-field>
    </mat-grid-tile>
    <mat-grid-tile>
      <mat-form-field>
        <input matInput placeholder="to" [(ngModel)]="job.to">
      </mat-form-field>
    </mat-grid-tile>
    <mat-grid-tile>
      <mat-form-field>
        <input matInput placeholder="cc" [(ngModel)]="job.cc">
      </mat-form-field>
    </mat-grid-tile>
    <mat-grid-tile>
      <mat-form-field>
        <input matInput placeholder="bcc" [(ngModel)]="job.bcc">
      </mat-form-field>
    </mat-grid-tile>
    <mat-grid-tile>
      <mat-form-field>
        <input matInput placeholder="subject" [(ngModel)]="job.subject">
      </mat-form-field>
    </mat-grid-tile>
    <mat-grid-tile colspan="2">
      <mat-form-field>
        <textarea matInput placeholder="message" [(ngModel)]="job.messageBody"></textarea>
      </mat-form-field>
    </mat-grid-tile>
  </mat-grid-list>
</mat-dialog-content>
<mat-dialog-actions>
  <button mat-raised-button mat-dialog-close>Close</button>
  <button mat-raised-button [mat-dialog-close] (click)="onCreateJob(job)">Create</button>
</mat-dialog-actions>

At this point we add the createJobForGroup() function to add jobs:

file: src/app/quartz.service.ts

...
createJobForGroup(group: string, jobDescriptor: any): Observable<any> {
  const url = `${this.resourceUrl}/groups/${group}/jobs`;
  return this.http.post(url, jobDescriptor);
}
...

Add the modules for dialog box in the app.module.ts:

file: src/app/app.module.ts

import { NgModule } from '@angular/core';
import {
  ...
  MatDialogModule,
  MatGridListModule,
  MatFormFieldModule,
  MatInputModule
} from '@angular/material';
import { FormsModule } from '@angular/forms';
import { ModalComponent } from './modal/modal.component';
...

@NgModule({
  ...
  imports: [
    MatDialogModule,
    MatGridListModule,
    MatFormFieldModule,
    MatInputModule,
    FormsModule
    ...
  ],
  entryComponents: [ModalComponent],
  ...
})
export class AppModule { }

Create Job Modal

Create Job Modal

Run the project and start hacking.

Conclusion

In this part of the series we learned how to create a client app to consume a REST service. In the post we will implement update and delete functionality.
As usual you can find the full example to this guide in the github repository. Until the next post, keep doing cool things :+1:.