Within this topic, we're going to implement the changes needed to move our simple task list into a structure that is organized by projects. For this purpose, we need to modify the main layout of our components as well as introduce a new component that represents our projects.
First, let's update our application model to include project data. For this, we're going to create a new model for a project as well as update the model of our tasks to add a project ID.
Open up the src/app/model.ts file and apply the following changes:
export interface Task {
readonly id?: number;
readonly projectId?: number;
readonly title: string;
readonly done: boolean;
}
export type TaskListFilterType = 'all' | 'open' | 'done';
export interface Project {
readonly id?: number;
readonly title: string;
readonly description: string;
} Each task is now including a reference to a project. The project entities are consisting of an ID, individual title, and description property. Let's also update our in-memory web API database. Open the src/app/database.ts file and apply the following changes:
import {InMemoryDbService} from 'angular-in-memory-web-api';
import {Project, Task} from './model';
export class Database implements InMemoryDbService {
createDb() {
const projects: Project[] = [
{id: 1, title: 'My first project', description: 'This is your first project.'},
{id: 2, title: 'My second project', description: 'This is your second project.'}
];
const tasks: Task[] = [
{id: 1, projectId: 1, title: 'Task 1', done: false},
{id: 2, projectId: 1, title: 'Task 2', done: false},
{id: 3, projectId: 1, title: 'Task 3', done: true},
{id: 4, projectId: 1, title: 'Task 4', done: false}
];
return {projects, tasks};
}
} We've added two projects to our database as well as updated all the tasks to include a reference to the first of the two projects.
Now, we are going to need a service to access our projects, and we should also update our task service to include a method that allows us to query for tasks that belong to a specific project.
First, let's apply the changes to the existing task service. Open up the src/app/tasks/task.service.ts file and implement the following changes. Effective changes are marked in bold, and the ellipsis character is indicating more code that is irrelevant for the changes to be applied:
import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {BehaviorSubject} from 'rxjs';
import {map} from 'rxjs/operators';
import {Task} from '../model';
@Injectable()
export class TaskService {
…
getProjectTasks(projectId: number) {
return this.tasks
.asObservable()
.pipe(
map((tasks) => tasks.filter((task) => task.projectId === projectId))
);
}
} The added getProjectTasks method is providing a mapped observable that takes our source tasks subject and maps each tasks array to produce a filtered tasks array that only includes tasks of a specific project.
Alright, now we need to create a new service that allows us to obtain information about the projects from our in-memory web API database. Let's use the Angular CLI to create a new service:
ng generate service --spec false project/project
The Angular CLI should have created our service on the path src/app/project/project.service.ts. Let's open that file and replace its content with the following code:
import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {BehaviorSubject, Observable, combineLatest} from 'rxjs';
import {map} from 'rxjs/operators';
import {Project} from '../model';
@Injectable()
export class ProjectService {
private projects = new BehaviorSubject<Project[]>([]);
private selectedProjectId = new BehaviorSubject<number>(1);
private selectedProject: Observable<Project>;
constructor(private http: HttpClient) {
this.loadProjects();
this.selectedProject = combineLatest(this.projects, this.selectedProjectId)
.pipe(
map(([projects, selectedProjectId]) =>
projects.find((project) => project.id === selectedProjectId)
)
);
}
private loadProjects() {
this.http.get<Project[]>('/api/projects')
.subscribe((projects) => this.projects.next(projects));
}
selectProject(id: number) {
this.selectedProjectId.next(id);
}
getSelectedProject() {
return this.selectedProject;
}
} Let's discuss the preceding code changes briefly. Our project service contains three members:
- projects: BehaviourSubject<Project[]>
The projects member behaviour subject is emitting our whole project list once loaded from our database. This subject is the basis for all operations within our service. - selectedProjectId: BehaviourSubject<number>
Since we will need to know which of the projects is currently selected within our application, we need to store this information in our service. We're using a behaviour subject for emitting the currently selected project ID. This allows us to simply emit a project ID through selectedProjectId if we wish to select a given project. - selectedProject: Observable<Project>
The selectedProject observable will always emit the currently selected project. We'll make use of combineLatest to make sure if either projects or selectedProjectId emits a change. We will re-emit the updated, selected project through the selectedProject observable stream.
Within the constructor of our service, we're first calling the loadProjects method to do the HTTP call to our in-memory web API database to obtain the list of projects. Within the loadProjects method, we're sticking to the same pattern from our task service. We're subscribing to the HTTP service observable and emitting the resulting items through our internal projects subject.
After executing the loadProjects method within our constructor, we will create the selectedProject observable. We will use combineLatest, which we've discussed already in the previous chapter, to combine the projects and the selectedProjectId subjects into a single observable stream. Whenever one of those two input observables emits an event, combineLatest will combine the latest result of both input observables into a single item that is emitted through the output observable stream. We're using the map operator to extract the selected project from the list of projects and returning it as an item into the observable output stream.
Finally, the selectProject method is merely emitting the new project ID through the selectedProjectId subject. Since we're using this subject within the selectedProject observable created with combineLatest, this change will cause the selectedProject observable to re-emit the currently selected project.
As the last step, we need to add our new service to the app module providers. Let's open the src/app/app.module.ts file and apply the following changes:
…
import {ProjectService} from './proje...