Adding reactive object to ref array and breaking the link - vuejs3

Vue3, composition api, newPost is reactive object, posts is ref array.
I gather all fields of new post into newPost, and then on submit - want to add it to posts array like this.
let found = posts.value.findIndex(o => o.id == newPost.id)
if (found >= 0)
posts.value[found] = newPost
else
posts.value.unshift(newPost)
the problem here is that once I start changing newPost object after these lines, posts array reflects those changes as well.
next thing I tried is to use toRaw like this:
let found = posts.value.findIndex(o => o.id == newPost.id)
if (found >= 0)
posts.value[found] = toRaw(newPost)
else
posts.value.unshift(toRaw(newPost))
but problem persists. As soon as I start adding values in newPost object, array posts changes along. What am I doing wrong here? Thank you!

Related

Vue computed rearranges array order when nothing is happening in firebase

I have an array that i get from firebase bindings. I am showing that array inside vue component like this:
<tr v-for="member in activeMembers" :key="member.id">
<td>{{member.name}}</td>
<td>{{member.email}}</td>
</tr>
And this is the computed property:
const activeMembers = computed(function() {
let filtered = members.value.filter(
member => member.status === 'active',
)
return filtered.sort((a, b) => (a.updated < b.updated) ? 1 : -1)
})
When a member is added or changed in firebase it should recompute and reorder and that part works, but there is a glitch where sometimes the whole members array is reordering but nothing is happening in the firebase. Can someone explain why is this happening and how to work around it? I am new in Vue so maybe i am missing something.
And also, when the page loads it rearranges the array multiple times. How can i avoid that, and do the ordering only once but still keep the computed?

What's the best way to paginate and filters large set of data in Firebase?

