Vue js - reload component on database insert - firebase

I have the following setup
Component.vue (display db collections as grid in main page)
...
<v-flex v-for="i in items" :key="i.id" xs6 sm3 md3>
<v-card color="primary">
<v-card-text>
<h2
class="font-weight-regular mb-4"
>
{{ i.description }}
</h2>
</v-card-text>
</v-card>
</v-flex>
...
<script>
import { db } from '~/plugins/firebase.js'
export default {
data: () => ({
items: []
}),
props: ['reload'],
watch: {
reload: function (newVal, oldVal) {
this.items = items
alert('changed reload')
}
},
methods: {
firestore() {
db.collection('items')
.get()
.then(querySnapshot => {
const items = []
querySnapshot.forEach(function(doc) {
const item = doc.data()
item.id = doc.id
items.push(useritem)
})
this.items = items
})
.catch(function(error) {
alert('Error getting documents: ' + error)
})
}
}
}
</script>
index.vue (main page that has grid component and button to add new collection)
....
<v-layout mb-4>
<v-btn
#click="submit"
>
Add Item
</v-btn>
</v-layout>
<v-layout mb-4>
<component :reload="reload" />
</v-layout>
....
<script>
import { db } from '~/plugins/firebase.js'
import component from '~/components/Component.vue'
import moment from 'moment'
export default {
components: {
component
},
data() {
return {
description: 'test',
date: moment(),
reload: false
}
},
methods: {
submit() {
db.collection('items')
.add({
description: this.description,
deadline: new Date(moment(this.date)),
status: true
})
.then(docRef => {
this.reload = true
})
.catch(error => {
alert('Error adding document: ', error)
})
}
}
}
</script>
As can be seen, I've added a prop to the component to sort of trigger a reload of data from database whenever a new item is added on the main page using the button.
On successful insert the value changes from false to true. However the component grid does not reload. Refreshing the page shows the new item in grid.
How can i make the component reactive or trigger reload on addition of new item?

