Creating the Contact Edit Page | Full-Stack Google Contacts Clone with Adonis.js/Node.js and Quasar (Vue.js)

Creating the Contact Edit Page | Full-Stack Google Contacts Clone with Adonis.js/Node.js and Quasar (Vue.js)

In this lesson, we will design the contact edit page for editing the details/properties of each contact in our Google Contacts Clone App. We will begin by adding a new edit_contact route to our application and then adding an click event listener to the edit button on each row of our Contacts table so that when users click on the Edit button, they will be taken to the Edit page for that contact. We will also add a router navigation (to) prop to the Edit button on our contact view page, so that users can quickly edit a contact while viewing it.

Let's start by creating a new branch for our repo:

# Make sure you are within your project
git checkout -b 09-creating-the-contact-edit-page

The video below shows what we will achieve in this lesson:

Setup

Let's make some few changes to other files before we create the Contact Edit page

1. Add a Pointer Cursor to the Toolbar Title of the Header

Open ui/src/layouts/MainLayout.vue. At Line 33, add the following:

            <span
              :class="[
                $q.screen.gt.xs && 'q-ml-sm',
                $q.screen.lt.xs && 'hidden',
              ]"
+              style="cursor: pointer"
              >Contacts
           </span>

Refer to this snapshot.

At Line 228, we add a back button to the default title toolbar above our pages:

        <q-btn
          color="primary"
          flat
          round
          icon="arrow_back"
          @click.prevent="$router.go(-1)"
        />

Refer to these lines in our snapshot.

2. Improve our Types

Open ui/src/types/index.ts. And make the following changes:

export interface FormItem {
  label: string;
  required: boolean;
-  value: string;
+  value: string | number | null | undefined;
  inputType?: "text" | "number" | "date" | "email" | "url" | "textarea";
  icon?: string;
  autocomplete?:
...
- export interface Contact {
+ export interface Contact
+   extends Record<string, string | null | undefined | number> {
    id: string;
    firstName: string;

Refer to here and here in the snapshot.

3. Add the edit_contact route

Open ui/src/router/routes.ts; add the following below the route object for view_contact route.

{
        path: "contacts/:contactId/edit",
        name: "edit_contact",
        component: () => import("pages/contacts/EditContact.vue"),
        meta: { title: "Edit Contact", showDefaultTitle: true },
        props: true,
}

Refer to this lines in the snapshot for the file. Here, we've added a new route with name: edit_contact. The edit_contact route has a single route parameter named: contactId. We set showDefaultTitle as true within the meta object so that the top title toolbar above the page is rendered. We also set props to true so that the route parameters will be injected to the route component: pages/contacts/EditContact.vue.

If you are following me step-by-step, you might have an error because the route component (pages/contacts/EditContact.vue) has not be created yet.

4. Make the Edit button on our Contacts table rows functional

Open ui/src/pages/Index.vue. Make the following changes:

  1. At Line 57, change: @click.stop.prevent to @click.prevent
  2. At Line 105, change <q-btn flat round color="primary" icon="edit" /> to:

                 <q-btn
                   flat
                   round
                   color="primary"
                   icon="edit"
                   @click.stop.prevent="
                     $router.push({
                       name: 'edit_contact',
                       params: { contactId: props.row.id },
                     })
                   "
                 />
    

This adds a click event listener which navigates to the newly-added edit_contact route with props.row.id as the value of the contactId route parameter. The event modifier click.stop ensures that the click event does not propagate to the q-tr component where we have an event listener which pushes to the Contact details page. So the click event on this q-btn component will be handled at this level and stopped. click.prevent modifier prevents the default behaviours of buttons which reloads the entire page when clicked.

5. Make the Edit button on our Contact Details Page functional

Open ui/src/pages/contacts/ViewContact.vue and add the following:

  1. At Line 73, change <q-btn color="primary">Edit</q-btn> to:

             <q-btn
               :to="{
                 name: 'edit_contact',
                 params: { contactId: $route.params.contactId },
               }"
               color="primary"
               >Edit</q-btn
             >
    

    Here, we have made the Edit button on the top-right side (desktop screen) of our Contact Details page functional.

  2. At Line 171, change <q-btn round flat icon="outbound"></q-btn> to:

                       <q-btn
                           :to="{
                             name: 'edit_contact',
                             params: { contactId: $route.params.contactId },
                           }"
                           round
                           flat
                           icon="outbound"
                         >
                      </q-btn>
    

