Designing the Contact Details Page | Full-Stack Google Contacts Clone with Adonis.js/Node.js and Quasar (Vue.js)
In this lesson, we will design the contact details page for viewing the details/properties of each contact. We will begin by adding a new route to our application and then adding an click
event listener to the rows on our Contacts page so that when users click on any row/contact, the details of that contact will be opened.
Let's start by creating a new branch for our repo:
# Make sure you are within your project
git checkout -b 08-designing-the-contact-detail-page
The video below shows what we will achieve in this lesson:
Setup
Let's make some few changes to other files before we design the Contact Details page.
1. Improve ui/.eslintrc.js
Open ui/.eslintrc.js
and add the line below. Refer to this snapshot.
"@typescript-eslint/explicit-module-boundary-types": "off",
+ "@typescript-eslint/restrict-template-expressions": "off",
// allow debugger during development only
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
2. Improve ui/src/router/routes.ts
and Add New Route
Open ui/src/router/routes.ts
and make the following changes. Refer to this snapshot.
children: [
{
path: "",
name: "home",
component: () => import("pages/Index.vue"),
- meta: { title: "Home" },
+ meta: { title: "Home", showDefaultTitle: false },
},
{
path: "contacts/new",
name: "new_contact",
component: () => import("pages/contacts/CreateContact.vue"),
- meta: { title: "New Contact" },
+ meta: { title: "New Contact", showDefaultTitle: true },
+ },
+ {
+ path: "contacts/:contactId/details",
+ name: "view_contact",
+ component: () => import("pages/contacts/ViewContact.vue"),
+ meta: { title: "View Contact", showDefaultTitle: false },
+ props: true,
},
],
Here, we are perform two tasks:
For the existing routes:
home
andnew_contact
, we add an extra properties to themeta
property:showDefaultTitle
. This property will be used to determine if thetitle toolbar
above that page will be displayed or not. So, we setshowDefaultTitle
tofalse
andtrue
for thehome
andnew_contact
routes, respectively.We add a new route definition for our Contact Details page. The route is named
view_contact
and points at"pages/contacts/ViewContact.vue"
as the route component.showDefaultTitle
is set tofalse
. Most importantly, we introduce a new route property calledprops
. Theprops
property is a standardVue Router
route property used to make allroute parameters
defined in the routepath
to be available in the routecomponent
as props. In this case, we have the path:"/contacts/:contactId/details"
. This path contains one route parameter:contactId
. A route parameter is introduced into a route by appending it with a colon (:
).Vue Router
is match the path and extract the route paramaters (if more than one) from the path. Whenprops
is set totrue
, these route parameters will be made available within your route component (in this case,pages/contacts/ViewContact.vue
) as part of the component props. You do not have to define these props set fromVue Router
within the component. But defining them is a good practice for readability and type-checking.Let's dive into route paths
So, for a URL like this:
http://localhost:8008/#/contacts/309b20ac-bbcd-4268-b01f-43770527540d/details
. Our actual route path is:/contacts/309b20ac-bbcd-4268-b01f-43770527540d/details
. Within the route path for theview_contact
route, we didn't add the leading (beginning) forward slash (/
) because it was already defined as the path of thelayout
(layouts/MainLayout.vue
) route. Since ourview_contact
route is a child of thelayout
route,Vue Router
will append the leading/
. Now,Vue Router
will match the actual path and the route path. For example:contacts/309b20ac-bbcd-4268-b01f-43770527540d/details
will be matched withcontacts/:contactId/details
. The route parametercontactId
will be set to309b20ac-bbcd-4268-b01f-43770527540d
. Withinpages/contacts/ViewContact.vue
, the propcontactId
will be programmatically assigned the value309b20ac-bbcd-4268-b01f-43770527540d
. We will make use of this prop and its value when we want to fetch the properties of the current contact being value withinpages/contacts/ViewContact.vue
. This will be discussed soon.
3. Improve ui/src/layouts/MainLayout.vue
Open ui/src/layouts/MainLayout.vue
and change Line 224 as below. Here, we are making use of the meta.showDefaultTitle
property of our route
definition as the condition for displaying the default title toolbar
above our pages. In this case, the title toolbar
won't be displayed since the home
route has the property: meta.showDefaultTitle
equals false
. Refer to this snapshot.
- v-if="$route.name !== 'home'"
+ v-if="$route.meta.showDefaultTitle"
4. Add the click
Event Listener to Contacts
rows
Open ui/src/pages/Index.vue
and add the following lines from Line 57. Refer to this snapshot. Here we are adding the click
event listener to each table row (q-tr
). We assign the Vue Router
's push
method as the event handler so that we can navigate to the view_contact
route (i.e. the Contact Details page) for that specific row (props.row.id
).
<template #body="props">
<q-tr
:props="props"
@mouseover="handleMouseEvents"
@mouseleave="handleMouseEvents"
+ @click.stop.prevent="
+ $router.push({
+ name: 'view_contact',
+ params: { contactId: props.row.id },
+ })
+ "
>
You will also notice that we assigned an object: { contactId: props.row.id }
as the value of the params
property of the $router.push
's payload. The params
object is used to specify all the parameters (and their values) of the route we want to navigate to. For our view_contact
route, we have one parameter (contactId
), so we assign props.row.id
as the value of the contactId
parameter. If the route we are navigating to does not have any route parameter, there is no need assigning the params
property at all.
Contact Details Page Design
Refer to this snapshot as the content of our Contact details page.
Create and open the file: ui/src/pages/contacts/ViewContact.vue
# Ensure that you are in the root directory of your application
code ui/src/pages/contacts/ViewContact.vue
# Opens the `ViewContact.vue` file with VS code
# CTRL+S to save for the first time
Copy the entire content of the snapshot into ui/src/pages/contacts/ViewContact.vue
.
Now, let's discuss what's going on within the file. Beginning from the script
section.
The script
section
From Line 202, we define our props and assign contactId
as our only prop for this component. This prop will be automatically injected by Vue router
as discussed in this section. However, we still define it for the purpose of readability and type-checking by TypeScript. If we do not explicitly define it, TypeScript will be unable of the props.contactId
object within our setup
function. We use the imported PropType
type from vue
to properly cast the type
of the prop to TypeScript's string
type. Read more about type annotations for props.
props: {
contactId: {
type: String as PropType<string>,
required: true,
default: "",
},
},
At Line 209, we declare and initialise our contact
variable. The contact
variable will hold the contact to be displayed on the page. We assign properties to the object because of TypeScript type-checking.
let contact: Contact | null = reactive({
id: "",
firstName: "",
surname: "",
email1: "",
phoneNumber1: "",
});
At Line 217, we call the watchEffect
hook (imported from vue
). The watchEffect
hook is used to execute statements when the setup
function is called during the rendering of the application. Read more about Vue's watchEffect
here. The watchEffect
hook takes a handler function as the first parameter. Within the handler, we filter the contacts
array imported from /ui/src/data/Google_Contacts_Clone_Mock_Data
to obtain the current contact being viewed. We make use of the prop (contactId
) which is automatically set by Vue Router
as discussed earlier in this section. We return the result of the filtering operation into fetchedContact
. fetchedContact
is an array. On Line 221, we destructure fetchedContactObject
from the fetchedContact
array. fetchedContactObject
contains the object for our contact. We assign that object to our contact
variable.
const stopContactsEffect = watchEffect(() => {
const fetchedContact = contacts.filter(
(cont) => cont.id === props.contactId
);
const [fetchedContactObject] = fetchedContact;
contact = fetchedContactObject;
});
The watchEffect
hook returns a function which is store in the stopContactsEffect
constant. On Line 319, we call that function to stop the watchEffect
hook before the component unmounts.
onBeforeUnmount(() => {
void stopContactsEffect();
});
Though Vue will automatically stop the
watchEffect
for you, I demonstrated it here because it best practice to explicitly stop yourwatchEffect
s to prevent memory leaks in your application.
At Line 225, we compute our contact's full name from the firstName
and lastName
properties.
const fullName = computed(
() => `${contact?.firstName} ${contact?.surname}`
);
At Line 229, we compute the job description of the contact from the jobTitle
and company
properties. We use of the String.trim()
method ensures that any trailing or leading spaces are removed.
const jobDescription = computed(() =>
`${contact?.jobTitle ?? ""}${contact?.jobTitle ? " at " : ""}${
contact?.company ?? ""
}`.trim()
);
At Line 235, we compute the contactData
by returning an array containing new properties which will be used for rendering the contact on the page. The array is should contain the following properties:
{
icon: string;
text: string | undefined | null | Array<string | null | undefined>;
label: string;
key: string;
side?: string | undefined;
sideColor?: string | undefined;
clampLines?: number | "none";
linkAs?: "email" | "tel" | "website";
}
It is important to note that the text
property can be a string
or an array of string
s. The array of string
s is used when specifying the object for address
. Within the template
section, you will see how we check for this array and modify how the labels are rendered with an array is detected.
At Line 323, we return contact
, fullName
, contactData
, jobDescription
, and isNullArray
to the template section.
return { contact, fullName, contactData, jobDescription, isNullArray };
The template
section
The Contact view page is designed to be responsive. Save the ui/src/pages/contacts/ViewContact.vue
file. Take some time to study the template
section.
Ensure that your dev server is running:
# Ensure that you are in root directory
cd ui
yarn serve
# Allow frontend to be compiled and served
When the UI opens in your broswer, click any row on the table to open the Contact details page showing the details of the contact.
Press F12
on your keyboard the dev tools. Switch to the responsive mode and resize the window. You will see how the page adapts to different screen sizes.
Let's continue.
At Line 6, we introduce a q-toolbar
at the top part of our page. The toolbar contains a back
button on the left side and two other buttons on the right side. At Line 12, we add a click
event listener to the back
button so that we can go back to the previous view easily. The event listener called $router.go(-1)
which simulates going back one
step in our history. Read more that $router.go()
here.
At Line 41, we interpolate the fullName
of the contact. At Line 50, we interpolate the jobDescription
of the contact. At Line 54, we use v-for
to loop over the number 3
and display three buttons which mimics the groups of the contact. These groups will be implemented when we have a backend server for the application.
At Line 71, we have toolbar which contains three buttons for starring, showing more options, and editing a contact. These buttons will be implemented when we have the backend server for the app. The edit
button will be implemented soon.
At Line 78, we introduce another row which wraps our Contact Details
list. The list
starts at Line 80 and makes use of Quasar's q-list
component. We use a v-for
directive to loop over our contactData
array. At Line 87, we check that item.text
exist or item.text
is a string
or item.text
is an array. The v-ripple
directive on q-item
give each list item a click
event capability and ripple effect when clicked.
At Line 98, we render the icon for the list item with :name="item.icon"
At Line 104, we render the block if item.text
is an array:
<q-item-section v-if="Array.isArray(item.text)">
<q-item-label class="text-caption" caption>{{
item.label
}}</q-item-label>
<q-item-label
v-for="(line, index) in item.text.filter((l) => l)"
:key="'item_line_' + index"
:lines="
line?.clampLines && line?.clampLines !== 'none'
? line.clampLines
: line?.clampLines && line?.clampLines === 'none'
? undefined
: 1
"
>{{ line }}</q-item-label
>
</q-item-section>
The caption
for the item is renders with the first q-item-label
component by interpolating item.label
. Then we loop through item.text
to render the second q-item-label
. Before looping, we filter out any falsy value from the item.text
array within the v-for
directive: item.text.filter((l) => l)
. The lines
prop on the second q-item-label
is used to display ellipsis if there is no sufficient space to display all the text for the label. Read more that it here. If line.clampLines
is set and it is not equal to none
, we make use of that value. If line.clampLines
is set and the value is none
, we return undefined
which mimics the default behaviour when the lines
prop is not set at all. Else, we use 1
as the value of the lines
prop.
At Line 121, we check if the item.text
is a string and render the block if true
. This one is simpler but similar to the rendering when item.text
is an array as described above.
At Line 137, we render the side q-item-section
which shows extra information for our items. It is used to display a badge (q-badge
) when item.side
and item.sideColor
is set. It is also used to display links when item.linkAs
is set.
Lastly, there is a sticky button containing an edit
button which displays in responsive mode when the window is not greater than the sm
breakpoint. See Line 175.
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 details page"
git push origin 08-designing-the-contact-detail-page
git checkout master
git merge master 08-designing-the-contact-detail-page
git push origin master
In the next lesson, we will create the Contact edit page/form.