Designing the Contacts Table (Part 2) | Full-Stack Google Contacts Clone with Adonis.js/Node.js and Quasar (Vue.js)
Table of contents
Due to the length of this lesson, I had to split it into two parts. The first part surprisingly reached a 12-minute read. I think that was too long.
In this second part, we will continue with discussion on how the Contacts table is built.
We remove some redundant import and bring in new ones:
<script lang="ts">
- import { Todo, Meta } from "components/models";
- import ExampleComponent from "components/CompositionComponent.vue";
- import { defineComponent, ref } from "vue";
+ import { defineComponent, ref, computed, nextTick } from "vue";
+ import { VirtualScrollCtx } from "../types";
+ import { contacts } from "../data/Google_Contacts_Clone_Mock_Data";
+ import columns from "../data/table-definitions/contacts";
...
The imports are self-explanatory.
We define some constants within the script
scope. The constants could have equally being defined within the setup
function.
<script lang="ts">
...
+ const pageSize = 50;
+ const lastPage = Math.ceil(contacts.length / pageSize);
...
We remove the redundant todo
array ref.
Our entire script
function should look like this:
<script lang="ts">
import { defineComponent, ref, computed, nextTick } from "vue";
import { VirtualScrollCtx } from "../types";
import { contacts } from "../data/Google_Contacts_Clone_Mock_Data";
import columns from "../data/table-definitions/contacts";
const pageSize = 50;
const lastPage = Math.ceil(contacts.length / pageSize);
export default defineComponent({
name: "HomePage",
components: {},
setup() {
const nextPage = ref(2);
const loading = ref(false);
const selected = ref([]);
const rows = computed(() =>
contacts.slice(0, pageSize * (nextPage.value - 1))
);
const onScroll = function ({ to, ref: ref2 }: VirtualScrollCtx): void {
const lastIndex = rows.value.length - 1;
if (
loading.value !== true &&
nextPage.value < lastPage &&
to === lastIndex
) {
loading.value = true;
setTimeout(() => {
nextPage.value++;
void nextTick(() => {
ref2.refresh();
loading.value = false;
});
}, 500);
}
};
return {
selected,
columns,
rows,
nextPage,
loading,
pagination: {
rowsPerPage: 0,
rowsNumber: rows.value.length,
},
onScroll,
};
},
});
</script>
So what's going on inside the setup
function?
When we begin scrolling on the table and approach the end of the initial number of rows loaded (the
pageSize
constant defined the initial amount of rows), thevirtual-scroll
event will be fired and theonScroll
function executed. The main thing happening is that thecomputed ref
,rows
, is computed by slicing thecontacts
array from index 0 topageSize * (nextPage.value - 1) when the
lastIndexconst inside the
onScrollfunction is initiated. For example, when the table is first rendered, rows from index 0 to index 49 (50 rows) will be extracted and returned to
q-tablefor rendering. In the next firing of the event, rows from 0 to index 99 (100 rows) will be returned to
q-table). So the array consumed by the
rowsprop of
q-tablekeeps getting longer as we scroll down. This is the magic behind
virtual scrolling` and this behaviour is very different from the normal pagination which returns a constant number of rows for every new page requested and these new (longer) rows replaces the existing ones.However, if we do not increment the
nextPage
ref, virtual scrolling won't work because the same number of rows will be returned each time. So, we incrementnextPage
at Line 79 but only after checking that the table is not in a loading state (loading.value !== true
) and that we haven't gotten to the last page (nextPage.value < lastPage
) and that variableto
equals constlastIndex
(to === lastIndex
). Theto
variable is a property of the event payload sent when thevirtual-scroll
event is emitted. It is passed into theonScroll
function as the only argument. We destructure the payload to getto
andref
(the payload contains other properties. Read the QTable API). Theto
variable represents the next page of rows which should be loaded, while theref
variable which is renamed toref2
to avoid colliding with theref
variable imported from thevue
library represents the current component i.e.QTable
. If theif
condition is satisfied, the table is put into a loading state (Line 76) while thesetTimeout
function simulates a 500 milliseconds delay before thenextPage
ref is increment, theQTable
component is refreshed (ref2.refresh()
) andloading
is set to false again.The
setTimeout
function is used to simulate an asynchronous operation in a real-life scenario where you have to fetch the new rows from the database.Note that
nextTick
is used to refresh the table after the next DOM update afternextPage
is incremented. The proper use ofnextTick
can help resolve DOM update issue and ensure that a particular constant or variable is properly updated before it is read. Read more aboutnextTick
here.On Line 96, the pagination object is returned with
rowsPerPage
always being0
and therowsNumber
always being the length of therows
array. Recall thatrowsPerPage
being0
indicates that we want to always display all the rows within the page. That is, skip the traditional pagination strategy.
The QTable
slots used
Within the template
section, we have used two slots: top-row
and body
slots. Read more about these slots here.
The
top-row
slot is used to override the content of the very first row of theQTable
component with a custom content. This is the slot we used to render the text:Starred Contacts (xx)
. The placeholderxx
will be replaced with an actual number down the line.The
body
slot is used to customise the rendering of the body of theQTable
component. For our use-case, this was necessary to inject a component,QAvatar
, for rendering theprofilePicture
column. This is how the profile pictures are displayed. Thebody
slot exposes theprops
object fromQTable
which contains (but not limited) to the following properties:
{
key : Any, // Row's key
row : Object, // Row object
rowIndex : Number, // Row's index (0 based) in the filtered and sorted table
cols : Object, // Column definitions
selected: Boolean, // Is row selected? Can directly be assigned new Boolean value which changes selection state
}
We loop through all our columns props.cols
. When props.cols === 'profilePicture'
, we render the QAvatar
component. Else, we interpolate the value
of that column (col.value
) with the span
element.
Customising the body
slot breaks down the checkbox
functionality. So, we have manually include the checkbox via the QCheckbox
component within the first q-td
component and assign props.selected
as the value of the v-model
for the QCheckbox
component to check when the checkbox is checked or not.
So, this covered everything about how the Contact table was designed/developed. In the next lesson, we will customise the QTable
a little more and improve some other aspects of the UI a little more before we go into the backend scope of this series.
Save all your files, commit and merge with the master branch.
git add .
git commit -m "feat(ui): complete design of the contacts table"
git push --set-upstream origin 05-the-contacts-table
git checkout master
git merge master 05-the-contacts-table
git push