New Contact Form Design | Full-Stack Google Contacts Clone with Adonis.js/Node.js and Quasar (Vue.js)

New Contact Form Design | Full-Stack Google Contacts Clone with Adonis.js/Node.js and Quasar (Vue.js)

In this lesson, we will design the contact-creation form of our Google Contacts clone app. We will also learn how to create a route for the contact form and define the component (page) for the route. We will only focus on the UI/UX of the app in this lesson. The field validations and submission mechanisms will be discussed in subsequent lessons.

At the end of the lesson, your app should look like the video below.

Start by creating a new branch of your project.

git checkout -b 03-contact-creation-form-design

Change Overview

Three files will be modified:

  1. ui/src/css/app.scss (snapshot)
  2. ui/src/layouts/MainLayout.vue (snapshot)
  3. ui/src/router/routes.ts (snapshot)

Two files will be created:

  1. ui/src/pages/contacts/CreateContact.vue (snapshot)
  2. ui/src/types/index.ts (snapshot)

Create the TypeScript type definition file

The type definition file exports interfaces which will be used for type-safety throughout our application. For the current purpose, the FormInterface interface defines the form object with string indexes. Each index will be a FormItem with label, required, value, inputType, icon, and autocomplete properties. The inputType, icon, and autocomplete properties of the FormItem interface are optional. You can read more about TypeScript interfaces here.

Follow the steps below. Don't include the comments.

   mkdir -p ui/src/types # Create the `types` folder
   touch ui/src/types/index.ts # Create the `indext.ts` file
   code ui/src/types/index.ts # Open the created file. For VS Code users

Copy and paste the content from this snapshot into the created ui/src/types/index.ts file. This file is imported into CreateContact.vue file and used to define the type for the form reactive variable inside the setup function.

Create CreateContact.vue

  1. Create the file. Follow the steps below. Don't include the comments.

    mkdir -p ui/src/pages/contacts # Create the `contacts` folder
    touch ui/src/pages/contacts/CreateContact.vue # Create the `CreateContact.vue` file
    code ui/src/pages/contacts/CreateContact.vue # Open the created file. For VS Code users
    
  2. Open this snapshot of CreateContact.vue. You can type out the content into the newly-created CreateContact.vue file or copy-and-paste the entire content from the snapshot.

    I will explain the content of the files in details later.

Modify ui/src/router/routes.ts

Open ui/src/router/routes.ts. Update the children property of the "/" route object to:

   children: [
      {
        path: "",
        name: "home",
        component: () => import("pages/Index.vue"),
        meta: { title: "Home" },
      },
      {
        path: "/contacts/new",
        name: "new_contact",
        component: () => import("pages/contacts/CreateContact.vue"),
        meta: { title: "New Contact" },
      },
    ]

You might notice that new properties have been added to the children routes, namely: name and meta property. The name property is a standard property for Vue Router routes. It is used to name each route and will be used for navigating to them. So, instead of navigating to the contact-creation page with $router.push(path: "/contacts/new");, you can use: $router.push(name: "new_contact");. The advantage of using route names for navigation is that the path strings for routes could be updated or the nesting levels could change, but if the name property is the same, you won't have to update the router-navigation calls within your application.

Additionally, there is the meta property. The meta property is a custom property. It could be called anything else. As a matter of fact, you can define extra properties on each route object as you wish and they will be available on $route object for each route you are currently on. In this case, the meta property is used to define the title property which is the title of each page. This title could be used to update the page title later, but for now, it is used to display the page title within a q-toolbar component added within the q-page-container. See these lines in MainLayout.vue. The q-toolbar is not rendered on the home route.

Most importantly, there is a new route object for which defines the component for the contact-creation page.

   children: [
      ...
      {
        path: "/contacts/new",
        name: "new_contact",
        component: () => import("pages/contacts/CreateContact.vue"),
        meta: { title: "New Contact" },
      },
    ]

The component is loaded with an asynchronous import: component: () => import("pages/contacts/CreateContact.vue"). Asynchronous imports ensures that route components are only loaded when you request to navigate into that page/route. It helps speed up the initial load time of your application and is very important for large applications with tens to hundreds of route definitions. This is known as route lazy-loading. Read more here.

Add toolbar for page title in MainLayout.vue

Open ui/src/layouts/MainLayout.vue and add the following from Line 219. Or copy-and-paste these lines.

    <q-page-container class="GPL__page-container">
+      <q-toolbar
+        v-if="$route.name !== 'home'"
+        class="text-primary q-mt-sm sticky-top"
+      >
+        <q-toolbar-title class="text-center">
+          {{ $route.meta.title }}
+        </q-toolbar-title>
+      </q-toolbar>
      <router-view />
    </q-page-container>

