Vue, firestore: how to display LIVE data after merging collections - javascript

See EDIT Below
I have massively improved over my last question, but I am stuck again after some days of work.
Using Vue, Vue-router, Vuex and Vuetify with the Data on Googles Could Firestore
I want to update my data live, but i cannot find a way to do this.
Do i need to restructure, like moving products and categories into one collection?
Or is there any bind or query magic to get this done.
As you can see below, it loads the data on click quite well, but I need the live binding 'cause you could have the page open and someone could sell the last piece (amountLeft = 0). (And a lot of future ideas).
My data structure is the following:
categories: {
cat_food: {
name: 'Food'
parentCat: 'nC'
},
cat_drinks: {
name: 'Food'
parentCat: 'nC'
},
cat_beer: {
name: 'Beer'
parentCat: 'cat_drinks'
},
cat_spritz: {
name: 'Spritzer'
parentCat: 'cat_drinks'
},
}
products: {
prod_mara: {
name: 'Maracuja Spritzer'
price: 1.5
amountLeft: 9
cat: ['cat_spritz']
},
prod_capp: {
name: 'Cappuccino'
price: 2
cat: ['cat_drinks']
},
}
The categories and the products build a tree. The GIF shows me opening the categories down to show a product. You see that it's a product when you have a price tag.
You can see there are two categories that have the same parent (cat_drinks).
The product prod_capp is also assigned to the category and shown side by side to the categories.
I get the data currently this way:
catsOrProd.js
import { catsColl, productsColl } from '../firebase'
const state = {
catOrProducts: [],
}
const mutations = {
setCats(state, val) {
state.catOrProducts = val
}
}
const actions = {
// https://vuefire.vuejs.org/api/vuexfire.html#firestoreaction
async bindCatsWithProducts({ commit, dispatch }, CatID) {
if (CatID) {
// console.log('if CatID: ', CatID)
await Promise.all([
catsColl.where('parentCat', '==', CatID).orderBy('name', 'asc').get(),
productsColl.where('cats', 'array-contains', CatID).orderBy('name', 'asc').get()
])
.then(snap => dispatch('moveCatToArray', snap))
} else {
// console.log('else CatID: ', CatID)
await Promise.all([
catsColl.where('parentCat', '==', 'nC').orderBy('name', 'asc').get(),
productsColl.where('cats', 'array-contains', 'nC').orderBy('name', 'asc').get()
])
.then(snap => dispatch('moveCatToArray', snap))
}
},
async moveCatToArray({ commit }, snap) {
const catsArray = []
// console.log(snap)
await Promise.all([
snap[0].forEach(cat => {
catsArray.push({ id: cat.id, ...cat.data() })
}),
snap[1].forEach(cat => {
catsArray.push({ id: cat.id, ...cat.data() })
})
])
.then(() => commit('setCats', catsArray))
}
}
export default {
namespaced: true,
state,
actions,
mutations,
}
This is a part of my vue file that is showing the data on screen. I have left out the unnecessary parts.
To open everything a have a route with props and clicking on the category sends the router to the next category. (this way i can move back with browser functionality).
Sale.vue
<template>
...........
<v-col
v-for="catOrProduct in catOrProducts"
:key="catOrProduct.id"
#click.prevent="leftClickProd($event, catOrProduct)"
#contextmenu.prevent="rightClickProd($event, catOrProduct)">
....ViewMagic....
</v-col>
............
</template>
<script>
.........
props: {
catIdFromUrl: {
type: String,
default: undefined
}
},
computed: {
// https://stackoverflow.com/questions/40322404/vuejs-how-can-i-use-computed-property-with-v-for
...mapState('catOrProducts', ['catOrProducts']),
},
watch: {
'$route.path'() { this.bindCatsWithProducts(this.catIdFromUrl) },
},
mounted() {
this.bindCatsWithProducts(this.catIdFromUrl)
},
methods: {
leftClickProd(event, catOrProd) {
event.preventDefault()
if (typeof (catOrProd.parentCat) === 'string') { // when parentCat exists we have a Category entry
this.$router.push({ name: 'sale', params: { catIdFromUrl: catOrProd.id } })
// this.bindCatsWithProducts(catOrProd.id)
} else {
// ToDo: Replace with buying-routine
this.$refs.ProductMenu.open(catOrProd, event.clientX, event.clientY)
}
},
}
</script>
EDIT 24.09.2020
I have changed my binding logic to
const mutations = {
setCatProd(state, val) {
state.catOrProducts = val
},
}
const actions = {
async bindCatsWithProducts({ commit, dispatch }, CatID) {
const contain = CatID || 'nC'
const arr = []
catsColl.where('parentCat', '==', contain).orderBy('name', 'asc')
.onSnapshot(snap => {
snap.forEach(cat => {
arr.push({ id: cat.id, ...cat.data() })
})
})
productsColl.where('cats', 'array-contains', contain).orderBy('name', 'asc')
.onSnapshot(snap => {
snap.forEach(prod => {
arr.push({ id: prod.id, ...prod.data() })
})
})
commit('setCatProd', arr)
},
}
This works, as the data gets updated when I change something in the backend.
But now i get an object added everytime something changes. As example i've changed the price. Now i get this:
I don't know why, because i have the key field set in Vue. The code for the rendering is:
<v-container fluid>
<v-row
align="center"
justify="center"
>
<v-col
v-for="catOrProduct in catOrProducts"
:key="catOrProduct.id"
#click.prevent="leftClickProd($event, catOrProduct)"
#contextmenu.prevent="rightClickProd($event, catOrProduct)"
>
<div>
<TileCat
v-if="typeof(catOrProduct.parentCat) == 'string'"
:src="catOrProduct.pictureURL"
:name="catOrProduct.name"
/>
<TileProduct
v-if="catOrProduct.isSold"
:name="catOrProduct.name"
... other props...
/>
</div>
</v-col>
</v-row>
</v-container>
Why is this not updating correctly?

