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.
Create the ChecklistItem Interface
We are creating a new entity in our application now — items that belong to a specific checklist. We are first going to define a new type that represents this entity in the application.
import { RemoveChecklist } from './checklist';
export interface ChecklistItem { id: string; checklistId: string; title: string; checked: boolean;}
export type AddChecklistItem = { item: Omit<ChecklistItem, 'id' | 'checklistId' | 'checked'>; checklistId: RemoveChecklist;};export type EditChecklistItem = { id: ChecklistItem['id']; data: AddChecklistItem['item'];};export type RemoveChecklistItem = ChecklistItem['id'];In general, this is the same idea as our Checklist type but there is some
extra weirdness happening here. First of all, our ChecklistItem type is just
different — as well as the title we also have a checklistId to indicate what
Checklist it belongs to, and a checked to indicated whether it is currently
in the completed state or not.
We have the same style of types for adding and editing as we did with the
Checklist but for this one we also have our checklistId property based on
whatever the RemoveChecklist type is from our Checklist interfaces.
We could just give checklistId a type of string which would match the id
from our Checklist interface. This is just a bit safer because if we ever
update the type of the id in Checklist we won’t need to remember to change
this too.
Creating the Checklist Item service
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.
import { Injectable, computed, signal } from '@angular/core';import { takeUntilDestroyed } from '@angular/core/rxjs-interop';import { Subject } from 'rxjs';import { AddChecklistItem, ChecklistItem,} from '../../shared/interfaces/checklist-item';
export interface ChecklistItemsState { checklistItems: ChecklistItem[];}
@Injectable({ providedIn: 'root',})export class ChecklistItemService { // state private state = signal<ChecklistItemsState>({ checklistItems: [], });
// selectors checklistItems = computed(() => this.state().checklistItems);
// sources add$ = new Subject<AddChecklistItem>();
constructor() { this.add$.pipe(takeUntilDestroyed()).subscribe((checklistItem) => this.state.update((state) => ({ ...state, checklistItems: [ ...state.checklistItems, { ...checklistItem.item, id: Date.now().toString(), checklistId: checklistItem.checklistId, checked: false, }, ], })) ); }}Again, nothing really new here — this is almost identical to what we did with
the ChecklistService. Our reducer is a bit more complicated so let’s talk
through what is happening here:
this.state.update((state) => ({ ...state, checklistItems: [ ...state.checklistItems, { ...checklistItem.item, id: Date.now().toString(), checklistId: checklistItem.checklistId, checked: false, }, ], }))We are trying to update the checklistItems state. This service will contain an
array of all items for all checklists. We add a checklistId to each
item to tell which checklist it belongs to.
When updating our state, we first copy all of the existing checklistItems into
the new array:
...state.checklistItems,We then add a new checklist item at the end of the array:
{ ...checklistItem.item, id: Date.now().toString(), checklistId: checklistItem.checklistId, checked: false,},We supply all of the data provided from the add$ source (which is actually
just the title) by spreading ...checklistItem.item. Then we manually
override the rest of the properties — we generate a unique id, we add the
appropriate checklistId for whatever checklist this item is being added to,
and we initialise the checked property to false.
Using our Generic Form Component to Add Items
Now we have a source/action in our service to add checklist items, we need to support supplying data to that in our user interface. This is where creating that generic form modal component is going to pay off, as we are able to utilise that again here.
export default class ChecklistComponent { checklistService = inject(ChecklistService); checklistItemService = inject(ChecklistItemService); route = inject(ActivatedRoute); formBuilder = inject(FormBuilder);
checklistItemBeingEdited = signal<Partial<ChecklistItem> | null>(null);
params = toSignal(this.route.paramMap);
checklist = computed(() => this.checklistService .checklists() .find((checklist) => checklist.id === this.params()?.get('id')) );
checklistItemForm = this.formBuilder.nonNullable.group({ title: [''], });
constructor() { effect(() => { const checklistItem = this.checklistItemBeingEdited();
if (!checklistItem) { this.checklistItemForm.reset(); } }); }}This is essentially exactly the same as what we did for the Checklist
component. We have created a form, a checklistItemBeingEdited signal that is
going to control opening/closing the modal, and we even have the same effect
for clearing the checklist item for when it is closed/saved.
Now let’s add the modal and form to the template.
<app-modal [isOpen]="!!checklistItemBeingEdited()"> <ng-template> <app-form-modal title="Create item" [formGroup]="checklistItemForm" (save)="checklistItemService.add$.next({ item: checklistItemForm.getRawValue(), checklistId: checklist()?.id!, })" (close)="checklistItemBeingEdited.set(null)" ></app-form-modal> </ng-template> </app-modal>All we have to do is make sure we add the appropriate imports for our
app-modal and app-form-modal and we can re-use the entire structure for
displaying the modal and form that we built up before — we just pass in the
checklistItemForm to it and we’re good to go.
Now we just need a button to launch the modal on our ChecklistComponent page.
We are going to handle this by adding a button in our ChecklistHeaderComponent
that will emit an event. We will detect that event in our ChecklistComponent
to trigger opening the modal. It is a bit of extra work versus adding the button
directly in the ChecklistComponent, but it helps keep our components
organised.
addItem = output(); <header> <a routerLink="/home">Back</a> <h1> {{ checklist().title }} </h1> <div> <button (click)="addItem.emit()">Add item</button> </div> </header>Now we can react to that output in our ChecklistComponent.
<app-checklist-header [checklist]="checklist" (addItem)="checklistItemBeingEdited.set({})"/>This is a good time to test our application. We should find that we are able to:
- Create a checklist
- Go to that checklist
- Add a checklist item
But then we won’t actually be able to see that item. We will handle that in the next section.
Displaying checklist items
Just like we did with the HomeComponent and the ChecklistListComponent, we
are going to create a dumb component for our ChecklistComponent called
ChecklistItemListComponent that will handle displaying the checklist items.
import { Component, input } from '@angular/core';import { ChecklistItem } from '../../shared/interfaces/checklist-item';
@Component({ selector: 'app-checklist-item-list', template: ` <section> <ul> @for (item of checklistItems(); track item.id){ <li> <div> {{ item.title }} </div> </li> } @empty { <div> <h2>Add an item</h2> <p>Click the add button to add your first item to this quicklist</p> </div> } </ul> </section> `,})export class ChecklistItemListComponent { checklistItems = input.required<ChecklistItem[]>();}Again, absolutely nothing new going on here. Now, this component expects an
input of checklistItems which means we will need to pass those checklist items
in from our ChecklistComponent page.
Before we can pass the items in… we need to figure out what they are. We can
do this by creating another computed.
items = computed(() => this.checklistItemService .checklistItems() .filter((item) => item.checklistId === this.params()?.get('id')) );<app-checklist-item-list [checklistItems]="items()" />If we run our application now, we should see that we are able to:
- Create a checklist
- Go to that checklist
- Add a checklist item
- See the checklist item!
In the next lesson, we are going to work on making this look like an actual checklist item list by adding the ability to toggle the completion state.