Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add VueJS Composition API support to Vuefire #646

Closed
gartmeier opened this issue Mar 22, 2020 · 5 comments
Closed

Add VueJS Composition API support to Vuefire #646

gartmeier opened this issue Mar 22, 2020 · 5 comments

Comments

@gartmeier
Copy link

gartmeier commented Mar 22, 2020

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>
@gartmeier gartmeier changed the title Support VueJS Composition API add VueJS Composition API support to Vuefire Mar 22, 2020
@gartmeier gartmeier changed the title add VueJS Composition API support to Vuefire Add VueJS Composition API support to Vuefire Mar 22, 2020
@posva
Copy link
Member

posva commented 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

@electric-skeptic
Copy link

@torfeld6 are ./use-firebase and ./use-auth also available by any chance?
Thank you for your hard work on this, it looks like terrific progress toward having something working on Vue 3. I'm really keen to avoid going back to a VueFire-less life!

@gartmeier
Copy link
Author

gartmeier commented 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

@electric-skeptic
Copy link

@torfeld6 Thanks so much for sharing all of this - it's definitely an easier starting point than the track I've been wandering down trying to get this working!

@posva
Copy link
Member

posva commented Oct 13, 2022

The composition API is on the way 🚢 #1241

@posva posva closed this as completed Oct 13, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants