Angular, Angular Material, and TDD [Part 2]— Creating a Login Page
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.
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