<!-- template -->
<div>
<textarea v-model="someText">{{someText}}</textarea>
<div v-if="hasError">Something wrong here</div>
<input v-on:click="store" type="submit" value="update" />
</div>
//script
{
data() {
hasError: false,
someText: ""
},
store(){
return axios.post('/my/api/endpoint', { myvalue: this.someText })
.then(() => {
this.hasError= false;
})
.catch(() => {
this.hasError= true;
};
}
}
//test
import { mount } from 'vue-test-utils';
import MyComponent from "./component.vue";
import * as httpMock from 'moxios';
import Vue from "vue";
it("notifies when updates fail", (done) => {
const wrapper = mount(MyComponent);
httpMock.stubFailure("PUT", "/my/api/endpoint",
{
status: 500
});
httpMock.wait(() => {
wrapper.find(".button").trigger ("click");
Vue.nextTick(() => {
expect(wrapper.html()).toContain("Something wrong here");
done();
});
});
I have the above code to test error state in the vue app. Simply, i'm trying to test that if there is an error calling the server, a bit of text is displayed to say so. i've manually tested this in the browser and its all good, but i can't get a unit test around it. it just fails, saying expected '...' does not contain Something wrong here
probably something to do with the dom not being updated yet? But I thought that was what Vue.nextTick was for?
You're running wait before you actually trigger the axios call. Your call of the click event must be outside of wait.
wrapper.find(".button").trigger ("click");
httpMock.wait(() => {
Vue.nextTick(() => {
expect(wrapper.html()).toContain("Something wrong here");
done();
});
})
Also, I'm assuming you're importing axios in the component, as I don't actually see the import.
Related
I am new to Vue and stuck. I am trying to send user input data from a form into a vuex store. From that vuex store, an action will be called (fetching from API) and I would like that data back into my app and components.
<template>
<div>
<h1>APP NAME</h1>
<form action="submit" #submit.prevent="sendCityName()">
<label for="query"></label>
<input
type="text"
id="query"
v-model="cityName"
>
<button type="submit">Submit</button>
</form>
<h3>{{ lat }}</h3>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
data() {
return {
cityName: ''
}
},
computed: {
coordinates () {
return this.$store.state.lat
}
},
methods: {
sendCityName() {
this.$store.commit('fetchCity', this.cityName)
}
},
}
</script>
Here is my index.vue and getting the error "Cannot read properties of undefined (reading 'commit')"
here is my store.js. I want to use the lat and lon across my app.
export const state = () => ({
lat: '',
lon: ''
})
export const mutations = {
SET_LAT(state, payload){
state.lat = payload
},
SET_LON(state, payload){
state.lon = payload
}
}
export const actions = {
async fetchCity({ commit }, cityName) {
// make request
axios.get(
`https://api.openweathermap.org/geo/1.0/direct`, {
params: {
appid: "xxxxxxx",
q: cityName,
}
}).then((response) => {
commit('SET_LAT', response.data[0].lat);
commit('SET_LON', response.data[0].lng);
});
},
};
When I button submit I get the error "Cannot read properties of undefined (reading 'commit')"
Here is my working repo with the fixes mentioned below.
There are 3 things in your code:
remove vuex from package.json and run yarn again, that one is already baked into Nuxt as stated in the official documentation, those are the only steps needed
all the files inside of store will be namespaced by default for you, since you do have store/store.js, the proper syntax will be
async sendCityName() {
await this.$store.dispatch('store/fetchCity', this.cityName) // 👈🏻 store prefix
}
since you do use the axios module, you should have the following in your action (using the async/await syntax since it's more modern and preferable)
async fetchCity({ commit }, cityName) {
const response = await this.$axios.get(
`https://api.openweathermap.org/geo/1.0/direct`, {
params: {
appid: "3d91ba5b3c11d13158a2726aab902a0b",
q: cityName,
}
})
commit('SET_LAT', response.data[0].lat)
commit('SET_LON', response.data[0].lng)
}
Looking at the browser's console, you also have some errors to fix.
I can also recommend an ESlint + Prettier configuration so that you keep your code error-proof + properly formatted at all times.
So, I'm creating a Pokemon application and I would like to display the pokemon names using the api : https://pokeapi.co/api/v2/pokemon/.
I'm doing a fetch request on the api and then display the pokemon names in my template. I have 0 problem when I try to display only 1 pokemon but I have this error when I try to display all my pokemons using v-for.
Do you have any idea why I meet this error ?
<template>
<p class="dark:text-white"> {{pokemons[0].name}} </p> //working
<div v-for="(pokemon, index) in pokemons" :key="'poke'+index"> //not working...
{{ pokemon.name }}
</div>
</template>
<script>
const apiURL = "https://pokeapi.co/api/v2/pokemon/"
export default {
data(){
return{
nextURL:"",
pokemons: [],
};
},
created(){
this.fetchPokemons();
},
methods:{
fetchPokemons(){
fetch(apiURL)
.then( (resp) => {
if(resp.status === 200){
return resp.json();
}
})
.then( (data) => {
console.log(data.results)
// data.results.forEach(pokemon => {
// this.pokemons.push(pokemon)
// });
// this.nextURL = data.next;
this.pokemons = data.results;
console.log(this.pokemons);
})
.catch( (error) => {
console.log(error);
})
}
}
}
</script>
<style>
</style>
I've just pasted your code into a Code Pen and removed the working/not working comments and the code runs and shows the names.
Maybe the problem is in the parent component where this component is mounted, or the assignment of the :key attribute
try :key="'poke'+index.toString()", but I'm pretty sure js handels string integer concats quiet well.
Which version of vuejs do you use?
Edit from comments:
The parent component with the name PokemonListVue imported the posted component as PokemonListVue which resulted in a naming conflict. Renaming either one of those solves the issue.
In the error message posted, in line 3 it says at formatComponentName this is a good hint.
In my template, in a v-slot (which means users is not available in <script setup>), I have
<template v-slot:body-cell-assignedTo="props">
<q-td :props="props">
<div v-for="u in props.users" :key="u">{{u}}</div>
</q-td>
</template>
This displays
john
mary
I can enrich this information by calling an API:
fetch('https://example.com/api/user/john')
.then(r => r.json())
.then(r => console.log(r))
This displays in the console John de Brown, 1262-1423.
My question: how to combine these two mechanisms? In other words, how to asynchronously update the value in {{}}?
I would need to do something like
<div v-for="u in props.users" :key="u">{{enrichFetchFunction(u)}}</div>
but it would need to be asynchronous, and yet somehow return a value.
EDIT: I will ultimately enrich the source data that is displayed in the v-slot. I would still be interested, though, if waiting for such an asynchronous function there (à la await) is doable in Vuie.
I assume you are using Compositions API. See this playground
<script setup>
import { ref, onMounted } from 'vue'
const users = ref([])
onMounted(async() => {
fetch('https://mocki.io/v1/67abcfb6-4f25-4513-b0f9-1eb6c4906413')
.then(r => r.json())
.then(r => users.value = r)
})
</script>
<template>
<div v-for="u in users" :key="u">{{u}}</div>
</template>
This is doable with Lifecycle hooks such as mounted(), yet you will need some sort of listener to react to the information being changed. here is an example that updates the values as soon as it is mounted and includes a button that will also update the values (you can run the code here in Vue SFC Playground):
<template>
<div id="app">
<h1 v-for="u in enrichedUsers" :key="u">{{ u }}</h1>
<button #click="myAsyncFunction">
update
</button>
</div>
</template>
<script>
// pseudo api
const fetchEnrichedAPI = function(user) {
return new Promise( (resolve, reject) => {
var enrichedUsers = []
if (user.includes('john')) {
enrichedUsers.push('John de Brown, 1262-1423')
}
if (user.includes('mary')){
enrichedUsers.push('Mary de Purple, 1423-1262')
}
setTimeout(() => {
resolve(enrichedUsers);
}, 300);
});
}
export default{
data() {
return {
props : { users: ['john','mary'] },
enrichedUsers: []
}
},
mounted() {
// when mounted run this async function
this.myAsyncFunction()
},
methods: {
async myAsyncFunction() {
// call api passing the list of users
await fetchEnrichedAPI(this.props.users)
.then((data) => {
// if api work
this.enrichedUsers = data;
return true;
})
.catch((e) => {
// if the api doesn't work
console.error(e);
this.enrichedUsers = this.props.users;
})
}
},
}
</script>
I am aware that this does not use props, but it does work. If you would like to expand this to use props you may be able to do this with computed properties or functions in the v-for. See this post for more info on that.
I have typical scenario where I call REST API in vuex actions to fetch some data and then I commit that to mutation.
I use async/await syntax and try/catch/finally blocks. My vuex module looks something like this:
const state = {
users: null,
isProcessing: false,
operationError: null
}
const mutations = {
setOperationError (state, value) {
state.operationError = value
},
setIsProcessing (state, value) {
state.isProcessing = value
if (value) {
state.operationError = ''
}
},
setUsers(state, value) {
state.users= value
}
}
const actions = {
async fetchUsers ({ commit }) {
try {
commit('setIsProcessing', true)
const response = await api.fetchUsers()
commit('setUsers', response.result)
} catch (err) {
commit('setUsers', null)
commit('setOperationError', err.message)
} finally {
commit('setIsProcessing', false)
}
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
Notice that I handle catch(err) { } in vuex action and don’t rethrow that error. I just save error message in the state and then bind it in vue component to show it if operationError is truthy. This way I want to keep vue component clean from error handling code, like try/catch.
I am wondering is this right pattern to use? Is there a better way to handle this common scenario? Should I rethrow error in vuex action and let it propagate to the component?
What I usually do, is have a wrapper around the data being posted, that handles the api requests and stores errors. This way your users object can have the errors recorded on itself and you can use them in the components if any of them are present.
For example:
import { fetchUsers } from '#\Common\api'
import Form from '#\Utils\Form'
const state = {
isProcessing: false,
form: new Form({
users: null
})
}
const mutations = {
setIsProcessing(state, value) {
state.isProcessing = value
},
updateForm(state, [field, value]) {
state.form[field] = value
}
}
const actions = {
async fetchUsers ({ state: { form }, commit }) {
let users = null
commit('setIsProcessing', true)
try {
users = await form.get(fetchUsers);
} catch (err) {
// - handle error
}
commit('updateForm', ['users', users])
commit('setIsProcessing', false)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
Then in the component you can use the errors object on the wrapper like so:
<template>
<div>
<div class="error" v-if="form.erros.has('users')">
{{ form.errors.get('users') }}
</div>
<ul v-if="users">
<li v-for="user in users" :key="user.id">{{ user.username }}</li>
</ul>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
computed: {
...mapState('module' ['form']),
users () {
return this.form.users
}
}
</script>
This is just my personal approach that I find very handy and it served me well up to now. Don't know if there are any standard patterns or if there is an explicit "correct way" to do this.
I like the wrapper approach, because then your errors become automatically reactive when a response from api returns an error.
You can re-use it outside vuex or even take it further and inject the errors into pre-defined error boundaries which act as wrapper components and use the provide/inject methods to propagate error data down the component tree and display them where ever you need them to show up.
Here's an example of error boundary component:
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {
props: {
module: {
type: String,
required: true,
validator: function (value) {
return ['module1', 'module2'].indexOf(value) !== -1
}
},
form: {
type: String,
default: 'form'
}
},
provide () {
return {
errors: this.$store.state[this.module][this.form].errors
}
}
}
</script>
Wrap some part of the application that should receive the errors:
<template>
<div id="app">
<error-boundary :module="module1">
<router-view/>
</error-boundary>
</div>
</template>
Then you can use the errors from the users wrapper in child components like so:
If you have a global error like no response from api and want to display it in the i.e.: sidebar
<template>
<div id="sidebar">
<div v-if="errors.has('global')" class="error">
{{ errors.get('global').first() }}
</div>
...
</div>
</template>
<script>
export default {
inject: [
'errors'
],
...
}
</script>
And the same error object re-used somewhere inside a widget for an error on the users object validation:
<template>
<div id="user-list">
<div v-if="errors.has('users')" class="error">
{{ errors.get('users').first() }}
</div>
...
</div>
</template>
<script>
export default {
inject: [
'errors'
],
...
}
</script>
Jeffrey Way did a series on Vue2 a while ago and he proposed something similar. Here's a suggestion on the Form and Error objects that you can build upon: https://github.com/laracasts/Vue-Forms/blob/master/public/js/app.js
Using Vue TreeSelect Plugin to load a nested list of nodes from firebase backend. It's doc page says,
It's also possible to have root level options to be delayed loaded. If no options have been initially registered (options: null), vue-treeselect will attempt to load root options by calling loadOptions({ action, callback, instanceId }).
loadOptions (in my App.vue) dispatch vuex action_FolderNodesList, fetches (from firebase) formats (as required by vue-treeselect), and mutates the state folder_NodesList, then tries to update options this.options = this.get_FolderNodesList but this does not seems to work.
Here is the loadOptions method (in app.vue)
loadOptions() {
let getFolderListPromise = this.$store.dispatch("action_FolderNodesList");
getFolderListPromise.then(_ => {
this.options = this.get_FolderNodesList;
});
}
Vue errors out with Invalid prop: type check failed for prop "options". Expected Array, got String with value ""
I am not sure what am I doing wrong, why that does not work. A working Codesandbox demo
Source
App.vue
<template>
<div class="section">
<div class="columns">
<div class="column is-7">
<div class="field">
<Treeselect
:multiple="true"
:options="options"
:load-options="loadOptions"
:auto-load-root-options="false"
placeholder="Select your favourite(s)..."
v-model="value" />
<pre>{{ get_FolderNodesList }}</pre>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex";
import Treeselect from "#riophae/vue-treeselect";
import "#riophae/vue-treeselect/dist/vue-treeselect.css";
export default {
data() {
return {
value: null,
options: null,
called: false
};
},
components: {
Treeselect
},
computed: mapGetters(["get_FolderNodesList"]),
methods: {
loadOptions() {
let getFolderListPromise = this.$store.dispatch("action_FolderNodesList");
getFolderListPromise.then(_ => {
this.options = this.get_FolderNodesList;
});
}
}
};
</script>
Store.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
folder_NodesList: ""
},
getters: {
get_FolderNodesList(state) {
return state.folder_NodesList;
}
},
mutations: {
mutate_FolderNodesList(state, payload) {
state.folder_NodesList = payload;
}
},
actions: {
action_FolderNodesList({ commit }) {
fmRef.once("value", snap => {
var testObj = snap.val();
var result = Object.keys(testObj).reduce((acc, cur) => {
acc.push({
id: cur,
label: cur,
children: recurseList(testObj[cur])
});
return acc;
}, []);
commit("mutate_FolderNodesList", result);
});
}
}
});
Any help is appreciated.
Thanks
It seems you are calling this.options which would update the entire element while only the current expanding option should be updated.
It seems loadOptions() is called with some arguments that you can use to update only the current childnode. The first argument seems to contain all the required assets so I wrote my loadTreeOptions function like this:
loadTreeOptions(node) {
// On initial load, I set the 'children' to NULL for nodes to contain children
// but inserted an 'action' string with an URL to retrieve the children
axios.get(node.parentNode.action).then(response => {
// Update current node's children
node.parentNode.children = response.data.children;
// notify tree to update structure
node.callback();
}).catch(
errors => this.onFail(errors.response.data)
);
},
Then I set :load-options="loadTreeOptions" on the <vue-treeselect> element on the page. Maybe you were only missing the callback() call which updates the structure. My installation seems simpler than yours but it works properly now.