Vuex reusable module pattern. Using function for state not working
Asked Answered
B

1

6

I have a form composed of 1 or more (user decides) Transactions. I display Transactions inside a component on the parent and set the Transaction's attributes with computed properties in the child (Transaction) component.

User data is updated by the computed properties just fine however, when a user clicks to add an additional Transaction component the values from the first Transaction are duplicated for any new Transaction component/object created.

I have read in the forum here and here that the solution was to use a function for state in the module definition. This doesn't appear to work for me, I'd like to learn why.

Here is the declaration of the composite component Transaction:

<template v-for="(fund_transaction, index) in fund_transactions">
  <div class="card">
    <div class="card-body">
      <FundTransactionComponent
        v-bind:fund_transactions="fund_transactions"
        v-bind:key="index"
        v-on:removedTransaction="removeFundTransaction(id)"
        v-on:submittedTransaction="applyFundTransaction(fund_transaction.id)"
      >
      </FundTransactionComponent>
    </div><!--END .card-body-->
  </div><!--END .card-->
</template>

And here is the child component (computed props truncated for brevity, they are just the state attributes both getters and setters):

<template>
  <div class="row">
    <div class="col-10 float-left" v-if="this.fund_transactions.length > 0"></div>
    <div class="col-2 float-right" v-if="this.fund_transactions.length > 0">
      <button v-on:click="removeTransaction(index)" class="btn btn-icon btn-danger px-2 py-1 float-right">
        <i class="fa fa-times"></i>
      </button>
    </div><!--END .col-1 float-right-->
    <div class="col-md-6 col-sm-12">
      <label class="form-control-label text-semibold">Transaction Date:</label>
      <el-date-picker
        type="date"
        placeholder="select a date"
        v-model="date_of_record"
        style="width: 100%;"
        format="MMMM dd, yyyy"
        clearable
        default-date="Date.now()"
        >
      </el-date-picker>
    </div><!--END .col-md-6 .col-sm-12-->
    <div class="col-md-6 col-sm-12">
      <label class="form-control-label text-semibold">Reason for Transaction:</label>
      <textarea class="form-control" placeholder="Enter reason here.." v-model="reason_for_transaction">
      </textarea>
    </div><!--END .col-md-6 .col-sm-12-->
    <div class="col-md-6 col-sm-12">
      <label class="form-control-label text-semibold">Transaction Amount:</label>
      <input type="text" class="form-control" v-model="amount"/>
    </div><!--END .col-md-6 .col-sm-12-->
    <div class="col-md-6 col-sm-12">
      <label class="form-control-label text-semibold">Type of Transaction:</label><br>
      <el-radio-group
        v-model="transaction_type">
        <el-radio-button label="Deposit"></el-radio-button>
        <el-radio-button label="Withdrawal"></el-radio-button>
      </el-radio-group>
    </div><!--END .col-md-6 .col-sm-12-->
    <div class="col-md-6 col-sm-12">
      <label class="form-control-label text-semibold">Current Balance:</label>
      <input type="text" class="form-control" v-model="current_balance"/>
    </div><!--END .col-md-6 .col-sm-12-->
    <div class="col-md-6 col-sm-12">
      <label class="form-control-label text-semibold">Forwarded:</label>
      <input type="text" class="form-control" v-model="forwarded"/>
    </div><!--END .col-md-6 .col-sm-12-->
  </div><!--END .row-->
</template>
<script>
import moment from "moment";
import DatePicker from 'vuejs-datepicker';
import FundRecordForm2 from "@/store/modules/forms/FundRecordForm2";
import FundTransaction from "@/store/modules/auxillary/FundTransaction";
import Resident from "@/store/modules/actors/Resident";

export default {
  name: "FundTransaction",
  components: {
    DatePicker,
  },
  props: {
    index: {
      type: Number,
      required: false,
    },
  },
  computed: {
    id: {
      get() {
        return this.$store.getters['FundTransaction/getId'];
      },
      set(value) {
        this.$store.dispatch('FundTransaction/setId', value);
      },
    },
  },
  .
  .
  .
};
</script>
<style scoped>