I have a large Firestore collection with 10,000 documents.
I want to show these documents in a table by paging and filtering the results at 25 at a time.
My idea, to limit the "reads" (and therefore the costs), was to request only 25 documents at a time (using the 'limit' method), and to load the next 25 documents at the page change.
But there's a problem. In order to show the number of pages I have to know the total number of documents and I would be forced to query all the documents to find that number.
I could opt for an infinite scroll, but even in this case I would never know the total number of results that my filter has found.
Another option would be to request all documents at the beginning and then paging and filtering using the client.
so, what is the best way to show data in this type of situation by optimizing performance and costs?
Thanks!
You will find in the Firestore documentation a page dedicated to Paginating data with query cursors.
I paste here the example which "combines query cursors with the limit() method".
var first = db.collection("cities")
.orderBy("population")
.limit(25);
return first.get().then(function (documentSnapshots) {
// Get the last visible document
var lastVisible = documentSnapshots.docs[documentSnapshots.docs.length-1];
console.log("last", lastVisible);
// Construct a new query starting at this document,
// get the next 25 cities.
var next = db.collection("cities")
.orderBy("population")
.startAfter(lastVisible)
.limit(25);
});
If you opt for an infinite scroll, you can easily know if you have reached the end of the collection by looking at the value of documentSnapshots.size. If it is under 25 (the value used in the example), you know that you have reached the end of the collection.
If you want to show the total number of documents in the collection, the best is to use a distributed counter which holds the number of documents, as explained in this answer: https://stackoverflow.com/a/61250956/3371862
Firestore does not provide a way to know how many results would be returned by a query without actually executing the query and reading each document. If you need a total count, you will have to somehow track that yourself in another document. There are plenty of suggestions on Stack Overflow about counting documents in collections.
Cloud Firestore collection count
How to get a count of number of documents in a collection with Cloud Firestore
However, the paging API itself will not help you. You need to track it on your own, which is just not very easy, especially for flexible queries that could have any number of filters.
My guess is you would be using Mat-Paginator and the next button is disabled because you cannot specify the exact length? In that case or not, a simple workaround for this is to get (pageSize +1) documents each time from the Firestore sorted by a field (such as createdAt), so that after a new page is loaded, you will always have one document in the next page which will enable the "next" button on the paginator.
What worked best for me:
Create a simple query
Create a simple pagination query
Combine both (after validating each one works separately)
Simple Pagination Query
const queryHandler = query(
db.collection('YOUR-COLLECTION-NAME'),
orderBy('ORDER-FIELD-GOES-HERE'),
startAt(0),
limit(50)
)
const result = await getDocs(queryHandler)
which will return the first 50 results (ordered by your criteria)
Simple Query
const queryHandler = query(
db.collection('YOUR-COLLECTION-NAME'),
where('FIELD-NAME', 'OPERATOR', 'VALUE')
)
const result = await getDocs(queryHandler)
Note that the result object has both query field (with relevant query) and docs field (to populate actual data)
So... combining both will result with:
const queryHandler = query(
db.collection('YOUR-COLLECTION-NAME'),
where('FIELD-NAME', 'OPERATOR', 'VALUE'),
orderBy('FIELD-NAME'),
startAt(0),
limit(50)
)
const result = await getDocs(queryHandler)
Please note that the field in the where clause and in orderBy must be the same one! Also, it is worth mentioning that you may be required to create an index (for some use cases) or that this operation will fail while using equality operators and so on.
My tip: inspect the error itself where you will find a detailed description describing why the operation failed and what should be done in order to fix it (see an example output using js client in image below)
Firebase V9 functional approach. Don't forget to enable persistence so you won't get huge bills. Don't forget to use where() function if some documents have restrictions in rules. Firestore will throw error if even one document is not allowed to read by user. In case bellow documents has to have isPublic = true.
firebase.ts
function paginatedCollection(collectionPath: string, initDocumentsLimit: number, initQueryConstraint: QueryConstraint[]) {
const data = vueRef<any[]>([]) // Vue 3 Ref<T> object You can change it to even simple array.
let snapshot: QuerySnapshot<DocumentData>
let firstDoc: QueryDocumentSnapshot<DocumentData>
let unSubSnap: Unsubscribe
let docsLimit: number = initDocumentsLimit
let queryConst: QueryConstraint[] = initQueryConstraint
const onPagination = (option?: "endBefore" | "startAfter" | "startAt") => {
if (option && !snapshot) throw new Error("Your first onPagination invoked function has to have no arguments.")
let que = queryConst
option === "endBefore" ? que = [...que, limitToLast(docsLimit), endBefore(snapshot.docs[0])] : que = [...que, limit(docsLimit)]
if (option === "startAfter") que = [...que, startAfter(snapshot.docs[snapshot.docs.length - 1])]
if (option === "startAt") que = [...que, startAt(snapshot.docs[0])]
const q = query(collection(db, collectionPath), ...que)
const unSubscribtion = onSnapshot(q, snap => {
if (!snap.empty && !option) { firstDoc = snap.docs[0] }
if (option === "endBefore") {
const firstDocInSnap = JSON.stringify(snap.docs[0])
const firstSaved = JSON.stringify(firstDoc)
if (firstDocInSnap === firstSaved || snap.empty || snap.docs.length < docsLimit) {
return onPagination()
}
}
if (option === "startAfter" && snap.empty) {
onPagination("startAt")
}
if (!snap.empty) {
snapshot = snap
data.value = []
snap.forEach(docSnap => {
const doc = docSnap.data()
doc.id = docSnap.id
data.value = [...data.value, doc]
})
}
})
if (unSubSnap) unSubSnap()
unSubSnap = unSubscribtion
}
function setLimit(documentsLimit: number) {
docsLimit = documentsLimit
}
function setQueryConstraint(queryConstraint: QueryConstraint[]) {
queryConst = queryConstraint
}
function unSub() {
if (unSubSnap) unSubSnap()
}
return { data, onPagination, unSub, setLimit, setQueryConstraint }
}
export { paginatedCollection }
How to use example in Vue 3 in TypeScript
const { data, onPagination, unSub } = paginatedCollection("posts", 8, [where("isPublic", "==", true), where("category", "==", "Blog"), orderBy("createdAt", "desc")])
onMounted(() => onPagination()) // Lifecycle function
onUnmounted(() => unSub()) // Lifecycle function
function next() {
onPagination('startAfter')
window.scrollTo({ top: 0, behavior: 'smooth' })
}
function prev() {
onPagination('endBefore')
window.scrollTo({ top: 0, behavior: 'smooth' })
}
You might have problem with knowing which document is last one for example to disable button.

