Creating Checklist Items
The path we take to implement this feature is going to look pretty similar to creating the checklists, and it will be the same with just about any feature we would implement. We will generally follow a process like this:
- Add a method to an existing service or create a new service to handle the data
- Add the user interface required to invoke that new method
- Display the result in the UI
In this case, we are going to start by creating a new service for managing checklist items.
Creating the Checklist Item service
First, we will need to define an interface for our checklist items.
Create a file at
src/app/shared/interfaces/checklist-item.ts
:
export interface ChecklistItem {
id: string;
checklistId: string;
title: string;
checked: boolean;
}
export type AddChecklistItem = Pick<ChecklistItem, 'title'>;
This is the same basic idea as the Checklist
except we also have a checklistId
property so that we know what checklist a particular item belongs to, and a checked
field to indicate whether the item has been completed or not.
Our checklist service was created in the shared/data-access
folder because it is used by multiple features in the application. However, our ChecklistItemService
will only be used by the checklist item feature - so, we will store it inside of the checklist/data-access
folder.
Create a new service at
src/app/checklist/data-access
and add the following:
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { map } from 'rxjs/operators';
import {
AddChecklistItem,
ChecklistItem,
} from '../../shared/interfaces/checklist-item';
@Injectable({
providedIn: 'root',
})
export class ChecklistItemService {
private checklistItems$ = new BehaviorSubject<ChecklistItem[]>([]);
getItemsByChecklistId(checklistId: string) {
return this.checklistItems$.pipe(
map((items) => items.filter((item) => item.checklistId === checklistId))
);
}
add(item: AddChecklistItem, checklistId: string) {
const newItem = {
id: Date.now().toString(),
checklistId,
checked: false,
...item,
};
this.checklistItems$.next([...this.checklistItems$.value, newItem]);
}
}
Again, nothing really new here - we are using the same technique with a BehaviorSubject
and emitting new items on that stream. We don’t need to worry about a “nice” id
for our checklist items because we will never see it, so we just use Date
this time.
Our getItemsByChecklistId
is a bit different. For the checklists we just did this:
getChecklists() {
return this.checklists$.asObservable();
}
But our checklistItems$
stream contains all of the items for all of the checklists. We only want the items for one specific checklist that matches the checklistId
passed in, so we map
our stream. There is also no need to use asObservable()
here because when you add the pipe
method onto a BehaviorSubject
it converts it into a standard observable anyway.
Using our generic form component to add items
Now we have a method in a service to add checklist items, we need to support supplying data to that in our interface. This is where creating that generic form modal component is going to pay off, as we are able to utilise that again here.
Add the following to the
ChecklistComponent
class:
formModalIsOpen$ = new BehaviorSubject<boolean>(false);
checklistItemForm = this.fb.nonNullable.group({
title: ['', Validators.required],
});
NOTE: You will need to inject the FormBuilder
as fb
in the constructor and import Validators
and BehaviorSubject
at the top of the file
Now, before we continue on to adding the modal, we are going to address a little issue. Currently, our template looks like this:
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button defaultHref="/"></ion-back-button>
</ion-buttons>
<ion-title *ngIf="checklist$ | async as checklist">
{{ checklist.title }}
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content> </ion-content>
We are subscribing the the checklist$
stream to get the title in the ion-title
component. This is fine, but when we add the modal we are also going to need to use the async pipe to subscribe to the stream there again. We don’t want to have to keep subscribing to streams using the async pipe over and over again, so what we are going to do is restructure this a little.
Modify the template to reflect the following:
<ng-container *ngIf="checklist$ | async as checklist">
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button defaultHref="/"></ion-back-button>
</ion-buttons>
<ion-title>
{{ checklist.title }}
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content> </ion-content>
</ng-container>
We have added an ng-container
to wrap our whole template, and we use the async
pipe there. This will make checklist
available to the entire template. This is convenient, but one thing to keep in mind with this method is that if the stream does not emit immediately, then our entire template will not show until the stream emits a value. This is fine in this case because checklist$
does emit a value immediately, but it is something to keep in mind.
This still isn’t quite suitable. Because now we also have a formModalIsOpen$
stream that we need to subscribe to as well. Let’s use this same technique to incorporate that as well:
Modify the template to reflect the following:
<ng-container
*ngIf="{
checklist: checklist$ | async,
formModalIsOpen: formModalIsOpen$ | async
} as vm"
>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button defaultHref="/"></ion-back-button>
</ion-buttons>
<ion-title>
{{ vm.checklist?.title }}
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content> </ion-content>
</ng-container>
Now we are creating an object in the ng-container
that subscribes to both streams, and makes that available to the template as vm
(you can see how we are using this in ion-title
). Now the values from both the streams are available everywhere in the template. This also means that our ng-container
will always instantly render now - it is being assigned an object, and an object is always truthy, it doesn’t matter if the streams we are setting up within that object have emitted yet or not. The downside is that now our checklist
and formModalIsOpen
values are now potentially null
again according to TypeScript (even though they are not), and so to access title
we need to use the safe navigation operator ?
to check that checklist
is not null.
Instead of doing this, we are going to use the non-null assertion operator instead.
Modify the template to reflect the following:
<ng-container
*ngIf="{
checklist: (checklist$ | async)!,
formModalIsOpen: (formModalIsOpen$ | async)!
} as vm"
>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button defaultHref="/"></ion-back-button>
</ion-buttons>
<ion-title>
{{ vm.checklist.title }}
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content> </ion-content>
</ng-container>