    Here, we make the Edit button in our bottom right sticky (in mobile responsive screens) functional.

Creating the Contact Edit Page

From my experience, my recommended approach to working with creation and edit forms is to either derive the creation form from the edit form (if the edit form was designed first) or derive the edit form from the creation form (if the creation form was designed first).

It is total waste of time to design to separate forms for creation and edit of entities because they will have very similar JavaScript logic and HTML markup. So, why repeat yourself completely. Don't forget the DRY (Don't Repeat Yourself) principle in software development.

As a result, we will derive the Contact Edit page/form from the Contact Creation page/form.

Create the Contact Edit component

# With VS code
code ui/src/pages/contacts/EditContact.vue # Opens the `EditContact.vue` file
### CTRL+S to save for the first time.

# With other code editors
touch ui/src/pages/contacts/EditContact.vue # Creates the `EditContact.vue` file
# Open by searching with CTRL+P or opening from the file browser

Copy all the contents of this snapshot into the created EditContact.vue file. The content is also shown below:


<template>
  <CreateContact :contact-id="contactId" edit-mode />
</template>

<script lang="ts">
import { defineComponent, PropType } from "vue";
import CreateContact from "./CreateContact.vue";
export default defineComponent({
  name: "EditContact",
  components: { CreateContact },
  props: {
    contactId: {
      type: String as PropType<string>,
      required: true,
      default: "",
    },
  },
  setup() {
    //
  },
});
</script>

The component has one prop, the contactId prop. Remember that this prop is injected from Vue Router because we set props to true in our route definition object.

To derive the Contact Edit page from the Contact Creation page, we do the following:

  1. In the script section and at Line 7, we import the CreateContact.vue file which is our Contact Creation page/form into our EditContact.vue file.

  2. At Line 11, we add it to our components objects so that it is available for use in our EditContact.vue component.

  3. At Line 2 (within the template section), we add the CreateContact component into our markup, thereby consuming and rendering it.

