Error: vuex do not mutate vuex store state outside mutation handlers With uploading image - vue.js

i'm kinda stuck. i get this error every time i try to update my store. what i'm doing is, i fetch my product and store it. here on upload image section (component) at created fill the image with state to show the already uploaded images, then on each upload change my state via dispatch and mutation. i know it has something to do with created() when i fill my data with state, but don't know how to solve that.
here is my template:
<v-card flat class="pb-4" max-width="450px">
<v-img class="rounded-lg theme__main__color" :src="url"></v-img>
<v-list container inline class="transparent text-center pa-0">
<v-list-item class="px-0">
<v-list-item class="px-1">
<v-btn
width="100%"
class="theme__btn__w text_main_color"
:loading="isSelecting"
#click="onChooseClick"
>{{lang.chooseimage}}</v-btn>
</v-list-item>
<v-list-item class="px-1">
<v-btn
width="100%"
class="theme__btn__s text_main_color"
#click.prevent="uploadImage"
>{{lang.upload}}</v-btn>
<input
ref="uploader"
class="d-none"
type="file"
accept="image/*"
#change="onFileChanged"
>
</v-list-item>
</v-list-item>
</v-list>
</v-card>
and this is my script:
export default {
data(){
return{
isSelecting: false,
selectedFile: null,
url: null,
placeholder: '/images/placeholder/place-800.png',
section: 'img',
pimages: [],
pId: null
}
},
methods:{
onChooseClick(){
this.isSelecting = true
window.addEventListener('focus', () => {
this.isSelecting = false
}, { once: true })
this.$refs.uploader.click()
},
onFileChanged(e) {
this.selectedFile = e.target.files[0]
this.url = URL.createObjectURL(this.selectedFile)
// do something
},
async uploadImage(){
if(this.notEmpty(this.selectedFile) && this.notEmpty(this.pId)){
const data = new FormData()
data.append('image', this.selectedFile)
data.append('pId', this.pId)
// let response = await this.axiosPost('product/createproimg', data)
this.pimages.push({"url": this.url})
this.url = this.placeholder
this.setEditProductImg(this.pimages)
}
}
},
created(){
this.url = this.placeholder
this.pId = this.editProduct.pId
this.pimages = this.editProduct.images
this.$nuxt.$on('insert',(section)=>{
if(section === 'desc' && !this.pId){
this.pId = this.editProduct.pId
}
})
}
}
and of course my store:
state:
editProduct: {
pId: null,
images: []
}
getter:
editProduct(state){
return state.editProduct
}
mutation:
SET_EDITPRODUCT_IMG(state, img){
state.editProduct.images = img
},
action:
setEditProductImg({commit}, img){
commit('SET_EDITPRODUCT_IMG', img)
},
UPDATE
thanks to #skirtle the above problem has been solved! but got a new same error on something else. as advise by #skirtle used const to mutate my state but get error. to be more clear, my state is an empty array, it will be filled the first time, but i get the error if i even try to change my select!!! let alone send another mutation!! here is the codes:
<template>
<div class="pt-6">
<v-row class="ma-0">
<v-col cols="12" md="12" class="pa-0">
<v-row class="ma-0">
<!-- form 1 -->
<template v-for="(select, index) in selects">
<component
:is="select"
:key="select.name"
v-model="catId"
#changed="addComponent(index)"
:catid="catId"
:selectindex="index"
:pcat="productCat[index]"
:subcat="subCat"
></component>
</template>
<!-- btn -->
<v-col cols="12" sm="6" md="3" class="px-1">
<v-btn width="100%" class="theme__little__color2 text_main_color px-2" #click.prevent="addCatBtn()">{{lang.addcat}}</v-btn>
</v-col>
{{selectedCatArr}}
<!-- btn -->
<addproductbtn :section="section" />
</v-row>
</v-col>
</v-row>
</div>
</template>
<script>
import addproductbtn from '~/components/global/cms/addproductbtn'
import selectcategory from '~/components/global/cms/selectcategory'
export default {
components:{
'addproductbtn': addproductbtn
},
data(){
return{
section: 'cat',
selects: [selectcategory],
catId: 0,
subCat: true,
selectedCatArr: []
}
},
methods:{
addComponent(index){
this.selects.length = index + 1
setTimeout(() => {
this.selects.push(selectcategory)
}, 1);
},
addCatBtn(){
this.goToRedirect('/cms/category/insert', this.$route.path, this.ProductId)
},
async insertCategory(){
const data = {
pId: this.editProduct.pId,
catId: this.catId
}
// let response = await this.axiosPost('product/catupdate', data)
const productCategory = this.selectedCatArr
this.setEditProductCat(productCategory)
}
},
computed:{
productCat(){
return this.editProduct.categories
},
ProductId(){
return this.editProduct.pId
}
},
created(){
this.$nuxt.$on('insert', ()=>{
this.insertCategory()
})
this.$nuxt.$on('nextcat', (subCat)=>{
this.subCat = subCat
})
this.$nuxt.$on('nextpanel', ()=>{
this.insertCategory()
})
this.$nuxt.$on('selectedcat', (selected, index)=>{
delete selected.subCategory
this.selectedCatArr.length = index
this.selectedCatArr.push(selected)
})
}
}
</script>
and my select component:
<template>
<v-col cols="12" sm="6" md="3" class="px-1 text_details_color3" v-if="showCat">
<v-select
return-object
:items="items"
:label="lang.category"
v-model="selected"
#change="emitEvent"
item-text="title"
item-value="id"
outlined></v-select>
{{selected}}
</v-col>
</template>
<script>
export default {
props:['selectindex','catid','pcat','subcat'],
data(){
return{
selected:{},
items:[],
showCat: true
}
},
async fetch(){
// this.items = await this.axiosGet(`categories/${this.catid}/1`)
this.items = [
{id: this.catid + 1, title: this.catid+'title1', subCategory: true},
{id: this.catid + 2, title: this.catid+'title2', subCategory: true},
{id: this.catid + 3, title: this.catid+'title3', subCategory: false},
{id: this.catid + 4, title: this.catid+'title4', subCategory: true}
]
},
methods:{
emitEvent(){
this.$emit('input', this.selected.id)
this.$emit('changed')
$nuxt.$emit('nextcat', this.selected.subCategory)
$nuxt.$emit('selectedcat', this.selected, this.selectindex)
}
},
computed:{
//
},
created(){
},
mounted(){
this.selected = this.pcat
this.showCat = this.subcat
}
}
</script>

I believe the problem is this line:
this.pimages.push({"url": this.url})
The array this.pimages is the same array that's inside the store state and by calling push you're modifying it outside the store.
There are a couple of ways you could fix this.
One way would be to perform the push inside a mutation, e.g. by having an ADD_EDITPRODUCT_IMG mutation:
mutation:
ADD_EDITPRODUCT_IMG(state, img){
state.editProduct.push(img)
},
You'd then call that in much the same way as with your current mutation, except that you'd just pass it the new image to add rather than passing the full array.
An alternative approach that is a bit closer to what you currently have would be to take a copy of the array rather than modifying the original:
const newImages = [...this.pimages, {"url": this.url}]
this.setEditProductImg(newImages)
Using this approach you wouldn't need any changes to your existing store.
Update:
I also suggest making pimages a computed property. There doesn't seem to be any good reason to 'copy' the data out of the store state in a created hook.

Related

Vuetify v-dialog not showing the second time

I'm trying to to conditionally show a dialog. The dialog shows up the first time just fine.
But when second time I try to remount the component I can't see the dialog.
The dialog also fetches some data when it is mounted. I can see the logs that the dialog is being mounted and unmounted and also the network request on chrome devtools network tab but I can't see the dialog.
index.vue
<v-list-item-title #click="changeDialogState">
<TestDialog
v-if="showEditDialog"
:id="item.id"
#editDone="changeDialogState"/>
Edit
</v-list-item-title>
------------------------
//This is defined inside methods
changeDialogState() {
this.showEditDialog = !this.showEditDialog // this.showEditDialog is just a boolean value define inside data()
},
Testdialog.vue
<template>
<v-dialog v-model="dialog" width="700">
<v-progress-circular
v-if="fetching"
:size="70"
:width="7"
color="purple"
indeterminate
></v-progress-circular>
<v-card v-else>
<v-card-title> New Customer </v-card-title>
<v-divider></v-divider>
<v-card-text>
<customer-form
v-model="customer"
:errors="errors"
:initial-customer="customer"
#submit="editCustomer"
>
<template v-slot:footer>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text #click="$emit('editDone')"> Cancel </v-btn>
<v-btn color="primary" type="submit" class="ma-1">
Save Customer
</v-btn>
</v-card-actions>
</template>
</customer-form>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script>
import { formHandling, paginatedResponse } from '~/mixins'
export default {
mixins: [formHandling, paginatedResponse],
props: {
value: {
type: Object,
default: () => ({
//some stuff here
}),
},
error: {
type: Array,
required: true,
default: () => {
return []
},
},
id: {
type: String,
required: true,
},
},
async fetch() {
this.fetching = true
this.customer = await this.$axios.$get(`/customer/${this.id}/`)
this.fetching = false
},
data() {
return {
dialog: true,
customer: {
...
},
errors: {
...
},
fetching: true,
}
},
mounted() {
console.log('TestDialog - mounted')
},
beforeDestroy() {
console.log('TestDialog - unmounted')
},
methods: {
async editCustomer(customer) {
try {
await this.$axios.$put(`/customer/${this.id}/`, customer)
this.dialog = false
} catch (err) {
this.setErrors(err)
}
},
},
}
</script>

