Angular, Angular Material, and TDD [Part 2]— Creating a Login Page

Aysha Williams
Aysha’s Handmade Code
7 min readJan 31, 2021

Part 2 of this series will use angular material to build a login page.

Angular material comes with an abundance of pre-built components. I’ll be using them to build out a simple login page that looks similar to this:

Note: This tutorial builds off of Part 1.

1. Add Angular Material

Adding angular material can be done using the Angular cli:

ng add @angular/material

The cli will prompt for a theme color. The link to the theme will be displayed in the terminal. The link goes to the Angular Material website which has a list of predefined themes located in the context menu of its paint bucket icon.

Click on the paint bucket to see a list of themes. Choose a theme to see what the color scheme is like. Note: choosing a theme will change the entire theme for the Angular Material website.

2. Add a Login Form

Create a LoginForm component

The first step is to create a component that will represent the login form.

ng g component components/login-form

Note: Components created for the app will reside inside of a components folder.

Update login-form.component.spec.ts

All the existing tests are passing. So, now a new test will be added to the login-form.component.spec.ts to look for the welcome text, the email field, the password field, and the login button.

The new tests are in bold.

import { ComponentFixture, TestBed } from '@angular/core/testing';import { LoginFormComponent } from './login-form.component';describe('LoginFormComponent', () => {
let component: LoginFormComponent;
let fixture: ComponentFixture<LoginFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ LoginFormComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(LoginFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('says "Welcome"', () => {
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.header').textContent)
.toContain('Welcome');
});
it('has an email field', () => {
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.email')).not.toEqual(null);
});
it('has a password field', () => {
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.password')).not.toEqual(null);
});
it('has a login button', () => {
const compiled = fixture.nativeElement;
expect(compiled.querySelector('button.login'))
.not.toEqual(null);
});

});

There are now four failing tests.

Add the Welcome header to login-form.component.html

Adding the welcome header will be easy. It will be an <h1> tag containing the text “Welcome”.

The login-form.component.html will look like this now:

<h1 class="header">Welcome</h1>

And now three tests are failing instead of four.

Add the form fields to login-form.component.html

Since this app will be using Angular Material components, all of the form fields will use mat-form-field.

To give the form field a similar appearance to what’s in the wireframe, the mat-form-field will use the outline appearance and it will set the placeholder text to “Email”.

The login-form.component.html file will have the following code changes (new additions are in bold):

<h1 class="header">Welcome</h1>
<form>
<mat-form-field appearance="outline" class="email">
<input matInput placeholder="Email">
</mat-form-field>
<mat-form-field appearance="outline" class="password">
<input matInput placeholder="Password">
</mat-form-field>
</form>

Now only one test is failing.

Add the login button to login-form.component.html

The login button will be a regular html button, but the mat-raised-button attribute is added to it to enhance the button and allow it to use the Angular Material design system.

Changes to login-form.component.html are in bold.

<h1 class="header">Welcome</h1>
<form>
<mat-form-field appearance="outline" class="email">
<input matInput placeholder="Email">
</mat-form-field>
<mat-form-field appearance="outline" class="password">
<input matInput placeholder="Password">
</mat-form-field>
<button mat-raised-button class="login">Login</button>
</form>

Now, all tests are passing!

Update tests for login-form.component.spec.ts

Although all the tests are passing, there is an error in the console for the tests.

ERROR 'NG0304: 'mat-form-field' is not a known element:
1. If 'mat-form-field' is an Angular component, then verify that it is part of this module.

This error is easy to fix. It requires that the MatButtonModule be added to the test imports. The same will need to be done for MatFormFieldModule.

Changes to the login-form.component.spec.ts file are below in bold.

import { ComponentFixture, TestBed } from '@angular/core/testing';import { LoginFormComponent } from './login-form.component';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '
@angular/material/form-field';
describe('LoginFormComponent', () => {
let component: LoginFormComponent;
let fixture: ComponentFixture<LoginFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ LoginFormComponent ],
imports: [
MatButtonModule,
MatFormFieldModule,
],

})
.compileComponents();
});
// Tests are below

Now, the tests have revealed a new error.

Error: mat-form-field must contain a MatFormFieldControl

Adding the MatInputModule to the test’s imports resolves the above error but another error has appeared.

Error: Found the synthetic property @transitionMessages. Please include either "BrowserAnimationsModule" or "NoopAnimationsModule" in your application.

To resolve this error, the BrowserAnimationsModule needs to be imported.

