3d cartoon hands holding a phone

Unlock full course by purchasing a membership

Lesson 9

Creating Checklist Items

Adding items to a particular checklist

STANDARD

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. We will generally follow a process like this:

  • Add a method to an existing service or create a new service to handle the data
  • Add the user interface required to invoke that new method
  • Display the result in the UI

In this case, we are going to start by creating a new service for managing checklist items.

Creating the Checklist Item service

First, we will need to define an interface for our checklist items.

Create a file at src/app/shared/interfaces/checklist-item.ts:

export interface ChecklistItem {
  id: string;
  checklistId: string;
  title: string;
  checked: boolean;
}

export type AddChecklistItem = Pick<ChecklistItem, 'title'>;

This is the same basic idea as the Checklist except we also have a checklistId property so that we know what checklist a particular item belongs to, and a checked field to indicate whether the item has been completed or not.

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.

Create a new service at src/app/checklist/data-access and add the following:

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { map } from 'rxjs/operators';
import {
  AddChecklistItem,
  ChecklistItem,
} from '../../shared/interfaces/checklist-item';

@Injectable({
  providedIn: 'root',
})
export class ChecklistItemService {
  private checklistItems$ = new BehaviorSubject<ChecklistItem[]>([]);

  getItemsByChecklistId(checklistId: string) {
    return this.checklistItems$.pipe(
      map((items) => items.filter((item) => item.checklistId === checklistId))
    );
  }

  add(item: AddChecklistItem, checklistId: string) {
    const newItem = {
      id: Date.now().toString(),
      checklistId,
      checked: false,
      ...item,
    };

    this.checklistItems$.next([...this.checklistItems$.value, newItem]);
  }
}

Again, nothing really new here - we are using the same technique with a BehaviorSubject and emitting new items on that stream. We don’t need to worry about a “nice” id for our checklist items because we will never see it, so we just use Date this time.

Our getItemsByChecklistId is a bit different. For the checklists we just did this:

getChecklists() {
    return this.checklists$.asObservable();
}

But our checklistItems$ stream contains all of the items for all of the checklists. We only want the items for one specific checklist that matches the checklistId passed in, so we map our stream. There is also no need to use asObservable() here because when you add the pipe method onto a BehaviorSubject it converts it into a standard observable anyway.

Using our generic form component to add items

Now we have a method in a service to add checklist items, we need to support supplying data to that in our interface. This is where creating that generic form modal component is going to pay off, as we are able to utilise that again here.

Add the following to the ChecklistComponent class:

  formModalIsOpen$ = new BehaviorSubject<boolean>(false);

  checklistItemForm = this.fb.nonNullable.group({
    title: ['', Validators.required],
  });

NOTE: You will need to inject the FormBuilder as fb in the constructor and import Validators and BehaviorSubject at the top of the file

Now, before we continue on to adding the modal, we are going to address a little issue. Currently, our template looks like this:

    <ion-header>
      <ion-toolbar>
        <ion-buttons slot="start">
          <ion-back-button defaultHref="/"></ion-back-button>
        </ion-buttons>
        <ion-title *ngIf="checklist$ | async as checklist">
          {{ checklist.title }}
        </ion-title>
      </ion-toolbar>
    </ion-header>

    <ion-content> </ion-content>

We are subscribing the the checklist$ stream to get the title in the ion-title component. This is fine, but when we add the modal we are also going to need to use the async pipe to subscribe to the stream there again. We don’t want to have to keep subscribing to streams using the async pipe over and over again, so what we are going to do is restructure this a little.

Modify the template to reflect the following:

    <ng-container *ngIf="checklist$ | async as checklist">
      <ion-header>
        <ion-toolbar>
          <ion-buttons slot="start">
            <ion-back-button defaultHref="/"></ion-back-button>
          </ion-buttons>
          <ion-title>
            {{ checklist.title }}
          </ion-title>
        </ion-toolbar>
      </ion-header>

      <ion-content> </ion-content>
    </ng-container>

We have added an ng-container to wrap our whole template, and we use the async pipe there. This will make checklist available to the entire template. This is convenient, but one thing to keep in mind with this method is that if the stream does not emit immediately, then our entire template will not show until the stream emits a value. This is fine in this case because checklist$ does emit a value immediately, but it is something to keep in mind.

This still isn’t quite suitable. Because now we also have a formModalIsOpen$ stream that we need to subscribe to as well. Let’s use this same technique to incorporate that as well:

Modify the template to reflect the following:

    <ng-container
      *ngIf="{
        checklist: checklist$ | async,
        formModalIsOpen: formModalIsOpen$ | async
      } as vm"
    >
      <ion-header>
        <ion-toolbar>
          <ion-buttons slot="start">
            <ion-back-button defaultHref="/"></ion-back-button>
          </ion-buttons>
          <ion-title>
            {{ vm.checklist?.title }}
          </ion-title>
        </ion-toolbar>
      </ion-header>

      <ion-content> </ion-content>
    </ng-container>

Now we are creating an object in the ng-container that subscribes to both streams, and makes that available to the template as vm (you can see how we are using this in ion-title). Now the values from both the streams are available everywhere in the template. This also means that our ng-container will always instantly render now - it is being assigned an object, and an object is always truthy, it doesn’t matter if the streams we are setting up within that object have emitted yet or not. The downside is that now our checklist and formModalIsOpen values are now potentially null again according to TypeScript (even though they are not), and so to access title we need to use the safe navigation operator ? to check that checklist is not null.

Instead of doing this, we are going to use the non-null assertion operator instead.

Modify the template to reflect the following:

    <ng-container
      *ngIf="{
        checklist: (checklist$ | async)!,
        formModalIsOpen: (formModalIsOpen$ | async)!
      } as vm"
    >
      <ion-header>
        <ion-toolbar>
          <ion-buttons slot="start">
            <ion-back-button defaultHref="/"></ion-back-button>
          </ion-buttons>
          <ion-title>
            {{ vm.checklist.title }}
          </ion-title>
        </ion-toolbar>
      </ion-header>

      <ion-content> </ion-content>
    </ng-container>
STANDARD
Key

Thanks for checking out the preview of this lesson!

You do not have the appropriate membership to view the full lesson. If you would like full access to this module you can view membership options (or log in if you are already have an appropriate membership).