5

Delivering High-Quality UX with Material

In Chapter 4, Automated Testing, CI, and Release to Production, we mentioned the need to deliver a high-quality application. Currently, the app has a terrible look and feel to it that is only fit for a website created in the late 1990s. The first impression a user or a client gets about your product or your work is very important, so we must be able to create a great-looking application that also delivers a great user experience across mobile and desktop browsers.

As full-stack developers, it is difficult to focus on the polish of your application. This often gets worse as the feature set of an application rapidly grows. It is no fun to write great modular code backing your views, but then revert to CSS hacks and inline styles in a rush to improve the look and feel of your application.

Developed in close coordination with Angular, Angular Material is amazing. If you learn how to leverage Angular Material effectively, the features you create will look and work great from the get-go, whether you're working on small or large applications.

Angular Material will make you a far more effective web developer because it ships with a wide variety of user controls that you can leverage, and you won't have to worry about browser compatibility. As an added bonus, writing custom CSS will become a rarity.

While this chapter covers how to create an attractive user interface (UI) relying on Angular Material for a decent user experience (UX) out of the box, it is also important to know what not to do. There's a great website called User Interface, which demonstrates UI/UX worst practices, at https://userinyerface.com.

In this chapter, you are going to learn about the following:

The most up-to-date versions of the sample code for the book are on GitHub at the following repository linked. The repository contains the final and completed state of the code. You can verify your progress at the end of this chapter by looking for the end-of-chapter snapshot of code under the projects folder.

For Chapter 5:

  1. Clone the repo https://github.com/duluca/local-weather-app
  2. Execute npm install on the root folder to install dependencies
  3. The code sample for this chapter is under the following sub-folder:
    projects/ch5
    
  4. To run the Angular app for this chapter, execute:
    npx ng serve ch5
    
  5. To run Angular unit tests for this chapter, execute:
    npx ng test ch5 --watch=false
    
  6. To run Angular e2e tests for this chapter, execute:
    npx ng e2e ch5
    
  7. To build a production-ready Angular app for this chapter, execute:
    npx ng build ch5 --prod
    

Note that the dist/ch5 folder at the root of the repository will contain the compiled result.

Beware that the source code in the book or on GitHub may not always match the code generated by Angular CLI. There may also be slight differences in implementation between the code in the book and what's on GitHub because the ecosystem is ever-evolving. It is natural for the sample code to change over time. Also on GitHub, expect to find corrections, fixes to support newer versions of libraries, or side-by-side implementations of multiple techniques for you to observe. You are only expected to implement the ideal solution recommended in the book. If you find errors or have questions, please create an issue or submit a pull request on GitHub for the benefit of all readers.

Let's begin by understanding what makes Angular Material an excellent choice as a UI/UX library.

Angular Material

The goal of an Angular Material project is to provide a collection of useful and standard-setting high-quality UI components. The library implements Google's Material Design specification, which is pervasive in Google's mobile apps, web properties, and the Android operating system. Material Design has a particular digital and boxy look and feel, but it is not just another CSS library like Bootstrap. Consider the login experience coded using Bootstrap here:

Figure 5.1: Bootstrap login experience

Note that input fields and their labels are on separate lines, the checkbox is a small target to hit, the error messages are displayed as an ephemeral toast notification, and the Submit button just sits in the corner. Now, consider the following Angular Material sample:

Figure 5.2: Angular Material login experience

The input fields and their labels are initially combined, grabbing the user's attention in a compact form factor. The checkbox is touch-friendly and the Submit button stretches to take up the available space for a more responsive UX by default. Once a user clicks on a field, the label tucks away to the top-left corner of the input field, as shown:

Figure 5.3: Angular Material animations and error

In addition, the validation error messages are shown inline, combined with a color change in the label, keeping the user's attention on the input field.

Material Design helps you design a modular UI with your own branding and styling, while also defining animations that allow the user to have a better UX when using your application. The human brain subconsciously keeps track of objects and their locations. Any kind of animation that aids in transitions or reactions to changes that result from human input results in reduced cognitive load on the user, therefore allowing the user to focus on processing the content instead of trying to figure out the quirks of your particular app.

A combination of modular UI design and fluid motion creates a great UX. Look at how Angular Material implements a simple button:

Figure 5.4: Angular Material button animation

In the preceding screenshot, note how the click animation on the button originates from the actual location that the user has clicked on. However subtle, this creates a continuity motion, resulting in an appropriate onscreen reaction to a user's action. This particular effect becomes more pronounced when the button is used on a mobile device, leading to an even more natural human-computer interaction. Most users can't articulate what makes an intuitive UX actually intuitive, and these subtle yet crucial cues in design and experience allow you to make tremendous progress in designing such an experience for your users.

Angular Material also aims to become the reference implementation for high-quality UI components for Angular. If you intend to develop your own custom controls, the source code for Angular Material should be your first and foremost resource. The term high quality is used often, and it's really important to quantify what that means. The Angular Material team puts it well on their website:

What do we mean by "high quality"?

Internationalized and accessible so that all users can use them. Straightforward APIs that don't confuse developers and behave as expected across a wide variety of use cases without bugs. Behavior is well tested with both unit and integration tests. Customizable within the bounds of the Material Design specification. Performance cost is minimized. Code is clean and well documented to serve as an example for Angular devs. Browser and screen reader support.

Angular Material supports the most recent two versions of all major browsers: Chrome (including Android), Firefox, Safari (including iOS), and IE11/Edge.

Building web applications, especially ones that are also mobile-compatible, is really difficult. There are a lot of nuances that you must be aware of. Angular Material abstracts away these nuances, including supporting all major browsers, so that you can focus on creating your application. Angular Material is no fad, and it's not to be taken lightly. If used correctly, you can greatly increase your productivity and the perceived quality of your work.

It won't always be possible to use Angular Material in your projects. I would recommend either PrimeNG, found at https://www.primefaces.org/primeng, or Clarity, found at https://vmware.github.io/clarity, as component toolkits that can satisfy most, if not all, of your user-control needs. The one thing to avoid here would be to pull dozens of user controls from different sources and end up with a hodgepodge library with hundreds of quirks and bugs to learn, maintain, or work around.

One of the most significant challenges when working with UI components is the amount of bulk they can add to your app's bundle size. Next, let's see how using a cohesive component library can help keep the performance of your app in tip-top shape and configure Angular Material for your app.

Angular Material setup and performance

Angular Material is configured by default to optimize the package size of your final deliverable. In Angular JS and Angular Material 1.x, the entire dependent library would be loaded. However, now with Angular Material, we are able to specify only the components that we intend to use, resulting in dramatic performance improvements.

In the following table, you can see improvement of the performance characteristics of a typical Angular 1.x + Angular Material 1.x versus an Angular 6 + Material 6 application over a fiber connection with high speed and low latency:

Fiber Network Angular 6 + Material 6 Angular 1.5 + Material 1.1.5 % Diff

First paint (DOMContentLoaded) *

0.61 s

1.69 s**

~2.8x faster

JS bundle size*

113 KB

1,425 KB

12.6x smaller

*Images and other media content have not been included in the results for a fair comparison

**Average value: Lower quality infrastructure causes a wide range of render times from 0.9s to 2.5s

Under the ideal conditions of a high-speed and low-latency connection, Angular 6 + Material 6 apps load under a second. However, when we switch over to a more common moderate-speed and high-latency fast 3G mobile network, the differences become more pronounced, as in the following table:

Fast 3G Mobile Network Angular 6 + Material 6 Angular 1.5 + Material 1.1.5 % Diff

First paint*

1.94 s

11.02 s

5.7x faster

JS bundle size*

113 KB

1,425 KB

12.6x smaller

*Images or other media content have not been included in the results for a fair comparison

Even though the size differences of the apps remain consistent, you can see that the additional latency introduced by a mobile network results in a dramatic slowdown of the legacy Angular application to an unacceptable level.