From the Vuefire docs, this is how you would subscribe to changes with Firebase only:
// get Firestore database instance
import firebase from 'firebase/app'
import 'firebase/firestore'
const db = firebase.initializeApp({ projectId: 'MY PROJECT ID' }).firestore()
new Vue({
// setup the reactive todos property
data: () => ({ todos: [] }),
created() {
// unsubscribe can be called to stop listening for changes
const unsubscribe = db.collection('todos').onSnapshot(ref => {
ref.docChanges().forEach(change => {
const { newIndex, oldIndex, doc, type } = change
if (type === 'added') {
this.todos.splice(newIndex, 0, doc.data())
// if we want to handle references we would do it here
} else if (type === 'modified') {
// remove the old one first
this.todos.splice(oldIndex, 1)
// if we want to handle references we would have to unsubscribe
// from old references' listeners and subscribe to the new ones
this.todos.splice(newIndex, 0, doc.data())
} else if (type === 'removed') {
this.todos.splice(oldIndex, 1)
// if we want to handle references we need to unsubscribe
// from old references
}
})
}, onErrorHandler)
},
})
I would generally avoid any unnecessary dependencies, but according to your objectives, you can use Vuefire to add another layer of abstraction, or as you said, doing some "magic binding".
import firebase from 'firebase/app'
import 'firebase/firestore'
const db = firebase.initializeApp({ projectId: 'MY PROJECT ID' }).firestore()
new Vue({
// setup the reactive todos property
data: () => ({ todos: [] }),
firestore: {
todos: db.collection('todos'),
},
})

Related

Using array.sort() on Vue data() variable