NuxtJS component - component renders without data

I have a component that uses fetch to bring data via Axios and return data to render. I am using the store to set/get the data. The component renders with empty data and after debugging I see that data() method is called before fetch() method.
How to fix the problem and bring the data before the component is rendered
here is the component code:
<template>
<v-card
class="mx-auto"
max-width="500"
>
<v-sheet class="pa-4 primary lighten-2">
<v-text-field
v-model="search"
label="Search Company Directory"
dark
flat
solo-inverted
hide-details
clearable
clear-icon="mdi-close-circle-outline"
></v-text-field>
<v-checkbox
v-model="caseSensitive"
dark
hide-details
label="Case sensitive search"
></v-checkbox>
</v-sheet>
<v-card-text>
<v-treeview
:items="items"
:search="search"
:filter="filter"
:open.sync="open"
>
<template v-slot:prepend="{ item }">
<v-icon
v-if="item.children"
v-text="mdi-${item.id === 1 ? 'home-variant' : 'folder-network'}"
></v-icon>
</template>
</v-treeview>
</v-card-text>
</v-card>
</template>
<script>
export default {
async fetch(){
console.log("Hi from Fetch !!!")
let response = await this.$axios.get('/items/tasks', {baseURL: 'http://localhost:8055'});
let tasks = response.data.data;
debugger
this.$store.commit('SET_ASSIGNMENTS', tasks);
},
data () {
debugger
console.log("data assignments: ", this.$store.state.assignments);
return {
items: this.$store.state.assignments,
open: [1, 2],
search: null,
caseSensitive: false,
}
},
computed: {
filter () {
return this.caseSensitive
? (item, search, textKey) => item[textKey].indexOf(search) > -1
: undefined
},
}
}
</script>
For this I use vuex this way:
const appStore = {
state () {
return {
data: [],
}
},
getters: {
data(state) {
return state.data
},
},
mutations: {
SET_ASSIGNMENTS(state, payload) {
state.data = payload
},
},
actions: {
async getData({ commit }, {fromDate, toDate}) {
let response = await this.$axios.get('/items/tasks', {baseURL: 'http://localhost:8055'});
let tasks = response.data.data;
commit("SET_ASSIGNMENTS", tasks);
}
}
}
export default appStore
Component code is like this:
<template>
. . .
</template>
<script>
import { mapGetters } from 'vuex';
export default {
name: "MyComponent",
components: {
. . .
},
computed: {
...mapGetters({
data: 'data'
})
},
mounted(){
this.getData();
},
methods: {
getData() {
this.$store.dispatch('getData')
}
}
}
</script>
data is not reactive, you can create a computed property that returns your items
ex:reactiveItems() {return this.items}

Add/Edit list item at parent from child component form

I am developing a recipe app. At my CreateRecipe component, I have child component to add ingredients to the recipe or edit existing ingredients. Ill start by showing the code and what i want to achieve and then the problem
Parent component:
<template>
...
<v-dialog v-model="AddIgredientsDialog" max-width="800px">
<template v-slot:activator="{ on, attrs }">
<v-btn color="secondary" dark v-bind="attrs" v-on="on">
Add addIngredients
</v-btn>
</template>
<AddItemsForm
#addIngredient="SaveNewIgredient"
:newIngredientsItem="editedIgredient"
/>
</v-dialog>
</template>
<script>
import AddItemsForm from "./AddItemsForm"; //Child Component
data: () => ({
AddIgredientsDialog:false,
article: {
headline: "",
img: "",
content: "",
subHeader: "",
description: "",
igredients: [], //List to add/edit item at AddItemsForm child component
preperation: []
},
editedIgredient: { //Item to use for editing or adding new item to article.igredients
title: "",
subIgredients: []
},
defaultItem: { //Item used for resetting editedIgredient item
title: "",
subIgredients: []
},
editedIndex: -1, helper variable for knowing whether i need to add new item or edit exiting item
}),
methods:{
editIngredients(item) {
this.editedIndex = this.article.igredients.indexOf(item);
this.editedIgredient = Object.assign({}, item);
this.AddIgredientsDialog = true;
},
SaveNewIgredient(newItem) { //Triggered on #click of save button at child component New item is the
//item passed from children
if (this.editedIndex > -1) {
this.editedIgredient = Object.assign({}, newItem);
Object.assign(
this.article.igredients[this.editedIndex],
this.editedIgredient
);
} else {
this.article.igredients.push(this.editedIgredient);
}
this.AddIgredientsDialog = false;
this.$nextTick(() => {
this.editedIgredient = Object.assign({}, this.defaultItem);
this.editedIndex = -1;
});
},
}
</script>
Child Component:
<template>
<v-card>
<v-card-title>
<span class="headline">Add Ingredients</span>
</v-card-title>
<v-card-text>
<v-text-field v-model="newIngredientsItem.title" placeholder="כותרת">
</v-text-field>
<v-row align="center">
<v-col sm="11">
<v-text-field
v-model="newIgredient"
placeholder="New Igredient"
#keyup.enter="addNewIgredient"
>
</v-text-field>
</v-col>
<v-col sm="1">
<v-btn icon #click="addNewIgredient">
<v-icon>
mdi-plus
</v-icon>
</v-btn>
</v-col>
<v-col class="mt-0 pt-0" cols="12">
<v-row no-gutters>
<v-col cols="12">
<v-card flat tile>
<template
v-for="(item, index) in newIngredientsItem.subIgredients"
>
<v-list-item :key="index" class="mr-0 pr-0">
<v-list-item-content>
<v-list-item-title>
<v-edit-dialog #click.native.stop>
{{ item }}
<v-text-field
slot="input"
v-model="newIngredientsItem.subIgredients[index]"
></v-text-field>
</v-edit-dialog>
</v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-btn icon #click="removeIgredient(index)">
<v-icon small>
mdi-delete
</v-icon>
</v-btn>
</v-list-item-action>
</v-list-item>
<v-divider
v-if="index + 1 < newIngredientsItem.subIgredients.length"
:key="item + index"
></v-divider>
</template>
</v-card>
</v-col>
</v-row>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-btn color="primary" #click="AddIngredients">
Save
</v-btn>
</v-card-actions>
</v-card>
</template>
<script>
export default {
props: {
newIngredientsItem: {
type: Object,
default() {
return {
title: "",
subIgredients: [ ]
};
}
}
},
data: () => ({
newIgredient: ""
}),
methods: {
addNewIgredient() {
this.newIngredientsItem.subIgredients.push(this.newIgredient);
this.newIgredient = "";
},
AddIngredients() {
this.$emit("addIngredient", this.newIngredientsItem);
},
removeIgredient(index) {
this.newIngredientsItem.subIgredients.splice(index, 1);
}
}
};
</script>
My Problem:
At the moment im only trying to use the SaveNewIgredient() method.
After 1st time of adding item the item is added as it should and the parent defaultItem state remain as is which is good:
defaultItem: {
title: "",
subIgredients: []
},
After adding a second item the defaultItem changes and takes the editedItem properties.
For example if i add at the second time
{
title:'Test 1',
subIgredients: [
'Test 1 - 1',
'Test 1 - 2',
'Test 1 - 3',
]
}
That is what the defaultItem will be and then this assignment causes a bug
this.editedIgredient = Object.assign({}, this.defaultItem);
because editedItem should be:
{
title: "",
subIgredients: []
}
I tried to solve your problem. To do this I modified and in some places simplified your code to keep only what was close to the SaveNewIgredient() function. So here is my code.
Parent Component (for me App.vue):
<template>
<AddItemsForm #addIngredient="SaveNewIgredient" />
</template>
<script>
import AddItemsForm from "./AddItemsForm"; //Child Component
export default {
name: "App",
components: { AddItemsForm },
data() {
return {
article: {
igredients: [], //List to add/edit item at AddItemsForm child component
},
editedIgredient: {
//Item to use for editing or adding new item to article.igredients
title: "",
subIgredients: [],
},
defaultItem: {
//Item used for resetting editedIgredient item
title: "",
subIgredients: [],
},
};
},
methods: {
SaveNewIgredient(newItem) {
console.log("Received: ", newItem);
this.editedIgredient = newItem;
this.article.igredients.push({ ...this.editedIgredient });
console.log("defaultClear: ", this.defaultItem);
console.log("infoItem: ", this.editedIgredient);
this.editedIgredient = this.defaultItem;
console.log("defaultClear: ", this.defaultItem);
console.log("editedWillClear: ", this.editedIgredient);
console.log("listFinal: ", this.article.igredients);
},
},
};
</script>
Child Component (for me AddItemsForm.vue):
<template>
<div>
<input v-model="newIgredient" placeholder="New Igredient" />
<button #click="addNewIgredient">ADD</button>
<div>
<button color="primary" #click="AddIngredients">Save</button>
</div>
</div>
</template>
<script>
export default {
props: {
IngredientsItem: {
type: Object,
default() {
return {
title: "",
subIgredients: [],
};
},
},
},
data() {
return {
newIgredient: "",
title: "TEST",
titleNbr: 0,
resetIgredient: false,
};
},
computed: {
newIngredientsItem() {
return this.IngredientsItem;
},
},
methods: {
addNewIgredient() {
if (this.resetIgredient === true) {
this.newIngredientsItem.subIgredients = [];
}
this.newIngredientsItem.subIgredients.push(this.newIgredient);
this.newIgredient = "";
this.resetIgredient = false;
console.log("ADD: ", this.newIngredientsItem.subIgredients);
},
AddIngredients() {
this.newIngredientsItem.title = this.title + this.titleNbr;
this.titleNbr++;
console.log("EMIT: ", this.newIngredientsItem);
this.$emit("addIngredient", this.newIngredientsItem);
this.resetIgredient = true;
},
},
};
</script>

