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:
ui/src/css/app.scss
(snapshot)ui/src/layouts/MainLayout.vue
(snapshot)ui/src/router/routes.ts
(snapshot)
Two files will be created:
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
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
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:
- The
for
prop is used to define the value of theid
attribute of theinput
tag and thefor
attribute of thelabel
tag for theinput
tag. In this case, thefor
prop is derived from thekey
,inputType
scoped variables. Theid
andfor
attributes should always match for each field set. - The
bottom-slots
prop is used to enable the display of thehint
anderror
slots for theq-input
component. Theerror
slot will be used in subsequent lessons to display validation errors. - The
label
prop is used for defining the label for the field. - 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. - The
type
prop sets the type of theinput
tag. - The
autogrow
prop is set to true when theinputType
variable is strictly-equal totextarea
. - The
autofocus
prop is used to focus the cursor within thefirstName
field when the form is rendered. - The
aria-autocomplete
andautocomplete
props set theautocomplete
andaria-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.