Note: Some Angular Material components require the BrowserAnimationsModule to perform animations. The form field components can perform animations, but the current implementation above, does not take advantage of these abilities.

Changes to the login-form.component.spec.ts file are below.

import { ComponentFixture, TestBed } from '@angular/core/testing';import { LoginFormComponent } from './login-form.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatButtonModule } from '
@angular/material/button';
import { MatInputModule } from '
@angular/material/input';
import { MatFormFieldModule } from '
@angular/material/form-field';
describe('LoginFormComponent', () => {
let component: LoginFormComponent;
let fixture: ComponentFixture<LoginFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ LoginFormComponent ],
imports: [
BrowserAnimationsModule,
MatButtonModule,
MatFormFieldModule,
MatInputModule,
],

})
.compileComponents();
});
// Tests are below this line

3. Display the Login Form component in the app

Now that the LoginFormComponent is complete. It can be added to app.component.html, so that it will be displayed in the app.

Update app.component.spec.ts

One test will be added to the app.component.spec.ts file to look for the login component.

it('has a login form', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.login-form').textContent)
.not.toEqual(null);
});

And now, there is one test failure.

Add the LoginFormComponent to app.component.html

Let’s get the test to pass by adding the LoginFormComponent to app.component.html.

Changes to app.component.html are below in bold.

<title>{{title}}</title>
<app-login-form class="login-form"></app-login-form>

The test is still failing because it needs to add LoginFormComponent to its declarations.

Updates to app.component.spec.ts are below in bold.

import { RouterTestingModule } from '@angular/router/testing';
import { TestBed } from '@angular/core/testing';
import { AngularFireModule } from '@angular/fire';
import { AppComponent } from './app.component';
import { LoginFormComponent } from './components/login-form/login-form.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
RouterTestingModule,
],
declarations: [
AppComponent,
LoginFormComponent,
],
}).compileComponents();
});
// Tests are below

All the tests are passing, but errors are occurring while the app is running.

error NG8001: 'mat-form-field' is not a known element:
1. If 'mat-form-field' is an Angular component, then verify that it is part of this module.

Update app.module.ts

To fix the error above, the next change will be to update app.module.ts. The same components that were added to the test’s imports need to be added to the AppModule imports.

Changes to app.module.ts are in bold, below.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AngularFireModule } from '@angular/fire';
import { AngularFirestoreModule } from '@angular/fire/firestore';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { environment } from 'src/environments/environment';
import { LoginFormComponent } from './components/login-form/login-form.component';
import { MatButtonModule } from '@angular/material/button';
import { MatInputModule } from '
@angular/material/input';
import { MatFormFieldModule } from '
@angular/material/form-field';
@NgModule({
declarations: [
AppComponent,
LoginFormComponent
],
imports: [
AngularFireModule.initializeApp(environment.firebase),
AngularFirestoreModule,
AppRoutingModule,
BrowserAnimationsModule,
BrowserModule,
MatButtonModule,
MatInputModule,
MatFormFieldModule,

],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

Now the login page needs to be styled.

Update styles

Adding a few css styles and classes gets the login page closer to looking like the layout in the wireframe. The changes below will also include changes to app.component.css, app.component.html, login-form.component.css, and login-form.component.html

Changes to app.component.css

.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.toolbar {
justify-content: space-between;
}
.bill-manager-table {
width: 100%;
}
.bill-manager-table th {
font-size: 16px;
}

Changes to app.component.html are in bold below.

<title>{{title}}</title><div class="container mat-app-background">
<app-login-form class="login-form"></app-login-form>
</div>

Changes to login-form.component.css

.container, .login-form, .header {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
button.login {
width: 100%;
border-radius: 18px;
}
.header {
margin-bottom: 2em;
font-size: 3em;
}
.login-form {
border: 1px solid;
padding: 6vh;
border-radius: 8px;
}
mat-form-field, .login-form {
width: 45vw;
}

Changes to login-form.component.html are below.

<div class="container">
<h1 class="header">Welcome</h1>
<form>
<mat-form-field appearance="outline" class="email">
<input matInput placeholder="Email">
</mat-form-field>
<mat-form-field appearance="outline" class="password">
<input matInput placeholder="Password">
</mat-form-field>
<button mat-raised-button class="login"
color="primary">Login</button>
</form>
</div>

The final result

The completed code can be found at https://stackblitz.com/edit/angular-ivy-vp8ztn?file=src/app/app.component.ts

--

--