Design Goals
-
Responsive columns per row
- 1 card per row on phones (xs)
- 2 cards per row on tablets (sm)
- 3 cards per row on laptops (md)
- 4 cards per row on desktops (lg)
- 6 cards per row on wide (xl)
- Maximize rows per page, no scrolling
- Maximize items per page, no gaps
- Dynamic pagination
What makes this the Ultimate Responsive Data Iterator?
Without a doubt, creating a data iterator in Vuetify is NBD. However, creating a data iterator that meets our specific design goals is more challenging. Consider this SO post 💡...
We are using Vuetify's v-data-iterator to control pagination on a grid layout. We are setting the iterator's rows-per-page-items to [8, 16, 24, 36]. But this doesn't exactly determine the number of rows per page, but items per page. That is, because it's a grid layout, there may be several items…
There are several unknowns here. The columns in each row wrap responsively based on breakpoints, so it's difficult to determine the ideal number of items per page. It's also difficult to know the ideal number of columns per row. If we set a specific number of items per page, there may be gaps on the last row depending on the current viewport.
This is a common issue with responsive grids like Bootstrap and Vuetify's. This is because we iterate (repeat) the columns (items) in a single row
element.
<v-row>
<v-col v-for="n in items">..</v-col>
</v-row>
But the "visual" rows that are rendered wrap according to the responsive breakpoints we have set. Therefore, the challenge is to to sync-up the "wrapping" of the repeating data items with the current viewport width (breakpoint).
Vuetify's Data Iterator Component
Since I don't want to reinvent the wheel, I'm using the data iterator. What's cool is that you can customize it simply using a template. You can use whatever markup you want for each item (rows, list items, card, etc..). This solution makes full use of the Vuetify 2.x Data Iterator using customizations on the header, footer and default slot templates.
The Solution
Controlling the rows-per-page and items-per-row with responsive items is a little math challenge🤓. What we do know is:
- the total number of data items
- the current responsive breakpoint
- there are 12 column units per row (based on Vuetify's grid)
Using Vue computed values, determine the number of pages (for pagination), optimal rows per page (based on viewport height), items per row (based on how many cols you want on each breakpoint), and finally use this to calculate the items per page (ipp)...
computed: {
numberOfPages () {
return Math.ceil(this.beers.length / this.ipp)
},
rowsPerPage () {
return this.rpp
},
itemsPerRow () {
switch (this.$vuetify.breakpoint.name) {
case 'xs': return 1
case 'sm': return 2
case 'md': return 3
case 'lg': return 4
case 'xl': return 6
}
},
ipp () {
return Math.ceil(this.rowsPerPage * this.itemsPerRow)
},
},
The number of pages, and items-per-row is calculated as viewport width changes. This meets our responsive design goal of cards per row, while also preventing gaps (or missing items) in the last row.
methods: {
...
calcRowsPerPage () {
let container = document.getElementById('container')
let minItemHeight = 170
if (container) {
let containerHeight = parseInt(container.clientHeight, 0)
this.rpp = Math.floor(containerHeight/minItemHeight)
}
else {
this.rpp = 4
}
},
},
created() {
// re-calc on screen resize
window.addEventListener('resize', () => {
this.calcRowsPerPage()
})
},
The responsive behavior is achieved using the slot:default template. Notice the cols
prop is determined by dividing 12 (the number of columns in Vuetify's grid) by the computed itemsPerRow
value. As you can see above, itemsPerRow
is based on the Vuetify breakpoints.
<template v-slot:default="props">
<v-row class="fill-height overflow-auto" id="container">
<v-col
v-for="(item,idx) in props.items"
:key="item.name"
:cols="(12/itemsPerRow)"
class="py-2"
>
<v-card class="card fill-height">
<v-card-title>
<span class="font-weight-light text-truncate">
<span v-text="item.id"></span> {{ item.name }}
</span>
</v-card-title>
<v-divider></v-divider>
<v-card-text>
<span v-html="item.tagline" class="mr-2"></span>
<v-chip v-text="item.first_brewed"></v-chip>
</v-card-text>
</v-card>
</v-col>
</v-row>
</template>
Stacking with 1 item per row on mobile...
And..., on all these breakpoints the pagination is updated! This happens because the reactive props are synced...
<v-data-iterator
:items="items"
:items-per-page.sync="ipp"
:page.sync="page"
hide-default-footer
>...
Rows-per-page is dynamic too!
The visual rows are also maximized to fill the viewport height. For example, 3 rows on short screens, 5 rows on tall screens.
The end result is a responsive grid with dynamic paging that maximizes viewport height and width. No scrolling baby! 😎
Demo and Full Source
Top comments (1)
This is so cool in that it's both horizontally and vertically responsive. Nice work!