Build a Material Design app with Angular 2

This walkthrough reveals how to create a DialogComponent and to-do app with Angular Material and the Angular CLI.

Angular Material is a UI component framework that implements Google's Material Design specification for Angular 2 – the new, faster implementation of Angular, written in TypeScript. Although still in alpha, Angular Material already provides a set of reusable and accessible UI components based on Material Design.

Angular 2 itself is designed for use across all platforms (web, mobile and desktop), and has many new technologies associated with it. At the JavaScript level we have the additional syntax of ECMAScript 2015 (ES6), typing and interface support from TypeScript, along with decorators from the Metadata Reflection API. It uses observables from the Reactive Extensions library to manage sequences of events in a functional programming way. It uses zones to encapsulate and intercept asynchronous activity to provide a form of thread-local storage, allowing Angular to automagically respond to data changes in asynchronous events to maintain data bindings. Finally, module loading is handled by SystemJS. 

In this tutorial we are going to use Angular 2 to create a simple to-do app with some signature Material Design elements. Get the source files here.

Get set up

Setting up the initial environment can be difficult. There is an angular2-seed available, as well as an angular2-starter. However, there is something even better: with angular-cli you can configure your Angular 2 project with a single command.

Not only will it take care of the setup for all the technologies I mentioned in the last section (via Node and npm), it will also add in scaffolding for Jasmine unit testing, Protractor end-to-end testing, plus TSLint testing, and codelyzer static code analysis of Angular 2 TypeScript. Although you don’t have to use all of these, you definitely should. It’s so simple to use, you will wonder how you ever got along without it.

Angular CLI is available as an npm package, so you will need to install Node and npm globally on your machine using npm install -g angular-cli. Now create a new Angular 2 app with ng new material2-do. You are going to have to wait a little bit, because after it generates the necessary files, it initialises a Git repo and does an npm install to download all the necessary modules into node_modules/. Take a look at the package.json and get familiar with the modules and scripts there. 

You have now created a new Angular 2 application that follows the official best practices.

Add Material Design