The CreateContact component needs to props: contactId and editMode. We set the contactId prop for CreateContact to the value of contactId prop (coming from Vue Router. The editMode prop is set to true with the attribute edit-mode on the CreateContact component.

Now, we will make the rest of the changes in the ui/src/pages/contacts/CreateContact.vue file.

Open ui/src/pages/contacts/CreateContact.vue. Refer to this snapshot file for the updates.

  1. Add PropType, watchEffect, and onBeforeUnmount to the imports from "vue" at Line 56.

    import {
    defineComponent,
    reactive,
    computed,
    PropType,
    watchEffect,
    onBeforeUnmount,
    } from "vue";
    
  2. Add Contact to the imports from "../../types" at Line 67.

    import { FormInterface, Contact } from "../../types";
    
  3. Import contacts from "../../data/Google_Contacts_Clone_Mock_Data" at Line 69.

    import { contacts } from "../../data/Google_Contacts_Clone_Mock_Data";
    
  4. Update the regex validator at Line 73 to:

    const phoneNumberValidator = helpers.regex(
    /^[+]?[(]{0,1}[0-9]{1,4}[)]?[\(\)-\s\./0-9]*$/
    );
    
  5. Remove the component object:components: {}, at Line 66.

  6. Add the two props from Line 78:

    props: {
     editMode: {
       type: Boolean as PropType<boolean>,
       required: true,
       default: () => false,
     },
     contactId: {
       type: String as PropType<string>,
       required: false,
       default: "",
     },
    },
    
  7. Change setup() { to setup(props) { at Line 90. We need to pass in the props into the setup function.

  8. From Line 270, add the following:

     let contact: Contact = reactive({
       id: "",
       firstName: "",
       surname: "",
       email1: "",
       phoneNumber1: "",
     });
     const stopContactsEffect = watchEffect(
       () => {
         if (!props.contactId || !props.editMode) return;
         const fetchedContact = contacts.filter(
           (cont) => cont.id === props.contactId
         );
         const [fetchedContactObject] = fetchedContact;
         contact = fetchedContactObject;
         Object.keys(contact).forEach((key) => {
           if (key !== "id") {
             form[key].value = contact[key];
           }
         });
       },
       { flush: "pre" }
     );
     const submitPayload = computed(() => {
       const payload = {};
       Object.keys(form).forEach((key) => {
         Object.defineProperty(payload, key, {
           value: form?.[key]?.value,
           writable: false,
         });
       });
       return payload;
     });
    

    Here, we introduce a similar watchEffect hook we used in the previous lesson. See this section. After computing contact within the watchEffect hook, we initialise the form fields with the lines:

         Object.keys(contact).forEach((key) => {
           if (key !== "id") {
             form[key].value = contact[key];
           }
         });
    

    Here, we derived an array from the keys of our contact object using Object.keys(contact). Then, we loop through each key of the array. If the key is not named id, we set the form with the value of the key within contact. This is a clean and efficient way of doing the following:

    form.firstName.value = contact.firstName;
    form.surname.value = contact.surname;
    form.email1.value = contact.email1
    ...
    

    Then from Line 297, we compute a simple submitPayload from our form object. Here, we loop through the keys of the form variable. For each key, we set that key as the property of the payload object using the Object.defineProperty() method. The method takes a propertyDescriptor object as the third parameter. Within the propertyDescriptor, we set the value and declare that the value is not writable going forward. This finalises the value for submission to the server. At the end, we return the payload object.

    const submitPayload = computed(() => {
       const payload = {};
       Object.keys(form).forEach((key) => {
         Object.defineProperty(payload, key, {
           value: form?.[key]?.value,
           writable: false,
         });
       });
       return payload;
     });
    

    This is the same thing as doing:

    const submitPayload = {}
    submitPayload.firstName = form.firstName.value
    submitPayload.surname = form.surname.value
    submitPayload.email1 = form.email1.value
    ...
    

    You will agree that the former is cleaner though the latter is simpler to understand for learners. That's why, I'm taking time to explain both versions.

  9. At Line 321, we add a console statement to submitPayload.value when the form has not errors.

         console.log(submitPayload.value);
    
  10. At Line 324, we modify the message for the $q.notify() function:

        $q.notify({
          message: props.editMode ? "Contact edited" : "Contact created",
          type: "positive",
        });
    
  11. At Line 330, we stop the watchEffect hook with a call to the onBeforeUnmount hook.

    onBeforeUnmount(() => {
      void stopContactsEffect();
    });
    
  12. Lastly and within the template section, we improve the responsiveness of our submit button. At Line 38, we update div to a flex row, and introduce a responsive column to wrap thte q-btn component.

        <div class="q-mt-xl row justify-center">
          <div class="col-12 col-md-6 col-lg-6 col-xl-6">
            <q-btn
              class="full-width"
              label="Submit"
              type="submit"
              color="primary"
              @click.prevent="submitForm"
            />
          </div>
    

Take sometime, to fill and submit the form while check the console of the dev tool. Also inspect the relationship between the CreateContact and EditContact components with the Vue devtool. Also navigate to the Contact Edit page from the Contacts table and also from the Contact Details page.

This concludes our discussions for this lesson.

Save all your files, commit and merge with the master branch.

git add .
git commit -m "feat(ui): complete design of the contact edit page"
git push origin 09-creating-the-contact-edit-page
git checkout master
git merge master 09-creating-the-contact-edit-page
git push origin master

In the next lesson, we will begin discussions on the backend of our app by talking about how backend for software (applications) works. See you there.