Editing and Deleting Data
The last key feature we need to focus on is the ability to edit and delete the checklists and checklist items. This will involve a reasonably simple extension in terms of the functionality in our services.
Update the Services
We will start by adding support for editing and deleting items in the
ChecklistItemService. This is by no means easy or obvious, but this will serve
well as an opportunity to implement a slightly more advanced feature with no
guidance.
Before continuing, see if you can add new sources and reducers that allow
deleting and editing specific checklist items. We have already created the
appropriate types for EditChecklistItem and RemoveChecklistItem so you
can use these as reference for the type of data you will be working with.
See how much progress you can make on this and then take a look at my solution below (don’t worry if you get stuck).
Click here to reveal solution
Solution
remove$ = new Subject<RemoveChecklistItem>(); edit$ = new Subject<EditChecklistItem>(); this.edit$.pipe(takeUntilDestroyed()).subscribe((update) => this.state.update((state) => ({ ...state, checklistItems: state.checklistItems.map((item) => item.id === update.id ? { ...item, title: update.data.title } : item ), })) );
this.remove$.pipe(takeUntilDestroyed()).subscribe((id) => this.state.update((state) => ({ ...state, checklistItems: state.checklistItems.filter((item) => item.id !== id), })) );That’s all we need to do — we can now next the edit$ and remove$ sources
to edit or remove checklist items.
Now we want to handle removing and editing checklists in the
ChecklistService… but we have a bit of a surprise in store first.
Consider that if we want to remove a checklist, we will also want to remove
all of the checklist items for that checklist. That means that our
ChecklistItemService is going to need to know when the removal of a checklist
is triggered, so that it can handle removing all of the items related to that
checklist.
To handle this, we are going to create a shared source. We are actually
going to add a checklistRemoved$ source to our ChecklistItemService. Our
ChecklistItemService will react to that by removing all of the items. Then, in
our ChecklistService we will use the same checklistRemoved$ source from
ChecklistItemService and the ChecklistService will react to that same source
emitting by removing the appropriate checklist. When we trigger the
checklistRemoved$ source in the ChecklistItemService both our
ChecklistItemService and our ChecklistService will automatically react
(just in different ways).
checklistRemoved$ = new Subject<RemoveChecklist>(); this.checklistRemoved$.pipe(takeUntilDestroyed()).subscribe((checklistId) => this.state.update((state) => ({ ...state, checklistItems: state.checklistItems.filter( (item) => item.checklistId !== checklistId ), })) );Now when the checklistRemoved$ source emits we will remove all of the items
for that checklistId.
Now we will utilise this same checklistRemoved$ source in our
ChecklistService.
remove$ = this.checklistItemService.checklistRemoved$;Now we inject the ChecklistItemService and utilise the same source for the
removal in the ChecklistService. This is better than having two separate
sources in each service, because we would need to make sure we trigger the
sources in both services whenever we want to remove a checklist. This way, we
can just trigger the one shared source.
We also need a source to handle editing in the ChecklistService as well.
edit$ = new Subject<EditChecklist>(); this.remove$.pipe(takeUntilDestroyed()).subscribe((id) => this.state.update((state) => ({ ...state, checklists: state.checklists.filter((checklist) => checklist.id !== id), })) );
this.edit$.pipe(takeUntilDestroyed()).subscribe((update) => this.state.update((state) => ({ ...state, checklists: state.checklists.map((checklist) => checklist.id === update.id ? { ...checklist, title: update.data.title } : checklist ), })) );Now we can handle editing and removing in our ChecklistService too!
User Interface for Deleting and Editing Checklists
Our services support editing and deleting now, but we have no way to do that in our user interface yet.
Let’s start by adding a way to edit and delete a particular checklist. We are
going to start by adding buttons in our ChecklistListComponent that will
trigger delete and edit events.
delete = output<RemoveChecklist>(); edit = output<Checklist>(); <li> <a routerLink="/checklist/{{ checklist.id }}"> {{ checklist.title }} </a> <div> <button (click)="edit.emit(checklist)">Edit</button> <button (click)="delete.emit(checklist.id)">Delete</button> </div> </li>We have added both the edit and delete here, but for now we will focus on
just handling the delete event in the HomeComponent.
<app-checklist-list [checklists]="checklistService.checklists()" (delete)="checklistService.remove$.next($event)" (edit)="checklistBeingEdited.set($event)" />Pretty nice right? All we need to do now is next the remove$ source, or in
the case of editing we are setting the checklistBeingEdited to whatever
checklist we want to edit.
There is still one small problem we need to handle. When we set the
checklistBeingEdited it will open our form modal which is great… but the
form does not contain the information for that particular checklist.
To deal with this, we are going to need to update our effect.
constructor() { effect(() => { const checklist = this.checklistBeingEdited();
if (!checklist) { this.checklistForm.reset(); } else { this.checklistForm.patchValue({ title: checklist.title, }); } }); }Now as well as resetting the form, if there is a checklist set in the signal
we will call the patchValue method on our form. This will take whatever values
we supply to it and update the form with those values.
We are almost there now. However, if you try saving the checklist when
attempting to edit it, you will notice that it actually creates a new
checklist instead of editing this existing one. Our save binding is to blame
for this:
<app-form-modal [title]=" checklistBeingEdited()?.title ? checklistBeingEdited()!.title! : 'Add Checklist' " [formGroup]="checklistForm" (close)="checklistBeingEdited.set(null)" (save)="checklistService.add$.next(checklistForm.getRawValue())" />When we are saving a checklist we always next the add$ source, which is not
what we want if we are trying to edit.
(save)=" checklistBeingEdited()?.id ? checklistService.edit$.next({ id: checklistBeingEdited()!.id!, data: checklistForm.getRawValue() }) : checklistService.add$.next(checklistForm.getRawValue()) "Now we change what source we call based on whether an existing checklist has been
set into the checklistBeingEdited signal or not.
Now editing our checklists works
User Interface for Editing and Deleting Checklist Items
Once again, we are now basically just going to duplicate all of what we just did
for the ChecklistComponent in the ChecklistItemListComponent. See if you
can set all of this up yourself:
- Add the necessary outputs to
ChecklistItemListComponentand add buttons to trigger them - React to the
editanddeleteevents in theChecklistComponent - Update the
effectto patch the form - Update the
savebinding to call the correct source
Click here to reveal solution
Solution
delete = output<RemoveChecklistItem>(); edit = output<ChecklistItem>(); <div> <button (click)="toggle.emit(item.id)">Toggle</button> <button (click)="edit.emit(item)">Edit</button> <button (click)="delete.emit(item.id)">Delete</button> </div> <app-checklist-item-list [checklistItems]="items()" (delete)="checklistItemService.remove$.next($event)" (edit)="checklistItemBeingEdited.set($event)" (toggle)="checklistItemService.toggle$.next($event)" /> constructor() { effect(() => { const checklistItem = this.checklistItemBeingEdited();
if (!checklistItem) { this.checklistItemForm.reset(); } else { this.checklistItemForm.patchValue({ title: checklistItem.title, }); } }); } <app-form-modal title="Create item" [formGroup]="checklistItemForm" (save)=" checklistItemBeingEdited()?.id ? checklistItemService.edit$.next({ id: checklistItemBeingEdited()!.id!, data: checklistItemForm.getRawValue(), }) : checklistItemService.add$.next({ item: checklistItemForm.getRawValue(), checklistId: checklist()?.id!, }) " (close)="checklistItemBeingEdited.set(null)" ></app-form-modal>…and that’s it! We can now edit and delete both checklists and checklist items.
One thing I think is really cool about what we’ve set up here is that as we are deleting and editing all of these items, we never have to worry about triggering a save to storage because of the effect we set up to automatically save whenever the state of the checklists or checklist items change.