The default app knows nothing about Material Design (an oversight I'm sure), so we have to add it ourselves. You'll find the full instructions here

There is a list of published Angular 2 Material Design packages in the @angular2-material library. In this example we are going to use core (required for all Angular Material 2 apps), as well as button, card, checkbox, icon, input, list and toolbar:

npm install --save @angular2-material/{core,button,card,checkbox,icon,input,list,toolbar}@2.0.0-alpha.5

For the vendor bundle to work, we need to add @angular2-material/**/*  to the array of vendorNpmFiles in angular-cli-build.js. We also need to add the path to @angular2-material to the maps object:

const map: any = {
  '@angular2-material': 'vendor/@angular2-material'
};

Let SystemJS know how to process new modules by pointing to the main files of each of the packages: 

const packages:any = {};

// put the names of any of your Material components here
const materialPkgs:string[] = ['core','button','card','checkbox',
'icon','input','list','toolbar'];

materialPkgs.forEach((pkg) => {
  packages[`@angular2-material/${pkg}`] = {main: `${pkg}.js`};
});

Now it’s time to load the Material Design icon font in the of src/index.html. Any font will work, but we are using the standard Material Design icons:

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

Create an MD dialog

We can now work with Material Design in our to-do app. One of the components currently missing from Angular 2 Material Design is a prompt or dialog, so for our first task we will make one! 

Let’s create a new component using a Material Design card, a toolbar, an input and a couple of buttons. In the src/app folder of your repo, type ng generate component dialog. This generates a new DialogComponent in src/app/dialog, and adds a barrel to system-config.ts so SystemJS knows how to load it.

If you look at the generated dialog.component.ts file, you will see the first line is: import { Component, OnInit } from '@angular/core'; . Component is one of the main building blocks of Angular, and OnInit is one of the interfaces it implements. However, in order to have access to communication between nested components, as well as the Material Design components mentioned above, we need to import Input , Output and EventEmitter from @angular/core; and MdCard, MdInput, MdToolbar and MdButton from their corresponding modules in the @angular2- material library.

To render these Material components, we need to inject the directives our DialogComponent requires. We will add the following directives to the @Components metadata:

templateUrl: 'dialog.component.html',
directives: [MdCard, MdToolbar, MdInput, MdButton],
styleUrls: ['dialog.component.css']

We then declare a number of @Input variables (okText, cancelText and so on) that allow us to define the contents of the dialog. We also need to add one @Output emitter that allows us to trigger a function with a value in the parent component when the dialog is closed.

Now we can replace the generated constructor in dialog.component.ts with the following code:

constructor() {
 this.okText = 'OK';
 this.cancelText = 'Cancel';
}

emitValue(value) {
 this.valueEmitted.emit(value);
}

As well as using the @Input variables inside our DialogComponent within the dialog.component.html template, the md-input allows us to accept input from the user:

<md-input class="full-width" [placeholder]="placeholder"
        [(ngModel)]="value" #dialogInput
        (keyup.enter)="emitValue(value)"
        (keyup.escape)="emitValue(null)"></md-input>

The md-buttons allow the user to click 'OK', 'Cancel' or whatever you decide to label these buttons:

<button md-button (click)="emitValue(null)" color="primary">
  {{cancelText}}
</button>
<button md-raised-button (click)="emitValue(value)" color="primary">
  {{okText}}
</button>

Notice the keyup event handlers, which take care of things when the Enter or Escape key is pressed. These handlers are identical to the click event handlers for cancelText and okText. Escape does the same thing as cancel (emitValue(null) ), and hitting Enter will have the same result as clicking OK (emitValue(value) ). This allows us to prompt the user for a value via an md-input, and receive emitted output. 

We can see examples of a number of Material Design components: md-card, md-button, and so on. We also need to add some CSS in dialog.component.css in order to achieve the layout we desire – you can view the full code in the accompanying GitHub repo.

Now let's add this DialogComponent to material2-do.component.html to see what it looks like:

<app-dialog [title]="'Dialog Prompt'"
  [template]="'This is our new Material Design based Component:'"
  [placeholder]="'Enter text here'"
  [okText]="'Yes'"
  [cancelText]="'No'"
  [value]="'Starting Text'"
  (valueEmitted)="log($event)"
  [showPrompt]="true">
</app-dialog>

Notice we have literal strings for all of the @Inputs. These require us to use both single and double quotes, otherwise Angular would interpret the contents as a variable name in the Component scope.

We also have the one emitted @Output. This makes the dialog simple and extremely configurable. Most of the inputs would have defaulted to empty strings if omitted. 

Let’s modify Material2DoComponent. We need to import  DialogComponent  and declare it as a directive, otherwise Material2DoComponent will not be able to render it. We will also add the log function:

 log(text) {
   console.log(text);
 }

Let's take a look at our handiwork. You can serve up the app (the default port is 4200) by running npm run-script start, which in turn runs ng server. You should see a prompt that looks like the one shown overleaf. If you open up the console, you can see what is logged: the contents of the input is emitted when you click 'Yes', and null is emitted when you click 'No'.

We are now ready to use this new DialogComponent to create our to-do app.

Create the main app

We are going to use the following MD Components for the main app: toolbar, list, list-item, checkbox, button, icon, icon-registry, and its dependent, HTTP_PROVIDER, from the Angular HTTP library. So these need to be added to the import section of Material2DoComponent.

Once again, in order to be able to render these components, we need to include them in the @Components metadata directives array along with DialogComponent, which we just added:

directives: [MdCard, MdToolbar, MdList, MdListItem, MdCheckbox, MdButton, MdIcon, DialogComponent],

To gain access to the MdIconRegistry, we need to inject it, along with HTTP_PROVIDERS, via the @Components metadata providers array: 

providers: [MdIconRegistry, HTTP_PROVIDERS],

Now we leverage our DialogComponent, adding in enough logic for a simple to-do application. The todoDialog is called to open our dialog, either with the task to edit (todo) or null if we are creating a new one. 

We set up the default variables for a new task, or if we are editing a task instead we change them accordingly. We then show the DialogComponent by setting the showDialog variable to true:

  todoDialog(todo = null) {
    this.okButtonText = 'Create task';
    this.fieldValue = '';
    this.editingTodo = todo;
    if (todo) {
      this.fieldValue = todo.title;
      this.okButtonText = 'Edit task';
    }
    this.showDialog = true;
  }

The updateTodo function is called when we wish to close it. The other functions (editTodo, addTodo, hideDialog) are helper methods for updateTodo.

  updateTodo(title) {
    if (title) {
      title = title.trim();
      if (this.editingTodo) {
        this.editTodo(title);
      } else {
        this.addTodo(title);
      }
    }
    this.hideDialog();
  }

In material2-do.component.html we've given our application an md-toolbar where we have put the title, and an md-icon called add (which looks like a plus sign) for our floating action button (FAB), which allows us to create a new task: 

    <button md-fab class="fab-add" (click)="todoDialog()">
      <md-icon>add</md-icon>
    </button>

We use md-card-content to hold an md-list and an *ngFor to iterate through, and display, our todoList array as md-list-items:

      <md-list-item *ngFor="let todo of todoList; let index=index">

md-checkbox enables us to tick off items on our list. And we have two md-mini-fab buttons we can use to delete and edit our task: md-icons delete_forever and mode_edit:

<button md-mini-fab (click)="remove(index)" color="primary">
  <md-icon>delete_forever</md-icon>
</button>

<button md-mini-fab (click)="todoDialog(todo)" color="primary"
        [disabled]="todo.completed">
  <md-icon>mode_edit</md-icon>
</button>

With a little CSS, these remain hidden until you rollover (or click). You can see the code in the repo.

Going forward

As Angular Material 2 is still in alpha, there are some things missing – in particular, the signature MD button ripple effect. While there may be breaking changes to the API ahead, it is very much functioning. It also lives up to the claim of having a straightforward API that doesn't confuse developers, and is easy to leverage for the creation of great looking applications.  

This article originally appeared in net magazine issue 284; buy it here


ABOUT THE AUTHOR

Daniel Zen is system architect and CEO at zen.digital. His areas of expertise lie in full-stack JavaScript, agile deployment and best practices.