Thursday, December 19, 2019

To do list in Angular

Here is how to create a simple todo-list in Angular. We will be also using extensively RxJs BehaviorSubject as well as Observables. If you want more information on the techniques used, I advise you to practice with the full Angular for beginners course.




First install angular with: sudo npm i -g @angular/cli
Then create a new todo project: ng new todo
Inside the todo directory create 2 new components and service using the Angular CLI:
ng g c todos
ng g c form
ng g s services/todos

Next, modify the following files:
app.component.html
// to include the new component tags
<div class="container">
<h1>{{appTitle}}</h1>
<app-todos [limit]="3"></app-todos>
<app-form></app-form>
</div>
...
app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { TodosComponent } from './todos/todos.component';

// import httpclientmodule to be able to perform http requests
import { HttpClientModule } from '@angular/common/http';

// import forms module to be able to use 2-way [(binding)]
import { FormsModule } from '@angular/forms';
import { FormComponent } from './form/form.component';

@NgModule({
declarations: [
AppComponent,
TodosComponent,
FormComponent
],
// and place it (FormsModule) inside modules to be visible by all derived services and componenets
imports: [
BrowserModule,
HttpClientModule,
FormsModule //  add into imports the formsmodule to be accessible from the components
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

...
form.component.html

<div>
<input type="text" placeholder="Add todo..."
<!-- bind the title input field of the form to the model this.title -->
[(ngModel)]="title"
<!-- allow with enter to be able to run addTodo() -->
(keydown.enter)="addTodo()" />
<button (click)="addTodo()">Add todo</button>
</div>

...
form.component.ts

import { Component } from '@angular/core';
// import both the interface Todo as well as the TodoService
import { Todo, TodosService } from '../services/todos.service';

@Component({
selector: 'app-form',
templateUrl: './form.component.html',
styleUrls: ['./form.component.scss']
})

export class FormComponent implements OnInit {
// title will be bound to the title input of the form
title: string = '';

// use dependency injection to inject the TodosService
constructor(private todosService: TodosService) { }

addTodo() {
// construct todo object and add it using the service
const todo: Todo = {
id: Date.now(),
title: this.title,
complete: false,
}
this.todosService.addTodo(todo);
}
}
...


todos.component.ts
import { Component, OnInit, Input } from '@angular/core';
import { TodosService, Todo } from '../services/todos.service';
import { Observable } from 'rxjs';

@Component({
selector: 'app-todos',
templateUrl: './todos.component.html',
styleUrls: ['./todos.component.scss']
})
export class TodosComponent implements OnInit {

constructor(private todosService: TodosService) { }
private fetchData$: Observable<boolean>;
private todos$: Observable<Todo[]>;

// receive the limit from the parent(appcomponent) via input
@Input() limit: number;

ngOnInit() {
// initialize the both observables
this.fetchData$ = this.todosService.getTodos(this.limit);
this.todos$ = this.todosService.todos$;
}

// call the service
onChange(id: number) {
this.todosService.onToggle(id);
}

// call the service
removeTodo(id: number) {
this.todosService.removeTodo(id);
}

}

...

todos.component.html

<!-- mainly to fetch the http json data & setup this.todos -->
<div *ngIf="(fetchData$ | async ); else errorFetch"></div>

<!-- get the observable data and spread it on the page -->
<ul *ngIf="(todos$ | async ) as todos; else loading">
<li *ngFor="let todo of todos; let i = index">
<span [class.done]="todo.complete">
<input type="checkbox" [checked]="todo.complete" (change)="onChange(todo.id)">
<!--  bind [checked] to the state complete of todo
 pass todo.id on(change) event,
and run onChange function from the .ts file
-->
{{i + 1}} {{todo.title}}
</span>
<small>{{todo.date | date}}</small>
<!-- use pipe date to format the data: you can see other pipes from API/datepipe on angular website -->
<button class="remove" (click)="removeTodo(todo.id)">×</button>
</li>
</ul>

<ng-template #loading> Loading, please wait ...</ng-template>
<ng-template #errorFetch> There is an error while loading the data, please try again ...</ng-template>

...

todos.service.ts

import { Injectable } from '@angular/core';
import { Observable, BehaviorSubject, of } from 'rxjs';
import { tap, map, catchError } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';

// create interface with the structure of a simple todo item
export interface Todo {
id: number
title: string
complete: boolean
}

@Injectable({
providedIn: 'root'
})
export class TodosService {

constructor(private http: HttpClient) { }
public todos: Todo[] = [];
private subject = new BehaviorSubject<Todo[]>([]); // to be able to retain the last emitted version !
public todos$ = this.subject.asObservable();

getTodos(limit: number): Observable<boolean> {

return this.http.get<Todo[]>(`https://jsonplaceholder.typicode.com/todos?_limit=${limit}`)
//  just return observable of true or false
.pipe(
map((fetchedTodos: Todo[]) => {
// setup the local this.todos member
this.todos = fetchedTodos;
// propagade the todos to subscribers via subject
this.subject.next(this.todos);
return true;
}),
catchError(err => {
alert(err.message);
return of(false);
})
);
}

onToggle(id: number) {
// get the todo index by the provided ID
const idx = this.todos.findIndex(todo => todo.id === id);
this.todos[idx].complete = !this.todos[idx].complete;
}

removeTodo(id: number) {
this.todos = this.todos.filter(todo => { return todo.id !== id }
);

// propagade the updated data back to all observables
this.subject.next(this.todos);
}

addTodo(todo: Todo) {
this.todos = [...this.todos, todo];
this.subject.next(this.todos);
}
}

Now you can run: ng serve
and browse: http://127.0.0.1:4200

Congratulations and enjoy learning Angular.

Subscribe To My Channel for updates