Using Vue Composition API with Firebase: Part II

Aaron K Saunders - Nov 2 '19 - - Dev Community

Please checkout and subscribe to my video content on YouTube. Feel free to leave comments and suggestions for what content you would like to see.
YouTube Channel

Overview

We are following up on the previous blog post that showed hoe to encapsulate the Firebase authentication functionaly using the new vue composition functionality. In this blog post we will show how to get a document collection and how to get and delete documents from a Firebase Firestore Database.

Video Series Playlist

https://www.youtube.com/playlist?list=PL2PY2-9rsgl2bgNTX9omlDisiWh1NYInz

We are assuming there is a basic understanding of VueJS and Firebase. Addition information on setting up you application to use the Firebase Javascript SDK can be found at this link

Please take a look at my video here VueJS Composition API Sample App w/video to get started with understanding the VueJS Composition API if you are not already familiar

Composition Functions

Since there is an assumption there is already an awareness of how Firebase works, we are focusing in this blog post on how one could isolate that functionality using vue composition functions to clean up their vue components.

The code provided below is pretty well documented and for the most part we are returning/exposing reactive properties and functions to support interacting with a Firebase Firestore Database in a consistent manner.

Source Code for use-collections.js

import { toRefs, reactive, onMounted } from "@vue/composition-api";
import firebase from "firebase";
// Required for side-effects
import "firebase/firestore";

/**
 *
 * @param { String } collectionName name of the desired collection
 * @param { object } queryOptions
 * @param { boolean | undefined } queryOptions.onMounted if true run query on mount
 * @param { string | undefined } queryOptions.query query string, see firebase documentation
 * @param { string | undefined } queryOptions.orderBy order results,  string, see firebase documentation
 * @param { number | undefined } queryOptions.limit number of object to return,  string, see firebase documentation
 */
export default function(collectionName, queryOptions) {
  let state = reactive({
    // error if one happens
    error: null,
    // the results of the query
    collectionData: {},
    // if the query is loading or ot
    loading: false
  });

  // get the database
  let db = firebase.firestore();

  /**
   * there is the option to load the query when the component
   * is mounted, you need to set the option in the `queryOptions`
   * params that you pass in
   *
   */
  onMounted(() => {
    queryOptions && (queryOptions.onMount && getCollection());
  });

  /**
   *
   * @param { object } queryOptions
   * @param { boolean | undefined } queryOptions.onMounted
   * @param { string | undefined } queryOptions.query
   * @param { string | undefined } queryOptions.orderBy
   * @param { number | undefined } queryOptions.limit
   */
  const getCollection = ({ query, orderBy, limit } = queryOptions) => {
    state.loading = true;
    state.error = null;

    let resultArray = [];
    let theQuery = query
      ? db.collection(collectionName).where(_query)
      : db.collection(collectionName);

    theQuery = limit ? theQuery.limit(limit) : theQuery;
    theQuery = orderBy ? theQuery.orderBy(orderBy) : theQuery;

    theQuery
      .get()
      .then(querySnapshot => {
        querySnapshot.forEach((doc)=> {
          resultArray.push({ id: doc.id, ...doc.data() });
        });
        state.collectionData = resultArray;
        state.error = null;
      })
      .catch((error) => {
        console.log("Error getCollection: ", error);
        state.error = error;
      })
      .finally(() => {
        state.loading = false;
      });
  };

  return {
    ...toRefs(state),
    'getCollection' : getCollection
  };
}

Enter fullscreen mode Exit fullscreen mode

Source Code for use-document.js

import { toRefs, reactive, onMounted } from "@vue/composition-api";
import firebase from "firebase";
// Required for side-effects
import "firebase/firestore";

/**
 *
 * @param { String } collectionName name of the desired collection
 * @param { object } queryOptions
 * @param { boolean | undefined } queryOptions.onMounted if true run query on mount
 * @param { string | undefined } queryOptions.documentId query string, see firebase documentation
 */
