Skip to content

Creating a Checklist Service

In this lesson, we are going to take the data entered into our form and save it using a service. This will be our first chance to put those state management concepts we have been talking about into practice.

The first thing we are going to do is add a few more types.

export interface Checklist {
id: string;
title: string;
}
export type AddChecklist = Omit<Checklist, 'id'>;
export type EditChecklist = { id: Checklist['id']; data: AddChecklist };
export type RemoveChecklist = Checklist['id'];

We have added three types here: AddChecklist, EditChecklist, and RemoveChecklist. We are going to use different types of data for different actions.

When we want to add a checklist we will only supply the title as the id is generated automatically. To handle this we create a new type using the Omit utility type that allows us to remove a particular property from an existing type.

When we edit a checklist we will supply the action in this format:

{
id: 'id of checklist being edited',
data: { /* data we want to update */ }
}

To do this, we create a new type using Checklist['id'] which will become whatever the type of the id property is in the Checklist type. We do a similar thing for our remove type too where we will only need to supply the id of the checklist we are removing.

Now let’s move on to creating the service.

import { Injectable, computed, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Subject } from 'rxjs';
import { AddChecklist, Checklist } from '../interfaces/checklist';
export interface ChecklistsState {
checklists: Checklist[];
}
@Injectable({
providedIn: 'root',
})
export class ChecklistService {
// state
private state = signal<ChecklistsState>({
checklists: [],
});
// selectors
checklists = computed(() => this.state().checklists);
// sources
add$ = new Subject<AddChecklist>();
constructor() {
// reducers
this.add$.pipe(takeUntilDestroyed()).subscribe((checklist) =>
this.state.update((state) => ({
...state,
checklists: [...state.checklists, this.addIdToChecklist(checklist)],
}))
);
}
private addIdToChecklist(checklist: AddChecklist) {
return {
...checklist,
id: this.generateSlug(checklist.title),
};
}
private generateSlug(title: string) {
// NOTE: This is a simplistic slug generator and will not handle things like special characters.
let slug = title.toLowerCase().replace(/\\s+/g, '-');
// Check if the slug already exists
const matchingSlugs = this.checklists().find(
(checklist) => checklist.id === slug
);
// If the title is already being used, add a string to make the slug unique
if (matchingSlugs) {
slug = slug + Date.now().toString();
}
return slug;
}
}

If we focus on the top half of this, things should look reasonably familiar:

// state
private state = signal<ChecklistsState>({
checklists: [],
});
// selectors
checklists = computed(() => this.state().checklists);
// sources
add$ = new Subject<AddChecklist>();
constructor() {
// reducers
this.add$.pipe(takeUntilDestroyed()).subscribe((checklist) =>
this.state.update((state) => ({
...state,
checklists: [...state.checklists, this.addIdToChecklist(checklist)],
}))
);
}

This is the exact structure we talked about in the state management lesson. We’ve defined the shape of our state, we set up a state signal using that type and an initial value, and we have a selector that creates a computed to access a portion of that state.

When we want to add a new checklist, we will next our add$ data source from somewhere, e.g:

add$.next({
title: 'hello'
})

Since we have subscribed to add$ in the constructor, whenever a value is emitted via next we will trigger this code:

this.state.update((state) => ({
...state,
checklists: [...state.checklists, this.addIdToChecklist(checklist)],
}))

This updates our state signal, copying all of the previous state by using ...state and then overwriting the checklists portion with our new value. We duplicate all of the existing checklists into the new array using ...state.checklists and then we add our new one on to the end.

Curiously, we don’t just supply the new checklist directly, we call addIdToChecklist(checklist). This is because we need to add a unique id to the checklist. We call this method which in turn calls our generateSlug method to generate a unique id for us.

This is a somewhat simplistic unique id generator, but it does the job well enough for us here. To make a “pretty” unique id (rather than just using a date string like we did in the todo application) we take the title of the checklist, and convert it to kebab-case. The problem with this is that if we give two checklists the same title they will have the same id. To counter this, we check if any existing checklists have the same id and if they do, then we append the unique Date.now() string to the end of the id. This guarantees us a unique id (at least in this sort of application, in an application with multiple users relying on dates for unique values, this would not be as safe).

I want to make a quick note about that statement from Mike Pearson we considered before:

The curly braces of functions that don’t return anything are like open arms inviting imperative code.

We are generally avoiding writing methods if we can, but note that these methods we have created return something.

They are not triggering any side effects, they are just returning something to the code that called it. They are just little helper functions. Technically, we could do all of this without needing to create a separate generateSlug method but it would make a big mess and serve no purpose anyway.

We have our service now, and it should work, but we actually need to use it somewhere first.

Use the Checklist Service

Now that we have our add$ source (or “action”) in the service, we need to pass it the data from our form. Our HomeComponent already has access to the FormGroup and our app-form-modal emits a save event when the save button is clicked, so we have everything we need.

<app-form-modal
[title]="
checklistBeingEdited()?.title
? checklistBeingEdited()!.title!
: 'Add Checklist'
"
[formGroup]="checklistForm"
(close)="checklistBeingEdited.set(null)"
(save)="checklistService.add$.next(checklistForm.getRawValue())"
/>

All we are doing here is passing along our form values to our add$ source whenever the save event emits:

(save)="checklistService.add$.next(checklistForm.getRawValue())"

And that’s all we need to do — our checklists are now being added to our state! However… we still don’t see anything yet because at no point are we actually displaying the checklists from our state in the template. This is what we will be tackling in the next lesson.