[VUE 3] Modal Data Issue After Reopening or Visiting the other Routes

Hello developers.

I’m creating my webpage’s admin panel by using Vue 3, Vue Router, Vuex, Tiny-Emitter.

One of the page is Trades and view file is Pages/Trades/Show.vue. This file contains:

<main class="page-content">
    <app-table />
    <app-pagination :content="closedTrades" />
    <trade-enter v-if="action === 'enter'" />
    <trade-edit v-if="action === 'edit'" />
    <trade-exit v-if="action === 'exit'" />
</main>

TradeEnter, TradeEdit, TradeExit are components that fetch data from store, include action-based methods, import mixins, and dispatch actions. These files’ templates are only:

<trade-modal
    :passiveInputs="passiveInputs"
    :disabledRadios="disabledRadios"
    :disabledExecution="disabledExecution"
    :modalHeader="modalHeader"
    :errors="formErrors"
    :warnings="formWarnings"
/>

TradeModal is a component that contains all form elements and displays them according to action.

<app-modal :visible="toggles.isVisible.modal" >
    <template #header>{{ modalHeader }}</template>
    <template #content>
         All form elements are components.
         They emit their data to grandparent (trade-*) by tiny-emitter.
    </template>
    <template #actions>Close and Execute Buttons</templates>
<app-modal>

AppModal is a teleport component, which has slots as well as visibility data.

If I go to /Trades route, and open modal, this is what I see in DevTool:

Root
   RouterView
      AppLayout
          Header
          Sidebar
          RouterView (Child routes, in my case, pages)
               Show
                  Table
                  Pagination
                  TradeEnter
                      TradeModal
                           Modal

When I click the “enter” (new trade) button, the fields down below change, modal opens, and everything work flawlessly.

state.action = "enter",
state.isVisible.modal = true,
state.trade = { ...rootState.Defaults.trade }

When I close the modal by clicking the “Close” or “Execute” button, the same fields become “”, false, and {}.

My Problem:

When I reopen the modal, and change any input value, which triggers some methods on the TradeEnter component, those methods can’t find the data that was fetched from the store. Despite the fact that I can see them on VueDevTool, I keep getting “Cannot read property ‘XXX’ of undefined” error.

I noticed that if I stop making state.action empty string at the close, everything works very well even if I keep closing and reopening the modal. But this time, nothing works if I go to another route and come back.

Instead of fetching data from Vuex in child components, I tried to pass them as props from Show.vue to children, but it didn’t make any difference.

I need to be able to open-close-reopen the models and visit the other routes and come back.

I hope someone can tell me what I’m doing wrong. Sorry for a such long question.

EDIT
TradeEnter Component codes.

<template>
  <trade-modal
    :passiveInputs="passiveInputs"
    :disabledRadios="disabledRadios"
    :disabledExecution="disabledExecution"
    :modalHeader="modalHeader"
    :errors="formErrors"
    :warnings="formWarnings"
  />
</template>

