I'm trying to make async autocomplete input with Vue, Nuxt, Axios and Buefy. It basically works, but I need to have different strings when user just starts typing and there's yet nothing to show, and when there is nothing found for such request.
I'm checking in computed variable if input value isn't empty and axios returns empty array to handle if the request address cannot be found. But it causes error
Cannot read property 'length' of undefined
The weird thing is that address variable is successfully used in other parts of my component.
My vue file below:
<template lang="pug">
b-field(label="Your address?")
b-autocomplete(
rounded,
v-model="address",
:data="data",
placeholder="Start typing",
icon="magnify",
#input="getAsyncData",
#select="option => selected = option",
:loading="isFetching"
)
template(slot="empty") {{ dummyText }}
</template>
<script>
import axios from 'axios'
import debounce from 'lodash/debounce'
export default {
data() {
return {
data: [],
address: '',
selected: null,
isFetching: false,
nothingFound: false,
test: false
}
},
computed: {
dummyText: () => {
if (this.address.length > 0 && this.nothingFound) { // This will return error
return 'There is no such address'
} else {
return 'Keep typing'
}
}
},
methods: {
getAsyncData: debounce(function () {
this.isFetching = true
axios.post('https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/address', {
"query": this.address,
"count": 8
}, {
headers: {
'Authorization': 'Token sometoken',
'Content-Type': 'application/json',
'Accept': 'application/json',
}
})
.then(response => {
this.isFetching = false
this.data = Object.values(response.data.suggestions)
if (response.data.suggestions.length===0) this.nothingFound = true
console.log(this.address.length) // This will work
})
.catch(error => {
this.isFetching = false
console.log(error);
})
}, 300)
}
}
</script>
This is not about ssr, I've tried to init component inside mounted hook. Think I'm missing out something obvious, but I've already spent hours trying to fix this without success
Don't use arrow function ()=>{} for computed, it will cause the wrong context (not current Vue instance).
Change to function () {} then it should work fine.
And for methods, watch, you should follow same rules.
computed: {
dummyText: function () { // change to function () {}
if (this.address.length > 0 && this.nothingFound) { // This will return error
return 'There is no such address'
} else {
return 'Keep typing'
}
}
},
You can also use es2015 shorthand for a method function:
computed: {
dummyText() {
return this.address.length > 0 && this.nothingFound ? 'There is no such address' : 'Keep typing';
}
}
The Vue Documentation states not to use arrow functions on a property or callback.
You are facing this error because an arrow function wouldn't bind this to the vue instance for which you are defining the computed property as arrow functions are bound to the parent context and this.address is undefined. Same would happen if you use arrow function for methods.
Use regular function:
dummyText: function () {
console.log(this.address)
}
Or use ES5 shorthand:
dummyText() {
console.log(this.address)
}
Or If you want to keep using the arrow function, you could pass the component instance (this) as parameter because computed properties receive component instance as their first argument. :
dummyText : ctx => console.log(ctx.address)
Related
From the code below, I'm sending email using EmailJs library and the alert method get's executed after submitting but the change method doesn't. What I'm I missing?
Error message on the console is
Uncaught (in promise) TypeError: Cannot read property 'change' of undefined
at app.js:2755
My Script
<script>
import emailjs from "emailjs-com";
export default {
data() {
return {
flash: false,
};
},
methods: {
sendEmail: (e) => {
emailjs
.sendForm(
"service_k9mhh",
"template_epghgfh",
e.target,
"user_w9U76tg77yhcggh"
)
.then(
(result) => {
console.log("SUCCESS!", result.status, result.text);
alert("Message Sent Successfully")
this.change();
},
(error) => {
console.log("FAILED...", error);
}
);
// Reset form field
},
change(){
this.flash = true;
}
},
};
</script>
Without arrow functions as shown below, It still throws the same error.
methods: {
sendEmail: function (e) {
emailjs
.sendForm(
"service_",
"template_",
e.target,
"user_"
)
.then(
function (result) {
console.log("SUCCESS!", result.status, result.text);
this.change();
},
function (error) {
console.log("FAILED...", error);
}
);
},
change: function () {
this.flash = true;
}
The problem is that in arrow functions this does not refer to the Vue instance so this.change is not defined there.
Read this:
https://v2.vuejs.org/v2/guide/instance.html#Data-and-Methods
Okay so you and eldo were both half right. You need to use a regular function syntax when declaring a method, and an arrow function in any nested .then's.
Using an arrow function in your then and catch handler's ensures that 'this' remains a reference to the Vue instance.
Your method wants to look like this:
sendEmail(e) {
emailjs
.sendForm("service_", "template_", e.target, "user_")
.then((result) => {
console.log("SUCCESS!", result.status, result.text);
this.change();
})
.catch((error) => {
console.log("FAILED...", error);
});
},
I have a component with a state property called "volume" which is bound to a slider element. I have a watcher bound to the volume property such that when the volume is updated, a function should fire
data () {return {
volume: 0,
}},
methods: {
testMethod () {
console.log("this is firing")
}
},
watch: {
volume: (v) => {
console.log("volume has been updated", v);
this.testMethod();
}
}
On running this code, the console shows the error "Cannot read property of "testMethod" of undefined
I have tried other things like accessing the $store (which was my initial issue), and that is failing to resolve too.
You can't use the fat-arrow notation within a Vue.js component (Nuxt or otherwise). The fat-arrow function definition uses the wrong context (this in your case), which is why you are running into this problem.
<script>
export default {
data () {return {
volume: 0,
}},
methods: {
testMethod () {
console.log("this is firing")
}
},
watch: {
// Your old code.
// volume: (v) => {
// console.log("volume has been updated", v);
// this.testMethod();
// }
// The corrected way.
volume(v) {
console.log("volume has been updated", v);
this.testMethod();
}
}
};
</script>
You are using an Arrow Function, which binds the this keyword to the object that defines the function, rather than the object that called the function (which is the current instance of the component in this case).
Use regular function syntax instead:
watch: {
volume(v) {
console.log("volume has been updated", v);
this.testMethod();
}
}
I want to show a progress bar in a component. The value of the progress bar should be set by the value of onUploadProgress in the post request (axios). Till so far, that works well. The state is updated with that value correctly.
Now, I am trying to access that value in the component. As the value updates while sending the request, I tried using a watch, but that didn't work.
So, the question is, how to get that updated value in a component?
What I tried:
component.vue
computed: {
uploadProgress: function () {
return this.$store.state.content.object.uploadProgressStatus;
}
}
watch: {
uploadProgress: function(newVal, oldVal) { // watch it
console.log('Value changed: ', newVal, ' | was: ', oldVal)
}
}
content.js
// actions
const actions = {
editContentBlock({ commit }, contentObject) {
commit("editor/setLoading", true, { root: true });
let id = object instanceof FormData ? contentObject.get("id") : contentObject.id;
return Api()
.patch(`/contentblocks/${id}/patch/`, contentObject, {
onUploadProgress: function (progressEvent) {
commit("setOnUploadProgress", parseInt(Math.round((progressEvent.loaded / progressEvent.total) * 100)));
},
})
.then((response) => {
commit("setContentBlock", response.data.contentblock);
return response;
})
.catch((error) => {
return Promise.reject(error);
});
},
};
// mutations
const mutations = {
setOnUploadProgress(state, uploadProgress) {
return (state.object.uploadProgressStatus = uploadProgress);
},
};
Setup:
Vue 2.x
Vuex
Axios
Mutations generally are not meant to have a return value, they are just to purely there set a state value, Only getters are expected to return a value and dispatched actions return either void or a Promise.
When you dispatch an action, a dispatch returns a promise by default and in turn an action is typically used to call an endpoint that in turn on success commits a response value via a mutation and finally use a getter to get the value or map the state directly with mapState.
If you write a getter (not often required) then mapGetters is also handy to make vuex getters available directly as a computed property.
Dispatch > action > commit > mutation > get
Most of your setup appears correct so it should be just a case of resolving some reactivity issue:
// content.js
const state = {
uploadProgress: 0
}
const actions = {
editContentBlock (context, contentObject) {
// other code
.patch(`/contentblocks/${id}/patch/`, contentObject, {
onUploadProgress: function (progressEvent) {
context.commit('SET_UPLOAD_PROGRESS',
parseInt(Math.round((progressEvent.loaded / progressEvent.total) * 100)));
},
}
// other code
}
}
const mutations = {
SET_UPLOAD_PROGRESS(state, uploadProgress) {
state.uploadProgress = uploadProgress
}
}
// component.vue
<template>
<div> {{ uploadProgress }} </div>
</template>
<script>
import { mapState } from 'vuex'
export default {
computed: {
...mapState('content', ['uploadProgress']) // <-- 3 dots are required here.
}
}
</script>
I cannot figure out why the details computed property in the following component is not updating when the fetch() method is called:
<template>
<div>
{{ haveData }} //remains undefined
</div>
</template>
<script>
export default {
props: {
group: {
type: Object,
required: true
},
},
computed: {
currentGroup() {
return this.$store.getters['user/navbar_menu_app_current_group'](
this.group.id
)
/*-- which is the following function
navbar_menu_app_current_group: state => item => {
return state.menu.find(m => {
return m.id == item
})
}
*/
/*-- this function returns an object like so
{
id: 1,
label: 'kity cats',
}
***details --> IS NOT DEFINED. If I add it to the current group as null, my problem goes away. However, this is a previous API call that does not set the `details` parameter.
*/
},
details() {
let c = this.currentGroup.details
console.log(c) // returns undefined, which makes sense, but it should be updated after this.fetch() is called
return c
},
haveData() {
return this.details != null
}
},
methods: {
async fetch() {
await this.$store.dispatch(
'user/navbar_menu_app_details_get',
this.group.id
)
//This is setting the "details" part of the state on menu which is referred to in the computed properties above
//Previous to this there is no state "this.group.details"
//If I add a console log to the mutation the action calls, I get what is expected.
}
},
created() {
if (!this.haveData) {
this.fetch()
}
}
}
</script>
If I change the array items to include details, it works:
{
id: 1,
label: 'kity cats',
details: null // <-- added
}
The unfortunate part is that the array is created from a large API call, and adding the details seems unnecessary, as it may never be needed.
How can I get the computed properties to work without adding the details:null to the default state?
Attempt 1:
// Vuex mutation
navbar_menu_app_details_set(state, vals) {
let app = state.menu.find(item => {
return item.id == vals[0] //-> The group id passing in the dispatch function
})
//option 1 = doesn't work
app = { app, details: vals[1] } //-> vals[1] = the details fetched from the action (dispatch)
//option 2 = doesnt work
app.details = vals[1]
//option 3 = working but want to avoid using Vue.set()
import Vue from 'vue' //Done outside the actual function
Vue.set( app, 'details', vals[1])
},
Attempt 2:
// Vuex action
navbar_menu_app_details_get(context, id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
context.commit('navbar_menu_app_details_set', [
context.getters.navbar_menu_app_current(id), //-> the same as find function in the mutation above
apps[id]
])
resolve()
}, 1000)
})
}
// --> mutation doesn't work
navbar_menu_app_details_set(state, vals) {
vals[0].details = vals[1]
},
The Vue instance is available from a Vuex mutation via this._vm, and you could use vm.$set() (equivalent to Vue.set()) to add details to the menu item:
navbar_menu_app_details_set(state, vals) {
let app = state.menu.find(item => {
return item.id == vals[0]
})
this._vm.$set(app, 'details', vals[1])
},
All Objects in Vue are reactive and are designed in a way such that only when the object is re-assigned, the change will be captured and change detection will happen.
Such that, in your case, following should do fine.
app = { ...app, details: vals[1] }
I've tried to bind it like it doesn't seem to make the trick :)
firebaseInstance.auth().fetchSignInMethodsForEmail(this.signUpData.email)
.then((response) => {
... all logic
}).bind(this)
...since it outputs the following error:
firebaseInstance.auth(...).fetchSignInMethodsForEmail(...).bind is not a function
Here is the component's logic, can someone please suggest a proper way to access this after firebase response resolves? :bowing:
import { VALIDATION_MESSAGES, VALUES } from './signup.module.config'
import GLOBAL_EVENTS from 'values/events'
import { firebaseInstance } from 'database'
export default {
name: `SignUpForm`,
data() {
return {
signUpData: {
email: ``,
password: ``,
confirmPassword: ``
}
}
},
methods: {
onEmailSignUp() {
// Here this is component
console.log(this.$refs)
firebaseInstance.auth().fetchSignInMethodsForEmail(this.signUpData.email)
.then((response) => {
// other logic
} else {
// Here this is lost and equals undefined
this.$refs.email.setCustomValidity(`error`)
}
})
}
}
}
The bind instruction should be used on a function object, not on a function return value.
By doing
firebaseInstance.auth().fetchSignInMethodsForEmail(this.signUpData.email)
.then((response) => {
... all logic
}).bind(this)
You try to use bind on the return of the then method of you promise, which is a promise object and can't use bind.
You can try firebaseInstance.auth().fetchSignInMethodsForEmail(this.signUpData.email)
.then(function(response){
... all logic
}.bind(this))
instead. Here the bind is put on the function send in the promise so it should work correctly. I also transformed the function from arrow function to normal, because I think there is no need for arrow function with bind.
Using ES8 async/await sugar syntax you can do it like this :
async onEmailSignUp () {
try {
const response = await firebaseInstance.auth().fetchSignInMethodsForEmail(this.signUpData.email)
// other logic
} catch (error) {
console.log(error)
this.$refs.email.setCustomValidity(`error`)
}
}