</style>

Here is the vuex module for the child component (Transaction):

import Axios from "axios";
import router from "../../../router";
import FundRecordForm2 from "../forms/FundRecordForm2";

const FundTransaction = {
  namespaced: true,
  // Expectation: this should return individual object state respsectively
  // state: () => ({})
  state () {
    return {
      id: null,
      provider_id: Number,
      employee_id: Number,
      account_id: Number,
      resident_id: Number,
      fund_record_form2_id: Number,
      transaction_date: '',
      reason_for_transaction: '',
      transaction_type: '',
      amount: 0.0,
      current_balance: 0.0,
      forwarded: 0.0,
      date_of_record: '',
      created_at: '',
      updated_at: '',
    }
  },
  getters: {
    getId: (state) => {
      return state.id;
    },
    getProviderId: (state) => {
      return state.provider_id;
    },
    getEmployeeId: (state) => {
      return state.employee_id;
    },
    getAccountId: (state) => {
      return state.account_id;
    },
    getResidentId: (state) => {
      return state.resident_id;
    },
    getFundRecordForm2Id: (state) => {
      return state.fund_record_form2_id;
    },
    getTransactionDate: (state) => {
      return state.transaction_date;
    },
    getReasonForTransaction: (state) => {
      return state.reason_for_transaction;
    },
    getTransactionType: (state) => {
      return state.transaction_type;
    },
    getAmount: (state) => {
      return state.amount;
    },
    getCurrentBalance: (state) => {
      return state.current_balance;
    },
    getForwarded: (state) => {
      return state.forwarded;
    },
    getDateOfRecord: (state) => {
      return state.date_of_record;
    },
    getCreatedAt: (state) => {
      return state.created_at;
    },
    getUpdatedAt: (state) => {
      return state.updated_at;
    },
    getFundTransaction: (state) => {
      return state.fund_transaction;
    },
  },
  mutations: {
    SET_ID: (state, payload) => {
      state.id = payload;
    },
    SET_PROVIDER_ID: (state, payload) => {
      state.provider_id = payload;
    },
    SET_EMPLOYEE_ID: (state, payload) => {
      state.employee_id = payload;
    },
    SET_ACCOUNT_ID: (state, payload) => {
      state.account_id = payload;
    },
    SET_RESIDENT_ID: (state, payload) => {
      state.resident_id = payload;
    },
    SET_FUND_RECORD_FORM2_ID: (state, payload) => {
      state.fund_record_form2_id = payload;
    },
    SET_TRANSACTION_DATE: (state, payload) => {
      state.transaction_date = payload;
    },
    SET_REASON_FOR_TRANSACTION: (state, payload) => {
      state.reason_for_transaction = payload;
    },
    SET_TRANSACTION_TYPE: (state, payload) => {
      state.transaction_type = payload;
    },
    SET_AMOUNT: (state, payload) => {
      state.amount = payload;
    },
    SET_CURRENT_BALANCE: (state, payload) => {
      state.current_balance = payload;
    },
    SET_FORWARDED: (state, payload) => {
      state.forwarded = payload;
    },
    SET_DATE_OF_RECORD: (state, payload) => {
      state.date_of_record = payload;
    },
    SET_CREATED_AT: (state, payload) => {
      state.created_at = payload;
    },
    SET_UPDATED_AT: (state, payload) => {
      state.updated_at = payload;
    },
    UPDATE_FUND_TRANSACTION: (state, pyaload) => {
      state.fund_transaction = payload;
    },
  },
  actions: {
    setId (context, payload) {
      context.commit('SET_ID', payload);
    },
    setProviderId (context, payload) {
      context.commit('SET_PROVIDER_ID', payload);
    },
    setEmployeeId (context, payload) {
      context.commit('SET_EMPLOYEE_ID', payload);
    },
    setAccountId (context, payload) {
      context.commit('SET_ACCOUNT_ID', payload);
    },
    setResidentId (context, payload) {
      context.commit('SET_RESIDENT_ID', payload);
    },
    setFundRecordForm2Id (context, payload) {
      context.commit('SET_FUND_RECORD_FORM2_ID', payload);
    },
    setTransactionDate (context, payload) {
      context.commit('SET_TRANSACTION_DATE', payload);
    },
    setReasonForTransaction (context, payload) {
      context.commit('SET_REASON_FOR_TRANSACTION', payload);
    },
    setTransactionType (context, payload) {
      context.commit('SET_TRANSACTION_TYPE', payload);
    },
    setAmount (context, payload) {
      context.commit('SET_AMOUNT', payload);
    },
    setCurrentBalance (context, payload) {
      context.commit('SET_CURRENT_BALANCE', payload);
    },
    setForwarded (context, payload) {
      context.commit('SET_FORWARDED', payload);
    },
    setDateOfRecord (context, payload) {
      context.commit('SET_DATE_OF_RECORD', payload);
    },
    setCreatedAt (context, payload) {
      context.commit('SET_CREATED_AT', payload);
    },
    setUpdatedAt (context, payload) {
      context.commit('SET_UPDATED_AT', payload);
    },
    updateFundTransaction (context, payload) {
      context.commit('UPDATE_FUND_TRANSACTION', payload);
    },
  },
}
export default FundTransaction;

