V
V
Vadim2018-06-06 09:47:24
Vue.js
Vadim, 2018-06-06 09:47:24

Why is there an error in Vuex with v-model?

I am doing my first project a trial project on Vue (rewriting an existing one with Jquery). At first I did everything simply without Vuex, then I added it (further it will come in handy for the sake of interest). Everything seems to be fine, but I turned on strict mode and got an error: storage changes outside the mutation, although I did not notice this. I'm trying to figure it out.
As I understand it, the v-model in the ProductRow component for some reason updates not the local copy of the item, but the state of the store, and starting only from the second firing of the input event. In general, I was able to fix it, but I want to understand why this option does not work.
Here is the code:
storage.js

import Vue from 'vue';
import Vuex from 'vuex';
import {ADD_PRODUCT, UPDATE_PRODUCT, DELETE_PRODUCT, UPDATE_FORM, UPDATE_PREPAYMENT_IN_PERCENT} from './mutation-types';

Vue.use(Vuex);

let id = 1;
const product = {
    id: id++,
    name: '',
    size: '',
    content: '',
    price: 0,
    number: 1,
    discount: '',
    guarantee: 0,
    promotion: 0,
    location: '',
    sum: 0,
};
export default new Vuex.Store({
    strict: true,
    state: {
        products: [
            Object.assign({}, product),
        ],
        form: {
            prepaymentInPercent: 100,
            prepaymentInRub: 0,
        }

    },
    getters: {
        total(state) {
            return state.products.reduce(function (acc, cur, index, array) {
                return array.length > 1 ? acc + cur.sum : cur.sum;
            }, 0);
        },
        rest(state, getters) {
            return Math.round(getters.total - getters.prepaymentInRub);
        },
        prepaymentInRub(state, getters) {
            return Math.round(getters.total * state.form.prepaymentInPercent / 100);
        }
    },
    mutations: {
        [ADD_PRODUCT](state, product) {
            state.products.push(product);
        },
        [UPDATE_PRODUCT](state, {product, index}) {
            state.products.splice(index, 1, product);
        },
        [DELETE_PRODUCT](state, index) {
            state.products.splice(index, 1);
        },
        [UPDATE_FORM](state, form) {
            state.form = form;
        },
        [UPDATE_PREPAYMENT_IN_PERCENT](state, percent) {
            state.form.prepaymentInPercent = percent;
        }

    },
    actions: {
        addProduct({commit}) {
            let newProduct = Object.assign({}, product);
            newProduct.id = id++;
            commit(ADD_PRODUCT, newProduct);
        },
        updateProduct({commit}, product) {
            commit(UPDATE_PRODUCT, product);
        },
        deleteProduct({commit, state}, index) {
            state.products.length > 1 && commit(DELETE_PRODUCT, index)
        },
        updatePrepaymentInPercentByRub({commit, getters}, rubles) {
            let percent = Math.round(rubles / getters.total * 100);
            commit(UPDATE_PREPAYMENT_IN_PERCENT, percent);
        }
    },
});

ProductTable.vue
<template>
    <table border="0">
        <thead>
        <tr>
            <th class="pointer" @click="addProduct">+</th>
            <th>Номер</th>
            <th>Название</th>
            <th>Размер</th>
            <th>Наполнение</th>
            <th>Цена</th>
            <th>Количество</th>
            <th>Скидка</th>
            <th>Акция</th>
            <th>Сумма</th>
            <th>Гарантия</th>
            <th>Местонахождение товара</th>
            <th class="pointer" @click="toJSON">JSON</th>
        </tr>
        </thead>
        <tbody>
        <template v-for="(product, index) in products">
            <ProductRow
                    :initialItem="product"
                    :key="product.id"
                    :index="index"
            />
        </template>
        <tr>
            <td colspan="12">{{total}}</td>
            <td>{{json}}</td>
        </tr>
        </tbody>
    </table>
</template>

<script>
    import ProductRow from './ProductRow';
    import {mapGetters, mapActions, mapState} from 'vuex';

    export default {
        components: {
            ProductRow,
        },
        name: 'ProductTable',
        data() {
            return {
                json: '',
            };
        },
        computed: {
            ...mapState(['products']),
            ...mapGetters(['total']),
        },
        methods: {
            ...mapActions(['addProduct']),
            toJSON() {
                this.json = JSON.stringify({
                    products: this.products,
                    total: this.total,
                }, null, '\t');
            },
        },
    };
</script>

ProductRow.vue
<template>
    <tr>
        <td colspan="2" class="id">{{indexFrom1}}</td>
        <Editable v-model="item.name" />
        <Editable v-model="item.size"/>
        <Editable v-model="item.content"/>
        <Editable v-model.number="item.price"/>
        <Editable v-model.number="item.number"/>
        <Editable v-model="item.discount"/>
        <td>
            <select v-model="item.promotion">
                <option selected="" value="0">Нет</option>
                <optgroup label="Новоселы">
                    <option data-text="Нов." value="5">Новоселы -5%</option>
                    <option data-text="Нов." value="10">Новоселы -10%</option>
                    <option data-text="Нов." value="15">Новоселы -15%</option>
                </optgroup>
            </select>
        </td>
        <td>{{sum}}</td>
        <Editable v-model.number="item.guarantee"/>
        <td>
            <select v-model="item.location">
                <option selected value="">Услуги</option>
                <option value="СКЛАД">Склад</option>
                <option value="ЗАКАЗ">Заказ</option>
                <option value="Выст. образец из М1">Выставочный образец из 1 магазина</option>
                <option value="Выст. образец из М12">Выставочный образец из 12 магазина</option>
                <option value="Выст. образец из М14">Выставочный образец из 14 магазина</option>
                <option value="Выст. образец из М16">Выставочный образец из 16 магазина</option>
                <option value="Выст. образец из М17">Выставочный образец из 17 магазина</option>
                <option value="Выст. образец из М18">Выставочный образец из 18 магазина</option>
                <option value="Выст. образец из М19">Выставочный образец из 19 магазина</option>
                <option value="Выст. образец из М20">Выставочный образец из 20 магазина</option>
                <option value="Выст. образец из М21">Выставочный образец из 21 магазина</option>
                <option value="Выст. образец из М22">Выставочный образец из 22 магазина</option>
            </select>
        </td>
        <td>
            <span class="table-remove" @click="removeProduct">Удалить</span>
        </td>
    </tr>
</template>

<script>
    import Editable from './EditableCell';

    export default {
        components: {
            Editable,
        },
        name: 'ProductRow',
        props: {
            initialItem: Object,
            index: Number,
        },
        data() {
            return {
                'item': this.initialItem
            }
        },
        computed: {
            sum() {
                let prod = this.item.price * this.item.number;
                let discounted = this.isDiscountInPercent(this.item.discount) ?
                    prod * this.getCoeffFromPercent(this.item.discount) :
                    prod - this.item.discount;
                let result = Math.round(discounted * this.getCoeffFromPercent(this.item.promotion));
                return result > 0 ? result : 0;
            },
            indexFrom1() {
                return this.index + 1;
            },
        },
        methods: {
            getCoeffFromPercent(percent) {
                return 1 - parseInt(percent) / 100;
            },
            isDiscountInPercent(discount) {
                return ~discount.indexOf('%') ? true : false;
            },
            removeProduct() {
                this.$store.dispatch('deleteProduct', this.index)
            },
        },
        watch: {
            sum() {
                this.item.sum = this.sum;
            },
            item: {
                handler(value) {
                    this.$store.dispatch('updateProduct', {
                        product: value,
                        index: this.index,
                    });
                },
                deep: true,
            },
        },
    };
</script>

Answer the question

In order to leave comments, you need to log in

1 answer(s)
0
0xD34F, 2018-06-06
@vadimek

As I understand it, the v-model in the ProductRow component for some reason updates not the local copy of the item, but the state of the store

And where did you get the idea that you have some kind of "local copy" here somewhere? No, seriously - try to show the place in your code where you think you are creating copies of the values ​​from the store.
I guess that...
...под копированием вы подразумеваете
data() {
    return {
        'item': this.initialItem
    }
}

Нет, это не копирование объекта - так вы лишь создали ещё одну ссылку на объект. Для создания копии данных делайте так: Object.assign({}, this.initialItem). Или так: { ...this.initialItem }.

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question