2
votes

I make my first Vue project, everthing was OK, so I added Vuex (It wouldn't be superfluous in the future and I tried it for the sake of interest), and everything still was OK, untill I enabled strict mode. It turns out, component(s) mutate(s) state of storage outside mutation, however I haven't wanted it and have created a local copy of nessesary object in a component with data().

My intention was to create a local object in the parent component, then pass its properties to child components with v-model (I know it's an event-driven syntactic sugar for v-bind and v-on:input) and when local object gets updated (inside children by virtue of v-model), a method of parent component dispatches action to store. Instead of it, I get an error message due to outside mutating, moreover it happens only at the second and subsequent input events.

Also, it works if I replace these lines in ProdutRow component watcher:

item: {
                handler(value) {
                    this.$store.dispatch('updateProduct', {
                        product: value,
                        index: this.index,
                    });
                },
                deep: true,
            }

with: product: {...value}, or Object.assign({}, value), but the same code in the store's action doesn't: it throws the same error.

Does not data() create a copy of the specified prop? If it does, why Object.assign not working in store?

Code:

store.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';
    import _ from 'lodash';

    Vue.use(Vuex);

    let id = 1;
    const product = {
        order: 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,
            }

        },
        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 getters.total - getters.prepaymentInRub;
            },
            prepaymentInRub(state, getters) {
                return 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.order = id++;
                commit(ADD_PRODUCT, newProduct);
            },
            updateProduct: _.debounce(function ({commit}, product) {
                commit(UPDATE_PRODUCT, product);
            }, 1),
            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.order"
                  :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

<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>
            </select>
        </td>
        <td>
            <span class="table-remove" @click="removeProduct(index)">Удалить</span>
        </td>
    </tr>
</template>

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

    export default {
        components: {
            Editable,
        },
        name: 'ProductRow',
        props: {`enter code here`
            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(index) {
                // console.log(arguments);
                this.$store.dispatch('deleteProduct', index)

            }
        },
        watch: {
            sum() {
                this.item.sum = this.sum;
            },
            item: {
                handler(value) {
                    this.$store.dispatch('updateProduct', {
                        product: value,
                        index: this.index,
                    });
                },
                deep: true,

            },
        },
    };
</script>
1

1 Answers

2
votes

No, data() does not create a copy of the item object so in this code you are passing the object by reference.

data() {
   return {
        item: this.initialItem
    };
}

This means that the product object from your store is the same exact object as this.item in your ProductRow component. So when you attach a v-model to the inputs you are directly changing the product object in your store.

Cloning the product object using Object.assign() in your store will not work. You have to do the cloning in the ProductRow component.

data() {
   return {
      item: Object.assign({}, this.initialItem)
   };
}

This will create a copy so that you will not be directly modifying the product in the store.