Introduction
This article shows how to build an Angular front end of a ToDo list application. This is part 2 of a two-part tutorial. Part 1 showed how to build a ToDo list application using .Net 5 Web API, JWT authentication, and AspNetCore Identity. Part 1 can be found here. This article starts by stating user stories showing how a user wants to use the application.
The article will show all the necessary steps that are required to build a complete ToDo application frontend. In the end, the article demonstrates that only logged-in users can access the ToDo list endpoints.
Table of Contents
- Tools
- User Stories
- Create a new Angular application
- Create Angular components
- Create models
- Create services
- Update model files
- todo-item.model.ts
- login.model.ts
- register.model.ts
- Update service files
- todo-app.service.ts
- auth-guard.service.ts
- Update the app.module.ts file
- Update component files
- Add Bootstrap
- Allow CORS
- Conclusion
Tools
User Stories
As a user, I want to register to use the to-do application |
The user should be able to register her/his credentials to be able to use the to-do app. |
As a user, I want to login to use the to-do application |
The user should be able to login to the to-do application using her/his credentials. |
As a user, I want to create a new to-do item |
The user should be able to create a new to-do item, the item should have a name and a description. |
As a user, I want to edit an existing item |
The user should be able to edit an existing to-do item, the user should be able to edit the name and description. |
As a user, I want to view all my to-do items |
The user should be able to view all his/her to-do items. |
As a user, I want to update the status of a to-do item |
As a user, I want to mark a to-do item as done. |
Create a new Angular application
Create a new Angular application by opening the command prompt and navigating to the folder where you want to create the application. To create a new application, type the following command and press enter:
ng new ToDoApp
Select No for Angular routing and choose CSS for styling.
![]()
Navigate to the newly created ToDoApp and open the application using Visual Studio Code. The project template will have this structure.
![]()
Create Angular components
Create the application Angular components. The application will have four components and one child component. The todo-items component, todo-item-form child component, home component, login component, and register component. Create only the Typescript files and the HTML files by using the below commands:
ng g c todo-items -s --skipTests
ng g c todo-items/todo-item-form -s --skipTests
ng g c home -s --skipTests
ng g c login -s --skipTests
ng g c register -s --skipTests
The folder structure of the project now looks as shown below. The above command will also update the app.module.ts file accordingly.
![]()
Create models
Create a folder called models that will contain a login model, register model, and todo-item model. Create only the Typescript files by using the below commands:
ng g class models/todo-item --type=model --skipTests
ng g class models/login --type=model --skipTests
ng g class models/register --type=model --skipTests
The folder structure of the project now looks as shown below.
![]()
Create services
Create a folder called services models that will contain auth-guard services files and todo-app services. Create only the Typescript files by using the below commands:
ng g s services/auth-guard --skip-tests
ng g s services/todo-app --skip-tests
The folder structure of the project now looks as shown below.
![]()
Update model files
The model fields must match the models that were created for the .NET Web API backend.
todo-item.model.ts
export class TodoItem {
itemId: number=0;
itemName: string="";
itemDescription: string="";
itemStatus: boolean=false;
}
login.model.ts
export class Login {
username: string="";
password: string="";
}
register.model.ts
export class Register {
username: string="";
email: string="";
password: string="";
}
Update service files
todo-app.service.ts
This service will communicate with the backend to send http requests and receive the http response.
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { TodoItem } from '../models/todo-item.model';
import { Login } from '../models/login.model';
import { Register } from '../models/register.model';
@Injectable({
providedIn: 'root'
})
export class TodoAppService {
readonly baseURL = "http://localhost:24288/api/ToDoItem";
readonly authURL = "http://localhost:24288/api/Authentication";
list: TodoItem[]=[];
constructor(private http: HttpClient) { }
todoData: TodoItem = new TodoItem();
loginData: Login = new Login();
registerData: Register = new Register();
postToDoItem() {
return this.http.post(this.baseURL, this.todoData, {
headers: new HttpHeaders({
"Content-Type": "application/json"
})
});
}
putToDoItem() {
console.log(this.todoData.itemId);
console.log("Hello");
console.log(this.todoData);
return this.http.put(`${this.baseURL}/${this.todoData.itemId}`, this.todoData, {
headers: new HttpHeaders({
"Content-Type": "application/json"
})
});
}
deleteToDoItem(id: number) {
return this.http.delete(`${this.baseURL}/${id}`, {
headers: new HttpHeaders({
"Content-Type": "application/json"
})
});
}
refreshList() {
this.http.get(this.baseURL)
.toPromise()
.then(res => {
this.list = res as TodoItem[]
});
}
loginUser() {
return this.http.post(`${this.authURL}/login`, this.loginData, {
headers: new HttpHeaders({
"Content-Type": "application/json"
})
});
}
registerUser() {
return this.http.post(`${this.authURL}/register`, this.registerData, {
headers: new HttpHeaders({
"Content-Type": "application/json"
})
});
}
}
auth-guard.service.ts
Install the angular2-jwt library by running the following command:
npm install @auth0/angular-jwt
Update the auth-guard.service.ts file.
This service protects the Angular routes. It implements the canActivate method to act as a guard to Angular routes.
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { JwtHelperService } from '@auth0/angular-jwt';
@Injectable({
providedIn: 'root'
})
export class AuthGuardService implements CanActivate {
constructor(private jwtHelper: JwtHelperService,
private router: Router) { }
canActivate() {
const token = localStorage.getItem("jwt");
if (token && !this.jwtHelper.isTokenExpired(token)) {
return true;
}
this.router.navigate(["login"]);
return false;
}
}
Update the app.module.ts file
Install the ngx-toastr library by running the following command:
npm i ngx-toastr
Update the angular.json file. Make sure you update the styles section under the build section with the following code
"styles": [
"src/styles.css",
"node_modules/ngx-toastr/toastr.css"
],
Update the app.module.ts file.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ToastrModule } from 'ngx-toastr';
import { HttpClientModule } from '@angular/common/http';
import { RouterModule } from '@angular/router';
import { JwtModule } from '@auth0/angular-jwt';
import { AppComponent } from './app.component';
import { TodoItemsComponent } from './todo-items/todo-items.component';
import { TodoItemFormComponent } from './todo-items/todo-item-form/todo-item-form.component';
import { HomeComponent } from './home/home.component';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
import { AuthGuardService } from './services/auth-guard.service';
export function tokenGetter() {
return localStorage.getItem("jwt");
}
@NgModule({
declarations: [
AppComponent,
TodoItemsComponent,
TodoItemFormComponent,
HomeComponent,
LoginComponent,
RegisterComponent
],
imports: [
BrowserModule,
FormsModule,
HttpClientModule,
BrowserAnimationsModule,
ToastrModule.forRoot(),
RouterModule.forRoot([
{ path: '', component: HomeComponent },
{ path: 'register', component: RegisterComponent },
{ path: 'login', component: LoginComponent },
{ path: 'dashboard', component: TodoItemsComponent, canActivate: [AuthGuardService],
children: [
{ path: 'payment', component: TodoItemFormComponent, canActivate: [AuthGuardService] }
] },
]),
JwtModule.forRoot({
config: {
tokenGetter: tokenGetter,
allowedDomains: ["localhost:24288"],
disallowedRoutes: []
}
})
],
providers: [AuthGuardService],
bootstrap: [AppComponent]
})
export class AppModule { }
Update component files
app.component.html
<div class="container">
<router-outlet></router-outlet>
</div>
home.component.ts
This file contains a isUserAuthenticated() method that checks if a user is authenticated by checking if they have a valid JSON Web Token. It also has a logOut() method.
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { JwtHelperService } from '@auth0/angular-jwt';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styles: [
]
})
export class HomeComponent implements OnInit {
constructor(private jwtHelper: JwtHelperService,
private router: Router) { }
ngOnInit(): void {
}
isUserAuthenticated() {
const token: any = localStorage.getItem("jwt");
if (token && !this.jwtHelper.isTokenExpired(token)) {
return true;
} else {
return false;
}
}
logOut() {
localStorage.removeItem("jwt");
}
}
home.component.html
If a user is authenticated she/he has access to add a to-do item, update a to-do item and delete a to-do item. If they have not authenticated they can either login to the application or register to the application.
<h1>Home Page</h1>
<br>
<div *ngIf="isUserAuthenticated()" style="color:blue;">
<h2>
<app-todo-items></app-todo-items>
</h2>
</div>
<br>
<ul>
<!-- <li>
<div class="form-group">
<button class="btn btn-info btn-lg btn-block" type="submit"><a [routerLink]="['/dashboard']" routerLinkActive="active">ToDo Item</a></button>
</div>
</li> -->
<li *ngIf="!isUserAuthenticated()">
<div class="form-group">
<a [routerLink]="['/login']" routerLinkActive="active"><button class="btn btn-info btn-lg btn-block" type="submit">Login</button></a>
</div>
</li>
<li *ngIf="!isUserAuthenticated()">
<div class="form-group">
<a [routerLink]="['/register']" routerLinkActive="active"><button class="btn btn-info btn-lg btn-block" type="submit">Register</button></a>
</div>
</li>
<li *ngIf="isUserAuthenticated()">
<div class="form-group">
<button class="btn btn-info btn-lg btn-block" type="submit"><a class="logout" (click)="logOut()">Logout</a></button>
</div>
</li>
</ul>
login.component.ts
This file contains a login method that communicates with the service to login to a user and returns a JWT and saves it in the local storage.
import { Component, OnInit } from '@angular/core';
import { NgForm } from '@angular/forms';
import { Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr';
import { TodoAppService } from '../services/todo-app.service';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styles: [
]
})
export class LoginComponent implements OnInit {
invalidLogin: boolean | undefined;
constructor(public service: TodoAppService,
private toastr: ToastrService, private router: Router) { }
ngOnInit(): void {
}
login(form: NgForm) {
this.service.loginUser().subscribe(
res => {
const token = (<any>res).token;
localStorage.setItem("jwt", token);
this.invalidLogin = false;
this.router.navigate(["/"]);
this.toastr.success("LoggedIn successfully", "Payment Detail Register");
},
err => {
this.invalidLogin = true;
}
);
}
}
login.component.html
This file has a form to login to a user. It implements input validation.
<form novalidate #loginForm="ngForm" (submit)="login(loginForm)" autocomplete="off">
<div class="form-group">
<label>USERNAME</label>
<input
class="form-control form-control-lg"
placeholder="Username"
name="username"
#username="ngModel"
[(ngModel)]="service.loginData.username"
required
[class.invalid]="username.invalid && username.touched"
>
<div *ngIf="username.touched">
<p *ngIf="username.errors?.required">Username is required!</p>
</div>
</div>
<div class="form-group">
<label>PASSWORD</label>
<input
type="password"
class="form-control form-control-lg"
placeholder="Password"
name="password"
#password="ngModel"
[(ngModel)]="service.loginData.password"
required
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$"
[class.invalid]="password.invalid && password.touched"
>
<div *ngIf="password.touched">
<p *ngIf="password.errors?.required">Password is required!</p>
<p *ngIf="password.errors?.pattern">
Minimum eight characters, at least one uppercase letter, one lowercase letter, one number and one special character
</p>
</div>
</div>
<div class="form-group">
<button class="btn btn-info btn-lg btn-block" type="submit" [disabled]="loginForm.invalid">SUBMIT</button>
</div>
<div class="form-group">
<a [routerLink]="['/register']" routerLinkActive="active"><button class="btn btn-info btn-lg btn-block" type="submit">Register</button></a>
</div>
<div class="form-group">
<a [routerLink]="['']" routerLinkActive="active"><button class="btn btn-info btn-lg btn-block" type="submit">Home</button></a>
</div>
</form>
register.component.ts
This file contains a register method that communicates with the service to register a user and store their credentials in the database.
import { Component, OnInit } from '@angular/core';
import { NgForm } from '@angular/forms';
import { Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr';
import { Register } from '../models/register.model';
import { TodoAppService } from '../services/todo-app.service';
@Component({
selector: 'app-register',
templateUrl: './register.component.html',
styles: [
]
})
export class RegisterComponent implements OnInit {
constructor(public service: TodoAppService,
private toastr: ToastrService, private router: Router) { }
ngOnInit(): void {
}
register(form: NgForm) {
this.service.registerUser().subscribe(
res => {
this.resetForm(form);
this.service.refreshList();
this.toastr.success("Submitted successfully", "User Register");
},
err => { console.log(err); }
);
}
resetForm(form: NgForm) {
form.form.reset();
this.service.registerData = new Register();
}
}
register.component.html
This file has a form to register a user. It implements input validation.
<form novalidate #registerForm="ngForm" (submit)="register(registerForm)" autocomplete="off">
<div class="form-group">
<label>USERNAME</label>
<input
class="form-control form-control-lg"
placeholder="Username"
name="username"
#username="ngModel"
[(ngModel)]="service.registerData.username"
required
[class.invalid]="username.invalid && username.touched"
>
<div *ngIf="username.touched">
<p *ngIf="username.errors?.required">Username is required!</p>
</div>
</div>
<div class="form-group">
<label>EMAIL</label>
<input
class="form-control form-control-lg"
placeholder="[email protected]"
name="email"
#email="ngModel"
[(ngModel)]="service.registerData.email"
required
pattern="^[\w!#$%&'*+\-/=?\^_`{|}~]+(\.[\w!#$%&'*+\-/=?\^_`{|}~]+)*@((([\-\w]+\.)+[a-zA-Z]{2,4})|(([0-9]{1,3}\.){3}[0-9]{1,3}))$"
[class.invalid]="email.invalid && email.touched"
>
<div *ngIf="email.touched">
<p *ngIf="email.errors?.required">Email is required!</p>
<p *ngIf="email.errors?.pattern">You have entered an invalid email address!!!</p>
</div>
</div>
<div class="form-group">
<label>PASSWORD</label>
<input
type="password"
class="form-control form-control-lg"
placeholder="Password"
name="password"
#password="ngModel"
[(ngModel)]="service.registerData.password"
required
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$"
[class.invalid]="password.invalid && password.touched"
>
<div *ngIf="password.touched">
<p *ngIf="password.errors?.required">Password is required!</p>
<p *ngIf="password.errors?.pattern">
Minimum eight characters, at least one uppercase letter, one lowercase letter, one number and one special character
</p>
</div>
</div>
<div class="form-group">
<button class="btn btn-info btn-lg btn-block" type="submit" [disabled]="registerForm.invalid">SUBMIT</button>
</div>
<div class="form-group">
<a [routerLink]="['/login']" routerLinkActive="active"><button class="btn btn-info btn-lg btn-block" type="submit">Login</button></a>
</div>
<div class="form-group">
<a [routerLink]="['']" routerLinkActive="active"><button class="btn btn-info btn-lg btn-block" type="submit">Home</button></a>
</div>
</form>
todo-item-form.component.ts
This file has a method that creates a new to-do item and a method that updates an existing to-do item.
import { Component, OnInit } from '@angular/core';
import { NgForm } from '@angular/forms';
import { ToastrService } from 'ngx-toastr';
import { TodoItem } from 'src/app/models/todo-item.model';
import { TodoAppService } from 'src/app/services/todo-app.service';
@Component({
selector: 'app-todo-item-form',
templateUrl: './todo-item-form.component.html',
styles: [
]
})
export class TodoItemFormComponent implements OnInit {
constructor(public service: TodoAppService,
private toastr: ToastrService) { }
ngOnInit(): void {
}
onSubmit(form: NgForm) {
console.log(this.service.todoData);
if (this.service.todoData.itemId == 0) {
console.log("Hello");
this.insertRecord(form);
} else {
this.updateRecord(form);
}
}
insertRecord(form: NgForm) {
this.service.postToDoItem().subscribe(
res => {
this.resetForm(form);
this.service.refreshList();
this.toastr.success("Submitted successfully", "ToDo Item Register");
},
err => { console.log(err); }
);
}
updateRecord(form: NgForm) {
this.service.putToDoItem().subscribe(
res => {
this.resetForm(form);
this.service.refreshList();
this.toastr.info("Updated successfully", "ToDo Item Register");
},
err => { console.log(err); }
);
}
resetForm(form: NgForm) {
form.form.reset();
this.service.todoData = new TodoItem();
}
}
todo-item-form.component.html
This file has a form to create a to-do item and to update a to-do item. The item status is implemented in a form of a checkbox, if the checkbox is checked then a to-do task is done.
<form novalidate #form="ngForm" (submit)="onSubmit(form)" autocomplete="off">
<input
type="hidden"
name="itemId"
[value] = "service.todoData.itemId"
>
<div class="form-group">
<label>ITEM NAME</label>
<input
class="form-control form-control-lg"
placeholder="Item Name"
name="itemName"
#itemName="ngModel"
[(ngModel)]="service.todoData.itemName"
required
maxlength="50"
[class.invalid]="itemName.invalid && itemName.touched"
>
<div *ngIf="itemName.touched">
<p *ngIf="itemName.errors?.required">Item name is required!</p>
</div>
</div>
<div class="form-group">
<label>ITEM DESCRIPTION</label>
<input
class="form-control form-control-lg"
placeholder="Item Description"
name="itemDescription"
#itemDescription="ngModel"
[(ngModel)]="service.todoData.itemDescription"
required
maxlength="150"
[class.invalid]="itemDescription.invalid && itemDescription.touched"
>
<div *ngIf="itemDescription.touched">
<p *ngIf="itemDescription.errors?.required">Item description is required!</p>
</div>
</div>
<div class="form-group">
<label>ITEM STATUS</label>
<input
class="form-control form-control-lg"
type="checkbox"
name="itemStatus"
#itemStatus="ngModel"
[(ngModel)]="service.todoData.itemStatus"
[ngModelOptions]="{standalone: true}"
required
>
</div>
<div class="form-group">
<button class="btn btn-info btn-lg btn-block" type="submit" [disabled]="form.invalid">SUBMIT</button>
</div>
</form>
todo-items.component.ts
This file has a method populateForm() that displays all the to-do items and a method to delete a to-do item.
import { Component, OnInit } from '@angular/core';
import { ToastrService } from 'ngx-toastr';
import { TodoItem } from '../models/todo-item.model';
import { TodoAppService } from '../services/todo-app.service';
@Component({
selector: 'app-todo-items',
templateUrl: './todo-items.component.html',
styles: [
]
})
export class TodoItemsComponent implements OnInit {
constructor(public service: TodoAppService,
private toastr: ToastrService) { }
ngOnInit(): void {
this.service.refreshList();
}
populateForm(selectedRecord: TodoItem) {
this.service.todoData = Object.assign({}, selectedRecord);
}
onDelete(id: number) {
if (confirm("Are you sure you want to delete this record?")) {
this.service.deleteToDoItem(id)
.subscribe( res => {
this.service.refreshList();
this.toastr.error("Deleted susseccfully", "ToDo Item Register");
},
err => {
console.log(err);
})
}
}
}
todo-items.component.html
This file displays the form to create a to-do item or to update a to-do item and a table that displays all the to-do items.
<div class="jumbotron py-3">
<h1 class="display-4 text-center">ToDo Item Register</h1>
</div>
<div class="row">
<div class="col-md-6">
<app-todo-item-form></app-todo-item-form>
</div>
<div class="col-md-6">
<table class="table">
<thead class="table-light">
<tr>
<th>Item Name</th>
<th>Item Description</th>
<th>Item Status</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let pd of service.list">
<td (click)="populateForm(pd)">{{ pd.itemName }}</td>
<td (click)="populateForm(pd)">{{ pd.itemDescription }}</td>
<td (click)="populateForm(pd)">{{ pd.itemStatus }}</td>
<td>
<i class="fa fa-trash-o fa-lg text-danger" (click)="onDelete(pd.itemId)"></i>
</td>
</tr>
</tbody>
</table>
</div>
</div>
Add Bootstrap
This application uses Bootstrap and font-awesome for styling. Add Bootstrap and font-awesome links on the index.html file found at the root of the folder structure.
![]()
Allow CORS
Update the Startup.cs file, to allow CORS, of the ToDoAPI application that we did in part 1 of the tutorial.
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ToDoAPI.Authentication;
using ToDoAPI.Models;
namespace ToDoAPI
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "ToDoAPI", Version = "v1" });
});
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("SQLConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidAudience = Configuration["JWT:ValidAudience"],
ValidIssuer = Configuration["JWT:ValidIssuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Secret"]))
};
});
services.AddCors();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseCors(options =>
options.WithOrigins("http://localhost:4200")
.AllowAnyMethod()
.AllowAnyHeader());
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "ToDoAPI v1"));
}
app.UseAuthentication();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
If you try to access the to-do item before login in into the application, you get redirected to the login page. Once a user has login she/he is redirected to the ToDo page where she/he can add a to-do item, update a to-do item and delete a to-do item. A user can update a to-do item by clicking on the item.
![]()
Conclusion
In this article, I showed how to build a ToDo list application frontend using Angular. This is a continuation of the last tutorial, which can be found here. You can find the source code in my GitHub repository.