I'm loading Api data into a reactive variable called 'MyListings', and attempting to sort it based on latest id -> earliest id.
The problem I'm having is that since array.sort is an 'in-place' function it overwrites the original array which causes the Vue page to infinitely re-render.
Is there some way to clone the data out of data()? (I tried setting a constant to this.MyListings which produced the same results as the original)
api data
[
{
id: 1,
title: "sdf",
start_date: "2021-01-13",
poster:
"http://localhost:8000/media/images/2-Artwork-by-Tishk-Barzanji_iZookmb.jpg",
genre: "Action"
},
{
id: 3,
title: "ooga booga",
start_date: "2021-01-07",
poster: "http://localhost:8000/media/images/1_bdm0Lld.jpg",
genre: "Thriller"
}
];
Relevant Vue bits
updated() {
this.sortedArray();
},
data() {
return {
genre: "",
MyListings: []
};
},
methods: {
// no access to AsnycData in components, handle the incoming data like axios in vanilla vue.
memberListings($axios) {
this.$axios
.get("/api/v1/memberlistings/")
.then(response => {
// console.log(response, "response");
this.MyListings = response.data;
})
.catch(error => {
console.log(error);
});
},
formSubmit() {
this.update(); //all dropdown combinations below include a POST request
},
update() {
if (this.genre !== "") {
console.log("genre submitted");
this.$axios
.get(`/api/v1/memberlistings/`, {
params: {
genre: this.genre
}
})
.then(response => {
this.MyListings = response.data;
})
.catch(error => {
console.log(error);
});
} else {
this.$axios
.get(`/api/v1/memberlistings/`)
.then(response => {
this.MyListings = response.data;
})
.catch(error => {
console.log(error);
});
}
},
sortedArray() {
const sortedMyListings = this.MyListings;
sortedMyListings.sort((a, b) => b.id - a.id);
console.log(sortedMyListings, "sortedlist");
console.log("works");
}
}
**This is what the endless rendering looks like in console log **
[1]: https://i.stack.imgur.com/CTVAH.png
I believe, you want to display data in sorted order. hence, you should create a computed property.
computed:{
sortedData() {
return this.MyListings.map(item=>item).sort((a,b)=> a.id - b.id)
}
},
and use that computed property in template
<template>
<div id="app">
<div v-for="item in sortedData" :key="item.id">
{{item.id}}-{{item.title}}
</div>
</div>
</template>

Error in v-on handler: "TypeError: Cannot read property 'emailAddress' of undefined" VUEX

I'm having trouble figuring this out with the vuex store. I have a created a subscription form to be able to enter your email address and I want to make a post request to the backend api but the data is not showing when I console.log it. It only console.logs the item that has been dispatch to the store and when I'm trying to mutate the item to the state.emailAddress and try to get the data that to the POST I get an empty object. enter image description here
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
pizzaItems: [],
shopCart: [],
choosePizzaSize: [],
total: 0,
deliveryCharge: 0,
emailAddress: '',
},
getters: {
deliveryCharge: state => state.deliveryCharge,
pizzaItems: state => state.pizzaItems,
shopCart: state => state.shopCart,
choosePizzaSize: state => state.choosePizzaSize,
checkOut: state => state.total,
},
mutations: {
Add_To_Cart(state, item) {
const shopItem = state.shopCart.find(x => x.id === item.id && x.size === item.size);
if (shopItem) {
shopItem.count += item.count;
} else {
state.shopCart.push(item);
}
},
Price_Total(state, total) {
state.total = total;
},
fetchProducts(state, products) {
state.pizzaItems = products;
},
fetchPizzasize(state, size) {
state.choosePizzaSize = size;
},
free_Shipping(state, freeshipping) {
state.deliveryCharge = freeshipping;
},
subScribeMail(state, item) {
state.emailAddress = item;
},
},
actions: {
fetchData({ commit }) {
axios.get('http://localhost:3000/saltbageproducts').then((response) => {
commit('fetchProducts', response.data.products);
commit('fetchPizzasize', response.data.pizzasize[0].pizzaSize);
})
.catch((err) => {
console.log(err);
});
},
addTocart(context, item) {
context.commit('Add_To_Cart', item);
},
priceCalculation(context, item) {
context.commit('Price_Total', item);
},
changeFreeshippingStatus(context, shipping) {
context.commit('free_Shipping', shipping);
},
emailCheckUp(context, item, state) {
context.commit('subScribeMail', item);
axios.post('http://localhost:3000/saltbagehome', {
email: state.emailAddress,
}).then((reponse) => {
console.log(reponse.data);
}).catch((err) => {
console.log(err);
});
},
},
modules: {
},
});
Acoording Vuex documentation;
Register actions on the store. The handler function receives a context
object that exposes the following properties:
{
state, // same as `store.state`, or local state if in modules
rootState, // same as `store.state`, only in modules
commit, // same as `store.commit`
dispatch, // same as `store.dispatch`
getters, // same as `store.getters`, or local getters if in modules
rootGetters // same as `store.getters`, only in modules
}
And also receives a second payload argument if there is one.
Then in your first parameter of your action u have the vuex context, there should be:
emailCheckUp(context, item) { ... }
instead of
emailCheckUp(context, item, state) { ... }
now to access to vuex context inside of your action, just:
emailCheckUp(context, item) {
context.commit('subScribeMail', item);
...
email: context.state.emailAddress,
...
}
In practice, they often use ES2015 argument destructuring to simplify the code a bit:
emailCheckUp({commit, state}, item) {
commit('subScribeMail', item);
...
email: state.emailAddress,
...
}

