Lesson 9

[Sprint Two] - Adding Clients

Feature to add clients

PRO

Lesson Outline

Feature: See a list of all clients

Let's move on to our next user story:

User story: As a massage therapist, I want to be able to record a client's details so that I don't need to ask them for the same information on their next visit


Project management

Remember to move the card for this user story to the Test column, and create a new task branch to work on.


As always, our first step is going to be to create an E2E test for this user story. We already have an interface for a clients details, so we know what details we need to create a new client:

interface ClientName {
  first: string;
  last: string;
}

interface SurveyResponse {
  values: string;
}

export interface Client {
  name: ClientName;
  email: string;
  phone: string;
  appointments: string[];
  notes: string;
  survey: SurveyResponse[];
}

Let's encapsulate that in a new E2E test.

Add the following test to clients.cy.ts:

it('can add a new client', () => {
  // Add a new client
  getAddClientButton().click();
  getFirstNameField().type('Josh');
  getLastNameField().type('Morony');
  getEmailField().type('[email protected]');
  getPhoneField().type('555555555');
  getNotesField().type('this is a note');
  getSaveButton().click();

  // Expect that the client is now in the clients list
  const listOfClients = getItemsInList();
  listOfClients.should('contain.text', 'Josh Morony');
});

We will also need to make sure to add those utility methods to support/utils.ts and import them into our test (if you are using VS Code you can just put your cursor over the missing imports, bring up the Quick Fix options, and choose the Add all missing imports option):

// Client form
export const getAddClientButton = () => cy.get('[data-test="add-client-button"]');
export const getFirstNameField = () => cy.get('[data-test="first-name-input"] input');
export const getLastNameField = () => cy.get('[data-test="last-name-input"] input');
export const getEmailField = () => cy.get('[data-test="email-input"] input');
export const getPhoneField = () => cy.get('[data-test="phone-input"] input');
export const getNotesField = () => cy.get('[data-test="notes-input"] textarea');
export const getSaveButton = () => cy.get('[data-test="save-client-button"]');

NOTE: We need to make sure we access the underlying input and textarea elements, not the <ion-input> and <ion-textarea> wrapper components otherwise not all of the Cypress methods will work.

For now we will just keep all of the utility methods in one file, but as this grows you might decide to split this out into multiple files.

Let's see what happens when we run this test:

Timed out retrying after 4000ms: Expected to find element: [data-test="add-client-button"], but never found it.

We get an error because it can't find the button for adding a new client. Now we could address each error we run into one at a time, i.e. we could just add an add button and nothing else first, but this is going to slow us down. We know that after our test clicks the add button it's going to try to get the firstName field and it's going to fail to find that. Then it's going to fail to find the lastName field and so on. We're going to plan ahead a bit, and think about how we are going to approach this.

Deciding on the Architecture

Our client-add page does not exist yet so we will need to add that. This will be a smart/routed component. We also have a form we want to display within this page. Last time we just put everything into our routed component and later we refactored it. This time, we are going to create smart/dumb components from the beginning. Our strategy here is going to be:

  1. Add the button to trigger the client-add page
  2. Create the client-add page
  3. Create a client-editor component for the client details form
  4. Use the component in the client-add page
  5. Redirect back to the client-dashboard page when the form is submitted

We aren't going to implement all of the functionality for the form right away. At the moment, we are just interested in progressing our E2E test, so we mostly just want to be able to visit the page, enter in the details, and then get back to the original page - it doesn't actually have to do anything with the data (yet). We are also planning ahead here a bit by making the dumb component for the form a generic "editor" as opposed to being specifically for adding clients - we could easily just pass in an existing client to this form to have it function as a way to edit a client.

Add the add button to client-dashboard.page.html:

<ion-header>
  <ion-toolbar>
    <ion-title data-test="page-title">Clients</ion-title>
    <ion-buttons slot="end">
      <ion-button
        data-test="add-client-button"
        routerLink="add"
        routerDirection="forward"
      >
        <ion-icon slot="icon-only" name="add"></ion-icon>
      </ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

Create the client-add page:

ionic g page clients/feature/ClientAdd --module clients/feature/client-shell/client-shell-routing.module --route-path add

NOTE: We are making full use of the generator here so that it automatically adds this to the client-shell-routing instead of the default app-routing, and we also want to use a path of add instead of the default client-add

Create the client-editor component:

ionic g component clients/ui/ClientEditor --create-module --export

Again, we are supplying some extra options here to do some config work for us. The --create-module flag will automatically create a module for this component, and the --export flag will export the component from that module (effectively creating a SCAM component for us).

Implementing the Client Editor

Now let's do some work - we are going to implement the basics of our client-editor before moving on. All we want for is for the form fields to be available in the component, we aren't going to have them do anything just yet.

This brings us to a similar spot as before when we created out client-list component - do we write a test for this? Again, the fields we will be adding are currently covered by our E2E test, but what if we want to use this component somewhere else? We might eventually write unit tests for this component when we are implementing important behaviour, but we aren't going to add unit tests just for the presence of particular form elements. In my opinion, this is better suited to the E2E test (even if we do reuse this somewhere else).

Modify client-editor.component.html to reflect the following:

<form>
  <ion-item>
    <ion-label position="floating">First Name</ion-label>
    <ion-input
      data-test="first-name-input"
      type="text"
    ></ion-input>
  </ion-item>
  <ion-item>
    <ion-label position="floating">Last Name</ion-label>
    <ion-input
      data-test="last-name-input"
      type="text"
    ></ion-input>
  </ion-item>
  <ion-item>
    <ion-label position="floating">Email</ion-label>
    <ion-input data-test="email-input" type="email"></ion-input>
  </ion-item>
  <ion-item>
    <ion-label position="floating">Phone</ion-label>
    <ion-input data-test="phone-input" type="tel"></ion-input>
  </ion-item>
  <ion-item>
    <ion-label position="floating">Notes</ion-label>
    <ion-textarea data-test="notes-input"></ion-textarea>
  </ion-item>
  <ion-button data-test="save-client-button" type="submit">
    Save
  </ion-button>
</form>

Use the component in the client-add page:

PRO

Thanks for checking out the preview of this lesson!

The full version of this lesson is only available to pro members. If you would like full access to this module and all of the other pro modules on Elite Ionic you can become a pro member (or log in if you are already a member).