Last active
May 4, 2020 09:16
-
-
Save CaiJimmy/400fe6a76745483e58f28113538d4c5b to your computer and use it in GitHub Desktop.
Firebase pagination
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* HTTP Function to initialize / reset question counter manually */ | |
const express = require('express'); | |
const cors = require('cors')({ | |
origin: true | |
}); | |
const app = express(); | |
app.use(cors); | |
app.get('/', (req, res) => { | |
const topicID = req.query.topicID, | |
topicRef = db.collection('topics').doc(topicID); | |
if (!topicID) { | |
res.send("Topic ID is required"); | |
return; | |
}; | |
const allQuestions = db.collection('questions').where('topic', '==', topicID); | |
allQuestions.get().then(snap => { | |
const totalCount = snap.size | |
/* | |
Set counter | |
*/ | |
topicRef.set({ | |
'count': { | |
total: totalCount | |
} | |
}, { | |
merge: true | |
}).then(() => { | |
/* | |
Send response | |
*/ | |
res.json({ | |
total: totalCount | |
}); | |
}); | |
}) | |
}); | |
exports.reCount = functions.https.onRequest(app); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
const functions = require('firebase-functions'); | |
const admin = require('firebase-admin'); | |
admin.initializeApp(); | |
const db = admin.firestore(); | |
exports.questionChange = functions.firestore.document('questions/{questionID}').onWrite((change) => { | |
const oldQuestion = change.before.data(), | |
newQuestion = change.after.data(); | |
let oldTopic, | |
oldTopicRef, | |
newTopic, | |
newTopicRef; | |
/* Load variables */ | |
if (change.before.exists) { | |
oldTopic = oldQuestion.topic; | |
oldTopicRef = db.collection('topics').doc(oldTopic); | |
}; | |
if (change.after.exists) { | |
newTopic = newQuestion.topic; | |
newTopicRef = db.collection('topics').doc(newTopic); | |
}; | |
if (!change.before.exists) { | |
/* New document Created : plus one to count.total */ | |
return newTopicRef.get().then(snap => { | |
return newTopicRef.set({ | |
count: { | |
total: snap.data().count.total + 1 | |
} | |
}, { | |
merge: true | |
}); | |
}); | |
}; | |
if (!change.after.exists) { | |
/* Question deleted : subtract one from count.total */ | |
return oldTopicRef.get().then(snap => { | |
return oldTopicRef.set({ | |
count: { | |
total: snap.data().count.total - 1 | |
} | |
}, { | |
merge: true | |
}); | |
}); | |
}; | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as firebase from "firebase/app"; | |
import "firebase/firestore"; | |
export default { | |
name: 'QuestionList', | |
data: () => ({ | |
questions: [], | |
paging: { | |
question_per_page: 20, | |
end: false, | |
loading: false | |
}, | |
ref: { | |
questions: null, | |
questionsNext: null | |
} | |
}), | |
created() { | |
/* Set common Firestore reference */ | |
this.ref.questions = firebase.firestore().collection('questions').where('topic', '==', this.ref.topic.id).orderBy("date", 'desc'); | |
/* Load first page */ | |
const firstPage = this.ref.questions.limit(this.paging.question_per_page); | |
this.handleQuestions(firstPage); | |
}, | |
methods: { | |
loadMore() { | |
if (this.paging.end) { | |
return; | |
}; | |
this.paging.loading = true; | |
this.handleQuestions(this.ref.questionsNext).then((documentSnapshots) => { | |
this.paging.loading = false; | |
if (documentSnapshots.empty) { | |
/* If there is no more questions to load, set paging.end to true */ | |
this.paging.end = true; | |
} | |
}) | |
}, | |
handleQuestions(ref) { | |
/* | |
Fetch questions of given reference | |
*/ | |
return new Promise((resolve, reject) => { | |
ref.get().then((documentSnapshots) => { | |
/* If documentSnapshots is empty, then we have loaded all of pages */ | |
if (documentSnapshots.empty) { | |
this.paging.end = true; | |
resolve(documentSnapshots); | |
}; | |
documentSnapshots.forEach((doc) => { | |
let questionData = doc.data(); | |
questionData.id = doc.id; | |
this.questions.push(questionData); | |
}); | |
/* Build reference for next page */ | |
const lastVisible = documentSnapshots.docs[documentSnapshots.size - 1]; | |
if (!lastVisible) { | |
return; | |
}; | |
this.ref.questionsNext = this.ref.questions | |
.startAfter(lastVisible) | |
.limit(this.paging.question_per_page); | |
resolve(documentSnapshots); | |
}); | |
}); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as firebase from "firebase/app"; | |
import "firebase/firestore"; | |
export default { | |
name: 'QuestionList', | |
props: { | |
topicData: Object | |
}, | |
data: () => ({ | |
questions: [], | |
paging: { | |
question_per_page: 20, /* Number of questions per page */ | |
loading: false, | |
loaded: [] /* Loaded pages, to avoid querying data again */ | |
}, | |
ref: { | |
questions: null, /* Old questions Firestore Reference */ | |
} | |
}), | |
created () { | |
this.init(); | |
}, | |
methods: { | |
init () { | |
const count = this.topicData.count; | |
/* Fill array with placeholders, to build pagination */ | |
this.questions = new Array(count.total).fill({ | |
loading: true | |
}); | |
/* Set common Firestore reference */ | |
this.ref.questions = firebase.firestore().collection('questions') | |
.where('topic', '==', this.topicData.id) | |
.orderBy("date", 'desc'); | |
/* | |
Load questions of first page | |
*/ | |
this.onPageChange(0); | |
}, | |
handleQuestions (ref, index = 0) { | |
/* | |
Fetch questions of given reference | |
*/ | |
return new Promise((resolve, reject) => { | |
console.log('Questions reference: ', ref); | |
ref.get().then((documentSnapshots) => { | |
let _questions = [], | |
_index = index || 0; | |
console.log('Questions document snapshots: ', documentSnapshots); | |
documentSnapshots.forEach((doc) => { | |
let questionData = doc.data(); | |
questionData.id = doc.id; | |
_questions.push(questionData); | |
this.$set(this.questions, _index, { | |
loading: false, | |
...questionData | |
}); | |
_index++; | |
}); | |
resolve(documentSnapshots); | |
}); | |
}); | |
}, | |
async onPageChange (toPage, fromPage) { | |
const currentPage = toPage, | |
per_page = this.paging.question_per_page; | |
let startAfter = null, | |
limit = per_page, | |
index = per_page * (currentPage - 1), /* Start after the question before that page */ | |
startAfterAvailable; | |
if (this.paging.loaded.includes(currentPage)) { | |
return; | |
}; | |
/* Display progress spinner */ | |
this.paging.loading = true; | |
const questionBefore = this.questions[index - 1]; | |
if (questionBefore && !questionBefore.loading) { | |
/* | |
If the question before that page is loaded, | |
we can build it's documentSnapshot to query the following page only | |
*/ | |
const questionBeforeRef = firebase.firestore().collection('questions').doc(questionBefore.id), | |
questionBeforeSnapshot = await questionBeforeRef.get(); | |
startAfter = this.ref.questions.startAfter(questionBeforeSnapshot).limit(per_page); | |
if (index < 0) { | |
index = 0; | |
}; | |
startAfterAvailable = true; | |
} | |
else { | |
/* | |
But if it's not loaded, we'll have to request all questions between first page and current page | |
*/ | |
limit = currentPage * per_page; /* Load all question from first page to current page */ | |
if (limit <= 0) { | |
/* | |
Limit can not be less or equal to zero | |
This happend when currentPage == 0 | |
*/ | |
limit = per_page; | |
}; | |
startAfter = this.ref.questions.limit(limit); | |
index = 0; /* Start loop from first page */ | |
startAfterAvailable = false; | |
}; | |
this.handleQuestions(startAfter, index).then((documentSnapshots) => { | |
this.paging.loading = false; | |
this.loading.questions = false; | |
if (startAfterAvailable) { | |
/* startAfterAvailable = true | |
=> Only requested current page | |
Add current page to paging.loaded to avoid requesting the data again | |
*/ | |
this.paging.loaded.push(currentPage); | |
} | |
else { | |
/* | |
Request from 0 to current page | |
Add those pages to paging.loaded | |
*/ | |
this.paging.loaded = Array.from(Array(currentPage).keys()); | |
}; | |
}) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment