Vuex: Computed Property Returns Undefined - asyc Axios - asynchronous

I have async axios calls to my DB that I am dispatching with an action when the site loads.
(I've tried dispatching in beforeEnter() on my Vue-router, beforeCreated(), and Created() on my Form.Vue)
I have a computed property return the info using a getter.
The problem I am experiencing is the data is getting there after the page has finished loading and returns Undefined - nothing renders on the page.
If I check my Vue DevTools, all the data is in the correct places in the State.
How can I get the data to finish loading before the page?
//ACTIONS
async loadInputs({ state, getters, commit, dispatch }) {
if (!state.loading) {
commit('setLoading', true)
const inputLists = axios.get('/companyInputLists')
const inputs = axios.get('/companyInputs')
commit('setLoading', false)
commit('loadInputs' , [await inputLists, await inputs])
}
},
set ({commit}, value) {
commit('updateValue', value)
},
//MUTATORS
setLoading(state, value) {
state.loading = value
},
loadInputs(state, data){
state.layout = {}
state.data = {}
data[0].data.map(list => {
list['inputs'] = []
state.layout[list.order] = list
data[1].data.map(input => {
if(input.list_id == list.id){
state.layout[list.order].inputs.push(input)
state.data[input.label] = ''
}
})
})
},
updateValue(state, value) {
state.data[value.type] = value.value
},
//GETTERS
get(state) {
console.log(state)
return state
},
}
//FORM.VUE
<span>
//LIST TEST and v-if test
<div v-if="lists">
{{lists}}
</div>
test
{{ lists }}
<v-layout row wrap justify-center>
<draggable class="dragArea layout row wrap justify-center" :options="{group:'lists'}">
<v-flex v-for="list in lists" v-bind:key="list.index" :class="[list.label, flexBoxSize(list.size)]">
<v-subheader v-text="list.label"></v-subheader>
<draggable class="dragArea layout row wrap" :options="{group:'inputs'}">
<v-flex v-for="item in list.inputs" v-bind:key="item.index" :class="[item.label, flexBoxSize(item.size)]">
<textfield v-if="item.type_id == 1" formType="clientForm" :label="item.label" :required="item.required"></textfield>
<email v-if="item.type_id == 2" formType="clientForm" :label="item.label" :required="item.required"></email>
<phone v-if="item.type_id == 3" formType="clientForm" :label="item.label" :required="item.required"></phone>
<calendar v-if="item.type_id == 4" formType="clientForm" :label="item.label" :required="item.required"></calendar>
<googleMap v-if="item.type_id == 5" formType="clientForm" :label="item.label" :required="item.required"></googleMap>
<autocomplete v-if="item.type_id == 6" formType="clientForm" :label="item.label" :required="item.required"></autocomplete>
</v-flex>
</draggable>
</v-flex>
</draggable>
<v-layout row wrap justify-center>
<submitButton formType="clientForm" path="/clientForm" :references="this.$refs"></submitButton>
<clearButton formType="clientForm" :references="this.$refs"></clearButton>
</v-layout>
</v-layout>
</span>
</template>
<script>
export default {
components: {
draggable,
},
beforeCreate(){
this.$store.dispatch('clientForm/loadInputs')
},
computed: {
lists: {
get() {
return this.$store.getters['clientForm/get'].layout
},
set(value) {
this.$store.commit('clientForm/updateInputList', value)
}
}
},
Vuex Dev Tools Showing Data in State After Page Loads

I figured out the answer over the winter holidays last year and realized there never was a definitive conclusion posted here. After much trial and error and reading through documentation I came across the answer in the Vue.js documentation.
https://v2.vuejs.org/v2/api/#Vue-set
Vue.set(target, key, value)
Adds a property to a reactive object, ensuring the new property is also reactive, so triggers view updates. This must be used to add new properties to reactive objects, as Vue cannot detect normal property additions (e.g. this.myObject.newProperty = 'hi').
Using this function, I was able to load my data via an axios call and have Vue detect the changes and update the DOM.
Also you may want to take note of Vue.delete for removing data with reactivity.

I think you can simplify your loadInputs action:
async loadInputs ({commit, state}) {
if (!state.loading) {
commit('setLoading', true)
const inputLists = axios.get('/companyInputLists')
const inputs = axios.get('/companyInputs')
commit('setLoading', false)
commit('loadInputs' , [await inputLists, await inputs])
}
}
Component:
export default {
components: {
draggable,
},
computed: {
lists: {
get() {
return this.$store.getters['clientForm/get'].layout
},
set(value) {
this.$store.commit('clientForm/updateInputList', value)
}
}
},
created () {
this.$store.dispatch('clientForm/loadInputs')
},
}

Related

passing reactive data object as argument in function Vue 3 composition API

I am new to Vue and although I could find way around most problems I've encountered, this one has been bugging me for last two days and just cannot find solution. Any help much appreciated, thanks in advance!
I've got following code:
<template>
<header><h1>HomeShop</h1></header>
<main>
<section>
<Item-Card
v-for="(item, index) in items"
:key="index"
:item="item.item"
:check="item.check"
#item-checked="checkClicked(index, item.id, items)"
#item-deleted="deleteClicked(item.id, items)"
/>
<Add-New-Item #item-submited="newItemCard" />
</section>
</main>
<button #click="$log(JSON.stringify(items, null, 1))">Log</button>
</template>
<script setup>
import { reactive, toRefs } from "vue";
import axios from "axios";
import _ from "lodash";
//data + explicit expression
const state = reactive({
items: [],
});
const { items } = toRefs(state);
//fce
const checkClicked = (index, id, items) => {
if (items[index].check === false) {
items[index].check = true;
items[index].checkTime = _.now();
items = _.sortBy(items, ["check", "checkTime"])
console.log(JSON.stringify(items, null, 1));
axios.patch("http://localhost:3000/items/" + id, { check: true });
} else {
items[index].check = false;
delete items[index].checkTime;
axios.patch("http://localhost:3000/items/" + id, { check: false });
}
};
I pass my data object array (items) as argument to checkClicked method. This function changes check status and adds checkTime timestamp based on which I sort the array (using lodash method sortBy). From within the checkClicked method I log the items array with expected correct result:
[
{
"item": "2",
"check": false,
"id": 2
},
{
"item": "3",
"check": false,
"id": 3
},
{
"item": "4",
"check": false,
"id": 4
},
{
"item": "5",
"check": true,
"id": 5,
"checkTime": 1671719753796
},
{
"item": "1",
"check": true,
"id": 1,
"checkTime": 1671719755363
}
]
However, when I log items from outside the method (the custom function $log at template) I get following result:
[
{
"item": "1",
"check": true,
"id": 1,
"checkTime": 1671719755363
},
{
"item": "2",
"check": false,
"id": 2
},
{
"item": "3",
"check": false,
"id": 3
},
{
"item": "4",
"check": false,
"id": 4
},
{
"item": "5",
"check": true,
"id": 5,
"checkTime": 1671719753796
}
]
How do I manipulate the reactive data object array items from inside the function checkClicked? My intention is to sort the data array each time timestamp checkTime is added (that is what the checkClicked function does).
My understanding is that passing the data object array items as argument into function creates separate instance of the array, that is why I am getting two different results while loging the array. However I cannot find solution how to manipulate the real items from inside the function checkClicked.
You are right. You are only changing the parameter variable items in the function. See my other answer for the clarification.
Pay also attention to the following:
When you assign a new value to your reactive proxy object, you can lose the reactivity.
Here it is:
items = _.sortBy(items, ["check", "checkTime"])
I guess, this line could also remove reactivity from the array.
There are ways to fix it, by passing the array through vue reactivity system again.
But, this causes too much work for vue to full recalculate the array items and is not efficient.
My way to provide sorted results is to use a Computed Property.
Like this:
<Item-Card v-for="(item, index) in sortedItems">
and then
const sortedItems = computed(() => {
return _.sortBy(items, ["check", "checkTime"])
})
The advantage of using a computed property is that, it will be recalculated by Vue Reactivity System only when your items array changes.
It looks like you only change the local variable items in the checkClicked() function. I have build a test playground to check it.
You have to change the line items = []; to this.items = []; if you want to change the property, not the parameter variable.
See my second answer for the right sorting solution.
const { ref, reactive, createApp, toRefs } = Vue;
const data = [
{
"item": "1",
"check": true,
"id": 1,
"checkTime": 1671719755363
},
{
"item": "2",
"check": false,
"id": 2
},
{
"item": "3",
"check": false,
"id": 3
}
]
const App = {
methods: {
clear(items) {
items = [];
}
},
setup() {
//data + explicit expression
const state = reactive({
items: data,
});
const { items } = toRefs(state);
return {
items
}
}
}
const app = createApp(App)
app.mount('#app')
<div id="app">
<div v-for="(item, index) in items"
:key="index"
:item="item.item"
:check="item.check"> #{{index}}: {{item.item}}
</div>
<button #click="clear(items)">clear</button>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js">
</script>
sending update with solution I got at another forum. I feel dumbed I would not figured that out in two days on my own, whatever. Although advice from Tolbxela can be right track it brings as well more problems so in the end I prefer the more cleaner way:
"I can reference the state declared within . i.e. don’t pass it as a value to the function."
Thats it. Simply not passing the data object array into function, just reference it right away from within the function. So the correct code goes like this:
<template>
<header><h1>HomeShop</h1></header>
<main>
<section>
<Item-Card
v-for="(item, index) in items"
:key="item.id"
:item="item.item"
:check="item.check"
#item-checked="checkClicked(index, item.id)"
#item-deleted="deleteClicked(item.id, items)"
/>
<Add-New-Item #item-submited="newItemCard" />
</section>
</main>
<button #click="$log(JSON.stringify(sortedItems, null, 1))">Log</button>
</template>
<script setup>
import { reactive, toRefs, computed } from "vue";
import axios from "axios";
import _ from "lodash";
//data + explicit expression
const state = reactive({
items: [],
});
const { items } = toRefs(state);
//fce
const checkClicked = (index, id) => {
if (state.items[index].check === false) {
state.items[index].check = true;
state.items[index].checkTime = _.now();
state.items = _.sortBy(state.items, ["check", "checkTime"])

VUE3 - Select element on mounted() or created() in a custom component without using "event"

i have a custom input component and i want to select it without clicking on it or something so i cant use "event", i want to select it on created() or mounted()
how can i do that?
<template>
<div class="form-control">
<label :for="id">
{{ label }}
</label>
<input
:id="id"
:type="mode"
:value="currentValue"
#input="$emit('update:modelValue', $event.target.value)"
#focus="pullLabel"
#blur="pushLabel"
/>
</div>
</template>
<script>
export default {
created() {
this.test();
},
methods: {
test() {
console.log(this); // -> How could this be like '<input id="title" type="text">'
},
},
};
</script>
I figured it out myself after 6~ hours of work.
The answer is $refs
I sent a "ref" from parent component like this:
<form ref="settingsForm" #submit.prevent="updateConfig">
Then in methods, i selected all the inputs in form like this: (except submit input)
methods: {
...
focusInput() {
const inputs = this.$refs.settingsForm.querySelectorAll(
'input:not([type="submit"])'
); // $refs.settingsForm -> comes from the parent element, you can name "settingsForm" whatever you want
Array.from(inputs).map((e) => {
if (e.value.length > 0) {
e.previousElementSibling.classList.add("clicked-input");
}
});
},
...
}
By the way, the most important thing is, you have to call the function on mounted() method, not on created()

Firebase: Why value event gets fired before new child ref gets added

Following code, is a very simple Firebase - VueJS app, (codeSandBox demo)
app.vue
<template>
<div class="container">
<!-- Adding Quote -->
<add-quote/>
<!-- Display Quotes -->
<quote-list/>
</div>
</template>
<script>
import addQuote from "./components/AddQuote.vue";
import quoteList from "./components/QuoteList.vue";
export default {
components: {
addQuote,
quoteList
},
methods: {
get_allQuotes: function() {
// var vm = this;
var localArr = [];
quotesRef
.once("value", function(snapshot) {
snapshot.forEach(function(snap) {
localArr.push({
key: snap.key,
category: snap.val().category,
quoteTxt: snap.val().quoteTxt
});
});
})
.then(data => {
this.$store.commit("set_allQuotes", localArr);
});
}
},
mounted() {
this.get_allQuotes();
console.log("App: mounted fired");
}
};
</script>
store.js(vuex store)
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
quotesList: []
},
getters: {
get_quotesList(state) {
return state.quotesList;
}
},
mutations: {
set_allQuotes(state, value) {
state.quotesList = value;
}
}
});
AddQuote.vue
<template>
<div class="row quote-edit-wrapper">
<div class="col-xs-6">
<textarea v-model.lazy="newQuoteTxt"
rows="4"
cols="50"></textarea>
<button #click="addQuote">Add Quote</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
newQuoteTxt: '',
}
},
computed: {
allQuotes() {
return this.$store.getters.get_quotesList;
},
newQuoteIdx() {
var localArr = [...this.allQuotes]
if(localArr.length > 0) {
var highestKEY, currKEY
localArr.forEach((element, idx) => {
currKEY = parseInt(element.key)
if(idx == 0) {
highestKEY = currKEY
} else {
if(highestKEY < currKEY) {
highestKEY = currKEY
}
}
})
return highestKEY + 1
} else {
return 1
}
}
},
methods: {
// ADD new Quote in DB
addQuote: function() {
var vm = this
var localArr = [...this.allQuotes]
//1. First attach 'value' event listener,
// Snapshot will contain data from that ref
// when any child node is added/updated/delete
quotesRef.on('value', function (snapshot) {
snapshot.forEach(function(snap) {
var itemExists = localArr.some(function (item, idx) {
return item.key == snap.key
})
// If newly added item doesn't yet exists then add to local array
if (!(itemExists)) {
localArr.push({
key: snap.key,
category: snap.val().category,
quoteTxt: snap.val().quoteTxt })
vm.$store.commit('set_allQuotes', localArr)
}
})
})
//2. Second set/create a new quotes in Firebase,
// When this quote gets added in Firebase,
// value event (attached earlier) gets fired
// with
var newQuoteRef = quotesRef.child(this.newQuoteIdx)
newQuoteRef.set({
category: 'motivation',
quoteTxt: this.newQuoteTxt
})
}
}
}
</script>
quoteList.vue
<template>
<div class="row">
<div class="col-xs-12 quotes-list-wrapper">
<template v-for="(quote,idx) in allQuotes">
<!-- Quote block -->
<div class="quote-block-item">
<p class="quote-txt"> {{quote.quoteTxt}} </p>
</div>
</template>
</div>
</div>
</template>
<script>
export default {
computed: {
allQuotes() {
return this.$store.getters.get_quotesList;
}
}
}
</script>
Note: The main code of concern is of addQuote.vue
User enter newQuoteTxt that gets added to Firebase (addQuote()) as a quote item under quotesRef. As soon as quote is added (on firebase), Firebase client side SDK's value event fires, and adds the new quote (via callback) to localArray (allQuotes). VueJS then updates the DOM with newly added Quote.
The addQuote() method works in the following manner:
First, attach a callback/listener to 'value' event on quotesRef
quotesRef.on('value', function (snapshot) {
....
})
Next, A firebase ref (child of quotesRef) is created with a ID this.newQuoteIdx
var newQuoteRef = quotesRef.child(this.newQuoteIdx)
Then set() is called (on this newly created Ref) adding newquote to firebase RealTime DB.
value event gets triggered (attached from step 1) and listener /callback is called.
The callback looks for this new quote's key in existing list of items by matching keys of localArr and snap.key, if not found, adds the newly quote to localArr. localArr commits to a vuex store.
`vm.$store.commit('set_allQuotes', localArr)`
VueX then updates all subscriber component of this array. VueJS then adds the new quote to the existing list of quotes (updates the DOM)
While debugging the addQuote method, the problem I notice, the execution/flow of script (via F8 in chrome debugger) first steps into the listener/callback attached to value event before the code newQuoteRef.set({ ... }) that adds new quote (on firebase), which in turn will cause 'value' event to trigger.
I am not sure why this occurs. Can anybuddy explain why the listener/callback is called before the quotes is created.
Are child nodes (of QuotesRef) are cached at clientside such that 'value' fires even before new quote is added.
Thanks
If I correctly understand your question (Your code is not extremely easy to follow! :-)) it is the normal behaviour. As explained in the documentation:
The value event will trigger once with the initial data stored at
this location, and then trigger again each time the data
changes.
Your sandbox demo does not actually shows how the app works, but normally you should not set-up the listener in the method that saves a new node to the database. These two things should be decoupled.
One common approach is to set the listener in the created hook of a component (see https://v2.vuejs.org/v2/guide/instance.html#Instance-Lifecycle-Hooks and https://v2.vuejs.org/v2/api/#created) and then in your addQuote method you just write to the database. As soon as you write, the listener will be fired.

Vue JS + Firestore - Link computed property to single document field

I'm pretty new on Vue JS and dbs like Firebase and I'm having some trouble with what I'd like to do.
Here is the idea: I have repeated components ('LaundryMachine.vue') which each have a boolean property (computed property) available.
I want users to be able to change the state of these components. The change is sent to the Firestore DB and the app needs to read the data from the DB.
I have been able link the VueJS code and the DB and edit the DB data within the app. I have however not been successful at reading the data from the DB.
More precisely, I have only been able to read the data from one or several documents of the DB and log at the console. But i can't manage to link the data to properties.
Here is what I have on the LaundryMachine.vue:
<template>
<div class="about">
<h2>Machine {{ this.machineNum }}</h2>
<img src="../assets/washing_machine.png" /><br />
<v-btn v-bind:color="buttonColor" v-on:click="changeAvailability">
{{ this.availability }}</v-btn
>
<v-btn v-bind:color="buttonColor" v-on:click="editState">Edit state</v-btn>
</div>
</template>
<script>
import db from './firebaseInit.js';
export default {
name: 'LaundryMachine',
props: {
name: String,
machineNum: Number,
residenceNum: Number,
},
methods: {
editState: function(event) {
console.log('available:' + this.available);
// Emit to parent component which succesfully edits the fields in the DB
this.$emit('update-availability', this.machineNum, this.residenceNum);
// This part is just about logging out on the console the db documents data which works fine
db.collection('Machines')
.get()
.then(querySnapshot => {
querySnapshot.forEach(doc => {
console.log(doc.id + doc.data() + doc.data().available);
});
});
}
},
computed: {
available: function() {
// This is where I want to link my computed property to the db document field but which doesn't work. If i print {{this.available}}, i'll get an undefined
let ref = db
.collection('Machines')
.doc('machine' + this.residenceNum + this.machineNum);
ref.get().then(snapshot => {
if (snapshot.exists) {
ref.get().then(snapshot => {
return snapshot.data().available;
});
} else {
return true;
}
});
},
availability: function() {
if (this.available) {
return 'disponible';
} else {
return 'indisponible';
}
},
buttonColor: function() {
if (this.available) {
return 'primary';
} else {
return 'red';
}
}
}
};
</script>
This is honestly really frustrating as I'm able to log on the console the fields of the data base but I can't link them to my "available" computed property. I've looked through a lot of posts and firestore doc and I can't find something which works.
Thanks in advance !

Vuefire get Firebase Image Url

I am storing relative paths to images in my firebase database for each item I wish to display. I am having trouble getting the images to appear on the screen, as I need to get the images asynchronously. The firebase schema is currently as follows:
{
items: {
<id#1>: {
image_loc: ...,
},
<id#2>: {
image_loc: ...,
},
}
}
I would like to display each of these images on my page with code such as:
<div v-for="item in items">
<img v-bind:src="item.image_loc">
</div>
This does not work, as my relative location points to a place in firebase storage. The relavent code to get the true url from this relative url is:
firebase.storage().ref('items').child(<the_image_loc>).getDownloadURL()
which returns a promise with the true url. Here is my current vue.js code:
var vue = new Vue({
el: '.barba-container',
data: {
items: []
},
firebase: function() {
return {
items: firebase.database().ref().child('items'),
};
}
});
I have tried using computed properties, including the use of vue-async-computed, but these solutions do not seem to work as I cannot pass in parameters.
Basically, how do I display a list of elements where each element needs the result of a promise?
I was able to solve this by using the asyncComputed library for vue.js and by making a promise to download all images at once, instead of trying to do so individually.
/**
* Returns a promise that resolves when an item has all async properties set
*/
function VotingItem(item) {
var promise = new Promise(function(resolve, reject) {
item.short_description = item.description.slice(0, 140).concat('...');
if (item.image_loc === undefined) {
resolve(item);
}
firebase.storage().ref("items").child(item.image_loc).getDownloadURL()
.then(function(url) {
item.image_url = url;
resolve(item);
})
.catch(function(error) {
item.image_url = "https://placeholdit.imgix.net/~text?txtsize=33&txt=350%C3%97150&w=350&h=150";
resolve(item);
});
});
return promise;
}
var vue = new Vue({
el: '.barba-container',
data: {
items: [],
is_loading: false
},
firebase: function() {
return {
items: firebase.database().ref().child('items'),
};
},
asyncComputed: {
processedItems: {
get: function() {
var promises = this.items.map(VotingItem);
return Promise.all(promises);
},
default: []
}
}
});
Lastly, I needed to use: v-for="item in processedItems" in my template to render the items with image urls attached
I was able to solve it without any extra dependencies not adding elements to the array until the url is resolved:
in my template:
<div v-for="foo in foos" :key="foo.bar">
<img :src="foo.src" :alt="foo.anotherbar">
...
</div>
in my component (for example inside mounted())
const db = firebase.firestore()
const storage = firebase.storage().ref()
const _this = this
db.collection('foos').get().then((querySnapshot) => {
const foos = []
querySnapshot.forEach((doc) => {
foos.push(doc.data())
})
return Promise.all(foos.map(foo => {
return storage.child(foo.imagePath).getDownloadURL().then(url => {
foo.src = url
_this.foos.push(foo)
})
}))
}).then(() => {
console.log('all loaded')
})

Resources