Vuejs - Same component with different data

I am trying to re-use the same component, but load the components with different data. I thought simply providing unique id's would do the trick, but no luck. I switched from a Vuex store for this data, to using a dataShare This is what I'm doing:
The components:
<logoHeader :panel=0 title="Add your logo / header" id="top" topPadding="pt-10" />
<logoHeader :panel=1 title="Add your main image" id="main" topPadding="pt-0"/>
So its the exact same component, with some different props and different ids
This is the logoHeader component:
<template>
<v-row
:class="topPadding"
align="center"
justify="center"
>
<v-col
align="center"
justify="center"
cols="12"
>
<v-hover v-slot:default="{ hover }">
<v-card
:elevation="hover ? 12 : 0"
class="mx-auto"
max-width="600"
>
<v-img
v-if="showImage"
:src="imageUrl"
max-width="600px"
class="pointer"
#click="panel = 0"
>
</v-img>
<v-icon
v-if="!showImage"
class="my_dark_purple_text"
size="100"
#click="sendToPanel"
>add_box</v-icon>
<p class="my_dark_purple_text">{{ title }}</p>
<p>URL {{ imageUrl }}</p>
<p>Show image? {{ showImage }}</p>
</v-card>
</v-hover>
</v-col>
</v-row>
</template>
<script>
import {mapGetters} from 'vuex';
import {mapActions} from 'vuex';
import {dataShare} from '../../../packs/fresh-credit.js';
export default {
props: ['panel', 'title', 'topPadding'],
data() {
return {
imageUrl: "",
showImage: false,
}
},
created() {
dataShare.$on('imageUrl', (data) => {
this.imageUrl = data;
});
dataShare.$on('showImage', (data) => {
this.showImage = data;
});
},
computed: {
...mapGetters('emailPanel', [
'returnPanel'
]),
},
methods: {
...mapActions('emailPanel', [
'updatePanel'
]),
sendToPanel() {
this.updatePanel(this.panel);
},
},
}
</script>
And then finally this is where the data enters the system:
<template>
<v-expansion-panel-content>
<h1 class="subtitle-1 font-weight-bold">Only images files accepted</h1>
<v-file-input
v-model="image"
accept="image/*"
label="Image Upload"
prepend-icon="add_a_photo"
color='#68007d'
></v-file-input>
<v-btn
:disabled="disableUpload"
color="#68007d"
class="white--text"
#click="sendImage"
>Submit</v-btn>
</v-expansion-panel-content>
</template>
<script>
import axios from 'axios';
import {dataShare} from '../../../../packs/fresh-credit.js';
export default {
data() {
return {
image: null,
disableUpload: true,
}
},
watch: {
image: function() {
if(this.image.size > 0){
this.disableUpload = false;
}
else{
this.disableUpload = true;
}
}
},
computed: {
},
methods: {
sendImage() {
let formData = new FormData();
formData.append('file', this.image);
axios.post('/admin/features/images', formData,
{
headers: {
'Content-Type': 'multipart/form-data'
}
}
).then(response => {
dataShare.$emit('imageUrl', response.data);
dataShare.$emit('showImage', true);
});
}
},
}
</script>
Where did I go astray?
Add the key property to the components and set it to different values (for example 1 and 2). If the key has different values Vue will differentiate them when rendering. Here is a basic explanation.