Adding all components to Material will result in about ~1.3 MB of additional payload that will need to be delivered to the user. As you can see from the earlier comparison, this must be avoided at all costs. To deliver the smallest app possible, which is crucial in mobile- and sales-related scenarios, where every 100 ms of load time has an impact on user retention, you may load and include modules individually. Webpack's tree-shaking process will divide modules into different files, trimming down the initial download size.

As a real-world example, when you're done building the final version of the LocalCast Weather app, your app's bundle size will come in at around 800 KB, with a first paint happening in just over 2 seconds on a fast 3G connection with Angular 9 + Material 9. A fully-featured multi-page application that leverages lazy loading only loads around ~300 KB of dependencies, while maintaining a sub 2 second first paint.

Note that the sample app contains example code that can be trimmed away to make the app even smaller. This is a testament to how the Angular ecosystem can deliver a rich and optimized UX.

Next, let's set up Angular Material.

Installing Angular Material

There are two ways you can configure Angular Material for your Angular app:

Let's get started with the task and improve the UX of the weather app with Angular Material. Let's move the Improve the UX of the app task to In Progress on our GitHub project. Here, you can see the status of my Kanban board:

Figure 5.5: GitHub project Kanban board

Automatically

Since Angular 6, you can automatically add Angular Material to your project, saving a lot of time in the process:

  1. Execute the add command, as shown:
    $ npx ng add @angular/material 
    
  2. Choose the prebuilt theme named indigo-pink
  3. When you get the prompt "Set up global Angular Material typography styles?" enter "no"
  4. When you get the prompt "Set up browser animations for Angular Material?" enter "yes"
  5. The output should be similar to the following example:
    Installing packages for tooling via npm.
    Installed packages for tooling via npm.
    ? Choose a prebuilt theme name, or "custom" for a custom theme: Indigo/Pink        [ Preview: https://material.angular.io?theme=indigo-pink ]
    ? Set up global Angular Material typography styles? No
    ? Set up browser animations for Angular Material? Yes
    UPDATE package.json (1348 bytes)
    √ Packages installed successfully.
    UPDATE src/app/app.module.ts (423 bytes)
    UPDATE angular.json (3740 bytes)
    UPDATE src/index.html (487 bytes)
    UPDATE src/styles.css (181 bytes)
    

    Note that the index.html file has been modified to add the icons library and the default font, as follows:

    src/index.html
    <head>
      ...
      <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap" rel="stylesheet">
      <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
    </head>
    

    The angular.json file has been updated to set up the default theme:

    angular.json
    ...
    "styles": [
      "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
      "src/styles.css"
    ],
    ...
    

    styles.css has been updated with the default global CSS styles:

    src/styles.css
    html, 
    body { 
      height: 100%; 
    }
    body { 
      margin: 0; 
      font-family: Roboto, "Helvetica Neue", sans-serif; 
    }
    

    Also note that app.module.ts has been updated to import BrowserAnimationsModule, as shown:

    src/app/app.module.ts
    import { BrowserAnimationsModule } from '@angular/platform- browser/animations';
    @NgModule({ 
      declarations: [
        AppComponent
      ],
      imports: [
        ...
        BrowserAnimationsModule
      ],
    
  6. Start your app and ensure that it works correctly:
    $ npm start
    

With that, you're done. Your app should be configured with Angular Material. You can now skip over to the Importing modules section to see how you can import Material modules in a robust manner.

I strongly recommend skimming over all the manual installation and configuration steps. The more you know!

It is still important to understand all the various components that make up Angular Material or maybe you dislike automatic things; in the next sections, we will go over the manual installation and configuration steps.

Manually

We will begin by installing all the required libraries. As of Angular 5, the major version of Angular Material should match the version of your Angular installation and with Angular 6, the versions should be synced:

  1. In the Terminal, execute npm install @angular/material @angular/cdk
  2. Observe the package.json versions:
    package.json
     "dependencies": { 
        "@angular/cdk": "9.0.0",
        "@angular/material": "9.0.0", 
        ...
    

In this case, all libraries have the same major and minor versions. If your major and minor versions don't match, you can rerun the npm install command to install a specific version or choose to upgrade your version of Angular by appending the server version of the package to the install command:

$ npm install @angular/material@9.0.0 @angular/cdk@9.0.0 

If you are working on a Bash-like shell, you can save some typing by using the bracket syntax to avoid having to repeat portions of the command, in the form of npm install @angular/{material,cdk}@9.0.0.

If you need to update your version of Angular, refer to the Updating Angular section in Appendix C, Keeping Angular and Tools Evergreen. You can find this appendix online from https://static.packt-cdn.com/downloads/9781838648800_Appendix_C_Keeping_Angular_and_Tools_Evergreen.pdf or at https://expertlysimple.io/stay-evergreen.

Understanding Material's components

Let's look at what exactly we are installing:

Manually configuring Angular Material

Now that the dependencies are installed, let's configure Angular Material in our Angular app. Note that if you used ng add @angular/material to install Angular Material, some of this work will be done for you.

Importing modules

We will start by creating a separate module file to house all of our Material module imports:

  1. Execute the following command in the Terminal to generate material.module.ts:
    $ npx ng g m material --flat -m app
    

    Note the use of the --flat flag, which indicates that an additional directory shouldn't be created for material.module.ts. Also, note that -m, an alias for --module, is specified so that our new module is automatically imported into app.module.ts.

  2. Observe the newly created file material.module.ts and remove CommonModule:
    src/app/material.module.ts
    import { NgModule } from '@angular/core' 
    @NgModule({
      imports: [], 
      declarations: [],
    })
    export class MaterialModule {}
    
  3. Ensure that the module has been imported into app.module.ts:
    src/app/app.module.ts
    import { MaterialModule } from './material.module'
    ...
    @NgModule({
      ...
      imports: [..., MaterialModule],
    }
    
  4. Add animations and gesture support (if not automatically added):
    src/app/app.module.ts
    import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
    @NgModule({
      ...
      imports: [..., MaterialModule, BrowserAnimationsModule],
    }
    
  5. Modify material.module.ts to import and export basic components for MatButton, MatToolbar, and MatIcon:
    src/app/material.module.ts
    import { NgModule } from '@angular/core'
    import { MatButtonModule } from '@angular/material/button'
    import { MatIconModule } from '@angular/material/icon'
    import { MatToolbarModule } from '@angular/material/toolbar'
    @NgModule({
      imports: [
        MatButtonModule, MatToolbarModule, MatIconModule
      ], 
      exports: [
        MatButtonModule, MatToolbarModule, MatIconModule
      ],
    })
    export class MaterialModule {}
    

    The imports and exports arrays can sometimes become long and duplicative. If you miss an element in one of the arrays, you could be chasing bugs for hours. Consider implementing a single array as a constant that you can assign to imports and exports properties for a more reliable configuration. Thanks to Brendon Caulkins for the tip.

  6. Optimize your code to store your modules in an array and reuse it to import and export:
    src/app/material.module.ts
    ...
    const modules = 
      [MatButtonModule, MatToolbarModule, MatIconModule]
    @NgModule({
      declarations: [],
      imports: modules,
      exports: modules,
    })
    export class MaterialModule {}
    

Material is now imported into the app; let's now configure a theme and add the necessary CSS to our app.

Importing themes

A base theme is necessary in order to use Material components. We already selected a default theme when installing Angular Material. We can define or change the default theme in angular.json:

angular.json
...
"styles": [
  {
    "input":
      "node_modules/@angular/material/prebuilt-themes/indigo-pink.css"
  },
  "src/styles.css"
],
...

Choose a new option from here:

Update angular.json to use the new Material theme.

You may create your own themes as well, which is covered in the Custom themes section of this chapter. For more information, visit https://material.angular.io/guide/theming.

Note that any CSS implemented in styles.css will be globally available throughout the application. That said, do not include view-specific CSS in this file. Every component has its own CSS file for this purpose.

Adding the Material Icon font

You can get access to a good default set of iconography by adding the Material Icon web font to your application. Clocking in at 48 KB in size, this is a very lightweight library.

For icon support, import the font in index.html:

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

Discover and search through the icons at https://material.io/resources/icons.

For a richer set of icons, check out MaterialDesignIcons.com. This icon set contains the base set of Material icons, plus a rich set of third-party icons that contains useful imagery from social media sites for a rich set of actions that cover a lot of ground. This font is 118 KB in size.

Our UI/UX library for Angular Material is now configured. We also need a layout library that can make life easier when placing components on the page.

Next, let's learn about different layout techniques, from Bootstrap to Flexbox CSS, and why Angular Flex Layout is a great tool to manage your layout. After configuring Angular Flex Layout for our app, we will be ready to implement Material UI components in our app.

Angular Flex Layout

Before you can make effective use of Material, you must be aware of its layout engine. If you have been doing web development for a while, you may have encountered Bootstrap's 12-column layout system. I find it enormously irritating, since it falls foul of a mathematical barrier in my brain, which is wired to divvy things up as parts of 100%. Bootstrap also demands strict adherence to a div column and row hierarchy that must be precisely managed from your top-level HTML to the bottom. This can make for a very frustrating development experience.

In the following screenshot, you can see how Bootstrap's 12-column scheme looks:

Figure 5.6: Bootstrap's 12-column layout scheme

Bootstrap's custom grid-layout system was revolutionary for its time, but then CSS3 Flexbox arrived on the scene. In combination with media queries, these two technologies allow for the creation of responsive UIs. However, it is very laborious to leverage these technologies effectively. As of Angular v4.1, the Angular team introduced its Flex Layout system that just works.

The Angular Flex Layout documentation on GitHub aptly explains the following:

Angular Flex Layout provides a sophisticated layout API using FlexBox CSS and mediaQuery. This module provides Angular (v4.1 and higher) developers with component layout features using a custom Layout API, mediaQuery observables, and injected DOM flexbox-2016 CSS stylings.

Angular's excellent implementation makes it very easy to use Flexbox. As the documentation further explains:

The Layout engine intelligently automates the process of applying appropriate FlexBox CSS to browser view hierarchies. This automation also addresses many of the complexities and workarounds encountered with the traditional, manual, CSS-only application of Flexbox CSS.

The library is highly capable and can accommodate any kind of grid layout you can imagine, including integration with all CSS features that you would expect, such as the calc() function. In the next illustration, you can see how columns can be described using CSS Flexbox:

Figure 5.7: Angular Flex Layout scheme

The great news is that Angular Flex Layout is in no way coupled with Angular Material and can be used independently of it. This is a very important decoupling that resolves one of the major pain points of using AngularJS with Material v1, where version updates to Material would often result in bugs in layout.

For more details, check out https://github.com/angular/flex-layout/wiki.

You will notice that @angular/flex-layout is installed with a beta tag. This has been the status quo for this library for a very long time. It hasn't been possible for the library to cover every edge case going back to Internet Explorer 11, which prevents it from exiting beta. However, in evergreen browsers, I've found the behavior of the library to reliable and consistent. Further, CSS Grid is poised to supersede CSS Flexbox and as a result, the underlying technology that this library uses may change. My wish is that this library acts as an abstraction layer to the layout engine underneath.

Responsive layouts

All UIs you design and build should be mobile-first UIs. This is not just to serve mobile phone browsers, but also cases where a laptop user may use your application in a window side by side with another one. There are many nuances to getting mobile-first design right.

The following is the Mozilla Holy Grail Layout, which demonstrates "the ability to dynamically change the layout for different screen resolutions" while optimizing the display content for mobile devices.

You can read more about basic concepts of Flexbox at https://mzl.la/2vvxj25.

This is a representation of how the UI looks on a large screen:

Figure 5.8: Mozilla Holy Grail Layout on a large screen

The same layout is represented on a small screen as follows:

Figure 5.9: Mozilla Holy Grail Layout on a small screen

Mozilla's reference implementation takes 85 lines of code to accomplish this kind of responsive UI. Angular Flex Layout accomplishes the same task with only half the code.

Installing Angular Flex Layout

Let's install and add Angular Flex Layout to our project:

  1. In the Terminal, execute npm i @angular/flex-layout
  2. Update app.module.ts, as shown:
    src/app.module.ts
    import { FlexLayoutModule } from '@angular/flex-layout'
    imports: [..., FlexLayoutModule ], 
    

With Flex Layout installed, let's cover the basics of how the library works.

Layout basics

Bootstrap and CSS Flexbox are different beasts than Angular Flex Layout. If you learn Angular Flex Layout, you will find yourself using a lot less layout code, because Angular Material automatically does the right thing most of the time, but you'll be in for a disappointment once you realize how much more code you have to write to get things working once you leave the protective cocoon of Angular Flex Layout. However, your skills still translate over since the concepts are mostly the same.

Let's review the Flex Layout APIs in the following sections.

If you're new to CSS or even Flexbox, some of the abbreviations used may not make sense. I recommend that you experiment with the live demo app provided in the documentation to get a better sense of the capabilities of the library at a more intuitive level. For more information and a link to the live demo visit https://github.com/angular/flex-layout/wiki/Declarative-API-Overview.

Flex Layout APIs for DOM containers

These directives can be used on DOM containers such as <div> or <span> to manipulate their layout direction, alignment, or gaps in between elements.

Consider the following example:

<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="15px">...</div>

The div is laid out as a row, so multiple divs would be rendered on top of each other, versus a column layout where they would render next to each other.

The div is aligned to horizontally left-aligned and vertically centered within its parent container.

The div has a 15-px gap between its surrounding elements.

Consider the following diagram to map fxLayout terminology spatially:

css3-flexbox-model

Figure 5.10: Spatial mapping of Angular Flex Layout terminology

The full list of options is presented in the following table:

HTML API Allowed values

fxLayout

<direction> | <direction> <wrap>

Use: row | column | row-reverse | column-reverse

fxLayoutAlign

<main-axis> <cross-axis>

main-axis: start |center | end | space-around | space-between

cross-axis: start | center | end | stretch

fxLayoutGap

% | px | vw | vh

Flex Layout APIs for DOM elements

These directives influence how DOM elements act within their containers.

Consider the following example:

<div fxLayout="column">
  <input fxFlex />
</div>

The input element will grow to fill all available space that the parent div provides. If fxFlex was set to fxFlex="50%", it would fill only half of the available space. In this case the fxFlexAlign property could be used to left-, right-, or center-align the element within the div.

The full list of options is presented in the following table:

HTML API Allowed values

fxFlex

"" | px | % | vw | vh | <grow> <shrink> <basis>

fxFlexOrder

int

fxFlexOffset

% | px | vw | vh

fxFlexAlign

start | baseline | center | end

fxFlexFill

none

Flex Layout APIs for any element

The following directives can be applied to any HTML element to show, hide, or change the look and feel of these elements.

Consider the following example:

<div fxShow fxHide.lt-sm></div>

fxShow, set to true by default, will show the div element. Unless the lt-sm condition becomes true, which occurs when the browser window shrinks below the threshold of small. Small is defined as a pixel value of 468 px. So, if the width of the browser window shrinks to 467 px or less, fxHide would hide the div element.

The full list of options is presented in the following table:

HTML API Allowed values

fxHide

TRUE | FALSE | 0 | ""

fxShow

TRUE | FALSE | 0 | ""

ngClass

@extends ngClass core

ngStyle

@extends ngStyle core

Now that our layout engine is configured and you have a rudimentary understanding of how it works, we can start building the screens for our app.

Using Material components

Now that we have all the various dependencies installed, we can start modifying our Angular app to add Material components. We will add a toolbar and a Material Design card element, and cover accessibility and typography concerns alongside basic layout techniques.

Angular Material schematics

Since Angular 6 and the introduction of schematics, libraries like Material can provide their own code generators. At the time of publication, Angular Material ships with three rudimentary generators to create Angular components with a side navigation, a dashboard layout, or a data table. You can read more about generator schematics at https://material.angular.io/guide/schematics.

For example, you can create a side navigation layout by executing the following command:

$ ng generate @angular/material:material-nav --name=side-nav
CREATE src/app/side-nav/side-nav.component.css (110 bytes) CREATE src/app/side-nav/side-nav.component.html (945 bytes) CREATE src/app/side-nav/side-nav.component.spec.ts (619 bytes) CREATE src/app/side-nav/side-nav.component.ts (489 bytes) UPDATE src/app/app.module.ts (882 bytes)

This command updates app.module.ts, directly importing Material modules into that file, breaking my suggested material.module.ts pattern from earlier. Further, a new SideNavComponent is added to the app as a separate component, but as mentioned in the Side navigation section in Chapter 8, Designing Authentication and Authorization, such a navigation experience needs to be implemented at the very root of your application.

In short, Angular Material Schematics makes it a lot less cumbersome to add various Material modules and components to your Angular app; however, as provided, these schematics are not suitable for creating a flexible, scalable, and well-architected code base, which is the goal pursued by this book.

For the time being, I would recommend using these schematics for rapid prototyping or experimentation purposes.

Now, let's start manually adding some components to our LocalCast Weather app.

Modifying the landing page with Material Toolbar

Before we start making further changes to app.component.ts, let's switch the component to use inline templates and inline styles, so we don't have to switch back and forth between files for a relatively simple component:

  1. Update app.component.ts to use an inline template. Cut and paste the contents of app.component.html to app.component.ts and remove the styleUrls property as shown below:
    src/app/app.component.ts
    import { Component } from '@angular/core'
    @Component({
      selector: 'app-root', 
      template: `
        <div style="text-align:center">
          <h1>
            LocalCast Weather
          </h1>
          <div>Your city, your forecast, right now!</div>
          <h2>Current Weather</h2>
          <app-current-weather></app-current-weather>
        </div>
      `,
    })
    export class AppComponent {}
    
  2. Delete the files app.component.html and app.component.css.
  3. Let's start improving our app by implementing an app-wide toolbar. Observe the h1 tag in app.component.ts:
    src/app/app.component.ts
    <h1>
      LocalCast Weather
    </h1>
    
  4. Update the h1 tag with mat-toolbar:
    src/app/app.component.ts
    <mat-toolbar>
      <span>LocalCast Weather</span>
    </mat-toolbar>
    
  5. Update mat-toolbar with a more attention-grabbing color:
    src/app/app.component.ts
    <mat-toolbar color="primary">
    

Note that your app will fail to compile if you didn't import MatToolbarModule as instructed in the earlier section Importing modules.

Note that Material adds the following style as a global style:

src/styles.css
body {
  margin: 0;
}

Having a 0 margin provides a native app feeling, where the toolbar touches the edges of the browser. This works well both on large- and small-screen formats. When you place clickable elements such as a hamburger menu or a help button on the far-left or far-right side of the toolbar, you'll avoid the potential that the user will click on empty space. This is why Material buttons actually have a larger hit-area than visually represented. This makes a big difference in crafting frustration-free UXs.

Similarly, if you were building an information-dense application, note that your content would go all the way to the edges of the application, making your content more difficult to read, which is not a desirable outcome. In these cases, you should wrap your content area in a div and apply the appropriate margins using CSS, as shown here:

example
.content-margin { 
  margin-left: 8px; 
  margin-right: 8px;
}

In the next screenshot, you can see the edge-to-edge toolbar with the primary color applied to it:

Figure 5.11: LocalCast Weather with improved toolbar

Now we have the toolbar configured, let's move on to making a container for the weather information.

Material cards

Material cards are a great container to represent the current weather information. The card element is surrounded by a drop-shadow that delineates the content from its surroundings:

  1. Import MatCardModule in material.module:
    src/app/material.module.ts
    import { MatCardModule } from '@angular/material/card'
    ...
    const modules = […, MatCardModule]
    
  2. In AppComponent's template, surround <app-current-weather> with <mat-card>:
    src/app/app.component.ts
    ...
      template: `
      ...
      <div style="text-align:center">
        <mat-toolbar color="primary">
          <span>LocalCast Weather</span>
        </mat-toolbar>
        <div>Your city, your forecast, right now!</div>
        <mat-card>
          <h2>Current Weather</h2>
          <app-current-weather></app-current-weather>
        </mat-card>
      </div>
      ...
      `,
    ...
    
  3. Observe the barely distinguishable card element with its shadow near the bottom of the screen below:

    Figure 5.12: LocalCast Weather with indistinguishable card element

    To lay out the screen better, we need to switch to the Flex Layout engine. We'll start by removing the training wheels from the component template.

  4. Remove style="text-align:center" from the outermost <div> element.
  5. Surround <mat-card> with the following HTML, where the contents of <mat-card> replaces the ellipses in the middle of the code:

    To center an element in a page, we need to create a row, assign a width to the center element, and create two additional columns on either side that can flex to take the empty space, such as this:

    src/app/app.component.ts
    ...
    <div fxLayout="row">
      <div fxFlex></div>
      <div fxFlex="300px">
          ...
      </div>
      <div fxFlex></div>
    </div>
    ...
    
  1. Observe that the mat-card element is properly centered, as follows:

Figure 5.13: LocalCast Weather with centered card

Reading through the card documentation and looking through the examples on Material's documentation site at https://material.angular.io/components/card/overview, you'll note that mat-card provides elements to house the title and content. We will implement this in the upcoming sections.

On material.angular.io, you can view the source code of any example by clicking on the brackets icons or launch a working example in StackBlitz.io by clicking on the arrow icon.

Card header and content

Now, let's implement the title and content elements of mat-card using mat-card-header and mat-card-content, as shown:

src/app/app.component.ts
...
<mat-toolbar color="primary">
  <span>LocalCast Weather</span>
</mat-toolbar>
<div>Your city, your forecast, right now!</div>
<div fxLayout="row">
  <div fxFlex></div>
  <div fxFlex="300px">
    <mat-card>
      <mat-card-header>
        <mat-card-title>Current Weather</mat-card-title>
      </mat-card-header>
      <mat-card-content>
        <app-current-weather></app-current-weather>
      </mat-card-content>
    </mat-card>
  </div>
  <div fxFlex></div>
</div>
...

All Material elements have native support for the Flex Layout engine. This allows us to optimize our HTML and merge <div fxFlex="300px"> with <mat-card> and simplify the code:

src/app/app.component.ts
...
<div fxLayout="row">
  <div fxFlex></div>
  <mat-card fxFlex="300px">
    ...
  </mat-card>
  <div fxFlex></div>
</div>
...

This has tremendous positive implications for maintainability in complicated UIs.

Don't forget: With Material, less is always more.

After we apply mat-card-header and mat-card-content, you can see this result:

Figure 5.14: LocalCast Weather card with title and content

Note that fonts within the card now match Material's Roboto font. However, Current Weather is no longer as attention-grabbing as it was before. If you add back in the h2 tag inside mat-card-title, Current Weather will visually look bigger; however, the font won't match the rest of your application. To fix this issue, you must understand Material's typography features.

Material typography

Material's documentation aptly puts it as follows:

Typography is a way of arranging type to make text legible, readable, and appealing when displayed.

Material offers a different level of typography that has different font-size, line-height, and font-weight characteristics that you can apply to any HTML element, not just the components provided out of the box.

In the following table are CSS classes that you can use to apply Material's typography.

Consider the following example:

<div class="mat-display-4">Hello, Material world!</div>

The display-4 typography is applied to the div by prepending it with "mat-".

See the following table for a full list of typographical styles:

Class Name Usage

display-4, display-3, display-2, and display-1

Large, one-off headers, usually at the top of the page (for example, a hero header)

h1, headline

Section heading corresponding to the <h1> tag

h2, title

Section heading corresponding to the <h2> tag

h3, subheading-2

Section heading corresponding to the <h3> tag

h4, subheading-1

Section heading corresponding to the <h4> tag

body-1

Base body text

body-2

Bolder body text

Caption

Smaller body and hint text

Button

Buttons and anchors

You can read more about Material typography at https://material.angular.io/guide/typography.

Applying typography

There are multiple ways to apply typography. One way is to leverage the mat-typography class and use the corresponding HTML tag, such as <h2>:

example
<mat-card-header class="mat-typography">
<mat-card-title><h2>Current Weather</h2></mat-card-title>
</mat-card-header>

Another way is to apply the specific typography directly on an element, as in class="mat-title":

example
<mat-card-title>
  <div class="mat-title">Current Weather</div>
</mat-card-title>

Note that class="mat-title" can be applied to div, span, or an h2 with the same results.

As a rule of thumb, it is usually a better idea to implement the more specific and localized option, which is the second implementation here.

As we implement Material typography in the upcoming sections, we need to ensure the card title stands out from rest of the elements on the screen. In this context, I prefer the look of the mat-headline typography to achieve this goal, so your implementation should look like:

src/app/app.component.ts
<mat-card-title>
  <div class="mat-headline">Current Weather</div>
</mat-card-title>

Next, let's see how we can align the other elements on the screen.

Flex Layout Align

We can center the tagline of the application using fxLayoutAlign and give it a subdued look using the mat-caption typography:

  1. Center the div containing the tagline using fxLayoutAlign:
    src/app/app.component.ts
    <div fxLayoutAlign="center">
      <div>
        Your city, your forecast, right now!
      </div>
    </div>
    
  2. Apply the mat-caption typography to the tagline:
    src/app/app.component.ts
    <div class="mat-caption">
      Your city, your forecast, right now!
    </div>
    
  3. Observe the results, as shown here:

Figure 5.15: LocalCast Weather with centered tagline

Next, we need to work on laying out the elements to match the design.

Flex Layout

There's still more work to do to make the UI look like the design. Observe the following design of the Current Weather card:

Figure 5.16: Lo-fi design of Current Weather

To design the layout, we'll leverage Angular Flex.

You'll be editing current-weather.component.html, which uses the <div> and <span> tags to establish elements that live on separate lines or on the same line, respectively. With the switch over to Angular Flex, we need to switch all elements to <div> and specify rows and columns using fxLayout.

Implementing layout scaffolding

We need to start by implementing the rough scaffolding. Consider the current state of the template:

src/app/current-weather/current-weather.component.html
 1  <div *ngIf="!current">
 2    no data
 3  </div>
 4  <div *ngIf="current">
 5    <div>
 6      <span>{{current.city}}, {{current.country}}</span>
 7      <span>{{current.date | date:'fullDate'}}</span>
 8    </div>
 9    <div>
10      <img [src]='current.image'>
11      <span>{{current.temperature | number:'1.0-0'}}˚F</span>
12    </div>
13    <div>
14      {{current.description}}
15    </div>
16  </div>

Let's go through the file step by step and update it. First let's make the structural changes to support Flex Layout:

  1. On lines 6, 7, and 11, update <span> elements to <div> elements.
  2. On line 10, wrap the <img> element with a <div> element.
  3. On lines 5 and 9, add the fxLayout="row" property to the outer <div> element that has multiple child elements.

Next, apply the fxFlex attribute to div elements to determine how much horizontal space elements should take:

  1. On line 6, the City and Country column should take roughly ²⁄³ of the screen, so add fxFlex="66%" to the <div> element.
  2. On line 7, add fxFlex to the <div> element to ensure that it fills up the rest of the horizontal space.
  3. On line 10, add fxFlex="66%" to the new <div> element, surrounding the <img> element.
  4. On line 11, add fxFlex to the <div> element.

The final state of the template should look like this:

src/app/current-weather/current-weather.component.html
 5    <div fxLayout="row">
 6      <div fxFlex="66%">{{current.city}}, ...</div>
 7      <div fxFlex>{{current.date | date:'fullDate'}}</div>
 8    </div>
 9    <div fxLayout="row">
10      <div fxFlex="66%"><img [src]='current.image'></div>
11      <div fxFlex>{{current.temperature | number:'1.0-0'}}˚F</div>
12    </div>
13    <div>
14      {{current.description}}
15    </div>

You can be more verbose in adding Angular Flex attributes; however, the more code you write, the more you'll need to maintain, making future changes more difficult. For example, on line 13, the <div> element doesn't need fxLayout="row", since a <div> implicitly gets a new line. Similarly, on lines 7 and 11, the right-hand column doesn't need an explicit fxFlex attribute, since the left-hand element automatically squeezes it. However, we are going to keep those fxFlex attributes in.

From a grid placement perspective, all your elements are now in the correct cell, as shown here:

Figure 5.17: LocalCast Weather with layout scaffolding

With a responsive design implemented, next, let's work on alignment of major elements.

Aligning elements with CSS

Now, we need to align and style each cell to match the design. For this purpose, we rely on CSS over fxLayoutAlign. The date and temperature need to be right-aligned and the description centered:

  1. To right-align the date and temperature, create a new CSS class named .right in current-weather.component.css:
    src/app/current-weather/current-weather.component.css
    .right {
      text-align: right
    }
    
  2. Add class="right" to the <div> element on lines 7 and 11.
  3. Center the <div> element for the description in the same way you centered the app's tagline earlier in the chapter. Use a surrounding div with an fxLayoutAlign="center" attribute.
  1. Observe that the elements are aligned correctly, as follows:

Figure 5.18: LocalCast Weather with correct alignments

After aligning the major elements, let's apply the first layer of styling to every element to match the design.

Individually styling elements

Finalizing the styling of elements is usually the most time-consuming part of front-end development. I recommend doing multiple passes to achieve a close-enough version of the design with minimal effort first and then have your client or team decide whether it's worth the extra resources to spend more time to polish the design:

  1. Add a new CSS property:
    src/app/current-weather/current-weather.component.css
    .no-margin { 
      margin-bottom: 0
    }
    
  2. For the city name, add class="mat-title no-margin".
  3. For the date, add "mat-h3 no-margin" to class="right".
  4. Change the display format of the date from 'fullDate' to 'EEEE MMM d' so it matches the design.
  5. Modify <img> to add style="zoom: 175%".
  6. For the temperature, add "mat-display-3 no-margin" to class="right".
  7. For the description, add class="mat-caption".

    This is the final state of the template:

    src/app/current-weather/current-weather.component.html
    <div *ngIf="!current">
      no data
    </div>
    <div *ngIf="current">
      <div fxLayout="row">
        <div fxFlex="66%" class="mat-title no-margin">
          {{current.city}}, {{current.country}}
        </div>
        <div fxFlex class="right mat-h3 no-margin">
          {{current.date | date:'EEEE MMM d'}}
        </div>
      </div>
      <div fxLayout="row">
        <div fxFlex="66%">
          <img style="zoom: 175%" [src]='current.image'>
        </div>
        <div fxFlex class="right mat-display-3 no-margin">
        {{current.temperature | number:'1.0-0'}}˚F
      </div>
    </div>
      <div fxLayoutAlign="center">
        <div class="mat-caption">
          {{current.description}}
        </div>
      </div>
    </div>
    
  8. Observe that the styled output of your code changes, as illustrated:

    Figure 5.19: LocalCast Weather with styling

We're done with adding the first layer of styles for our design. Next, let's fine-tune the spacing and alignment of elements.

Fine-tuning styles

The tagline can benefit from some top and bottom margins. This is common CSS that we're likely to use across the application, so let's put it in styles.css:

  1. Implement vertical-margin in the global styles.css:
    src/styles.css
    .vertical-margin { 
      margin-top: 16px; 
      margin-bottom: 16px;
    }
    
  2. In app.component.ts, apply vertical-margin to the app's tagline:
    src/app/app.component.ts
    <div class="mat-caption vertical-margin">
      Your city, your forecast, right now!
    </div>
    
  3. In current-weather.component.html, the image and the temperature aren't centered, so add fxLayoutAlign="center center" to the outer div surrounding these elements:
    src/app/current-weather/current-weather.component.html
    <div fxLayout="row" fxLayoutAlign="center center">
      ...
    </div>
    
  4. Observe the finalized layout of your app, which should look like this:

    Figure 5.20: LocalCast Weather finalized layout

Finally, let's add some more visual flair by tightening up our design, like fixing the missing line break between the day and the month, and adding some nice-to-have features.

Tweaking to match design

This is an area where you may spend a significant amount of time. If we were following the 80-20 principle, pixel-perfect tweaks usually end up being the last 20% that takes 80% of the time to complete. Let's examine the differences between our implementation in the previous figure, and the original design as shown in the following figure, and what it would take to bridge the gap:

Figure 5.21: LocalCast Weather original design

The date needs further customization. The numeric ordinal th is missing in our implementation; to accomplish this, we will need to bring in a third-party library such as moment or implement our own solution and bind it next to the date on the template:

  1. Implement a getOrdinal function in CurrentWeatherComponent:
    src/app/current-weather/current-weather.component.ts
    export class CurrentWeatherComponent implements OnInit {
      ...
      getOrdinal(date: number) {
        const n = new Date(date).getDate()
        return n > 0
          ? ['th', 'st', 'nd', 'rd'][(n > 3 && n < 21) || 
                                      n % 10 > 3 ? 0 : n % 10]
          : ''
      }
      ...
    }
    
  2. In the template, update current.date to append an ordinal to it:
    src/app/current-weather/current-weather.component.html
    <div fxFlex class="right mat-h3 no-margin">
      {{current.date | 
        date:'EEEE MMM d'}}{{getOrdinal(current.date)}}
    </div>
    

    Note that the implementation of getOrdinal boils down to a complicated one-liner that isn't very readable and is very difficult to maintain. Such functions, if critical to your business logic, should be heavily unit tested.

    Next, let's fix the missing line break between the day of the week and the month. On certain days like Monday Mar 23rd, Monday and Mar will be on the first line, leaving 23rd by itself on the second line. However, on Tuesday Mar 24th, the issue doesn't exist and Mar and 24th fall on the same line. Angular, at the time of publishing, doesn't support new line breaks in the date template; ideally, we should be able to specify the date format as "EEEE\nMMM d" to ensure that the line break is always consistent. We can, however, throw some inefficient code at the problem and enforce the behavior we desire.

  3. Break up the current date into two parts and separate them with the line break tag <br>, then remove the class right from the outer div:
    src/app/current-weather/current-weather.component.html
    <div fxFlex class="mat-h3 no-margin">
      {{current.date | date:'EEEE'}}<br>
      {{current.date | date:'MMM d'}}{{getOrdinal(current.date)}}
    </div>
    

    Never use <br> for layout purposes. It's acceptable in this limited case, because we're breaking up content within a div or a p tag.

    Now, let's add some visual flair, when displaying the temperature unit. To accomplish this, the temperature implementation needs to separate the digits from the unit with a <span> element, surrounded with a <p> element, so a superscript style can be applied to the unit along the lines of <span class="unit">˚F</span>, where unit is a CSS class that makes its content look like a superscript element.

  4. Implement a unit CSS class:
    src/app/current-weather/current-weather.component.css
    .unit {
    vertical-align: super;
    }
    
  5. Reduce the flex on the image to 55%, wrap the temperature and the unit with a p tag and apply mat-display-3 on the p tag. Then implement a span around the temperature unit with the p tag, and apply the classes unit and mat-display-1 to the span:
    src/app/current-weather/current-weather.component.html
    <div fxFlex="55%">
      <img style="zoom: 175%" [src]='current.image'>
    </div>
    <div fxFlex class="right no-margin">
      <p class="mat-display-3">
        {{current.temperature | number:'1.0-0'}}
        <span class="mat-display-1 unit">˚F</span>
      </p>
    </div>
    

You usually need to experiment with how much space the forecast image should have by tweaking the fxFlex value on the preceding first line. If it takes too much space, the temperature overflows to the next line. Your settings can further be affected by the size of your browser window. 60% seems to work well, but when I was coding this sample the current weather was 55˚F, so for entirely poetic reasons I decided to go with 55%. See the polished version of our app here:

Figure 5.22: LocalCast Weather after tweaks

As always, it is possible to further tweak the margins and paddings to further customize the design. However, each deviation from the library will have maintainability consequences down the line. Unless you're truly building a business around displaying weather data, you should defer any further optimizations to the end of the project, if time permits, and if experience is any guide, you will not be making this optimization.

With two negative margin-bottom hacks, you can attain a design fairly close to the original, but I will not include those hacks here and leave it as an exercise for the reader to discover on the GitHub repository. Such hacks are sometimes necessary evils, but in general, they point to a disconnect between design and implementation realities. The solution leading up to the tweaks section is the sweet spot, where Angular Material thrives. Beyond that you're probably wasting your time. I went ahead and wasted my time for you and here's the result:

Figure 5.23: LocalCast Weather after tweaks and hacks

Now that our layout and design is in great shape, let's look into creating a custom theme using Angular Material.

Custom themes

As we previously discussed, Material ships with some default themes including deeppurple-amber, indigo-pink, pink-blue-grey, and purple-green. However, your company or product may have its own color scheme. For this, you can create a custom theme to change the look of your application.

In order to create a new theme, you must implement a new SCSS file:

  1. Remove all definitions of your default theme from angular.json.
  2. Re-run the command npx ng add @angular/material.
  3. This time select Custom as the theme.
  4. After running the command make sure that your index.html and styles.css files have not been modified. If so, revert the changes.
  5. This will create a new file under src called custom-theme.scss. Rename it to localcast-theme.scss, shown as follows:
    src/localcast-theme.scss
    // Custom Theming for Angular Material
    // For more information: https://material.angular.io/guide/theming
    @import '~@angular/material/theming';
    // Plus imports for other components in your app.
    // Include the common styles for Angular Material.
    // We include this here so that you only have to
    // load a single css file for Angular Material in your app.
    // Be sure that you only ever include this mixin once!
    @include mat-core();
    // Define the palettes for your theme using
    // the Material Design palettes available in palette.scss
    // (imported above). For each palette, you can optionally
    // specify a default, lighter, and darker hue.
    // Available color palettes: https://material.io/design/color/
    $local-weather-app-primary: mat-palette($mat-indigo);
    $local-weather-app-accent: mat-palette(
      $mat-pink,
      A200, A100, A400
    );
    // The warn palette is optional (defaults to red).
    $local-weather-app-warn: mat-palette($mat-red);
    // Create the theme object (a Sass map containing
    // all of the palettes).
    $local-weather-app-theme: mat-light-theme(
      $local-weather-app-primary,
      $local-weather-app-accent,
      $local-weather-app-warn
    );
    // Custom Theming for Angular Material
    // For more information: https://material.angular.io/guide/theming
    @import '~@angular/material/theming';
    // Plus imports for other components in your app.
    // Include the common styles for Angular Material.
    // We include this here so that you only have to
    // load a single css file for Angular Material in your app.
    // Be sure that you only ever include this mixin once!
    @include mat-core();
    // Define the palettes for your theme using
    // the Material Design palettes available in palette.scss
    // (imported above). For each palette, you can optionally
    // specify a default, lighter, and darker hue.
    // Available color palettes: https://material.io/design/color/
    $local-weather-app-primary: mat-palette($mat-indigo);
    $local-weather-app-accent: mat-palette($mat-pink, A200, A100, A400);
    // The warn palette is optional (defaults to red).
    $local-weather-app-warn: mat-palette($mat-red);
    // Create the theme object (a Sass map containing
    // all of the palettes).
    $local-weather-app-theme: mat-light-theme(
      $local-weather-app-primary,
      $local-weather-app-accent,
      $local-weather-app-warn
    );
    // Include theme styles for core and each component used in
    // your app. Alternatively, you can import and @include the
    // theme mixins for each component that you are using.
    @include angular-material-theme($local-weather-app-theme);
    

    Note that mat-core() should only be included once in your application; otherwise, you'll introduce unnecessary and duplicated CSS payloads in your application.

    mat-core() contains the necessary SCSS functions to be able to inject your custom colors into Material, such as mat-palette, mat-light-theme, and mat-dark-theme.

    At a minimum, we must define a new primary and an accent color. Defining new colors, however, is not a straightforward process. Material requires a palette to be defined through mat-palette, which needs to be seeded by a complicated color object that can't just be overridden by a simple hex value such as #BFB900.

    To pick your colors, you can use the Material Design Color Tool, located at https://material.io/resources/color. Here's a screenshot of the tool:

    Figure 5.24: Material.io Color Tool

  6. Using Material Palette, select a Primary and a Secondary color:
  7. Observe how your selections would apply to a Material Design app by going through the six prebuilt screens on the left of the page.
  8. Evaluate the accessibility implications of your selections, as shown:

    Figure 5.25: Material.io Color Tool Accessibility tab

    The tool is warning us that our selections result in illegible text, when white text is used over the primary color. You should either take care to avoid displaying white text over your primary color or change your selection.

    If you want to create your own palette, then the interface for mat-palette looks like this:

    mat-palette($base-palette, $default: 500, $lighter: 100, $darker: 700)
    
  9. Define the primary and secondary mat-palette objects using the default hue from the tool:
    src/localcast-theme.scss
    $local-weather-app-primary: mat-palette($mat-red, 500);
    $local-weather-app-accent: mat-palette($mat-indigo, A400);
    

Even though your theme is in SCSS, you may continue using CSS in the rest of your application. The Angular CLI supports compiling both SCSS and CSS. If you would like to change the default behavior, you may switch to SCSS altogether by changing the defaults.styleExt property in the angular.json file from CSS to SCSS.

You may also choose to eliminate styles.css and merge its contents with localcast-theme.scss or convert styles.css to a SASS file by simply renaming it to styles.scss. If you do this, don't forget to update angular.json.

Congratulations! Your application should now bear your trademark color scheme:

Figure 5.26: LocalCast Weather with custom theme

Push your code to GitHub and check out your CircleCI pipeline.

Unit testing with Material

Once you commit your code, you will notice that your pipeline is now failing due to failed tests. In order to keep your unit tests running, you will need to import MaterialModule to any component's spec file that uses Angular Material:

*.component.spec.ts
...
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      ...
      imports: [..., MaterialModule],
    }).compileComponents()
  })
)

You will also need to update any test, including e2e tests, that search for a particular HTML element.

For example, since the app's title, LocalCast Weather, is not in an h1 tag anymore, you must update the spec file to look for it in a span element:

src/app/app.component.spec.ts
expect(compiled.querySelector('span').textContent).toContain('LocalCast Weather')

Another example is in the CurrentWeather component, where the surrounding element for city is no longer span, so you can use the mat-title CSS class instead:

src/app/current-weather/current-weather.component.spec.ts
import { By } from '@angular/platform-browser'
// Assert on DOM
const debugEl = fixture.debugElement
const titleEl: HTMLElement =
  debugEl.query(By.css('.mat-title')).nativeElement
expect(titleEl.textContent).toContain('Bethesda')

Similarly, in e2e tests, you will need to update your page object function to retrieve the text from the correct location:

src/e2e/app.po.ts
getParagraphText() {
  return element(by.css('app-root mat-toolbar span'))
    .getText() as Promise<string>
}

Once your tests are passing, push your code to GitHub again. When your CircleCI pipeline succeeds, ship your app using Vercel Now. Remember, if you don't ship it, it never happened!

We can now move the UX task to the Done column:

Figure 5.27: GitHub project Kanban board status

In Chapter 7, Creating a Router-First Line-of-Business App, you will learn about more sophisticated tools to be able to further customize the look and feel of your Material theme to create a truly unique experience that fits the brand you're representing.

Accessibility

It is important to be aware of potential accessibility issues with your app. You may familiarize yourself with accessibility concerns by visiting the A11Y project website at https://a11yproject.com. Material itself provides additional tooling to help you improve accessibility; you can read about it at https://material.angular.io/cdk/a11y/overview.

Leveraging such Material features may feel unnecessary; however, you must consider responsiveness, styling, spacing, and accessibility concerns when designing your app. The Material team has put in a lot of effort so that your code works correctly under most circumstances and can serve the largest possible user base with a high-quality UX. This can include visually impaired or keyboard-primary users, who must rely on specialized software or keyboard features such as tabs to navigate your app. Leveraging Material elements provides crucial metadata for these users to be able to navigate your app.

Material claims support for the following screen-reader software:

Beyond Material, you may be required or have a desire to support specific accessibility standards, like the US-based Section 508 or the W3C-defined Web Content Accessibility Guidelines (WCAG). Claiming official support for such standards requires expensive certifications and qualified testers to ensure compliance.

Consider pa11y, which is a command-line tool that automates accessibility testing. Since it is a CLI tool, you can easily integrate it with your CI pipeline. Being able to catch accessibility issues automatically and early in the development cycle dramatically decreases the cost of implementing accessibility features in your application.

You may learn more about pa11y at https://pa11y.org/. Next, let's configure the pa11y CLI tool in our project.

Configuring automated pa11y testing

pa11y is an automated accessibility tool that you can execute from the command line to check your web app against various accessibility rulesets like Section 508 or WCAG 2 AAA. You may configure pa11y to run on your project locally or your CI server. In both cases, you must be running the tests against a deployed version of your application.

Let's start with configuring pa11y for a local run:

  1. Install the pa11y and pa11y-ci packages with the following command:
    npm i -D pa11y pa11y-ci http-server
    
  2. Add npm scripts to execute pa11y for local runs, checking for Section 508 compliance issues:
    package.json
    ...
    "scripts": {
      ...
      "test:a11y": "pa11y --standard Section508 http://localhost:5000"
    }
    
  3. Ensure that the app is running by executing npm start.
  4. In a new Terminal window, execute npm run test:a11y. The output should be as follows:
    Welcome to Pa11y
     > Running Pa11y on URL http://localhost:5000
    Results for URL: http://localhost:5000/
     • Error: Img element missing an alt attribute. Use the alt attribute to specify a short text alternative.
       ├── Section508.A.Img.MissingAlt
       ├── html > body > app-root > div:nth-child(4) > mat-card > mat-card-content > app-current-weather > div > div:nth-child(2) > div:nth-child(1) > img
       └── <img _ngcontent-pbr-c132="" style="margin-bottom:32px; zoom:175%" src="">        
    1 Errors
    

    Note that we have one error. The error message indicates that under app-current-weather, the image we display inside mat-card-content is missing an alt attribute. Observe the following line of code that caused the error:

    src/app/current-weather/current-weather.component.html
    ...
    <img style="zoom: 175%" [src]="current.image" />
    

    The preceding code refers to the image that we grab from the OpenWeatherMap API. A visually impaired user, relying on a screen reader, would not be able to determine what the image is for without an alt attribute present. Since this is a dynamic image, a static alt attribute like the Current weather icon would be a disservice to our user. However, it would be appropriate to bind the current weather description value as the attribute. We can fix the accessibility issue as shown:

    src/app/current-weather/current-weather.component.html
    ...
    <img style="zoom: 175%" [src]="current.image" 
     [alt]="current.description" />
    
  5. Re-run pa11y to confirm that the issue has been fixed.

Now users relying on screen readers quickly figure out that the image on the page reflects the current weather. In this case, we already have the description on our page. This is a very important issue to fix because it is crucial to avoid having mystery elements on our page that an entire class of users is unable to decipher.

Now let's configure pa11y for our CI pipeline.

  1. Create a .pa11yci configuration file in the root of your project:
    .pa11yci
    {
      "default": {
        "timeout": 1000,
        "page": {
          "viewport": {
            "width": 320,
            "height": 480
          }
        }
      },
      "urls": [
        "https://localcast-weather.duluca.now.sh/"
      ]
    }
    
  2. Add npm scripts to execute pa11y for local runs, checking for Section 508 compliance issues:
    package.json
    ...
    "scripts": {
      ...
      "test:a11y:ci": "pa11y-ci"
    }
    

Now we can add the command npm run test:a11y:ci to .circleci/config.yml. However, as you may notice, we would be running the test against the already deployed version of our app. To overcome this challenge, you must create an alternative now:publish command that will deploy our branch to a different URL, update .pa11yci to check against the new URL, and perform a deployment in your pipeline. Since all actions involved here are CLI commands, you may execute them sequentially. I leave this as an exercise for the user to complete.

More advanced uses for CircleCI are covered in Chapter 9, DevOps Using Docker. Next, we are going to go over how you can build an interactive prototype to discover UI/UX issues early in development to reduce your development costs.

Building an interactive prototype

Appearances do matter. Whether you're working in a development team or as a freelancer, your colleagues, bosses, or clients will always take a well-put-together presentation more seriously. In Chapter 3, Creating a Basic Angular App, I mentioned the time and information management challenges of being a full-stack developer. We must pick a tool that can achieve the best results with the least amount of work. This usually means going down the paid-tool route, but UI/UX design tools are rarely free or cheap.

A prototyping tool will help you create a better, more professional-looking, mock-up of the app. Whatever tool you choose should also support the UI framework you choose to use, in this case, Material.

If a picture is worth a thousand words, an interactive prototype of your app is worth a thousand lines of code. An interactive mock-up of the app will help you vet ideas before you write a single line of code and save you a lot of code writing.

MockFlow WireframePro

I've picked MockFlow WireframePro, available at https://mockflow.com, as an easy-to-use, capable, online tool that supports Material Design UI elements and allows you to create multiple pages, which can then be linked together to create the illusion of a working application.

Most importantly, at the time of publishing, MockFlow allows one free project forever with the full feature set and capabilities available. This will give you a chance to truly vet the usefulness of the tool without artificial limits or a trial period that always seems to go by much quicker than you expect.

Balsamiq (available at https://balsamiq.com) is a better-known wireframing tool; however, it doesn't offer any free usage. If you are looking for a tool without a monthly cost, I would highly recommend Balsamiq's desktop application Mockups, which has a one-time purchase cost.

Building a mock-up

We start by adding a new task to create an interactive prototype and at the end of the task, I'll attach all artifacts to this task so that they're stored on GitHub and are accessible to all team members and can also be linked from the wiki page for persistent documentation.

Let's pull this new task to the In Progress column and take a look at the status of our Kanban board from Waffle.io:

Figure 5.28: Current Kanban board status

WireframePro is pretty intuitive as a drag and drop design interface, so I won't go into the details of how the tool works, but I will highlight some tips:

  1. Create your project
  2. Select a component pack, either Hand Drawn UI or Material Design
  3. Add each screen as a new page, as shown in the following screenshot:

Figure 5.29: MockFlow.com WireFrame Pro

I would recommend sticking to the hand-drawn UI look and feel, because it sets the right expectations for your audience. If you present a very high-quality mock-up on your first meeting with a client, your first demo will be an understatement. You will, at best, merely meet expectations and, at worst, underwhelm your audience.

Home screen

Here's the new mock-up of the home screen that we just created:

Figure 5.30: LocalCast Weather home screen wireframe

You'll note some differences, such as the app toolbar being conflated with the browser bar and the intentional vagueness of the repeating elements. I have made these choices to reduce the amount of design time I would need to spend on each screen. I simply used horizontal and vertical line objects to create the grid.

Search results

The search screen similarly remains intentionally vague to avoid having to maintain any kind of detailed information. Surprisingly, your audience is far more likely to focus on what your test data is rather than focusing on the design elements.

By being vague, we intentionally keep the audience's attention on what matters. Here's the search screen mock-up:

Figure 5.31: LocalCast Weather search screen wireframe

Settings pane

The Settings pane is a separate screen with the elements from the home screen copied over and with 85% opacity applied to create a model-like experience. The Settings pane itself is just a rectangle with a black border and a solid white background.

Take a look at the following mock-up:

Figure 5.32: LocalCast Weather settings wireframe

Adding interactivity

Being able to click around a mock-up and get a feel for the navigational workflow is an indispensable tool to get early user feedback. This will save you and your clients a lot of frustration, time, and money.

To link elements together, do as follows:

  1. Select a clickable element such as the gear icon on the home screen
  2. Under the Link subheading, click on Select Page
  3. On the pop-over window, select Settings
  4. Click on Create Link, as shown in this screenshot:

Figure 5.33: Adding a link in Wireframe Pro

Now, when you click on the gear icon, the tool will display the Settings page, which will create the effect of the sidebar actually being displayed on the same page. To go back to the home screen, you can link the gear icon and the section outside of the sidebar back to that page so that the user can navigate back and forth.

Exporting the functional prototype

Once your prototype is completed, you can export it as various formats:

  1. Under the Project menu, select the Export Wireframe button, as shown:

    Figure 5.34: Wireframe Pro's Export Wireframe menu option

  2. Now select your file format, as follows:

    Figure 5.35: File formats in Wireframe Pro

    I prefer the HTML format for flexibility; however, your workflow and needs will differ.

  3. If you selected HTML, you will get to download a ZIP bundle of all the assets.
  4. Unzip the bundle and navigate to it using your browser; you should get an interactive version of your wireframe, as illustrated:

    Figure 5.36: Interactive wireframe in Wireframe Pro

The interactive elements are highlighted in yellow (light gray in print) and pointed out by the fat arrows in the preceding screenshot. You can enable or disable this behavior with the Reveal Links option in the bottom-left corner of the screen.

Now add all assets to comment on the GitHub issue, including the ZIP bundle, and we are done.

You can also publish your prototype's HTML project using Vercel Now, as discussed in Chapter 4, Automated Testing, CI, and Release to Production.

Summary

In this chapter, you learned what Angular Material is, how to use the Angular Flex Layout engine, the impact of UI libraries on performance, and how to apply specific Angular Material components to your application. You became aware of pitfalls of over-optimized UI design with individual CSS tweaks and how to add a custom theme to your application.

We also went over how you can improve the accessibility of your application and build an interactive prototype to vet your designs before implementing them.

In the next chapter, we will update the weather app to respond to user input with reactive forms and keep our components decoupled, while also enabling data exchange between them using BehaviorSubject. After the next chapter, we will be done with the weather app and shift our focus to building larger line-of-business applications.

Further reading

Exercises

Implement pa11y in your CI pipeline by implementing an alternative Now deployment so that you can test against the changes in your branch.

Questions

Answer the following questions as best as you can to ensure that you've understood the key concepts from this chapter without Googling. Do you need help answering the questions? See Appendix D, Self-Assessment Answers online at https://static.packt-cdn.com/downloads/9781838648800_Appendix_D_Self-Assessment_Answers.pdf or visit https://expertlysimple.io/angular-self-assessment.

  1. What are the benefits of using Angular Material?
  2. Which underlying CSS technology does Angular Flex Layout rely on?
  3. Why is it important to test for accessibility?
  4. Why should you build an interactive prototype?