Update:

I pass an object literal like so..

SET_NEW_FUND_TRANSACTION_FIELDS: (state) => {
  state.fund_transactions.push({
    id: null,
    provider_id: Number,
    employee_id: Number,
    account_id: Number,
    resident_id: Number,
    fund_record_form2_id: Number,
    transaction_date: '',
    reason_for_transaction: '',
    transaction_type: '',
    amount: 0.0,
    current_balance: 0.0,
    forwarded: 0.0,
    date_of_record: '',
    created_at: '',
    updated_at: '',
  });
},

I also tried wrapping my state in Transaction namespace, setting up a getter for this object and using it in the parent.

SET_NEW_FUND_TRANSACTION_FIELDS: (state, getters, rootState, rootGetters) => {
  state.fund_transactions.push(rootGetters['FundTransaction/getFundTransaction']);
},

FundTransaction's state:

  state: () => ({
    fund_transaction: {
      id: null,
      provider_id: Number,
      employee_id: Number,
      account_id: Number,
      resident_id: Number,
      fund_record_form2_id: Number,
      transaction_date: '',
      reason_for_transaction: '',
      transaction_type: '',
      amount: 0.0,
      current_balance: 0.0,
      forwarded: 0.0,
      date_of_record: '',
      created_at: '',
      updated_at: '',
    }
  }),

getFundTransaction: (state) => {
  return state.fund_transaction;
},

But this returns the duplicates as before.

Looking forward to your recommendation.

Bridie answered 3/6, 2019 at 18:40 Comment(9)
When the user adds a new item to fund_transactions, what is its empty state in the parent? I suspect you need to handle that differently.Calk
Thanks for the quick reply ebbishop. I have additional comments in the Update.Bridie
I'm not sure I understand your design. Are you creating and registering a new module for each FundTransaction? Is it possible to create a single FundTransaction module which contains multiple FundTransactions?Usanis
Hi @Connor. That is essentially what I am aiming for. I have one Fund Record that contains many Transactions. I am able to move the list of transactions into the FundTransaction module.Bridie
I have one FundRecord module & one FundTransaction module.Bridie
Based on this line of code state.fund_transactions.push(rootGetters['FundTransaction/getFundTransaction']); it looks like you only have one FundTransaction instead of a module for each FundTransaction. In that case you might have something like rootGetters['FundTransaction-7/getFundTransaction'].Usanis
Unless you have a really good reason, I'd recommend moving away from using a module for each entity and having a deeply nested structure. Vuex is easier to work with using a flattened, normalized structure.Usanis
Not necessarily. You can keep all your transactions inside a single module. You'll just need to modify functions like SET_EMPLOYEE_ID to accept some sort of identitifer of the transaction along with the new value.Usanis
Could you provide a brief example of using vuex store to save/assign attributes from a form with composite objects in normalized fasion? (i.e. a user form with name, etc.. and multiple addresses).Bridie
U
3