How can I return each value from my for-each loop in javascript

I'm using Bootstrap vue table with contentful's API and could use some help with my code. I'm attempting to use a for loop to iterate over an array and get the property values. The console.info(episodes); call prints out each iteration for the var episodes, but now how do I bind this to my variable episodes. Using return only returns one result even outside of the for each loop. Any help or suggestions on another implementation is greatly appreciated. Full Template below.
<template>
<div>
<h1>Bootstrap Table</h1>
<b-table striped responsive hover :items="episodes" :fields="fields"></b-table>
</div>
</template>
<style>
</style>
<script>
import axios from "axios";
// Config
import config from "config";
// Vuex
import store from "store";
import { mapGetters, mapActions } from "vuex";
// Services
import { formatEntry } from "services/contentful";
// Models
import { entryTypes } from "models/contentful";
// UI
import UiEntry from "ui/Entry";
import UiLatestEntries from "ui/LatestEntries";
const contentful = require("contentful");
const client = contentful.createClient({
space: "xxxx",
environment: "staging", // defaults to 'master' if not set
accessToken: "xxxx"
});
export default {
name: "contentful-table",
data() {
return {
fields: [
{
key: "category",
sortable: true
},
{
key: "episode_name",
sortable: true
},
{
key: "episode_acronym",
sortable: true
},
{
key: "version",
sortable: true
}
],
episodes: []
};
},
mounted() {
return Promise.all([
// fetch the owner of the blog
client.getEntries({
content_type: "entryWebinar",
select: "fields.title,fields.description,fields.body,fields.splash"
})
])
.then(response => {
// console.info(response[0].items);
return response[0].items;
})
.then(response => {
this.episodes = function() {
var arrayLength = response.length;
var episodes = [];
for (let i = 0; i < arrayLength; i++) {
// console.info(response[i].fields.title + response[i].fields.splash + response[i].fields.description + response[i].fields.body );
var episodes = [
{
category: response[i].fields.title,
episode_name: response[i].fields.splash,
episode_acronym: response[i].fields.description,
version: response[i].fields.body
}
];
// episodes.forEach(category => episodes.push(category));
console.info(episodes);
}
return episodes;
};
})
.catch(console.error);
}
};
</script>
You can use the map method on the response array to return all the elements.
In your current example you keep re-setting the episodes variable, instead of the push() you actually want to do. The map method is still a more elegant way to solve your problem.
this.episodes = response.map((item) => {
return {
category: item.fields.title,
episode_name: items.fields.splash,
episode_acronym: item.fields.description,
version: item.fields.body
}
})
You can update the last then to match the last then below
]).then(response => {
return response[0].items;
})
.then((response) => {
this.episodes = response.map((item) => {
return {
category: item.fields.title,
episode_name: items.fields.splash,
episode_acronym: item.fields.description,
version: item.fields.body
};
});
})
.catch(console.error)
You do have an unnecessary second then, but I left it there so that you could see what I am replacing.

