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.