In my experience, trying to use Vuex Modules as some sort of entity class doesn't work too well. Since they're closely related, I'd recommend you move all of your transactions and fund records to a single static Vuex module. Although a transaction may be logically nested under a fund record, it'll be easier to keep your state flat and perform the nesting in a getter.

I think that will simplify the relationship of your components and your store and either fix the issue or make its cause more obvious.

Here's a quick sketch of what that module might look like:

const FundModule = {
  namespaced: true,
  state () {
    return {
      transactions: {
          // You'd probably make this an empty object, but it's 
          // an example of what the structure would look like.
          // You could us an array instead of an object, but I recommend 
          // keeping an object. It's easier to access items by key and 
          // Object.values() can quickly transform it to an array.
          1: {
            id: 1,
            provider_id: Number,
            employee_id: Number,
            account_id: Number,
            resident_id: Number,
            fund_record_form2_id: Number,
            transaction_date: '',
            reason_for_transaction: '',
            transaction_type: '',
            amount: 0.0,
            current_balance: 0.0,
            forwarded: 0.0,
            date_of_record: '',
            created_at: '',
            updated_at: '',
          }, 
          2: {
            id: 1,
            provider_id: Number,
            employee_id: Number,
            account_id: Number,
            resident_id: Number,
            fund_record_form2_id: Number,
            transaction_date: '',
            reason_for_transaction: '',
            transaction_type: '',
            amount: 0.0,
            current_balance: 0.0,
            forwarded: 0.0,
            date_of_record: '',
            created_at: '',
            updated_at: '',
          }
      },
      form_records: {
        /*Same idea here*/
      }
    }
  },
  getters: {
    // You can map your parent component to an array of transactions
    allTransactions: (state) => {
      return Object.values(state.transactions);
    },
    // If you only need transactions for a specific record or some 
    // other criteria you can perform that logic in a getter too.
    allTransactionsForFormRecord(state) => (form_record_id) => {
      return Object.values(state.transactions)
        .filter(t => t.form_record_form2_id === form_record_id);
    }
  },
  mutations: {
    SET_ID: (state, payload) => {
      state.transactions[payload.id].id = payload.value;
    },
    SET_PROVIDER_ID: (state, payload) => {
      state.transactions[payload.id].provider_id = payload.value;
    },
    SET_TRANSACTION_PROP: (state, payload) => {
      state.transactions[payload.id][payload.prop] = payload.value;
    },
    ADD_NEW_TRANSACTION: (state, payload) => {
      const id = generateNewId();
      state.transactions[id] = {
        id: id,
        provider_id: 0,
        employee_id: 0,
        account_id: 0,
        resident_id: 0,
        fund_record_form2_id: 0,
        transaction_date: '',
        reason_for_transaction: '',
        transaction_type: '',
        amount: 0.0,
        current_balance: 0.0,
        forwarded: 0.0,
        date_of_record: '',
        created_at: '',
        updated_at: '',
      };
    }
  },
  actions: {
    setId (context, payload) {
      context.commit('SET_ID', payload);
    },
    setProviderId (context, payload) {
      context.commit('SET_PROVIDER_ID', payload);
    },
    // If you want a generic way to set any property
    setTransactionProp (context, payload) {
      context.commit('SET_TRANSACTION_PROP', payload);
    },
    addNewTransaction (context, payload) {
      context.commit('ADD_NEW_TRANSACTION', payload);
    }
  },
}
export default FundModule;

Here's what your computed properties might look like, assuming your FundTransaction component has a fund_transaction prop.

computed: {
  id: {
    get() {
      return this.fund_transaction.id;
    },
    set(value) {
      this.$store.dispatch('FundModule/setId', {value, id: this.fund_transaction.id});
    },
  },
},
Usanis answered 11/6, 2019 at 23:9 Comment(1)
Thanks so much for bearing with me, your illustrations make sense. Also for the computed-prop example with unique value!Bridie

© 2022 - 2024 — McMap. All rights reserved.