vue.js component not updated after vuex action on another component

I've a component which render a booking table; When I update my store in another component, the table isn't updated (but the store does and so the computed properties; My guess is that the problem is related to the filter not being updated but I'm not sure at all.
To do so, I've a vuex store:
...
const store = new Vuex.Store({
state: {
bookings: [],
datesel: '',
},
getters: {
bookings: (state) => {
return state.bookings
},
},
mutations: {
SET_BOOKINGS: (state, bookings) => {
state.bookings = bookings
},
},
actions: {
setBookings: ({commit, state}, bookings) => {
commit('SET_BOOKINGS', bookings)
return state.bookings
},
}
})
export default store;
The table is basically a v-for with a filter:
<template v-for="booking in getBookings( heure, terrain )">
Where getBookings is a method:
getBookings(hour, court) {
return this.$store.state.bookings.filter(booking => booking.heure == hour && booking.terrain == court);
}
I've another component which will update my bookings state through a method:
bookCourt() {
axios.post('http://localhost/bdcbooking/public/api/reservations/ponctuelles',
{
date: this.datesel,
membre_id: '1',
heure: this.chosenHour,
terrain: this.chosenCourt,
saison_id: '1'
})
.then(response => {
// JSON responses are automatically parsed.
console.log(response.data);
})
.catch(e => {
this.errors.push(e)
})
axios.get('http://localhost/bdcbooking/public/api/getReservationsDate?datesel=' + this.datesel)
.then(response => {
// JSON responses are automatically parsed.
console.log(response.data);
this.bookings = response.data;
})
.catch(e => {
this.errors.push(e)
})
$(this.$refs.vuemodal).modal('hide');
}
While this.bookings is a computed property:
computed: {
bookings: {
get () {
return this.$store.getters.bookings
},
set (bookings) {
return this.$store.dispatch('setBookings', bookings)
console.log('on lance l action');
}
}
}
Your table is not updated because getBookings is a simple method and hence the method won't be fired again based on vuex state changes.
You can make this getBookings method as an computed property that returns filtered results and will also upadte on state changes.

Problem detaching listener from Firestore nested / sub-collection

My scenario is a chat app with the following setup in Firestore
channels (collection)
id (doc)
messages (collection)
{channelObj}
id (doc)
messages (collection)
{channelObj}
etc
I've successfully attached a listener to the sub collection messages although I am having trouble detaching that listener, so when I switch from and to chat channels I get duplicate entries as the listeners keep stacking.
Here's the script block from my vue file
<script>
import firestore from 'firebase/firestore'
import { mapGetters } from 'vuex'
import SingleMessage from './SingleMessage'
import MessageForm from './MessageForm'
export default {
name: 'messages',
components: {
SingleMessage,
MessageForm,
},
data() {
return {
channelsRef: firebase.firestore().collection('channels'),
messages: [],
channel: '',
unsubscribe: null
}
},
computed: {
...mapGetters(['currentChannel']),
},
watch: {
currentChannel: async function(newValue, oldValue) {
this.messages = []
oldValue &&
await this.detachListeners(newValue, oldValue)
await this.unsubscribe
await this.timeout(2000)
await this.addListeners(newValue)
},
},
methods: {
addListeners(newValue) {
this.channelsRef
.doc(newValue.id)
.collection('messages')
.onSnapshot(snapshot => {
snapshot.docChanges().forEach(change => {
if (change.type == 'added') {
let doc = change.doc
this.messages.push({
id: doc.id,
content: doc.data().content,
timestamp: doc.data().timestamp,
user: doc.data().user,
})
}
})
})
//
console.log('[addListeners] channel:', newValue.id)
},
detachListeners(newValue, oldValue) {
this.unsubscribe =
this.channelsRef
.doc(oldValue.id)
.collection('messages')
.onSnapshot(() => {})
//
console.log('[detachListeners] channel:', oldValue.id)
},
timeout(ms) {
console.log('waiting...')
return new Promise(resolve => setTimeout(resolve, ms));
},
},
}
</script>
As you can see I am using a Vue watcher to monitor when the channel changes. To clarify, the console.log are firing with the correct doc ids so it should be targeting correctly. I tried using asynchronous code to await the detach but that does not work.
The docs advising saving the detach code to a variable and calling that, which I am now doing in my watch block. When console logging that it says this
ƒ () {
asyncObserver.mute();
firestoreClient.unlisten(internalListener);
}
So I am a bit lost here, seems I am targeting the right collection with the right method for unlistening ... any other steps I can take to debug?
You have to store in data the function returned by the onSnapshot() method and call this function in order to detach the listener.
In your existing code you are indeed declaring an unsubscribe object in data but you are not correctly assigning to it the function returned by the onSnapshot() method (you should do that in the addListeners() method) and you are not calling it correctly (you do this.unsubscribe instead of this.unsubscribe()).
I've not reproduced your full case, since it implies a Vuex store and some extra components but you will find below a similar code that demonstrates how it works (my settings are a bit different than yours -I use require("../firebaseConfig.js"); and fb.db.collection(channel)- but you'll easily get the philosophy!):
<template>
<div>
<input v-model="currentChannel" placeholder="Enter Current Channel">
<p>CurrentChannel is: {{ currentChannel }}</p>
<div class="messagesList">
<li v-for="m in messages">{{ m.name }}</li>
</div>
</div>
</template>
<script>
const fb = require("../firebaseConfig.js");
export default {
data() {
return {
messages: [],
currentChannel: null,
listener: null //We store the "listener function" in the object returned by the data function
};
},
watch: {
currentChannel: function(newValue, oldValue) {
this.messages = [];
if (this.listener) {
this.listener(); //Here we call the "listener function" -> it detaches the current listener
this.addListeners(newValue);
} else {
this.addListeners(newValue);
}
}
},
methods: {
addListeners(channel) {
this.listener = fb.db.collection(channel).onSnapshot(snapshot => {
snapshot.docChanges().forEach(change => {
if (change.type == "added") {
let doc = change.doc;
this.messages.push({
id: doc.id,
name: doc.data().name
});
}
});
});
}
}
};
</script>
<style>
.messagesList {
margin-top: 28px;
}
</style>
So, if we try to apply this approach to your code, the modified code would be as follows:
<script>
import firestore from 'firebase/firestore'
import { mapGetters } from 'vuex'
import SingleMessage from './SingleMessage'
import MessageForm from './MessageForm'
export default {
name: 'messages',
components: {
SingleMessage,
MessageForm,
},
data() {
return {
channelsRef: firebase.firestore().collection('channels'),
messages: [],
channel: '',
unsubscribe: null
}
},
computed: {
...mapGetters(['currentChannel']),
},
watch: {
currentChannel: function(newValue, oldValue) {
this.messages = [];
if (this.unsubscribe) {
this.unsubscribe();
this.addListeners(newValue);
} else {
this.addListeners(newValue);
}
}
},
methods: {
addListeners(newValue) {
this.unsubscribe = this.channelsRef
.doc(newValue.id)
.collection('messages')
.onSnapshot(snapshot => {
snapshot.docChanges().forEach(change => {
if (change.type == 'added') {
let doc = change.doc
this.messages.push({
id: doc.id,
content: doc.data().content,
timestamp: doc.data().timestamp,
user: doc.data().user,
});
}
});
});
console.log('[addListeners] channel:', newValue.id)
}
}
}
</script>

Categories