Angular Todo Application
November 08, 2019
A while back I took a survey on twitter to see what might be of interest.
Ajax and Rest api were neck and neck. Due to the results I’ve decided to cover the basics of a full stack application. Todo tutorials are plentiful for a reason. They are simple enough to convey concepts without getting too deep into the weeds but are complex enough to cover many of the real world things that every application needs.
I won’t get into authentication or authorization this time. I think I got a little to ambitious in the tweet. :) I’ll focus directly on just the core functionality of a todo app and all the crud actions it needs. The app is first built purely on the front end and persisted with localStorage. Then I create a back-end that will serve up todos and refactor the front end code to connect it up with AJAX.
Todo Entity Class
First it is good to define the shape of the data you are dealing with. A simple
class will work fine. In this case, all we are dealing with are simple Todos.
What are the properties we might have for each todo?
For my implementation I decided it would be nice to have a priority so that todos can be searched by their importance. An id is useful to make each todo unique. Text is obviously the whole reason for a todo to exist and the completed property lets the user know the progress they have made so far. You don’t have to define a class to wrap the todo data. You could easily use an interface. Really, you could just as easily use an object literal.
One interesting difference to note is that typescript interfaces are stripped away at runtime. Classes are not. Notice how there is a method on the todo class that generates Ids. This type of encapsulation could also be extended to take care of changing priority and completed flags. Because I know that localStorage will strip away any methods when serializing to JSON format I’m choosing to keep that functionality inside the angular service. genId() will only be needed when posting so it won’t matter if the method disapears off the object instance later.
export class Todo {id: numbertext: stringcompleted: booleanpriority: numberconstructor(text, completed, priority) {this.id = this.genId()this.text = textthis.completed = completedthis.priority = priority}genId(): number {return Math.random() * 10}}
Deeper Reading
Todo Service
Next I create an angular service to handle all actions associated with Todos. Angular services are a great way to share functionality to other components. Right now we have no backend so all persistance will be done using localStorage available in all modern browsers. Later this mocked service will be replaced with the api service that will talk to the Node Express todo api.
// todo service snippetsimport { Injectable } from "@angular/core"import { Todo } from "./todo"@Injectable()export class RepositoryService {
private property to handle the todo state It is declared as an array of type Todo defined earlier as an entity class if no todos are found in local storage then load as an empty array.
private todos: Array<Todo> = JSON.parse(localStorage.getItem("todos")) || []
createTodo takes in a string as input and pushes a new Todo Entity to the todos array. It passes in defaults of completed false and sets a default priority of zero. Then we refresh local storage to reflect the todos in memory.
createTodo(todotext) {this.todos.push(new Todo(todotext, false, 0))localStorage.setItem("todos", JSON.stringify(this.todos))}
getTodos()
is an automatically resolved promise with a
payload of the private property defined as an array
of todo entities. I used resolved Promises so that
my component code will be written in a way that
expects asynchronously delivered todos.
This sets up the code to be easily switched out
for a real backend later.
getTodos() {return Promise.resolve(this.todos)}
Update Todo is a little tricky because before we can change the todo we need to find the todo we want to update first then update it. Array.prototype.findIndex will return to us an index in the array based on a boolean test. In this case we use the id to match uniqueness to. If a match is found it gets returned.
Using the returned index the changed todo passed into the updateTodo method can overwrite the right todo by passing it to the todos array.
updateTodo(todo) {const index = this.todos.findIndex(t => {return t.id === todo.id})this.todos[index] = todolocalStorage.setItem("todos", JSON.stringify(this.todos))return Promise.resolve(this.todos)}
using filter you can match on the id of a todo and return the entire array except the one matched. Another way to do this is with splice() but this is cleaner to read.
removeTodo(id) {this.todos = this.todos.filter(todo => todo.id !== id)localStorage.setItem("todos", JSON.stringify(this.todos))return Promise.resolve(this.todos)}changePriority(event) {if (event.operation === "+") {event.todo.priority += 1}if (event.operation === "-") {if (event.todo.priority > 0) {event.todo.priority -= 1}}}getFilteredTodos() {return Promise.resolve(this.filteredTodos)}
filterSearch()
takes in a string or number. If it’s a number it uses
the Array.prototype.filter method to check if the searched
priority number matches the input value passed in.
if the value is a string it filters down the todos by looking
at the text property and sends back all results that match
any combination of characters. indexOf will return either the
index or -1 which is coerced to false by javascript.
filterSearch(value) {if (!isNaN(value)) {this.filteredTodos = this.todos.filter(todo => parseInt(todo.priority, 10) === parseInt(value, 10))} else if (typeof value === "string") {this.filteredTodos = this.todos.filter(todo => {return todo.text.toLowerCase().indexOf(value.toLowerCase()) > -1})}// allows an empty thenable on the component side so things continue to work// when we plug into a real api that will have latency associated with it.return Promise.resolve()}
clearTodos()
resets the array to an empty array.
clearTodos() {localStorage.removeItem("todos")return Promise.resolve((this.todos = []))}}
Todo Component Template
I’m using template variables notated with hash symbols #inputvalue
to grab the input values
to pass to the component method addTodo()
on (click)
and on (keyup.enter)
. You could also bind a property and use ngModel banana in
the box syntax [()]
for two way data binding.
I have defined methods for the following:
- Adding a Todo
- Searching a list of Todos
- Clearing all Todos
Inside of this template is a dummy component that accepts a todo as input using @Input
decorator
Three methods are listed on this component
- onRemoveTodo
- onToggleComplete
- onChangePriority
All three emit parts of a todo object needed by the parent component methods. This
is accomplished with an @Output
decorator and the EventEmitter class to emit custom events.
Without this mechanism we would not be able to communicate between parent and child
components.
todo-item component
import { Component, Input, OnInit, Output, EventEmitter } from "@angular/core"import { Todo } from "./todo"@Component({selector: "todo-item",template: `<p class="todo-text" [class.completed]="todoItem.completed">{{ todoItem.text }}</p><span class="operators" (click)="onChangePriority('+', todoItem)">+</span><span class="operators" (click)="onChangePriority('-', todoItem)">-</span><p class="priority">priority: {{ todoItem.priority }}</p><button (click)="onRemoveTodo(todoItem.id)">Remove</button><button (click)="onToggleComplete(todoItem)">Completed</button>`,})export class TodoItem implements OnInit {@Input("todo") todoItem: Todo<string, boolean, string>@Output() removed = new EventEmitter<number>()@Output() toggled = new EventEmitter<Todo<string, boolean, string>>()@Output() priority = new EventEmitter<{}>()onChangePriority(operation, todo) {this.priority.emit({ operation, todo })}onToggleComplete(todo) {this.toggled.emit(todo)}onRemoveTodo(id) {this.removed.emit(id)}}
Notice how the events used in the template snippet below match up to the todo item component @Output properties.
The onAction()
methods defined there exist to do one thing and that is to pass the event data
back up the view tree to the parent. The custom events values are then captured and passed into the
parent level methods using the $event
object. Instead of a standard browser event object it will
contain the data you specified in the child component using the this.outputprop.emit()
syntax.
todo-item template snippet
<li *ngFor="let todo of todos"><todo-item[todo]="todo"(removed)="removeTodo($event)"(toggled)="toggleComplete($event)"(priority)="changePriority($event)"></todo-item></li>
Parent App Component Template
<div class="container"><header><h1>Angular Todo Demo</h1></header><nav class="add-todo"><inputclass="todo-input"#inputvalue(keyup.enter)="addTodo(inputvalue)"type="text"placeholder="Add a todo"/><button (click)="addTodo(inputvalue)">Add Todo</button><inputclass="todo-input"(keyup)="filterSearch($event.target.value)"placeholder="search your todos"type="text"/><button (click)="clearTodos()">Clear</button></nav><section><ul><li *ngFor="let todo of todos"><!-- Here is the todo item dumb component being consumed by the parent template --><todo-item[todo]="todo"(removed)="removeTodo($event)"(toggled)="toggleComplete($event)"(priority)="changePriority($event)"></todo-item></li></ul></section><footer><p>Find me at my blog <a href="https://jasonbelcher.dev">jasonbelcher.dev</a></p></footer></div>
Deeper Reading
Todo Component Consumes The Todo Service
Here is where the full worth of services comes into play. This service could be used in any component that needs it. If I need to change something about the implementation of any method inside the service it is only changed in one place. Here is a more detailed explanation of services
import { Component, OnInit } from "@angular/core"import { RepositoryService } from "./repository.service"import { Todo } from "./todo"@Component({selector: "app-root",templateUrl: "./app.component.html",styleUrls: ["./app.component.css"],})export class AppComponent implements OnInit {todos: any[]// dependancy inject the service to be used.constructor(private repository: RepositoryService) {}// initial list of todos currently storedngOnInit() {this.repository.getTodos().then(todos => {this.todos = todos})}/* calls the createTodo service method and passes inthe input value from the user. After posting theinput is cleared. If there is no value no call ismade */addTodo(input) {if (input.value) {this.repository.createTodo(input.value)this.getTodos()input.value = ""} else {return}}// clear all todosclearTodos() {this.repository.clearTodos().then(todos => {this.todos = todos})}// get all todosgetTodos() {this.repository.getTodos().then(todos => {this.todos = todos})}// change priority scoreschangePriority(event) {this.repository.changePriority(event)this.repository.updateTodo(event.todo)}// toggle complete or uncompletetoggleComplete(todo) {todo.completed = !todo.completedthis.repository.updateTodo(todo)}// remove a single todoremoveTodo(id) {this.repository.removeTodo(id).then(todos => {this.todos = todos})}/* filter search only if there is a value. If there isn'tload all stored todos from local storage.*/filterSearch(value) {if (!value) {this.repository.getTodos().then((todos: Todo[]) => {this.todos = todos})} else {this.repository.filterSearch(value).then(() => {// inside thenable it's safe to get the filtered// todos. It would work without it but when an api with latency// is added this will save us from race conditions.this.repository.getFilteredTodos().then((todos: Todo[]) => {this.todos = todos})})}}}