VueJS - How to pass function to global component

I have a confirm dialog, which should be shown when users perform delete action. I need to make it works globally (Many pages can use this component by passing confirm message and delete function to it). However, I haven't found a way to pass a function to this component.
Thanks in advance!
ConfirmDialog component:
<template>
<v-dialog
v-model="show"
persistent
max-width="350"
>
<v-card>
<v-card-text class="text-xs-center headline lighten-2" primary-title>
{{ message }}
</v-card-text>
<v-card-actions class="justify-center">
<v-btn color="back" dark #click="close">キャンセル</v-btn>
<v-btn color="primary" dark>削除</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
export default {
data () {
return {
show: false,
message: ''
}
},
created: function () {
this.$store.watch(state => state.confirmDialog.show, () => {
const msg = this.$store.state.confirmDialog.message
if (msg !== '') {
this.show = true
this.message = this.$store.state.confirmDialog.message
} else {
this.show = false
this.message = ''
}
})
},
methods: {
close () {
this.$store.commit('closeDialog')
}
}
}
</script>
ConfirmDialog store:
export default {
state: {
show: false,
message: '',
submitFunction: {}
},
getters: {
},
mutations: {
showDialog (state, { message, submitFunction }) {
state.show = true
state.message = message
state.submitFunction = submitFunction
},
closeDialog (state) {
state.show = false
state.message = ''
}
}
}
you can get and set states easily.
try getting the value of show with ...mapState
ConfirmDialog.vue :
<template>
<v-dialog
v-if="show"
persistent
max-width="350"
>
<v-card>
<v-card-text class="text-xs-center headline lighten-2" primary-title>
{{ message }}
</v-card-text>
<v-card-actions class="justify-center">
<v-btn color="back" dark #click="close">キャンセル</v-btn>
<v-btn color="primary" dark>削除</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import { mapState } from 'vuex';
export default {
data () {
return {
show: false,
message: ''
}
},
methods: {
close () {
this.$store.commit('closeDialog')
}
},
computed: {
...mapState({
show: 'show'
})
}
}
</script>
The store is, as the name says, a store. You have a centralized tree where you save data, not functionalities. Another reason is that functions are not serializable.
You could create this component in a global way by injecting the function as prop or by using emit and handling the functionality in the parent.