Saving Tasks to the Backend | Angular Todos
Table of Contents
Step 1 - HTTP Requests
Users can log in to their accounts, but their tasks are still not persisting. What users will require is the ability to create tasks, mark tasks as complete, and view all of their existing tasks.
We will need a new dependency though, qs
, in the web
subfolder. qs
is the library we use to stringify an object for GET
requests.
cd web
npm install qs
npm install -D @types/qs
We also need to create a service to handle these requests. Also in your web
subfolder run:
ng g s tasks
In the new file, web/src/app/tasks.service.ts
, we will add the relevant imports:
import { Injectable } from '@angular/core';
+ import { HttpClient } from '@angular/common/http';
+ import { of } from 'rxjs';
+ import { catchError, map } from 'rxjs/operators';
+ import qs from 'qs';
+ import { environment } from '../environments/environment';
First, add the
HttpClient
as an argument of the constructor:export class TasksService {
constructor(private http: HttpClient) {}
}Second, add the
create
method to theTasksService
:create(text: string, uid: string) {
const url = new URL('/api/tasks', environment.apiUrl).href;
return this.http
.post(url, { completed: false, text, uid: { id: uid } })
.pipe(
catchError(() => of(null)),
map((result: any) => (result ? result : alert('Could not create task')))
);
}create
will take two arguments, the text content of a new task as well as the unique ID of the user. It will make aPOST
request to the/api/tasks
endpoint, sending a task object. The task object has three properties:completed
- A boolean property that tracks if a task is completed. It's being assigned to false here by default as a new task will not be completed already.text
- The string of the task itself.uid.id
- The unique ID of the user, this allows for querying tasks created by a specific user.
One property that is not being included that we had before is
id
. Why aren't we assigning it? Well, we don't need to. The Amplication backend will assign a unique ID to all entries to the database, making management of data easier.If the request fails an alert will notify the user and the method will return an Observable that emits nothing. On the success of the request, an Observable that emits the new task object will be returned, with all the required properties to render it in the frontend.
Next, add the
getAll
method:getAll(uid: string) {
const query = qs.stringify({
where: { uid: { id: uid } },
orderBy: { createdAt: 'asc' },
});
const url = new URL(`/api/tasks?${query}`, environment.apiUrl).href;
return this.http.get(url).pipe(
catchError(() => of(null)),
map((result: any) => {
if (!result) {
alert('Could not get tasks');
return [];
}
return result;
})
);
}getAll
takes one argument, the unique ID of the user. It will make aGET
request to the/api/tasks
endpoint, sending a query. In this case, we're looking to return all the tasks for a user, and the query object reflects that. Looking at the object should help make sense of what's going on.In the query,
{ where: { uid: { id: uid } } }
, we're telling the backend that we are looking for all entitieswhere
theuid
value of a task is set to the unique ID of a user. Additionally, in the query there is{ orderBy: { createdAt: "asc" } }
, which returns the tasks in the order they were created, from oldest to newest (asc
ending).createdAt
is a property that Amplication adds to all database entries by default. If the request fails, an alert will pop up notifying the user of the failure. If the request succeeds, then an Observable emitting all the tasks created by a user will be returned.Then, add the
update
method:update(task: any) {
const url = new URL(`/api/tasks/${task.id}`, environment.apiUrl).href;
return this.http.patch(url, { completed: !task.completed }).pipe(
catchError(() => of(null)),
map((result: any) => (result ? result : alert('Could not update task')))
);
}update
takes one argument, the task object. It will make aPATCH
request to the/api/tasks/{TASK_ID}
endpoint. The ID of the task object is included in the request and all that is being sent in the body of the request is acompleted
property, which is toggled to its new state.PATCH
requests do not require a complete object, and only update the properties included in the request. In this case, we only want to update thecompleted
property, so that's the only value we send. If the request fails an alert will pop up notifying the user of the failure. If the request succeeds, then an Observable emitting the updated task object will be returned.Finally we'll need to add the
TasksService
to theAppModule
. Openweb/src/app/app.module.ts
and importTasksService
:import { AuthService } from './auth.service';
import { JWTService } from './jwt.service';
+ import { TasksService } from './tasks.service';Then add and configure the
TasksService
to theproviders
in the@NgModule
decorator:providers: [
{ provide: HTTP_INTERCEPTORS, useClass: JWTService, multi: true },
AuthService,
+ TasksService,
],
bootstrap: [AppComponent]
})
export class AppModule { }
The Angular CLI and the Typescript compiler may complain about
qs
. This can be resolved in two steps. Inweb/angular.json
add the following to theprojects.web.architect.build.options
:"allowedCommonJsDependencies": [
"qs"
],Additionally in
web/tsconfig.json
add the following to thecompilerOptions
:"allowSyntheticDefaultImports": true,
Step 2 - Updating App
Presently the AppComponent
is handling the state of the user's tasks. Start by importing TasksService
into web/src/app/app.component.ts
.
import { Component, OnInit } from '@angular/core';
import { AuthService } from './auth.service';
+ import { TasksService } from './tasks.service';
First, add the
TasksService
as an argument of the constructor:export class AppComponent implements OnInit {
tasks: any[] = [];
user: any;
- constructor(private auth: AuthService) {}
+ constructor(private auth: AuthService, private ts: TasksService) {}In
AppComponent
we can now remove thecreateTask
, as the task object is created by thecreate
method of theTasksService
.- createTask(text: string) {
- return {
- id: this.tasks.length,
- text,
- completed: false,
- };
- }
addTask(task: string) {
const newTask = this.createTask(task);
this.tasks.push(newTask);
}We'll next modify the
addTask
method:addTask(task: string) {
- const newTask = this.createTask(task);
- this.tasks.push(newTask);
+ this.ts.create(task, this.user.id).subscribe({
+ next: (newTask: any) => {
+ if (!newTask) return;
+ this.tasks.push(newTask);
+ },
+ });
}Now that we're making an asynchronous HTTP request, we'll be working with RxJS Observables. RxJS is a library for writing asynchronous and event-based applications by using observable sequences. You can read more about it at rxjs.dev. We
subscribe
to see when thecreate
resolves, and when it does, we push the newly created task into thetasks
array of theAppComponent
. If the request fails thennewTask
will have no value, and thesubscribe
listener will end right away.Next, we'll make updates to the
completed
method:- completed(id: number) {
- const i = this.tasks.findIndex((t) => t.id === id);
- this.tasks[i].completed = !this.tasks[i].completed;
+ completed(task: any) {
+ this.ts.update(task).subscribe({
+ next: (updatedTask: any) => {
+ if (!updatedTask) return;
+ const i = this.tasks.findIndex((t) => t.id === updatedTask.id);
+ this.tasks[i] = updatedTask;
+ },
+ });
}completed
is now an asynchronous HTTP request as well, so again we'll be working with RxJS Observables. The method is also updated to instead accept the task object that is being toggled rather than the ID of the task being updated. Wesubscribe
to see when theupdate
resolves, and when it does, we update thecompleted
property of the task in thetasks
array of theAppComponent
. If the request fails thenupdatedTask
will have no value, and thesubscribe
listener will end right away.Finally, we'll make some updates regarding the
setUser
method:setUser(user: any) {
this.user = user;
+ if (!user) return;
+ this.ts.getAll(user.id).subscribe({
+ next: (tasks: any[]) => (this.tasks = [...tasks]),
+ });
}Now, when the
user
object is set, like on initialization of theAppComponent
, we also will attempt to fetch all tasks that belong to a user.
Step 3 - Updating Tasks
With almost everything in place, just a few changes to the TaskComponent
's template so that it now emits the task
object rather than just the task
's ID. Make the following changes to web/src/app/task/task.component.html
<li [class.completed]="task.completed">
<span>{{task.text}}</span>
- <input type="checkbox" [checked]="task.completed" (click)="completed.emit(task?.id)" readOnly />
+ <input type="checkbox" [checked]="task.completed" (click)="completed.emit(task)" readOnly />
</li>
Step 4 - Wrap Up
Run the application and try creating some tasks. Feel free to refresh the page as well.
Users' tasks are now being saved to the Amplication backend and still show when users refresh or revisit the application.
So far we've done everything through HTTP calls, however, Amplication also supports GraphQL. Next, we'll update the Todos
app to handle all data requests via GraphQL queries and mutations.
To view the changes for this step, visit here.