question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Add VueJS Composition API support to Vuefire

See original GitHub issue

I really liked the simplicity of using this.$bind and this.$rtdbBind this project provides. Recently, I started to use the Composition API for my projects and unfortunately, these functions do not make much sense since there is no single this.data reactive to bind firebase references to anymore.

Are you planning to add some sort of Composition API support to Vuefire anytime soon?

In the meantime, I came up with a workaround using the functions provided by vuefire-core libary and created some sort of Composition API wrapper for them. I hope it can be useful to some of you. I tried to find an existing solution, but what I found did not support both database types and made a distinction between binding documents and collections (Link).

use-firestore.js

import {bindCollection, bindDocument, walkSet,} from '@posva/vuefire-core'

const ops = {
  set: (target, key, value) => walkSet(target, key, value),
  add: (array, index, data) => array.splice(index, 0, data),
  remove: (array, index) => array.splice(index, 1),
}


const binds = []

export function useFirestore() {
  function bind(vm, key, ref, options) {
    return new Promise((resolve, reject) => {
      let unbind
      if ('where' in ref) {
        unbind = bindCollection(
          {
            vm,
            key,
            ops,
            collection: ref,
            resolve,
            reject,
          },
          options
        )
      } else {
        unbind = bindDocument(
          {
            vm,
            key,
            ops,
            document: ref,
            resolve,
            reject,
          },
          options
        )
      }

      binds.push({vm, key, ref, unbind})
    })
  }

  function unbind(vm, key, reset) {
    const i = binds.findIndex(u => u.vm == vm && u.key == key)

    if (i > -1) {
      binds[i](reset)
      binds.splice(i, 1)
    }
  }

  return {
    bind,
    unbind
  }

}

use-rtdb.js

import {rtdbBindAsArray as bindAsArray, rtdbBindAsObject as bindAsObject, walkSet} from '@posva/vuefire-core'

const ops = {
  set: (target, key, value) => walkSet(target, key, value),
  add: (array, index, data) => array.splice(index, 0, data),
  remove: (array, index) => array.splice(index, 1),
}

const binds = []

export function useRTDB() {
  function bind(vm, key, source, options) {
    return new Promise((resolve, reject) => {
      let unbind
      if (Array.isArray(vm[key])) {
        unbind = bindAsArray(
          {
            vm,
            key,
            collection: source,
            resolve,
            reject,
            ops,
          },
          options
        )
      } else {
        unbind = bindAsObject(
          {
            vm,
            key,
            document: source,
            resolve,
            reject,
            ops,
          },
          options
        )
      }

      binds.push({vm, key, source, unbind})
    })
  }

  function unbind(vm, key, reset) {
    const i = binds.findIndex(u => u.vm == vm && u.key == key)

    if (i > -1) {
      binds[i](reset)
      binds.splice(i, 1)
    }
  }

  return {
    bind,
    unbind
  }
}

use-meeting-store.js

import {createStore} from "pinia";
import {useFirebase} from "./use-firebase";
import {useRTDB} from "./use-rtdb";
import {useFirestore} from "./use-firestore";

export const useMeetingStore = createStore({
  id: 'meeting',
  state: () => ({
    id: null,
    meeting: null,
    participants: []
  }),
  getters: {
    availableSeats(state) {
      if (!isNaN(state.meeting?.maxParticipants)) {
        const audience = state.participants.filter(p => !p.host)
        return state.meeting.maxParticipants - audience.length
      }
    }
  },
  actions: {
    bind(id) {
      this.state.id = id

      return Promise.all([
        this.bindMeeting(),
        this.bindParticipants()
      ])
    },
    bindMeeting() {
      const {bind} = useFirestore()
      const {firebase} = useFirebase()

      return bind(
        this.state,
        'meeting',
        firebase
          .firestore()
          .collection('meetings')
          .doc(this.state.id)
      )
    },
    bindParticipants() {
      const {bind} = useRTDB()
      const {firebase} = useFirebase()

      return bind(
        this.state,
        'participants',
        firebase
          .database()
          .ref(`/participants/`)
          .orderByChild('meetingID')
          .equalTo(this.state.id)
      )
    },
    createParticipant() {
      return new Promise((resolve, reject) => {
        const auth = useAuthStore()
        const {firebase} = useFirebase()
        const database = firebase.database()

        const participantRef = database.ref(`/participants/${auth.state.uid}`)

        database.ref('.info/connected').on('value', snapshot => {
          if (snapshot.val() == false) {
            return
          }

          participantRef
            .onDisconnect()
            .remove()
            .then(() => participantRef.set({
              dateCreated: firebase.database.ServerValue.TIMESTAMP,
              meetingID: this.state.id
            }))
            .then(resolve)
            .catch(reject)
        })
      })
    }
  }
})

Splashscreen.vue

<template>
  <div>
    <div class="middle">
      <h1 class="ui header">
        JOIN
      </h1>
      <div class="ui tiny blue sliding indeterminate progress">
        <div class="bar"></div>
      </div>
    </div>
    <div class="bottom">
      <p>by</p>
      <img src="assets/pitcher-logo-COLOR-TM-300.png" alt="Pitcher AG">
    </div>
  </div>
</template>

