Let's create a todo application in Angular
Hello fellow coders. Today, we are going to build a todo app in Angular. You can follow along with me.
Prerequisites
To install Angular, we need to install the cli with npm. If you don't have npm on your machine, I recommend installing node with nvm with the following command:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
Then install the latest version of node:
nvm install --lts
Now you should been able to use the node
and npm
commands.
Oke, back to Angular. Now you can install the Angular CLI tool with npm.
npm install -g @angular/cli
With this command, you gain access to the ng
command which can be used to create and manage a angular application. First create a new workspace. A workspace can hold several applications. Run this command to create one:
ng new todo
This will create a workspace with the name todo in the todo
directory. You will get the option to select the stylesheet format. I personally like to use SCSS. But you can choice another or none if you prefer. Then you get the question if you want SSR (Server-Side rendering). Choice Y for now. If you have git installed, it will also initialize git. To test our application, go to the directory and run:
ng serve --open
It will open your browser and you get a default page like this:
Api
We are going to use a package to act as an API. The package provide endpoints for a given resource in json format.
npm install json-server
Then add this to the scripts in package.json:
"server": "json-server --watch db.json --port=5000"
Create a db.json file in the root of your project with the following contents:
{
"todos": []
}
This will make sure the endpoints to todos
exists.
You can start the server with this command now:
npm run server
You may also add the db.json to the gitignore file to ignore it from your repository.
The first component
Now it is time to generate our first component. In the component, we will store the list of todo's and add links to create and edit a todo. Generate the component with:
ng generate component components/todos
I like to generate the components in the components
directory instead of in the root of the application. In this component, we hold a array of todo's. To do that we first make a private property of todo's. To make typing the todo easier, we can create a interface with the properties of a single todo. We store the interface in a interfaces
directory. Create a todo.ts
file with the following contents:
export interface Todo {
id?: number;
title: string;
completed: boolean;
}
This is how a basic todo item looks like. Notice the id is nullable, so we can use it for both creating and editing the todo.
Now in the todos component we create the property like this:
public todos: Todo[] = [];
While we are there, we can create a unsubscriber as well with:
private unsubscriber$ = new Subject<void>();
By convension we suffix the property with $
. To use this unsubscriber, we will implement two interfaces from angular. The OnInit
and OnDestroy
interface.
In the ngOnInit
method we won't do anything for now, we will come back to that later.
The ngOnDestroy
method will look like this:
public ngOnDestroy(): void {
this.unsubscriber$.next();
this.unsubscriber$.complete();
}
What this is going to do is removing the subscription when the component is destroyed.
Service
To get our todo's from the API we get from the json-server
package, we will create a service. The service can be generated with:
ng generate service services/todo
Again we store the service in a specific directory instead of the root of the application, to make it clear what each file is. The service is responsable for the basic CRUD actions. This is how the file should look like:
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Todo } from '../interfaces/todo';
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json'
})
};
@Injectable({
providedIn: 'root'
})
export class TodoService {
private apiUrl: string = "http://localhost:5000/todos";
constructor(
private readonly http: HttpClient
) { }
public getAll(): Observable<Todo[]> {
return this.http.get<Todo[]>(this.apiUrl);
}
public add(todo: Todo): Observable<Todo> {
return this.http.post<Todo>(this.apiUrl, todo, httpOptions);
}
public update(todo: Todo): Observable<Todo> {
const url = `${this.apiUrl}/${todo.id}`;
return this.http.put<Todo>(url, todo, httpOptions);
}
public delete(todo: Todo): Observable<Todo> {
const url = `${this.apiUrl}/${todo.id}`;
return this.http.delete<Todo>(url, httpOptions);
}
}
Let me explain what is happening here. In the class we are storing the base url to our api. The port should be the same as the server
script in your package.json (the --port
flag). Then we are injecting the built-in http client provided by Angular. The methods below are our CRUD operations. There is also a global constant with http options. The important thing is the 'Content-Type': 'application/json'
to force the output to be json.
Todo component
Now we can create a todo component, which we use to show a single todo. Create the todo component with:
ng generate component components/todo
And add the following input in the class:
@Input() todo!: Todo;
With the !
we tell the compiler that the value will never be null (because we provide the todo via it's input, it can not be null). You can style the component to your liking, but a simple layout for it:
<div class="d-flex justify-content-between align-items-center p-2 border">
<h2>{{ todo.title }}</h2>
</div>
Form
To add a todo, we need a form. We can create one or atleast the component for it, with:
ng generate component components/todo-form
Then change the ts file with the following:
import { Component, Output, EventEmitter } from '@angular/core';
import { Todo } from '../../interfaces/task';
@Component({
selector: 'app-todo-form',
templateUrl: './todo-form.component.html',
styleUrls: ['./todo-form.component.css']
})
export class TodoFormComponent {
@Output() submitted: EventEmitter<Todo> = new EventEmitter();
public title: string = "";
public completed: boolean = false;
public onSubmit(): void {
const todo: Todo = {
id: Math.floor(Math.random() * 100000),
title: this.title,
completed: this.completed
};
this.submitted.emit(todo);
this.clearForm();
}
private clearForm(): void {
this.title = "";
this.completed = false;
}
}
Again, what is happening here. We store the title and the boolean whether the todo is completed as properties of the class. Then we create a submit method that is called when the form is submitted. Also a private method that just clears the input fields. Then use this html for the form:
<form class="border p-4" (ngSubmit)="onSubmit()">
<h2>Add a new task</h2>
<div class="form-group mt-4">
<label for="title" class="form-label">Title</label>
<input type="text" name="title" id="title" class="form-control" [(ngModel)]="title" />
</div>
<div class="form-check mt-4">
<label for="completed" class="form-check-label">Completed</label>
<input type="checkbox" name="completed" id="completed" class="form-check-input" [(ngModel)]="completed" />
</div>
<input type="submit" name="submit" value="Create" class="btn btn-primary mt-4" />
</form>
We are using the ngSubmit to call the onSubmit function whenever the form is submitted. We also use ngModel to bind the value to the properties.
Back to the todos component
Remember the todos component where the ngOnInit
method is empty? We will add something to it now! In the ngOnInit
method we get all the todo's from our json database. That is very easy because our service has a method that does exactly that. Add a private method to the todos
component and call it from the ngOnInit
method. Don't forget to import the TodoService
from the constructor. This would be the code:
constructor(
private readonly todoService: TodoService,
) {}
public ngOnInit(): void {
this.getTodos();
}
private getTodos(): void {
this.todoService.getAll()
.pipe(takeUntil(this.unsubscriber$))
.subscribe((todos) => {
this.todos = todos
});
}
We should also include a method to add a todo whenever the form is submitted.
public addTodo(todo: Todo): void {
this.todoService.add(todo)
.pipe(takeUntil(this.unsubscriber$))
.subscribe((t: Todo) => {
this.todos.push(t);
});
}
Then in the template of the todos
component, listen for the submitted
event and call the addTodo
method which accept the special $event
variable.
<app-todo-form (submitted)="addTodo($event)"></app-todo-form>
You can try it out now. When you type something in the title field and submit the form, you should see the title added to the list below.
Now we need a way to mark the task as completed. We can do that by double clicking the task by listening to the double click event on the title.
<h2 (dblclick)="onDblclick(todo)">{{ todo.title }}</h2>
Then add the following in the ts file:
@Output() dblclicked: EventEmitter<Todo> = new EventEmitter();
public onDblclick(todo: Todo): void {
this.dblclicked.emit(todo);
}
Now in the todos
component, we can listen to that event and toggle the completed boolean in the todo.
<app-todo-item *ngFor="let todo of todos" [todo]="todo" (dblclicked)="toggleCompleted(todo)"></app-todo-item>
Then create the toggleCompleted
method in the ts file:
public toggleCompleted(todo: Todo): void {
todo.completed = !todo.completed;
this.todoService.update(todo)
.pipe(takeUntil(this.unsubscriber$))
.subscribe();
}
And we are done! That took awhile but now you have a todo application built with Angular. You can view my repository here for reference.