Skip to content

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.