<script>
  import {onMounted} from '@vue/composition-api'
  import {useAuth} from "../use-auth";
  import {useMeetingStore} from "../use-meeting-store";

  export default {
  name: 'Splashscreen',
  props: {
    authToken: {
      type: String
    },
    meetingID: {
      required: true,
      type: String
    }
  },
  setup(props, {root}) {
    const {login} = useAuth()
    const meeting = useMeetingStore()

    onMounted(async () => {
      await login(props.authToken)
      await meeting.bind(props.meetingID)

      if (meeting.availableSeats === 0) {
        return root.$router.push({name: 'full'})
      }

      await meeting.createParticipant()      
      root.$router.push({name: 'room'})
    })
  }
}
</script>

<style lang="less" scoped>
  .middle {
    left: 50%;
    position: fixed;
    top: 50%;
    transform: translate(-50%, -50%);

    .ui.header {
      font-size: 96px;
    }
  }

  .bottom {
    bottom: 35px;
    left: 0;
    position: fixed;
    right: 0;
    text-align: center;

    img {
      max-width: 150px;
    }

    p {
      margin-bottom: .5em;
    }
  }
</style>

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:10
  • Comments:5 (2 by maintainers)

github_iconTop GitHub Comments

3reactions
posvacommented, Mar 22, 2020

I don’t have plans right now to add it but it would make sense for Vue 3. Good news is, as you found out, the core is ready for this change because it binds data to the property of an object. Which means, it could easily work by receiving a Ref and binding to its value property, or to a Reactive object and a provided key property to bind to that property. I think that the next version of vuefire should reunite vuexfire and vuefire together and expose lower level functions that are in core

2reactions
gartmeiercommented, Sep 17, 2020

Hi @electric-skeptic,

the project has changed quite a bit since I last posted March 22. I can recommend having a look at the guten-abend project.

The functions I created for the RTDB:

useRef.js

import firebase from 'firebase/app'
import 'firebase/database'

import {computed, reactive, toRefs} from '@vue/composition-api'

export default function useRef() {
  const database = firebase.database()

  const state = reactive({
    loading: true,
    exists: false,
    val: null,
    docs: computed(() => {
      if (state.val) {
        const res = []

        for (const key in state.val) {
          if (state.val.hasOwnProperty(key)) {
            res.push({
              ...state.val[key],
              key
            })
          }
        }

        return res
      } else {
        return []
      }
    }),
    path: null
  })

  async function loadByPath(path, filter = {}) {
    let ref = database.ref(path)

    for (const key in filter) {
      ref = ref.orderByChild(key).equalTo(filter[key])
    }

    const snapshot = await ref.once('value')
    state.exists = snapshot.exists()
    state.loading = false
    state.path = path
    state.val = snapshot.val()

    return state.exists
  }

  return {
    ...toRefs(state),
    loadByPath
  }
}

useLiveRef.js

import firebase from 'firebase/app'
import 'firebase/database'

import {computed, reactive, toRefs} from '@vue/composition-api'
import Vue from "vue";

const state = reactive({})

export default function useLiveRef(path, filter = {}) {
  if (!state[path]) {
    const database = firebase.database()
    let ref = database.ref(path)

    for (const key in filter) {
      ref = ref.orderByChild(key).equalTo(filter[key])
    }

    Vue.set(state, path, {})

    ref.on('child_added', data => {
      Vue.set(state[path], data.key, data.val())
    });

    ref.on('child_changed', data => {
      Vue.set(state[path], data.key, data.val())
    });

    ref.on('child_removed', data => {
      Vue.delete(state[path], data.key)
    });
  }

  const docs = computed(() => {
    if (!state[path]) {
      return []
    }

    return Object.keys(state[path]).map((key) => ({
      ...state[path][key],
      id: key
    }))
  })

  return {
    ...toRefs(state[path]),
    docs
  }
}

I prefer guten-abend’s syntax. You can use the documents directly in the component or wrap them in another function (i.e. useMeeting or useParticipants).

Screenshot 2020-09-17 at 18 20 35

I went back to just having a simple firebase.js initializing and exporting the components I need. I believe the use-functions are only necessary for view-states. But I’m still trying to figure out how to structure and divide things in Vue 3.

firebase_js

source

Kind Regards Joshua

Read more comments on GitHub >

github_iconTop Results From Across the Web

Upgrading to VueFire v3
VueFire v3 is built on top of the Composition API to provide an idiomatic way to use Firebase with Vue.js that works with...
Read more >
A Deep Dive into the Vue3 Composition API | by John Philip
The Option API enables us to create vue components by declaring the data, methods, created and lifecycle hooks in components structure.
Read more >
Using Vue Composition API with Firebase - DEV Community ‍ ‍
Overview We are going to use the new VueJS Composition API to separate out Firebase integr... Tagged with firebase, vue, javascript, webdev.
Read more >
Investing in Vue, Nuxt, and VueFire - The Firebase Blog
https://v3.vuefire.vuejs.org/ ... Full support for the Vue 3 Composition API while maintaining compatibility for the Options API.
Read more >
VueFire: CRUD Application with Vue.js and Firebase - YouTube
VueFire : CRUD Application with Vue.js and FirebaseWant to learn how to integrate Firebase and Vuefire into your Vue.js applications?
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found