export default function(collectionName, queryOptions) {
  let state = reactive({
    // error if one happens
    error: null,
    // the results of the query
    documentData: {},
    // if the query is loading or ot
    loading: false
  });

  // get the database
  let db = firebase.firestore();

  /**
   * there is the option to load the query when the component
   * is mounted, you need to set the option in the `queryOptions`
   * params that you pass in
   *
   */
  onMounted(() => {
    queryOptions &&
      (queryOptions.onMount && getDocument(queryOptions.documentId));
  });

  const deleteDocument = _documentId => {
    state.loading = true;
    state.error = null;
    db.collection(collectionName)
      .doc(_documentId)
      .delete()
      .then(() => {
        console.log("Document successfully deleted!");
        state.error = null;
        state.documentData = null;
      })
      .catch(error => {
        console.error("Error removing document: ", error);
        state.error = error;
        state.documentData = null;
      })
      .finally(() => {
        state.loading = false;
      });
  };

  const createDocument = _documentData => {
    state.loading = true;
    state.error = null;

    db.collection(collectionName)
      .add({
        ..._documentData,
        createdOn: firebase.firestore.FieldValue.serverTimestamp()
      })
      .then(docRef => {
        state.error = null;
        state.documentData.id = docRef.id;
      })
      .catch(function(error) {
        // The document probably doesn't exist.
        console.error("Error createDocument: ", error);
        state.error = error;
        state.documentData = null;
      })
      .finally(() => {
        state.loading = false;
      });
  };

  const updateDocument = _documentData => {
    state.loading = true;
    state.error = null;

    let data = { ..._documentData };
    delete data[id];

    db.collection(collectionName)
      .doc(_documentData.id)
      .update({
        ...data,
        updatedOn: firebase.firestore.FieldValue.serverTimestamp()
      })
      .then(() => {
        state.error = null;
        state.documentData = null;
      })
      .catch(function(error) {
        // The document probably doesn't exist.
        console.error("Error updating document: ", error);
        state.error = error;
        state.documentData = null;
      })
      .finally(() => {
        state.loading = false;
      });
  };

  /**
   *
   * @param { object } queryOptions
   * @param { boolean | undefined } queryOptions.onMounted
   * @param { string | undefined } queryOptions.documentId
   */
  const getDocument = documentId => {
    state.loading = true;
    state.error = null;

    db.collection(collectionName)
      .doc(documentId)
      .get()
      .then(doc => {
        if (doc.exists) {
          console.log("Document data:", doc.data());
          state.documentData = { id: doc.id, ...doc.data() };
          state.error = null;
        } else {
          // doc.data() will be undefined in this case
          console.log("No such document!: " + documentId);
          state.documentData(null);
          state.error = null;
        }
      })
      .catch(error => {
        console.log("Error getDocuent: ", error);
        state.error = error;
      })
      .finally(() => {
        state.loading = false;
      });
  };

  return {
    ...toRefs(state),
    getDocument: getDocument,
    createDocument,
    updateDocument,
    deleteDocument
  };
}
Enter fullscreen mode Exit fullscreen mode

Using use-collections and use-documents

Here is how we are using the vue composition functions in the component ThingsList.vue. When using the component, the only property passed in is collectionName which is the name of the collection to render and manipulate using the Vue Component.

<ThingList collectionName="things" />
Enter fullscreen mode Exit fullscreen mode

In the template section of ThingsList we are using the collectionData which came from the use-collections.js composition function to display the list from Firebase.

We are also using the getDocument function from the use-document.js composition function to load a specific document from firebase.

And finally inside of the local function deleteThing() we are using the deleteDocument also from the use-document.js vue composition function.

Source Code for: ThingsList.vue Template

<template>
  <div class="hello">
    <div v-for="item in collectionData" :key="item.id">
      <div>
        <button @click="deleteThing(item.id)">DELETE</button>
        &nbsp;
        <div @click="getDocument(item.id)">{{item.name}}&nbsp;{{item.createdOn}}</div>
      </div>
    </div>
    <template v-if="loading">
      <h2>Processing Request...</h2>
    </template>
    <template v-else>
      <button @click="addThing('New Item: ' +new Date())">ADD</button>
    </template>
    <div>
      <h3>Active Item</h3>
      {{documentData}}
    </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

For the script section of ThingsList, we are loading up the two vue composition functions and passing in the name of the collection to work with as a parameter. There are additional options that can be passed in, options are documented in the comments below and in the source code. The only one we are using is onMounted which if true will load the collection or load the document when the component is mounted.

let thingsCollectionProps = useThingsCollection(collectionName, {
  onMounted: false
});

let thingsDocumentProps = useThingsDocument(collectionName, {
  onMounted: false
});
Enter fullscreen mode Exit fullscreen mode

