Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Vuex reusable module pattern. Using function for state not working

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.

like image 876
sirramongabriel Avatar asked Jun 03 '19 18:06

sirramongabriel


1 Answers

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});
    },
  },
},
like image 176
Connor Avatar answered Sep 23 '22 11:09

Connor