<script>
import { mapActions, mapGetters, mapState } from "vuex";
import inputValueMixin from "./../../../../shared/Vuejs/Mixins/inputValueMixin";
import findAssetMixin from "./../../../../shared/Vuejs/Mixins/findAssetMixin";
import pipValueMixin from "./../../../../shared/Vuejs/Mixins/pipValueMixin";
import formatNumberMixin from "./../../../../shared/Vuejs/Mixins/formatNumberMixin";
import calcLotSizeMixin from "./../../../../shared/Vuejs/Mixins/calcLotSizeMixin";
import errorsMixin from "./../../../../shared/Vuejs/Mixins/errorsMixin";
import warningsMixin from "./../../../../shared/Vuejs/Mixins/warningsMixin";
import hotlistMixin from "./../../../../shared/Vuejs/Mixins/hotlistMixin";
import TradeModal from "./Modal";
export default {
  mixins: [
    findAssetMixin,
    pipValueMixin,
    calcLotSizeMixin,
    formatNumberMixin,
    warningsMixin,
    errorsMixin,
    hotlistMixin,
    inputValueMixin,
  ],
  components: {
    TradeModal,
  },
  data() {
    return {
      target: "Trades",
      key: "",
    };
  },
  methods: {
    async executeNumericFieldChain(data) {
      let name = data.name;
      let value = this.replaceWithDot(data, this.key);

      this.setTradeValue({ name, value });

      if (name === "stop" && this.trade.asset.pip.quote === "CLS") {
        this.setPipValue(1 / value, this.trade.asset.decimal);
      }

      if (name === "risk" && !isNaN(value)) {
        this.setTradeValue([
          { name: "change", value: (-1 * value).toFixed(2) },
        ]);
      }

      if (["risk", "entry", "stop"].includes(name)) {
        // If I fill any numeric fields:
        // error : Uncaught (in promise) TypeError: Cannot read property 'entry' of undefined
        // this error comes from the method down below. It's in mixin file and it starts like that:
        // calculateLotSize(trade, account_balance) {
        //      let entry = parseFloat(trade.entry);
        //      ....
        // So trade, in this case this.trade is undefined.
        // But I fetched it from Vuex and it can be seen on VueDevTool
        let lots = this.calculateLotSize(this.trade, this.margin);
        this.setTradeValue([
          { name: "lot_account", value: lots.lot_account },
          { name: "lot_slack", value: lots.lot_slack },
        ]);
      }
      
    },

    async executeAssetNameChain(data) {
      let name = data.name;
      let value = data.value.toUpperCase();

      this.setTradeValue({ name, value });

      let asset = await this.findAsset(value);
      if (asset) {
        this.setFoundAsset(asset, "add");
        this.setHotlistStatus(asset);
        let pip = await this.getPipValue({ asset });
        if (pip) {
          // If I type an asset name:
          // error : Uncaught (in promise) TypeError: Cannot read property 'asset' of undefined
          // So this.trade must be undefined. But I fetched it from vuex, and I see it on VueDevTool
          this.setPipValue(pip, this.trade.asset.decimal);
          this.putPipValueToBD(asset.pip.quote, pip);
        }
      } else {
        this.setFoundAsset();
      }
    },

    setTradeValue(data) {
      this.$store.dispatch("Trades/setTradeValue", data);
    },
  },
  computed: {
    ...mapState({
      trade: (state) => state.Trades.trade,
      action: (state) => state.Trades.action,
      numerics: (state) => state.Trades.numerics,
      assets: (state) => state.Assets.all,
    }),
    ...mapGetters({
      margin: "Account/margin",
      toggles: "Toggles/toggles",
      numericFields: "Trades/numericFields",
    }),
    modalHeader() {
      return this.trade.is_order ? "Place a Pending Order" : "Take a Trade";
    },
    disabledRadios() {
      let t = this.trade;
      let radios = [];
      if (
        t.on_the_list &&
        t.hasOwnProperty("asset") &&
        t.asset.hasOwnProperty("hotlist") &&
        t.asset.hotlist.is_private !== null
      ) {
        radios.push("is_private");
      }
      return radios;
    },
    passiveInputs() {
      let inputs = ["closed_on", "closed_at", "closed_lot"];
      if (
        this.trade.hasOwnProperty("asset") &&
        Object.keys(this.trade.asset).length === 0
      ) {
        return inputs.concat([
          "risk",
          "entry",
          "stop",
          "target",
          "lot_account",
          "change",
          "notes",
        ]);
      }
      if (!(parseFloat(this.trade.entry) > 0)) {
        return inputs.concat(["stop", "target"]);
      }
      return inputs;
    },
    disabledExecution() {
      let disabled = true;
      if (
        this.trade.asset !== {} &&
        parseFloat(this.trade.stop) > 0 &&
        this.formErrors.length === 0
      ) {
        disabled = false;
      }
      return disabled;
    },
  },
  created() {
    this.emitter.on(`passInputValueTo${this.target}`, (data) => {
      this.getInputValue(data);
    });
    this.emitter.on(
      `inputLostFocusOn${this.target}`,
      ({ name, value }) => {
        if (this.numericFields.includes(name) && value !== "") {
          // If I unfocus any numeric fields:
          // error : Uncaught (in promise) TypeError: Cannot read property 'fixedDecimals' of undefined
          // So this.numerics must be undefined. But I fetched it from vuex, and I see it on VueDevTool
          let fixed = this.numerics.fixedDecimals.includes(name) ? 1 : 0;
          this.setDecimalLength(this.trade, name, fixed);
        }
      }
    );
    this.emitter.on(`passRadioSelectionTo${this.target}`, (payload) => {
      this.$store.dispatch("Trades/changeSelectedRadio", payload);
    });
  },
};
</script>

isVisibile seems to be spelt incorrectly. I don’t know whether that matters or if it’s just a typo in the forum post.

What are the initial values for those 3 properties in your state?

It would also be useful to see the actual error message, changing it to XXX makes it harder for us to figure out what the message really means. It’d also help to see the code that throws the error.

Thanks for your reply.

This is a typo here. XXX part changes based on the input field I type in because different input fields call different methods. It’s just the key in object. So it can be the name, price, date, etc. The problem is the undefined part because it says the objects that I fetch from Vuex are undefined. But I see the objects in VueDevTool and in the first run everything works.

If you need further help then please provide the various sections of code I requested.

Sorry, I forgot that. Initial and after modal closing values:

action: “”,
isVisible.modal: false,
trade: {}

Could you please post as actual code? The problem, whatever it is, is likely to be in the code and to find that problem needs the actual code, not pseudo code.

Could you also post the code that throws the error?

I added the code of TradeEnter component to the end of my question. I also added some notes to show which part gives errors.

Could you post the relevant parts of the Trades module? That’d be the initial state and any actions or mutations that you use to modify the trade.

To debug this further yourself I suggest using console logging. Put logging anywhere that seems sensible, to check exactly what is going on. Be careful not to jump to wrong conclusions when logging objects. Logged objects will show their current value in the console when you expand them, not necessarily the value they had at the point they were logged. You can use JSON.stringify to work around that problem.

Specifically, put some logging in just before the error is thrown and log the value that is supposedly undefined. Confirm it for yourself, just to be sure. Put similar logging right at the start of the method too, just to check that the value doesn’t change between the method starting and the error occurring.

Also put logging everywhere that changes that value. e.g. in the mutation. It could be that this is all just a timing issue, or it may be that something is genuinely being set to the wrong value. Logging should help to illuminate the situation either way.

Don’t focus too heavily on what the Vue Devtools are saying. If the error says a value is undefined then it must have been undefined at that point. There are various reasons why it might appear to be defined when you subsequently look at it in the Devtools but I don’t want to start speculating. The console logging should help to narrow down where the real problem lies.

I follow your advice and found the problem, I hope. It is tiny-emitter, which causes the issue. When I opened TradeEnter first time, each key press trigger once the emit event. When I opened it second time, each key press triggerd twice and so on so forth.

I added the following code to each file that listens to emit event:

unmounted() {
   this.emitter.off(`passInputValueTo${this.target}`);
   this.emitter.off(`inputLostFocusOn${this.target}`);
   this.emitter.off(`passRadioSelectionTo${this.target}`);
}

I tried a few of times, and everyting seems working now.

Thanks for your help.

Yeah, the perils of using an event bus.

Most Vue veterans regard using an event bus as an anti-pattern. You’ll find a discussion of it here at around the 48 minute mark:

https://enjoythevue.io/episodes/24/

Thanks. I’ll listen to it asap.