Then next, like all setup functions, we export the associated properties from the vue composition functions. I have documented them in the code below because we are using javascript destructuring to simplify the code.

return {
  // this returns all of the state information and the function from
  // the userThingsCollection
  //
  // error: error if one happens
  // collectionData: the results of the query
  // loading: if the query is loading or not
  // getCollection : function exposed to run query manually
  ...thingsCollectionProps,

  // this returns all of the state information and the function from
  // the useThingsDocument
  ...thingsDocumentProps,

  // catch errors from both composition functions
  error: thingsDocumentProps.error || thingsCollectionProps
};
Enter fullscreen mode Exit fullscreen mode

In the ThingsList component onMounted lifecycle we are loading the collection.

mounted() {
  this.getCollection(/*{ limit: 5 }*/);
}
Enter fullscreen mode Exit fullscreen mode

Source Code for: ThingsList.vue Script

<script>
// import useThings from "../use-things";
import useThingsCollection from "../use-collection";
import useThingsDocument from "../use-document";
export default {
  name: "ThingList",
  props: {
    collectionName: {
      type: String,
      required: true
    }
  },
  /**
   * pass in the name of the collection into the setup so
   * it can be passed on to the composition function
   */
  setup({ collectionName }) {
    let thingsCollectionProps = useThingsCollection(collectionName, {
      onMounted: false
    });

    let thingsDocumentProps = useThingsDocument(collectionName, {
      onMounted: false
    });
    return {
      // this returns all of the state information and the function from
      // the userThingsCollection
      //
      // error: error if one happens
      // collectionData: the results of the query
      // loading: if the query is loading or not
      // getCollection : function exposed to run query manually
      ...thingsCollectionProps,

      // this returns all of the state information and the function from
      // the useThingsDocument
      // 
      // error: error if one happens
      // documentData: the results of the query
      // loading: if the query is loading or not
      // createDocument : function exposed to run against collection
      // deleteDocument : function exposed to run against collection
      // addDocument : function exposed to run against collection
      ...thingsDocumentProps,

      // catch errors from both composition functions
      error: thingsDocumentProps.error || thingsCollectionProps.error
    };
  },
  methods: {
    addThing(_name) {
      this.createDocument({ name: _name });
    },
    deleteThing(_id) {
      this.deleteDocument(_id);
    }
  },
  mounted() {
    this.getCollection(/*{ limit: 5 }*/);
  }
};
</script>
Enter fullscreen mode Exit fullscreen mode

Conclusion

The Vue Composition API is a pretty interesting addition to VueJS 3.0 release and I think it provides similar functionality to react-hooks so vuejs developers should not feel like they are missing out on anything here.

if you find some errors/typos/mistakes or something isn't clear, please leave a comment below.

GitHub logo aaronksaunders / vue-composition-firebase-app-2

use the new VueJS Composition API to seperate out Firebase integration into a VueJS Application.

PART TWO: VueJS with Composition API and Ionic Framework Components

Overview

[Note] To view the Source Code for Part III of the series, Using Vue Composition API with Firebase & Vuex: Part III, click here

We are going to use the new VueJS Composition API to seperate out Firebase integration into a VueJS Application.

We will focus in this blog post on creating a hook, composition function to query collection in a firebase firestore database.

See complete blog post : Using Vue Composition API with Firebase: Part II follow me on dev.to


Aaron K Saunders's DEV Profile

See the original project that this application is based on:

About Clearly Innovative

Clearly Innovative is a minority-owned solutions provider that develops digital products. We shape ideas into viable products and transform client needs into enhanced technology solutions. As a leader in early adoption and implementation of cutting edge technologies, Clearly Innovative provides services focused on product strategy, user experience, design and development. According to CEO, Aaron Saunders "We are not just designers and developers, but end-to-end digital solution providers." Clearly Innovative has created a tech education program, Clearly Innovative Education, whose mission is to create a world where people from underrepresented backgrounds can have a seat at the digital table as creators, innovators and entrepreneurs.

#TheFutureIsWrittenInCode

The Future is Written in Code series, as part of Inclusive Innovation Incubator, provides introductory and advanced programming classes as well as coding courses with a focus on business and entrepreneurship. Select programming offered includes Coding, UI/UX, Coding & Business, Coding & Entrepreneurship, Business Canvassing, Entrepreneurship: Developing Your Idea into App, to name a few. Please contact info@in3dc.com to find out more!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .