Vue JS form issue - mutating vuex store state outside handlers - javascript

I am currently getting the [vuex] Do not mutate vuex store state outside mutation handlers error only when I try to edit the form, if I am posting a new plugin it works fine. From this doc I'm not sure where I'm going wrong - while I fetch the plugin from vuex, I try to give the local state those values and then leave vuex alone. Ideally once fetched vuex, I wouldn't need to touch it again until the form is submitted. But I'm not sure what is causing the error exactly
<template>
<div>
<h4>{{this.$route.query.mode==="new"?"New":"Edit"}} Plugin</h4>
<form class="">
<label>Id</label>
<input :value="plugin.id" class="" type="text" #input="updateId">
<label>Name</label>
<input :value="plugin.name" class="" type="text" #input="updateName">
<label>Description</label>
<textarea :value="plugin.description" class="" type="text" #input="updateDescription"></textarea>
<label>Version</label>
<input :value="plugin.version" class="" type="text" #input="updateVersion">
<button type="submit" #click.prevent="submitForm">Submit</button>
</form>
</div>
</template>
<script>
import util from '~/assets/js/util'
export default {
created() {
if (this.mode === 'edit') {
this.plugin = this.$store.state.currentLicence.associatedPlugins.find(p => p.pluginId === this.$route.query.pluginId)
}
},
methods: {
updateId(v) {
this.plugin.id = v.target.value
},
updateName(v) {
this.plugin.name = v.target.value
},
updateDescription(v) {
this.plugin.description = v.target.value
},
updateVersion(v) {
this.plugin.version = v.target.value
}
},
computed: {
mode() { return this.$route.query.mode }
},
data: () => ({
plugin: {
id: null,
name: null,
description: null,
version: null
}
})
}
</script>
Thanks for any help, clearly my understanding of the way that vuex and local state are handled is flawed

You are getting this error because you are editing the state directly.
this.plugin = this.$store.state.currentLicence.associatedPlugins.find(p => p.pluginId === this.$route.query.pluginId) - this is exactly this part of code where you put the object from the store directly into the data, therefore by editing the field you are directly editing the state. Don't do that!
You should always use stuff like (I am not sure how nested computed will work but I don't think you have to nest it):
computed: {
plugin: {
id: {
get () { // get it from store }
set (value) { // dispatch the mutation with the new data }
}
}
}
There is a nice package whill will do most work for you: https://github.com/maoberlehner/vuex-map-fields . You can use it to semi-automatic generate computed with getters and setters for each field.

Related

Wait for definition before calling function - Vue.js

When it comes to creating methods in child components I'm having a hard time figuring a particular feature out.
I have this parent route/component (League.vue):
In this league.vue I render a child component:
<router-view :league="league" />
Child component:
<template>
<div v-if="teams_present">
<div class="page-container__table">
<h3 class="page-container__table__header">Teams</h3>
</div>
</div>
</template>
<script>
export default {
name: 'LeagueTeams',
props: [
'league'
],
data () {
},
computed: {
teams_present: function () {
return this.league.teams.length > 0
}
}
}
</script>
ERROR:
"TypeError: Cannot read property 'length' of undefined"
So it appears that the computed callback is called before the prop can be set, I think? and if a change it to methods it never gets called. How do I handle this case?
As Ali suggested, you can return this.league.teams && this.league.teams.length > 0, which definitely will work.
However, as my experience, to avoid these situation, and for good practice, always declare the type of the Props. So in your props:
export default {
name: 'LeagueTeams',
props: {
league: {
type: Object, // type validation Object
default() { return {teams: [] }} // add a default empty state for team, you can add more
}
},
data () {
},
computed: {
teams_present: function () {
return this.league.teams.length > 0 // now the old code should work
}
}
}
</script>
By doing this, you don't need to care much about checking the edge case of this.league.teams every time, since you may need to call it again in methods or in the <template> html
Update: Another suggestion is if you are using vue-cli 4, you can use Optional chaining and nullish coalescing.
return this.league?.teams.length ?? false // replace with only this line will work
Hope this will help you 2 more ways to deal with in these situations, and depends on situations you can choose the most suitable one

Vue - Render components depending on state of parent data

I'm trying to render components depending on the state of an array in the parent (App.vue). I'm not sure at all that this is the correct approach for this use case (new to Vue and not experienced programmer) so I will gladly take advice if you think this is not the right way to think about this.
I'm trying to build a troubleshooter that consists of a bunch of questions. Each question is a component with data that look something like this:
data: function() {
return {
id: 2,
question: "Has it worked before?",
answer: undefined,
requires: [
{
id: 1,
answer: "Yes"
}
]
}
}
This question is suppose to be displayed if the answer to question 1 was yes.
My problem is I'm not sure on how to render my components conditionally. Current approach is to send an event from the component when it was answered, and to listen to that event in the parent. When the event triggers, the parent updates an array that holds the "state" of all answered questions. Now I need to check this array from each component to see if there are questions there that have been answered and if the right conditions are met, show the question.
My question is: How can I check for data in the parent and show/hide my component depending on it? And also - is this a good idea or should I do something different?
Here is some more code for reference:
App.vue
<template>
<div id="app">
<div class="c-troubleshooter">
<one #changeAnswer="updateActiveQuestions"/>
<two #changeAnswer="updateActiveQuestions"/>
</div>
</div>
</template>
<script>
import one from './components/one.vue'
import two from './components/two.vue'
export default {
name: 'app',
components: {
one,
two
},
data: function() {
return {
activeQuestions: []
}
},
methods: {
updateActiveQuestions(event) {
let index = this.activeQuestions.findIndex( ({ id }) => id === event.id );
if ( index === -1 ) {
this.activeQuestions.push(event);
} else {
this.activeQuestions[index] = event;
}
}
}
}
</script>
two.vue
<template>
<div v-if="show">
<h3>{{ question }}</h3>
<div class="c-troubleshooter__section">
<div class="c-troubleshooter__input">
<input type="radio" id="question-2-a" name="question-2" value="ja" v-model="answer">
<label for="question-2-a">Ja</label>
</div>
<div class="c-troubleshooter__input">
<input type="radio" id="question-2-b" name="question-2" value="nej" v-model="answer">
<label for="question-2-b">Nej</label>
</div>
</div>
</div>
</template>
<script>
export default {
data: function() {
return {
id: 2,
question: "Bla bla bla?",
answer: undefined,
requires: [
{
id: 1,
answer: "Ja"
}
]
}
},
computed: {
show: function() {
// Check in parent to see if requirements are there, if so return true
return true;
}
},
watch: {
answer: function() {
this.$emit('changeAnswer', {
id: this.id,
question: this.question,
answer: this.answer
})
}
}
}
</script>
 Rendering questions conditionally
as #Roy J suggests in comments, questions data probably belongs to the parent. It is the parent who handles all the data and who decides which questions should be rendered. However, there are plenty of strategies for this:
Display questions conditionally with v-if or v-show directly in the parent template:
Maybe the logic to display some questions is not at all generic. It can depend upon more things, user settings... I don't know. If that's the case, just render the questions conditionally directly in the parent, so you don't need to access the whole questions data in any question. Code should be something like the following:
<template>
<div id="app">
<div class="c-troubleshooter">
<one #changeAnswer="updateActiveQuestions" v-if="displayQuestion(1)"/>
<two #changeAnswer="updateActiveQuestions" v-if="displayQuestion(2)"/>
</div>
</div>
</template>
<script>
import one from './components/one.vue'
import two from './components/two.vue'
export default {
name: 'app',
components: {
one,
two
},
data: function() {
return {
activeQuestions: [],
}
},
methods: {
updateActiveQuestions(event) {
let index = this.activeQuestions.findIndex( ({ id }) => id === event.id );
if ( index === -1 ) {
this.activeQuestions.push(event);
} else {
this.activeQuestions[index] = event;
}
},
displayQuestion(index){
// logic...
}
},
}
</script>
Pass a reference to the previous question to every question:
If any question should be visible only when the previous question has been answered or viewed or something like that, you can pass that as a prop to every question, so they know wether they must render or not:
<template>
<div id="app">
<div class="c-troubleshooter">
<one #changeAnswer="updateActiveQuestions"/>
<two #changeAnswer="updateActiveQuestions" prev="activeQuestions[0]"/>
</div>
</div>
</template>
And in two.vue:
props: ['prev'],
computed: {
show: function() {
return this.prev && this.prev.status === 'ANSWERED';
// or some logic related to this, idk
}
},
just pass the whole data to the children:
As you coded it, you can just pass the whole questions data as a prop to every question component, then use it in a computed property. This is not what I would do, but just works, and since objects are references this is not necessarily unperformant.
Using a generic component:
It seems weird to have a one.vue, two.vue for every question, and sure does not scale well.
I'm not really sure how modular I can do them since the template for each question can be a bit different. Some have images or custom elements in them for example, while others don't.
If template are really different from each question to another, this can get complicated. However, if, as I suspect, they share common HTML structure, with a defined header or a common 'ask' button at the bottom and stuff like that, then you should be able to address this using Vue slots.
Apart from template issues, I suppose that every question in your app can get an arbitrary number of 'sub-questions' (as two.vue having question-2-a and question-2-b). This will require a complex and flexible data structure for the questions data (which will get more complex when you start to add multiple choices, multiple possible answers etc. etc.). This can get very complex but you should probably work on this until you can use a single question.vue component, this will surely pay out.
tip: avoid watchers
You're using v-model to answer in the two.vue template, then using a watcher to track changes in the answer variable and emit the event. This is convoluted and difficult to read, you can use #input or #change events on the <input> element instead:
<input type="radio" id="question-2-a" name="question-2" value="ja" v-model="answer" #input="emitAnswer">
And then instead of the watcher, have a method:
emitAnswer() {
this.$emit('changeAnswer', {
id: this.id,
question: this.question,
answer: this.answer
})
This is a pretty broad question, but I'll try to give some useful guidance.
First data should be used for internal state. Very often, a component should use props for things you might think would be data it owns. That is the case here: the questions need to be coordinated by the parent, so the parent should own the data. That allows you to make a sensible function to control whether a question component displays.
Having the parent own the data also allows you to make one question component that configures itself according to its props. Or you might have a few different question component types (you can use :is to select the right one), but almost certainly some of them are reusable if you pass their question/answer/other info in.
To update answers, you will emit changes from the question components and let the parent actually change the value. I use a settable computed to allow the use of v-model in the component.
new Vue({
el: '#app',
data() {
return {
questions: [{
id: 1,
question: 'blah 1?',
answer: null
},
{
id: 2,
question: 'blah 2?',
answer: null,
// this is bound because data is a function
show: () => {
const q1 = this.questions.find((q) => q.id === 1);
return Boolean(q1.answer);
}
},
{
id: 3,
question: 'Shows anyway?',
answer: null
}
]
};
},
components: {
questionComponent: {
template: '#question-template',
props: ['props'],
computed: {
answerProxy: {
get() {
return this.answer;
},
set(newValue) {
this.$emit('change', newValue);
}
}
}
}
}
});
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
<div class="c-troubleshooter">
<question-component v-for="q in questions" v-if="!q.show || q.show()" :props="q" #change="(v) => q.answer = v" :key="q.id">
</question-component>
</div>
<h2>State</h2>
<div v-for="q in questions" :key="q.id">
{{q.question}} {{q.answer}}
</div>
</div>
<template id="question-template">
<div>
{{props.question}}
<div class="c-troubleshooter__input">
<input type="radio" :id="`question-${props.id}-a`" :name="`question-${props.id}`" value="ja" v-model="answerProxy">
<label :for="`question-${props.id}-a`">Ja</label>
</div>
<div class="c-troubleshooter__input">
<input type="radio" :id="`question-${props.id}-b`" :name="`question-${props.id}`" value="nej" v-model="answerProxy">
<label :for="`question-${props.id}-b`">Nej</label>
</div>
</div>
</template>

Vuex how to Access state data on component mounted or created hook?

I'm trying to create a Quill.js editor instance once component is loaded using mounted() hook. However, I need to set the Quill's content using Quill.setContents() on the same mounted() hook with the data I received from vuex.store.state .
My trouble here is that the component returns empty value for the state data whenever I try to access it, irrespective of being on mounted() or created() hooks. I have tried with getters and computed properties too. Nothing seems to work.
I have included my entry.js file, concatenated all the components to make things simpler for you to help me.
Vue.component('test', {
template:
`
<div>
<ul>
<li v-for="note in this.$store.state.notes">
{{ note.title }}
</li>
</ul>
{{ localnote }}
<div id="testDiv"></div>
</div>
`,
props: ['localnote'],
data() {
return {
localScopeNote: this.localnote,
}
},
created() {
this.$store.dispatch('fetchNotes')
},
mounted() {
// Dispatch action from store
var quill = new Quill('#testDiv', {
theme: 'snow'
});
// quill.setContents(JSON.parse(this.localnote.body));
},
methods: {
setLocalCurrentNote(note) {
console.log(note.title)
return this.note = note;
}
}
});
const store = new Vuex.Store({
state: {
message: "",
notes: [],
currentNote: {}
},
mutations: {
setNotes(state,data) {
state.notes = data;
// state.currentNote = state.notes[1];
},
setCurrentNote(state,note) {
state.currentNote = note;
}
},
actions: {
fetchNotes(context) {
axios.get('http://localhost/centaur/public/api/notes?notebook_id=1')
.then( function(res) {
context.commit('setNotes', res.data);
context.commit('setCurrentNote', res.data[0]);
});
}
},
getters: {
getCurrentNote(state) {
return state.currentNote;
}
}
});
const app = new Vue({
store
}).$mount('#app');
And here is the index.html file where I'm rendering the component:
<div id="app">
<h1>Test</h1>
<test :localnote="$store.state.currentNote"></test>
</div>
Btw, I have tried the props option as last resort. However, it didn't help me in anyway. Sorry if this question is too long. Thank you for taking your time to read this. Have a nice day ;)
I copied your code and tested it ( of-course I created my own dummy notes so I could remove the get request ) and I was able to get the notes display on a page.
A couple of things that I realized from your code, you may need to add a store property as there are places in your component ( test ) where you are referencing it, yet you only define it on the 'app' component. So in this section of your code modify as shown below:
props: ['localnote'],
data() {
return {
localScopeNote: this.localnote,
store : store
}
},
The key difference is the definition of the 'store' property. Please note that, what you have done, defining a "store" property in your app component, is correct, but the very same needs to be defined in "test" component as I have shown in the above code snippet above.
Second thing is, you are using $store and I guess that gives you undefined, unless as you said, in the libraries that you included this resolves accordingly, but on my side I had to remove all references of "$store" and replace it with just "store" (without the dollar sign).
Lastly for testing purposes, I would advise you to also

Binding Input in Vue not working

I would like to call a function with a value when a user starts typing in an input box. I have tried two approaches.
The first approach is trying to use two-way binding to a model. However, after following the documentation I get an error.
Here is the example from the official docs:
<div id="app-6">
<p>{{ message }}</p>
<input v-model="message">
</div>
var app6 = new Vue({
el: '#app-6',
data: {
message: 'Hello Vue!'
}
})
And here's my example:
<template lang="html">
<input
type="text"
v-model="handle"
/>
</template>
<script>
export default {
data: {
handle: 'model',
}
};
</script>
I am writing this as part of an application so I chose not to recreate the Vue instance and I declared that elsewhere. However, I get this error:
Property or method "handle" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
A second approach I've tried is this calling a function directly from the view via an event handler. I'm coming from React so this is my preferable approach. However, the function has undefined as an input value meaning it's not picking up the value of the input.
<template lang="html">
<input
type="text"
v-on:keyup="handleInput()"
/>
</template>
<script>
export default {
methods: {
handleInput(input) {
// input -> undefined
},
},
};
</script>
I really can't see why neither of these works. Wouldn't the expected behavior of an input listener would be to pass the value?
Where am I going wrong?
It seems like you might have to do something like this: How to fire an event when v-model changes ? (vue js). What I don't understand is why you have to manually attach a watcher when you have assigned a v-model? Isn't that what a v-model is supposed to do?
What finally worked was this:
<template lang="html">
<input
type="text"
v-model="searchTerm"
#keyup.enter="handleInput"
/>
</template>
<script>
export default {
data() {
return { searchTerm: '' }
},
methods: {
handleInput(event) {/* handle input */},
},
};
</script>
Shouldn't data be a function on your first example? I think this is how it works for vue components.
<script>
export default {
data: function () {
return { handle: 'model' }
}
};
</script>
I think this was explained somewhere on vuecasts.com, but I might be wrong. :)

Watching array stored in Vuex in VueJS

I have a customer list which is actually an array of objects. I store it in Vuex. I render the list in my component and each row has a checkbox. More precisely I use keen-ui and the checkbox rendering part looks like:
<tr v-for="customer in customers" :class="{ selected: customer.selected }">
<td>
<ui-checkbox :value.sync="customer.selected"></ui-checkbox>
</td>
<td>{{ customer.name }}</td>
<td>{{ customer.email }}</td>
</tr>
So the checkbox directly changes customers array which is bad: I use strict mode in Vuex and it throws me an error.
I want to track when the array is changed and call an action in order to change the vuex state:
watch: {
'customers': {
handler() {
// ...
},
deep: true
}
However it still changes the customer directly. How can I fix this?
First and foremost, be careful when using .sync: it will be deprecated in 2.0.
Take a look at this: http://vuex.vuejs.org/en/forms.html, as this problem is solved in here. Basically, this checkbox should trigger a vuex action on input or change. Taken from the docs:
<input :value="message" #input="updateMessage">
Where updateMessage is:
vuex: {
getters: {
message: state => state.obj.message
},
actions: {
updateMessage: ({ dispatch }, e) => {
dispatch('UPDATE_MESSAGE', e.target.value)
}
}
}
If you do not wish to track the mutations, you can move the state of this component away from vuex, to be able to use v-model in all its glory.
You just have to make a custom getter and setter:
<template>
<ui-checkbox :value.sync="thisCustomer"></ui-checkbox>
</template>
<script>
//this is using vuex 2.0 syntax
export default {
thisCustomer: {
get() {
return this.$store.state.customer;
},
set(val) {
this.$store.commit('SET_CUSTOMER', val);
// instead of performing the mutation here,
// you could also use an action:
// this.$store.disptach('updateCustomer')
}
},
}
</script>
In your store:
import {
SET_CUSTOMER,
} from '../mutation-types';
const state = {
customer: null,
};
const mutations = {
[SET_CUSTOMER](state, value) {
state.customer = value;
},
}
I'm not exactly sure what your store looks like, but hopefully this gives you the idea :)
if your customers are in the root state, you can try this:
watch: {
'$store.state.customers'{
handler() {
// ...
},
deep: true
}
}
try using mapState in your component and watch the customers like you have done above.worked for me

Categories