2

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.

Opening 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: Two products with identical ID's

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?

Diego
  • 349
  • 2
  • 16
  • So what is your actual question again? – Tallboy Sep 21 '20 at 06:58
  • 1
    Do I understand right, that you always want to have an up to date amount of available products? – sandrooco Sep 21 '20 at 07:00
  • 1
    At the moment, your code is very imperative, check these docs: https://firebase.google.com/docs/firestore/query-data/listen This allows you to value changes (in firestore) and then re-set your store data. – sandrooco Sep 21 '20 at 07:02
  • @Tallboy: How to get live Updates. – Diego Sep 21 '20 at 08:25
  • @sandrooco exactly this, and some other fields. so you think just going for the onSnapshot works here? – Diego Sep 21 '20 at 08:27
  • 1
    Yes that's probably the easiest solution. It's unlikely that you can just change from `.get()` to `.onSnapshot` - but I think you can handle a few line changes. :) – sandrooco Sep 21 '20 at 09:02

1 Answers1

0

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'),
  },
})
nickh
  • 279
  • 3
  • 12
  • this would only subscribe to *one* collection. My problem are the two collections at the same time in one array. You can clearly see this in the question, when I call prodColl.get() and catchall.get() – Diego Sep 22 '20 at 08:51
  • Yes, I'm sorry, you are definitely right. Before I edit my question, I would like to note that when you have every element of an array being shown `v-for="catOrProduct in catOrProducts"`, Vue [won't display](https://vuejs.org/v2/guide/reactivity.html#For-Arrays) a reactive array even when the binding works properly. You will have to setup a `watcher` or do `$forceUpdate()` to ensure that your data is up to date. Don't rely on what is displayed on screen before this is fixed. Rely on your [DevTools extension](https://github.com/vuejs/vue-devtools) to see live updates. – nickh Sep 22 '20 at 10:06