Creating the Contact Edit Page | Full-Stack Google Contacts Clone with Adonis.js/Node.js and Quasar (Vue.js)
Table of contents
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:
- At Line 57, change:
@click.stop.prevent
to@click.prevent
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:
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.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:
In the
script
section and at Line 7, we import theCreateContact.vue
file which is our Contact Creation page/form into ourEditContact.vue
file.At Line 11, we add it to our
components
objects so that it is available for use in ourEditContact.vue
component.At Line 2 (within the
template
section), we add theCreateContact
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.
Add
PropType
,watchEffect
, andonBeforeUnmount
to the imports from"vue"
at Line 56.import { defineComponent, reactive, computed, PropType, watchEffect, onBeforeUnmount, } from "vue";
Add
Contact
to the imports from"../../types"
at Line 67.import { FormInterface, Contact } from "../../types";
Import
contacts
from"../../data/Google_Contacts_Clone_Mock_Data"
at Line 69.import { contacts } from "../../data/Google_Contacts_Clone_Mock_Data";
Update the
regex
validator at Line 73 to:const phoneNumberValidator = helpers.regex( /^[+]?[(]{0,1}[0-9]{1,4}[)]?[\(\)-\s\./0-9]*$/ );
Remove the component object:
components: {},
at Line 66.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: "", }, },
Change
setup() {
tosetup(props) {
at Line 90. We need to pass in the props into the setup function.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 computingcontact
within thewatchEffect
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 usingObject.keys(contact)
. Then, we loop through each key of the array. If thekey
is not namedid
, we set the form with the value of the key withincontact
. 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 ourform
object. Here, we loop through the keys of theform
variable. For each key, we set that key as the property of thepayload
object using theObject.defineProperty()
method. The method takes apropertyDescriptor
object as the third parameter. Within thepropertyDescriptor
, 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 thepayload
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.
At Line 321, we add a
console
statement tosubmitPayload.value
when the form has not errors.console.log(submitPayload.value);
At Line 324, we modify the message for the
$q.notify()
function:$q.notify({ message: props.editMode ? "Contact edited" : "Contact created", type: "positive", });
At Line 330, we stop the
watchEffect
hook with a call to theonBeforeUnmount
hook.onBeforeUnmount(() => { void stopContactsEffect(); });
Lastly and within the
template
section, we improve the responsiveness of oursubmit
button. At Line 38, we updatediv
to aflex
row, and introduce a responsive column to wrap thteq-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.