In your firestore method in Component.vue, you are using the get method which according to the firestore documentation, only retrieves the data once, it doesn't listen to any change, you'd have to refresh your page to see your updated changes.
However, to listen to changes to your firestore DB and update accordingly on your website, you have to set a listener, Cloud Firestore sends your listener an initial snapshot of the data, and then another snapshot each time the document changes.
methods: {
firestore() {
db.collection("items").onSnapshot(
snapshot => {
const documents = snapshot.docs.map(doc => {
const item = doc.data();
item.id = doc.id;
return item;
});
this.items = documents;
},
error => {
// handle errors
alert("Error getting documents: " + error);
}
);
}

Related

How to populate text-fields from pinia store state without changing the rendered values from other components?

hope you're well!
I have a Vue 3 app using Pinia + Vuetify 3. I've defined a "client" store and a component that, upon render, will call a store action that calls my backend API and sets my client state (JSON) with the result.
clientStore.js:
export const useClientStore = defineStore('clients', {
state: () => ({
//Loading state and client(s)
loading: false,
clients: [],
client: {}
}),
getters: {
//Get all clients
getClients(state) {
return state.clients
},
//Get one client
getClient(state) {
return state.client
}
},
actions: {
//Get one client
async fetchClient(clientId) {
try {
this.loading = true
const data = await axiosConfig.get('/clients/' + clientId)
this.client = data.data
this.loading = false
} catch (error) {
this.loading = false
console.log("Error fetching client: " + clientId)
},
//snipped
I have a computed property that returns the client from the store and render them as follows:
Component.vue:
<template>
<div class="text-center py-5">
<div class="text-h4 font-weight-bold">{{ client.name }}</div>
</div>
<div class="d-flex justify-space-between">
<div class="text-h5">Description</div>
<v-btn #click="dialog = true" prepend-icon="mdi-cog" color="primary">Edit</v-btn>
</div>
<v-textarea class="py-5" :value="client.description" readonly auto-grow outlined>{{ client.description
}}</v-textarea>
<updateClient v-model="dialog" />
</template>
<script setup>
import updateClient from '#/components/clients/updateClient.vue'
import { useClientStore } from '#/store/clients'
import { computed, onMounted, ref } from 'vue';
import { useRoute } from 'vue-router'
const store = useClientStore()
const route = useRoute()
const dialog = ref(false)
const client = computed(() => {
return store.client
})
onMounted(() => {
store.fetchClient(route.params.clientId)
})
</script>
My aim is to make an "EDIT" component - a popup dialog - that takes the client state values and pre-populate them in my text fields and upon changing the values, submit and PATCH the client in the backend.
updateClient.vue
<template>
<v-dialog max-width="500">
<v-card class="pa-5">
<v-card-title>Edit client</v-card-title>
<v-text-field label="Name" v-model="client.name"></v-text-field>
<v-textarea label="Description" v-model="client.description"></v-textarea>
<v-btn block outlined color="primary" #click="updateClient">Update Client</v-btn>
</v-card>
</v-dialog>
</template>
<script setup>
import { useClientStore } from '#/store/clients'
import {computed} from 'vue'
const store = useClientStore()
const client = computed(() => {
return store.client
})
</script>
Problem is when I edit the pre-populated values in the fields, it changes the values outside the dialog as seen in the video and stay changed even after closing the pop-up. Ideally I'd like the values in my Component.vue to be static and have my state values unaltered. How can this be solved?
Thanks!
When you bind client.name to a text field in "Edit component", you directly change values stored in pinia. This, by design, changes values in your "View component".
A simple answer is... just create a copy of the object.
Now, I know, I know... there is a reason why you used computed properties in both places. Because you're waiting on the server to return the initial values.
The easiest way to solve this is to create a copy of the client object in pinia store. Then, just use copy of the object for text field binding in "Edit component".
state: () => ({
//Loading state and client(s)
loading: false,
clients: [],
client: {},
clientEdit: {} // Make changes to this object instead
})
In api response
actions: {
//Get one client
async fetchClient(clientId) {
try {
this.loading = true
const data = await axiosConfig.get('/clients/' + clientId)
this.client = data.data
this.clientEdit = { ...this.client } // Copy client object
this.loading = false
} catch (error) {
this.loading = false
console.log("Error fetching client: " + clientId)
},
}

How to use Firestore in Nuxt3 with SSR?

I am using Nuxt RC8 combined with Firestore.
My goal is to make the firestore request SSR and then combine it with Firestore's onSnapshot to get realtime updates after hydration is done.
I have created this composable useAssets:
import { computed, ref } from 'vue';
import { Asset, RandomAPI, RandomDatabase } from '#random/api';
/**
* Asset basic composable
* #param dbClient Database client
* #param options Extra options, like live data binding
*/
export function useAssets(dbClient: RandomDatabase) {
const assets = ref([]);
const unsubscribe = ref(null);
const searchQuery = ref('');
const randomAPI = RandomAPI.getInstance();
async function fetchAssets(options?: { live: boolean }): Promise<void> {
if (options?.live) {
try {
const query = randomAPI.fetchAssetsLive(dbClient, (_assets) => {
assets.value = _assets as Asset<any>[];
});
unsubscribe.value = query;
} catch (error) {
throw Error(`Error reading assets: ${error}`);
}
} else {
const query = await randomAPI.fetchAssetsStatic(dbClient);
assets.value = query;
}
}
const filteredAssets = computed(() => {
return searchQuery.value
? assets.value.filter((asset) =>
asset.name.toLowerCase().includes(searchQuery.value.toLowerCase())
)
: assets.value;
});
function reverseAssets(): void {
const newArray = [...assets.value];
assets.value = newArray.reverse();
}
return {
assets,
fetchAssets,
filteredAssets,
searchQuery,
reverseAssets,
unsubscribe,
};
}
The randomAPI.fetchAssetsLive comes from the firestore queries file:
export function fetchAssetsLive({
db,
callback,
options,
}: {
db: Firestore;
callback: (
assets: Asset<Timestamp>[] | QueryDocumentSnapshot<Asset<Timestamp>>[]
) => void;
options?: { fullDocs: boolean };
}): Unsubscribe {
const assetCollection = collection(db, 'assets') as CollectionReference<
Asset<Timestamp>
>;
if (options?.fullDocs) {
return onSnapshot(assetCollection, (querySnapshot) =>
callback(querySnapshot.docs)
);
}
// Return unsubscribe
return onSnapshot(assetCollection, (querySnapshot) =>
callback(querySnapshot.docs.map((doc) => doc.data()))
);
}
And then the component:
<template>
<div>
<h1>Welcome to Random!</h1>
<Button #click="reverseAssets">Reverse order</Button>
<ClientOnly>
<!-- <Input name="search" label="Search for an asset" v-model="searchQuery" /> -->
</ClientOnly>
<ul>
<li class="list-item" v-for="asset in assets" :key="asset.name">
Asset Name: {{ asset.name }} Type: {{ asset.type }}
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { Button, Input } from '#random/ui';
import { useNuxtApp, useAsyncData } from '#app';
const { $randomFirebase, $firestore, $getDocs, $collection } = useNuxtApp();
const { fetchAssets, filteredAssets, searchQuery, reverseAssets, assets } =
useAssets($randomFirebase);
// const a = process.client ? filteredAssets : assets;
onMounted(() => {
// console.log(searchQuery.value);
// fetchAssets({ live: true });
});
watch(
assets,
(val) => {
console.log('watcher: ', val);
},
{ deep: true, immediate: true }
);
// TODO: make SSR work
await useAsyncData(async () => {
await fetchAssets();
});
</script>
Why is it only loading via SSR and then assets.value goes []? Refreshing the page retrieves renders the items correctly but then once hydration comes in, it's gone.
Querying both, in onMounted and useAsyncData, makes it send correctly via SSR the values, makes it work client-side too but there is still a hydration missmatch, even being the values the same. And visually you only see the ones from the client-side request, not the SSR.
Is there a better approach? What am I not understanding?
I don't want to use firebase-admin as the SSR query maker because I want to use roles in the future (together with Firebase Auth via sessions).
I solved the hydration issue in two ways:
By displaying in the template only specific information, since JS objects are not ordered by default so there could be different order between the SSR query and the CS query.
By ordering by a field name in the query.
By making sure that the serverData is displayed until first load of the onsnapshot is there, so theres is not a mismatch this way: [data] -> [] -> [data]. For now I control it in the template in a very cheap way but it was for testing purposes:
<li class="list-item" v-for="asset in (isServer || (!isServer && !assets.length) ? serverData : assets)" :key="asset.name">
Asset Name: {{ asset.name }} Type: {{ asset.type }}
</li>
By using /server/api/assets.ts file with this:
import { getDocs, collection, query, orderBy, CollectionReference, Timestamp, Query } from 'firebase/firestore';
import { Asset } from '#random/api/dist';
import { firestore } from '../utils/firebase';
export default defineEventHandler(async (event) => {
const assetCollection = collection(firestore, 'assets');
let fullQuery: CollectionReference<Asset<Timestamp>> | Query<Asset<Timestamp>>;
try {
// #ts-ignore
fullQuery = query(assetCollection, orderBy('name'));
} catch (e) {
console.error(e)
// #ts-ignore
fullQuery = assetCollection;
}
const ref = await getDocs(fullQuery);
return ref.docs.map((doc) => doc.data());
});
And then in the component, executing:
const { data: assets } = useFetch('/api/assets');
onMounted(async () => {
fetchAssets({ live: true });
});
Still, if I try via useAsyncData it does not work correctly.

Vue.js - Invalid prop: type check failed for prop "src". Expected String, Object, got Promise

I'm trying to get an image inserted into my firebase's firestore and storage and display it on a v-card
my v-card code:
<v-row>
<v-col cols="3" v-for="massage in massages" :key="massage.id">
<v-card
class="mx-auto"
max-width="400"
>
<v-img
v-if="massage.image"
class="white--text align-end"
height="200px"
:src="massage.image"
>
</v-img>
<v-card-title>{{massage.title}}</v-card-title>
<v-card-text class="text--primary">
<div>{{massage.shortDescription}}</div>
</v-card-text>
<v-card-actions>
<v-btn
color="orange"
text
#click="goTodetail(massage.id)"
>
Explore
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
my script:
<script>
import{ db, storage} from '#/firebase.js';
export default {
el: '#vue',
name: 'BaseHeading',
// massId:this.$route.params.Pid,
components: {
BaseInfoCard: () => import('#/components/base/InfoCard'),
},
data() {
return{
massages:[],
showmassage: false,
showrehabilitation: false,
showsupport: false,
modal_1: true,
modal_2: false,
modal_3: false,
}
},
created() {
try{
db.collection("massages").get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
let img = ''
if(doc.data().image){
img = storage.ref().child(doc.data().image).getDownloadURL()
}
this.massages.push({
id: doc.id,
title: doc.data().title,
shortDescription: doc.data().shortDescription,
image: img
})
})
});
}catch(e){
console.log(e)
}
},
}
</script>
I think it provides promise but cannot figure out how to deal with it. The error is Invalid prop: type check failed for prop "src". Expected String, Object, got Promise.
I tried to put the following in the props:
props: {
src: [ String, Object],
},
but I still have the same error
Resolve the Promise when retrieving the image URL, before you pass it into your massage object.
created() {
try {
db.collection('massages')
.get()
.then((querySnapshot) => {
querySnapshot.forEach(async (doc) => {
// start pertinent change
if (doc.data().image) {
storage
.ref()
.child(doc.data().image)
.getDownloadURL()
.then((url) => {
this.massages.push({
id: doc.id,
title: doc.data().title,
shortDescription: doc.data().shortDescription,
image: url,
})
})
} else {
this.massages.push({
id: doc.id,
title: doc.data().title,
shortDescription: doc.data().shortDescription,
})
}
// end pertinent change
})
})
} catch (e) {
console.log(e)
}
}

how to display spinner when data is being fetched from cloud firestore in vuejs?

i'm working on firebase and vuejs with vuex as well. so in onauthStateChanged() method i try to get all the data form posts collection. its takes some time to display, In meanwhile i want to display spinner that specifies the user where some something is being loading.
i tried and its works cool, but the problem with code is
<loadingSpinner v-if="loading"></loadingSpinner>
<div v-if="posts.length">
<div v-for="post in posts" v-bind:key=post.id class="post">
<h5>{{ post.userName }}</h5>
<span>{{ post.createdOn | formatDate }}</span>
<p>{{ post.content | trimLength }}</p>
<ul>
<li><a #click="openCommentModal(post)">comments {{ post.comments }}</a></li>
<li><a #click="likePost(post.id, post.likes)">likes {{ post.likes }}</a></li>
<li><a #click="viewPost(post)">view full post</a></li>
</ul>
</div>
</div>
<div v-else>
<p class="no-results">There are currently no posts</p>
</div>
Spinner component responsible for spin animation:
<loadingSpinner v-if="loading"></loadingSpinner>
And the below html code is for displaying data from firebase
Where posts and loading variables are the computed properties from vuex state
problem is when is reload the page, spinner showing along the
<div v-else>
<p class="no-results">There are currently no posts</p>
</div>
I want to restrict the v-else condition when the spinner is being loaded.
By the way, the loading computed properties is a boolean that reacts based on onAuthstateChanged() firebase method
this is my entire vuex store file :
import Vue from 'vue'
import Vuex from 'vuex'
const fb = require('./firebaseConfig.js')
Vue.use(Vuex)
// handle page reload
fb.auth.onAuthStateChanged(user => {
if (user) {
store.commit('setCurrentUser', user)
store.dispatch('fetchUserProfile')
fb.usersCollection.doc(user.uid).onSnapshot(doc => {
store.commit('setUserProfile', doc.data())
})
// realtime updates from our posts collection
fb.postsCollection.orderBy('createdOn', 'desc').onSnapshot(querySnapshot => {
// check if created by currentUser
let createdByCurrentUser
if (querySnapshot.docs.length) {
createdByCurrentUser = store.state.currentUser.uid == querySnapshot.docChanges[0].doc.data().userId ? true : false
}
// add new posts to hiddenPosts array after initial load
if (querySnapshot.docChanges.length !== querySnapshot.docs.length
&& querySnapshot.docChanges[0].type == 'added' && !createdByCurrentUser) {
let post = querySnapshot.docChanges[0].doc.data()
post.id = querySnapshot.docChanges[0].doc.id
store.commit('setHiddenPosts', post)
} else {
store.commit('setLoading', true)
let postsArray = []
querySnapshot.forEach(doc => {
let post = doc.data()
post.id = doc.id
postsArray.push(post)
})
store.commit('setPosts', postsArray)
store.commit('setLoading', false)
}
})
}
})
export const store = new Vuex.Store({
state: {
currentUser: null,
userProfile: {},
posts: [],
hiddenPosts: [],
loading: true
},
actions: {
clearData({ commit }) {
commit('setCurrentUser', null)
commit('setUserProfile', {})
commit('setPosts', null)
commit('setHiddenPosts', null)
},
fetchUserProfile({ commit, state }) {
fb.usersCollection.doc(state.currentUser.uid).get().then(res => {
commit('setUserProfile', res.data())
}).catch(err => {
console.log(err)
})
},
updateProfile({ commit, state }, data) {
let name = data.name
let title = data.title
fb.usersCollection.doc(state.currentUser.uid).update({ name, title }).then(user => {
// update all posts by user to reflect new name
fb.postsCollection.where('userId', '==', state.currentUser.uid).get().then(docs => {
docs.forEach(doc => {
fb.postsCollection.doc(doc.id).update({
userName: name
})
})
})
// update all comments by user to reflect new name
fb.commentsCollection.where('userId', '==', state.currentUser.uid).get().then(docs => {
docs.forEach(doc => {
fb.commentsCollection.doc(doc.id).update({
userName: name
})
})
})
}).catch(err => {
console.log(err)
})
}
},
mutations: {
setLoading(state, payload){
state.loading = payload
},
setCurrentUser(state, val) {
state.currentUser = val
// console.log(val)
},
setUserProfile(state, val) {
state.userProfile = val
// console.log(val)
},
setPosts(state, val) {
if (val) {
state.posts = val
} else {
state.posts = []
}
},
setHiddenPosts(state, val) {
if (val) {
// make sure not to add duplicates
if (!state.hiddenPosts.some(x => x.id === val.id)) {
state.hiddenPosts.unshift(val)
}
} else {
state.hiddenPosts = []
}
}
},
})
any suggestions?
I would tweak your v-if/v-else logic at bit.
<loadingSpinner v-if="loading" />
<div v-else-if="posts.length"></div>
<div v-else>
<p class="no-results">There are currently no posts</p>
</div>
The difference is v-else-if on posts.length, instead of v-if. This way, there are 3 distinct states.
Loading, show spinner.
Not loading, show posts.
Not loading, there are no posts, show no results.

Rendering firestore values in Vue.js

I'm trying to build a simple app and am running into trouble getting Vue rendering data I'm trying to pull from Firestore. Below is the code for a vue page after someone logs in. Essentially I'm just trying to get it to display the name of the person who logged in. Currently I have each user's display name as the document ID for the get() function. Right now, it doesn't even seem like its running the 'firestore()' function since I tried outputting some text to the console at the beginning but I didn't see it. Any idea what's happening here? Thank you!
<template>
<v-container fluid>
<v-layout row wrap>
<v-flex xs12 class="text-xs-center" mt-5>
<h1>Home page</h1>
</v-flex>
<v-flex xs12 class="text-xs-center" mt-3>
<p>Welcome {{ name }}</p>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import firebase from 'firebase'
export default {
data () {
return {
name: ''
}
},
firestore () {
firebase.firestore().collection('user').doc(firebase.auth().currentUser.displayName).get()
.then(doc => {
if (doc) {
var data = doc.data()
return {
name: data.name
}
} else {
console.log('No document exists')
}
}).catch(error => { console.log('Error: ' + error) })
}
}
</script>
You can use created hook for this purpose.
created() {
firebase.firestore().collection('user').doc(firebase.auth().currentUser.displayName).get()
.then(doc => {
if (doc) {
var data = doc.data()
this.name = data.name
} else {
console.log('No document exists')
}
}).catch(error => { console.log('Error: ' + error) })
}
If you're using vuefire, you can do
firestore: {
users: firebase.firestore().collection('user').doc(firebase.auth().currentUser.displayName)
}
then use this.users in your Vue code

Resources