Is there a way to do pagination with firebase realtime database (vuejs)? - vue.js

I'm trying to paginate my data from firebase realtime database.
Do I have to change to firestore ? Where all is explain in Google's doc (https://firebase.google.com/docs/firestore/query-data/query-cursors) or it's also possible with rtdb ?
Here is my code (i'm using vue js) :
loadConcerts ({commit}) {
commit('setLoading', true)
firebase.database().ref('concerts')
.orderByChild('expires')
.startAt(Date.now() / 1e3)
.limitToFirst(10)
.once('value')
.then(data => {
const concerts = []
data.forEach(element => {
concerts.push({
id: element.key,
title: element.val().title,
day: element.val().day,
ticketlink: element.val().ticketlink,
description: element.val().descriptio
})
})
commit('setLoadedConcerts', concerts)
commit('setLoading', false)
})
.catch(
(error) => {
console.log(error)
commit('setLoading', false)
}
)
},
I would like to add pagination after 10 results, or infinite scrolling.

I have also had similar problem with pagination. The documentation seems to be insufficient i.e they show you how to go to next page but not how to move back to the previous page. Its just frustrating really.
I am using firestore
Below is how i implemented a simple pagination. I have already configured VueFire , Firebase and BootstrapVue i'll head straight to the code.
What to do different that no one shows you.
Use VueFire programmatic binding instead of declarative binding see here
To get firstVisible item in firebase run documentSnapshots.docs[0]
<template>
<div>
<p>{{countries}}</p>
<b-button-group size="lg" class="mx-2">
<b-button :disabled="prev_btn" #click="previous" >«</b-button>
<b-button :disabled="next_btn" #click="next">»</b-button>
</b-button-group>
</div>
</template>
<script>
import firebase from 'firebase/app'
import 'firebase/auth'
import { db } from '../main'
export default {
name: 'Countries',
data () {
return {
countries: [],
limit: 2,
lastVisible: '',
firstVisible: '',
next_btn: false,
prev_btn: true
}
},
methods: {
next () {
if (!this.next_btn) {
// bind data with countries
this.$bind('countries', db.collection('Countries').orderBy('createdAt').startAfter(this.lastVisible).limit(this.limit))
// set last and first visible items
db.collection('Countries').orderBy('createdAt').startAfter(this.lastVisible).limit(this.limit).get().then(documentSnapshots => {
this.lastVisible = documentSnapshots.docs[documentSnapshots.docs.length - 1]
this.firstVisible = documentSnapshots.docs[0]
}).then(() => {
// Peep on the next next query to see if it gives zero
db.collection('Countries').orderBy('createdAt').startAfter(this.lastVisible).limit(this.limit).get()
.then(snap => {
if (snap.size === 0) {
//disable button if the next peeped result gets zero
this.next_btn = true
// enable previous button
this.prev_btn = false
} else {
// enable next button if peeped result is not zero
this.next_btn = false
// enable previous button
this.prev_btn = false
}
})
})
}
},
previous () {
// Ensure previous is not zero
db.collection('Countries').orderBy('createdAt').endBefore(this.firstVisible).limitToLast(this.limit).get().then(snap => { return snap.size })
.then(size => {
//confirm is not zero here
if (size !== 0) {
//bind the previous to countries
this.$bind('countries', db.collection('Countries').orderBy('createdAt').endBefore(this.firstVisible).limitToLast(this.limit))
// Set last and first visible
db.collection('Countries').orderBy('createdAt').endBefore(this.firstVisible).limitToLast(this.limit).get().then(documentSnapshots => {
this.lastVisible = documentSnapshots.docs[documentSnapshots.docs.length - 1]
this.firstVisible = documentSnapshots.docs[0]
}).then(() => {
// peep the next previous query
db.collection('Countries').orderBy('createdAt').endBefore(this.firstVisible).limitToLast(this.limit).get()
.then(snap => {
if (snap.size === 0) {
//if next peeped previous button gets 0 disable
this.prev_btn = true
this.next_btn = false
} else {
//if next peeped result is does not get 0 enable buttons
this.prev_btn = false
this.next_btn = false
}
})
})
}
})
}
},
mounted () {
// run first query and bind data
this.$bind('countries', db.collection('Countries').orderBy('createdAt').limit(this.limit))
// set last and first Visible
db.collection('Countries').orderBy('createdAt').limit(this.limit).get().then(documentSnapshots => {
this.lastVisible = documentSnapshots.docs[documentSnapshots.docs.length - 1]
this.firstVisible = documentSnapshots.docs[0]
}).then(() => {
// peep to check if next should be on or off
db.collection('Countries').orderBy('createdAt').startAfter(this.lastVisible).limit(this.limit).get()
.then(snap => {
if (snap.size === 0) {
this.next_btn = true
}
})
})
}
}
</script>

Related

Vue 3 - watch when the value is changed in the watch

I have this watcher, where I am changing value of the activeTable - this value is stored inside Pinia store:
const { tables, freeTables, usedTables, activeTable, selectedTable } = storeToRefs(useTableStore())
watch([tables, urlPath], (newValue, oldValue) =>
{
if(urlPath.value[0] === 'tables' && Number.isInteger(+urlPath.value[1]))
{
let tableIndex = tables.value.findIndex(table =>
+table.id === +urlPath.value[1]
)
if(tableIndex > -1)
{
let table = tables.value[tableIndex]
if(+table.userID === +userData.id || +table.waiterID === +userData.id)
{
mineButton.click();
}
else
{
document.getElementById('other-tables').click()
}
activeTable.value = table
}
}
})
In another component I am watching activeTable
const {showTableOrder, activeTable, selectedTable} = storeToRefs(useTableStore())
watch(
() => activeTable.value,
async (newValue, oldValue) => {
console.log(newValue);
console.log(oldValue);
},
{ deep: true }
)
If you refresh the page, the first watcher is working as expected, and activeTable is set properly, but the second watcher is never activated. What am I doing wrong here?

how to get axios to load when row is expanded?

I have a dynamic table that has two axios.get methods. My methods work and the data get added to the table but I would like networkGroup to load only when I click toggle to expand a row. I can only get NetworkGroup to load automatically. How do I get it to work?
Here is my code:
toggle(gpId)
{
this.$set(this.networkGroups[audId], 'isExpanded', !this.networkGroups[gpId].isExpanded);
},
async getData(currPage)
{
const skip = (currPage - 1) * this.page.top;
const response = await this.axios.get(`/s/groups`, {
params: {
skip: skip,
top: this.page.top,
},
});
this.page = response.data.pages;
for (const audience of response.data.data)
await this.getNetworks(group.id);
this.groups = response.data.data;
},
async getNetworks(audId)
{
try
{
const response = await this.axios.get("/s/groups/network-groups?gpId=" + gpId);
if (!this.networkGroups[gpId])
{
this.$set(this.networkGroups, gpId, {});
this.$set(this.networkGroups[gpId], 'isExpanded', false);
}
this.$set(this.networkGroups[audId], 'data', response.data ? response.data.data : []);
}
catch (error)
{
if (error.message && error.message.includes("404"))
{
if (!this.networkGroups[gpId])
{
this.$set(this.networkGroups, gpId, {});
this.$set(this.networkGroups[gpId], 'isExpanded', false);
}
this.$set(this.networkGroups[gpId], 'data', []);
}
else
{
console.log(error.message);
}
}
},
<td class="audience-column">
<!-- expand button -->
<div class="ex_btn">
<span #click.prevent="toggle(group.id)">
<font-awesome-icon
:icon="
(networkGroups[group.id].isExpanded)
? 'caret-down'
: 'caret-right'
"
:class="[
'dropdown--arrow fa-2x cl-blue-primary relative t-3 g-link',
]"
/>
</span>
</div>
</td>
Not sure what I should change. Thanks!
seems it's not calling anything whenever you toggle the expand button, toggle method should call the getNetwork methods
try this:
toggle(gpId) {
this.$set(this.networkGroups[audId], 'isExpanded', !this.networkGroups[gpId].isExpanded); <<<--- change this
this.getNetwork(gpId) <<-- into this
},
and i think you set the wrong argument on getNetwork method:
getNetworks(audId) <<--- it says audId
{
try
{
const response = await this.axios.get("/s/groups/network-groups?gpId=" + gpId); <<--- but this calls gpId
....
hope my answer helps you!

BootstrapVue b-table toggleDetails opens multiple row details

I have an issue with Bootstrap Vue table showDetails for row.
When I open website for the first time and I click show details button it shows details with loading state, fetch the data and then when completed shows the details.
However when I hide the expanded row and click show Details for another row, after the fetch both of them are expended. Meaning I can see details of 2 rows.
(If I open and hide X of rows, after I expand one more, all previously opened and hiden X details will be visible)
Thank you for help!
Toggle details Method:
toggleDetails(item) {
if (item._showDetails) {
this.$set(item, '_showDetails', false)
} else if (this.listType === MEETING_LIST_TYPE.history) {
this.$set(item, '_showDetails', true)
} else {
item._showDetails = true
item.busy = true
this.showMeetingDetails({
meeting: item,
listType: this.listType
}).then(() => {
item.busy = false
})
}
}
Fetch method (action):
showMeetingDetails({ commit }, { meeting, listType }) {
return fetchMeetingDetails(meeting).then(res => {
commit('SET_MEETING_DETAILS', { meeting: res, listType })
})
}
Mutation:
SET_MEETING_DETAILS(state, { listType, meeting }) {
switch (listType) {
case MEETING_LIST_TYPE.today:
state.todayMeetings = state.todayMeetings.map(m =>
m.meetingID === meeting.meetingId
? { ...m, ...meeting, _showDetails: true }
: m
)
break
case MEETING_LIST_TYPE.all:
state.allMeetings = state.allMeetings.map(m =>
m.meetingID === meeting.meetingId
? { ...m, ...meeting, _showDetails: true }
: m
)
break
default:
break
}
}
Hello I figure out the solution. :D
So the problem was with vue reactivity when I was trying to change the value for the item which was inside the list and trying to jump over vuex a bit.
Helpful link here:
https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
To solve this problem I added mutations in my vuex for _showDetails and also busy state to show spinner when I am loading details row data. I also found out that in row item from bootstrap table, there are a lot of useful properties like index.
That all helped me to write cleaner mutations with usage of Vue.set()
Bellow my code. Probably I could still fix the methods naming ^^. If you have any ideas how to make it even simpler or cleaner I will appreciate!
Vue template (inside b-table):
<b-button id="detailsBtn" class="mr-2">
<b-icon
#click="toggleDetails(row)"
v-if="!row.detailsShowing"
icon="plus"
></b-icon>
<b-icon v-else icon="dash" #click="toggleDetails(row)"></b-icon>
</b-button>
my toggleDetails() function:
toggleDetails(row) {
if (row.item._showDetails) {
this.hideShowDetails(row, false)
} else {
this.showDetails(row)
}
},
showDetails(row) {
if (this.listType === MEETING_LIST_TYPE.history) {
this.hideShowDetails(row, true)
} else {
this.TOGGLE_BUSY_MEETING({
listType: this.listType,
index: row.index,
isBusy: true
})
this.hideShowDetails(row, true)
this.fetchMeetingDetails({
meeting: row.item,
listType: this.listType,
index: row.index
})
}
},
hideShowDetails(row, showDetails) {
this.TOGGLE_MEETING_DETAILS({
listType: this.listType,
index: row.index,
showDetails: showDetails
})
}
My action:
fetchMeetingDetails({ commit }, { meeting, listType, index }) {
return fetchMeetingDetails(meeting).then(res => {
commit('SET_MEETING_DETAILS', { meeting: res, listType, index })
})
}
My mutations:
SET_MEETING_DETAILS(state, { listType, meeting, index }) {
Vue.set(state.meetings[listType], index, {
...state.meetings[listType][index],
...meeting,
_showDetails: true,
busy: false
})
},
TOGGLE_MEETING_DETAILS(state, { listType, index, showDetails }) {
Vue.set(state.meetings[listType], index, {
...state.meetings[listType][index],
_showDetails: showDetails
})
},
TOGGLE_BUSY_MEETING(state, { listType, index, isBusy }) {
Vue.set(state.meetings[listType], index, {
...state.meetings[listType][index],
busy: isBusy
})
}
I hope someone might find it helpful! Cheers!

Duplicate items in list after an API update

I'm learning vuejs and I'm doing a weather app, the goal is to rank cities with an index (humidex). I fetch weather information by API (axios) in order to collect data from several cities. I want to auto update data every x minutes, problem : some of my results are duplicated (the new data don't replace the old one).
I tried to set an unique key (based on latitude and longitude) for each item, it works for several results but not for all.
data () {
return {
items:[],
show: false,
cities: cities,
newCity:''
}
},
components: {
Item
},
computed: {
sortHumidex() {
return this.items.slice().sort((a,b) => {
return this.getHumidex(b) - this.getHumidex(a) || b.current.temp_c - a.current.temp_c
})
}
},
methods: {
addCity() {
if (this.newCity.trim().length == 0) {
return
}
this.cities.push(this.newCity)
this.newCity = ''
},
getHumidex: (el) => {
const e = 6.112 * Math.pow(10,(7.5*el.current.temp_c/(237.7+el.current.temp_c)))
*(el.current.humidity/100)
return Math.round(el.current.temp_c + 5/9 * (e-10))
},
indexGeo: (e) => {
const lat = Math.round(Math.abs(e.location.lat))
const lon = Math.round(Math.abs(e.location.lon))
return lat.toString() + lon.toString()
},
getApi: function () {
const promises = [];
this.cities.forEach(function(element){
const myUrl = apiUrl+element;
promises.push(axios.get(myUrl))
});
let self = this;
axios
.all(promises)
.then(axios.spread((...responses) => {
responses.forEach(res => self.items.push(res.data))
}))
.catch(error => console.log(error));
}
},
created() {
this.getApi()
this.show = true
}
}
The render when I update API :
By pushing to the existing array of items, you have to deal with the possibility of duplicates. This can be eliminated simply by replacing items every time the API call is made.
Replace:
responses.forEach(res => self.items.push(res.data))
with:
self.items = responses.map(res => res.data)

Wait until API fully loads before running next function -- async/await -- will this work?

I am a beginner with Javascript with a bit of knowledge of VueJs. I have an array called tickets. I also have a data api returning two different data objects (tickets and user profiles).
The tickets have user ids and the user profiles has the ids with names.
I needed to create a method that looks at both of that data, loops through it, and assigns the full name of the user to the view.
I was having an issue where my tickets object were not finished loading and it was sometimes causing an error like firstname is undefined. So, i thought I'd try and write an async/await approach to wait until the tickets have fully loaded.
Although my code works, it just doesn't "feel right" and I am not sure how reliable it will be once the application gets larger.
Can I get another set of eyes as to confirmation that my current approach is OK? Thanks!
data() {
return {
isBusy: true,
tickets: [],
userProfiles: [],
}
},
created() {
this.getUserProfiles()
this.getTickets()
},
methods: {
getUserProfiles: function() {
ApiService.getUserProfiles().then(response => {
this.userProfiles = response.data
})
},
getTickets() {
ApiService.getTickets().then(response => {
this.tickets = response.data
this.assignNames(this.tickets)
this.isBusy = false
})
},
// lets wait until the issues are loaded before showing names;
async assignNames() {
let tickets = await this.tickets
var i
for (i = 0; i < this.tickets.length; i++) {
if (tickets[i].assigned_to !== null) {
const result = this.userProfiles.filter(profile => {
return profile.uid == tickets[i].assigned_to
})
tickets[i].assigned_to = result[0].firstname + ' ' + result[0].lastname
}
}
}
}
}
</script>
There are several ways you could do this. Here is the one I prefer without async/await:
created() {
this.load();
},
methods: {
getUserProfiles: function() {
return ApiService.getUserProfiles().then(response => {
this.userProfiles = response.data
})
},
getTickets() {
return ApiService.getTickets().then(response => {
this.tickets = response.data
})
},
load() {
Promise.all([
this.getUserProfiles(),
this.getTickets()
]).then(data => {
this.assignNames();
this.isBusy = false;
});
},
assignNames(){
const tickets = this.tickets;
for (let i = 0; i < this.tickets.length; i++) {
if (tickets[i].assigned_to !== null) {
const result = this.userProfiles.filter(profile => {
return profile.uid == tickets[i].assigned_to
})
tickets[i].assigned_to = result[0].firstname + ' ' + result[0].lastname
}
}
}
}