The only significant thing in the above code is that the route title is interpolated with the call: $route.meta.title. As mentioned earlier, we ensure that this toolbar doesn't appear on the home route with the v-if directive: v-if="$route.name !== 'home'".

At this stage, you should be able to navigate to the new_contact route. Visit: localhost:8008/#/contacts/new. This should load the contact-creation form.

You might notice that the q-input component for the birthday field isn't styled properly. This is the default behaviour from Quasar for q-input with the date type. Let's improve the styling for our purpose. Open the ui/src/css/app.scss file and paste these lines:

input[type="date"] ~ div.q-field__label {
  margin-top: -0.75rem;
}

Don't forget to save your files.

Now, let's look at what's going on within the CreateContact.vue file. Please refer to this snapshot.

Beginning from the script section. At Line 62, the reactive form object is defined and initialised with an object field definitions. The form object will be used as the schema to autogenerate the form fields instead of duplicating the q-input component. (I gave a lot of thoughts before deciding to autogenerate the fields. As a software engineer, you must always DRY (Don't Repeat Yourself) your codes, i.e. above repetitions where possible. Even though this is a tutorials, the template section will be very difficult to understand and maintain if the q-input component is repeated and all attributes hardcoded in the template. The mere thought of that makes me shudder.)

Continuing, the form object is typed with the FormInterface interface from ui/src/types/index.ts. So errors will be thrown if the types are not compatible. Each field definition should have six properties: label, required, value, inputType, icon, and autocomplete. The last three are optional properties. Most notably is the autocomplete property which should be defined so that each field can have the proper suggestions when you are typing. The autocomplete value will be mapped to the autocomplete attribute of each field. You can read more about the HTML autocomplete attribute. Refer to the official specifications for autocomplete too.

The setup function returns an object with three properties: form, dense, and the submitForm function. The dense property is set to true when the screen is less than the sm breakpoint. This ensures that the form is condensed for very small devices. The form property will be used as the schema for generating the form fields. The submitForm isn't functional yet.

Within the template section, we have a q-page styled as a flex row and centered horizontally with justify-center and a large top-margin. Then there is a div with responsive classes with ensures that the form displays well on different screen sizes. Then a q-form component which wraps all the q-input components which will be generated with the v-for directive. It is important to always use a q-form component or the bare form tag to wrap multiple form fields. The q-form can have a @submit event listener which can improve user experience during form submission.

        <q-input
          v-for="({ label, icon, inputType, autocomplete }, key) in form"
          :key="key"
          :for="`${key}_${inputType || 'text'}_input`"
          bottom-slots
          v-model="form[key].value"
          :label="label"
          :dense="dense"
          :class="!icon && 'q-pl-lg'"
          :type="inputType || 'text'"
          :autogrow="inputType === 'textarea'"
          :autofocus="key === 'firstName'"
          :aria-autocomplete="autocomplete"
          :autocomplete="autocomplete"
        >
          <template v-slot:before>
            <q-icon v-if="icon" :name="icon" />
          </template>

          <template v-slot:after>
            <q-icon
              v-if="form[key].value"
              name="close"
              @click="form[key].value = ''"
              class="cursor-pointer"
            />
          </template>
        </q-input>

The q-input component is a Quasar component for creating the input tag. It is laced with various props and slots to customise its behaviour and improve the user experience. Most importantly, a v-for directive is used to loop over the form object exported from the setup function within the script section. The v-for directive could have looked like this: v-for="(value, key) in form". value is basically each field definition and key is the string index of each object (entry) in the form object. So we can destructure value to expose the properties of the fields within the scope of the q-input component.

Some important props defined on q-input include:

  1. The for prop is used to define the value of the id attribute of the input tag and the for attribute of the label tag for the input tag. In this case, the for prop is derived from the key, inputType scoped variables. The id and for attributes should always match for each field set.
  2. The bottom-slots prop is used to enable the display of the hint and error slots for the q-input component. The error slot will be used in subsequent lessons to display validation errors.
  3. The label prop is used for defining the label for the field.
  4. The dense prop determines if the form fields will be condensed or not. It is useful in small screens to reduce the height of forms.
  5. The type prop sets the type of the input tag.
  6. The autogrow prop is set to true when the inputType variable is strictly-equal to textarea.
  7. The autofocus prop is used to focus the cursor within the firstName field when the form is rendered.
  8. The aria-autocomplete and autocomplete props set the autocomplete and aria-autocomplete attributes necessary for useful browser suggestions and autofills.

Two slots are consumed within the q-input component: before and after slots. They are used to display the icons before each field and the close icon after each for resetting each field.

If you have followed carefully, you should have the New Contact form well-rendered.

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

git add .
git commit -m "feat(ui): complete new contact form design"
git push --set-upstream origin 03-contact-creation-form-design
git checkout master
git merge master 03-contact-creation-form-design
git push

That's all for now. In the next lesson, you will learn how to perform form validations.