AngularFire NormalizedCollection and order in firebasearray

I use AngularFire and Firebase-util library.
I would like to order a list in descending order from the last element to the first insert (in the bottom of this list).
var ref = firebase.database().ref();
var nc = new firebase.util.NormalizedCollection(
ref.child('demandes/'+$state.params.chatId+'/reponses'), // main path
[ref.child('reponses'), 'widget2'] // second path
).select('widget2.timestamp_inverse', 'widget2.intitule', 'widget2.commentaires', 'widget2.prix')
.ref();
$scope.reponses = $firebaseArray(nc);
I saw that to order depends on the main path ! But this main path is just a list of Firebase keys like -AZE4AZ4RQ4F53.
How can I order this list like I want please ? Server-side or Client-side if needed ...
I think I'm forced to proceed like that : client-side
With firebasearray, don't use track by $index or it will display an error like "expected array but received ..." and filter & order your array like you want :
<ion-item ng-repeat="reponse in filteredItems = reponses | orderBy:'FIELD_CHOSEN':true ">
If you have any suggestions to descending sort server-side, It will be nice to share with me :) Thank you

Filter Products in a Dynamic Way using LINQ

After hours of trying and searching, I think its time to share my problem with you right now.
Problem Definition :
I have a Dictionary of KeyValuePairs(named filterPool) which includes an integer (PropertyID) and a string(McValue). What I am trying to do is filtering products depending on those KeyValuePairs and return them as a DataTable/List.
You may consider this as building dynamic "Where ... And .." clauses as SQL.
Here is the code that I am using :
foreach (KeyValuePair<int, string> filter in filterPool)
{
products = products.Where(i => i.PROPERTYID == filter.Key && i.MCVALUE.Equals(filter.Value));
}
return products.ToDataTable();
The problem is the foreach loop above seems to work only once, for the latest KeyValuePair available in the Dictionary.
As far as I could find on Stackoverflow, the closest solution to my problem was : this one, also using a Dictionary of values for filtering
There must be a way to achieve the goal of filtering using Dictionary and LINQ; or there's a huge thing that I am missing/ignoring to see somehow.
Hope the problem given is clear enough for all,
Thanks
^^
This is a closure issue. You can solve it by making a temporary:
foreach (KeyValuePair<int, string> filterTmp in filterPool)
{
var filter = filterTmp; // Make a temporary
products = products.Where(i => i.PROPERTYID == filter.Key && i.MCVALUE.Equals(filter.Value));
}
return products.ToDataTable();
For details on what's happening, see Eric Lippert's post Closing over the loop variable considered harmful.
Also note that this behavior has changed for C# 5. In C# 5/VS2012, this code would work as expected.
You're overwriting your products collection on every iteration of your foreach. I'm not sure what the data type on your collection is, but you'll want to do something like this in your foreach instead:
products.AddRange(products.Where(i => i.PROPERTYID == filter.Key && i.MCVALUE.Equals(filter.Value)));
I'm not sure if that makes sense, but it seems like you're trying to create a collection full of products that match your filterPool.
I think that it's better solved with aggregate:
return filter
.Aggregate(products, (acc, filter) => acc.Where(i => i.PROPERTYID == filter.Key && i.MCVALUE.Equals(filter.Value)));
.ToDataTable();

Delete with subsonic

I've had problems with the update method in subsonic, so instead of using:
a.Update()
I used
var qry = dbAnimals.Update<Notification>().
Set(n => n.NotName == notification.NotName).
Set(n => n.NotRecStatus == notification.NotRecStatus).
Set(n => n.NotModified == notification.NotModified).
Where(n => n.NotRecID == id).Execute();
The thing is that I now want to delete, and I get the same nullreference exception. so
a.Delete()
does not work. What would be the equivalent to delete that row depending on its ID? I have tried to find it but didn't get a clue.
Thank you:
To solve this problem I finally used this:
var query = new SubSonic.Query.Delete<Notification>(dbAnimals.DataProvider).
From<Notification>().
Where(NotificationTable.NotRecIDColumn).IsEqualTo(id).
Execute();

Resources