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
pageSizeconstant defined the initial amount of rows), thevirtual-scrollevent will be fired and theonScrollfunction executed. The main thing happening is that thecomputed ref,rows, is computed by slicing thecontactsarray from index 0 topageSize * (nextPage.value - 1) when thelastIndexconst inside theonScrollfunction is initiated. For example, when the table is first rendered, rows from index 0 to index 49 (50 rows) will be extracted and returned toq-tablefor rendering. In the next firing of the event, rows from 0 to index 99 (100 rows) will be returned toq-table). So the array consumed by therowsprop ofq-tablekeeps getting longer as we scroll down. This is the magic behindvirtual 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
nextPageref, virtual scrolling won’t work because the same number of rows will be returned each time. So, we incrementnextPageat 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 variabletoequals constlastIndex(to === lastIndex). Thetovariable is a property of the event payload sent when thevirtual-scrollevent is emitted. It is passed into theonScrollfunction as the only argument. We destructure the payload to gettoandref(the payload contains other properties. Read the QTable API). Thetovariable represents the next page of rows which should be loaded, while therefvariable which is renamed toref2to avoid colliding with therefvariable imported from thevuelibrary represents the current component i.e.QTable. If theifcondition is satisfied, the table is put into a loading state (Line 76) while thesetTimeoutfunction simulates a 500 milliseconds delay before thenextPageref is increment, theQTablecomponent is refreshed (ref2.refresh()) andloadingis set to false again.The
setTimeoutfunction 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
nextTickis used to refresh the table after the next DOM update afternextPageis incremented. The proper use ofnextTickcan help resolve DOM update issue and ensure that a particular constant or variable is properly updated before it is read. Read more aboutnextTickhere. -
On Line 96, the pagination object is returned with
rowsPerPagealways being0and therowsNumberalways being the length of therowsarray. Recall thatrowsPerPagebeing0indicates 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-rowslot is used to override the content of the very first row of theQTablecomponent with a custom content. This is the slot we used to render the text:Starred Contacts (xx). The placeholderxxwill be replaced with an actual number down the line. -
The
bodyslot is used to customise the rendering of the body of theQTablecomponent. For our use-case, this was necessary to inject a component,QAvatar, for rendering theprofilePicturecolumn. This is how the profile pictures are displayed. Thebodyslot exposes thepropsobject fromQTablewhich 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
