CyberPunkCodes
CyberPunkCodes

Reputation: 3777

Vue 3 - Buttons not updating after second dropdown changes

I am fairly new to Vue and am having a hard time figuring out how to solve this issue. I have gotten really far on an in-depth project, but am banging my head over what should be a simple feature...

Brief Background/Overview:

I get data from an API (my own), which fetches a study the user has created. They can swap to another one of their studies, which changes the data on the page (stars which indicate the item is in the study already). This is their "overall page view study". They can add items on the page to a study. They click the item, a Bootstrap 5 modal pops up with a dropdown of their studies. The current page study is selected by default, but they can choose any of their other studies to add it to if they want. So if they are adding the item to another study (not the one they are viewing), we assume they can add it and let the API return a special response (409), in which it would show the user an "that already is in the study" message (toast/flash message/etc). If it is their current "page study", then they should be able to add it if it doesn't exist, or remove it if it already is in the study.

I just wanted to provide a background overview just in case something in it can make a difference in the solution (Vue3, Bootstrap 5, bootstrap modal, select dropdown, etc).

Due to the complexity and propriety, I have created a generic mock-up replicating the issue, as well as a demo on CodePen.

Issue:

I have a dropdown (study selector) and 2 buttons (Add and Remove). If they choose a study from the dropdown that isn't the currently viewed "page study", we assume "Add". If it is the currently viewed "page study", we need to do a check. It checks if that "item" is in the study. If it is, then show the "Remove" button, else show the "Add" button.

When I choose an item that IS already in the selected "page study", it does show the "Remove" button (at first). Then when I toggle to a different study in the dropdown, it swaps the buttons (shows "Add"). So far so good... However, when I swap it back to the current "page study", the "Add" button stays when it SHOULD swap to the "Remove" button... It's like it updated once and is ignoring me and no longer listening lol.

I have tried watchers, computed, tried to forceUpdate... I just don't know enough about Vue to get this working.

Mock App Code with dummy data:

<div id="app">
  Study ID: {{ currentStudyId }}
  
  <br><br>

  Selected StudyData Item: 
  <select v-model="selectedStudyDataItem">
    <option value="1">1</option>
    <option value="2">2</option>
    <option value="3">3</option>
    <option value="4">4</option>
    <option value="5">5</option>
    <option value="6">6</option>
    <option value="7">7</option>
    <option value="8">8</option>
  </select>

  <br><br>

  <select v-model="selectedStudyModel" name="study_id" class="form-select">
    <option disabled value="">Choose a study...</option>
    <option v-for="item in studies" :value="item.id" :key="item.id">
      {{ item.title }}
    </option>
  </select>

  <button v-show="showAddButton()" type="submit" class="btn btn-primary">Add</button>

  <button v-show="showRemoveButton()" type="submit" class="btn btn-danger">Remove</button>
</div>

const TestApp = {
  data() {
    return {
      currentStudyId: 2,
      selectedStudyDataItem: 1,
      selectedStudyModel: "", // dropdown model
      studies: [
        { id: 1, title: "Study 1" },
        { id: 2, title: "Study 2" },
        { id: 3, title: "Study 3" },
        { id: 4, title: "Study 4" }
      ],
      studyData: {
        title: "My Cool Study",
        user_id: 1,
        items: [1, 3]
      }
    };
  },

  methods: {
    showAddButton() {
      // if it isnt the current study, we assume add and let api handle it
      if (this.selectedStudyModel !== this.currentStudyId) {
        return true;
      }

      // else, if it isn't in the current viewed study, we can add it
      if (!this.isInStudy(this.selectedStudyDataItem)) {
        return true;
      }

      return false;
    },
    showRemoveButton() {
      if (this.selectedStudyModel === this.currentStudyId) {
        if (this.isInStudy(this.selectedStudyDataItem)) {
          return true;
        }
      }

      return false;
    },
    isInStudy(something) {
      if (this.studyData) {
        const match = (item) => item === something;
        return this.studyData.items.some(match);
      }

      return false;
    }
  }
};

Vue.createApp(TestApp).mount("#app");

CodePen Example

Upvotes: 0

Views: 603

Answers (1)

Daniel
Daniel

Reputation: 35684

You are having an issue due to type casting.

  <select v-model="selectedStudyDataItem">
    <option value="1">1</option>
    <option value="2">2</option>
    <option value="3">3</option>
    <option value="4">4</option>
    <option value="5">5</option>
    <option value="6">6</option>
    <option value="7">7</option>
    <option value="8">8</option>
  </select>

👆 will use a Sting as the selectedStudyModel after a change

To get around that, you need to update showAddButton and include parseInt to handle

showAddButton() {
  // if it isnt the current study, we assume add and let api handle it
  if ( parseInt(this.selectedStudyModel) !== this.currentStudyId ) {
    return true;
  }

  // else, if it isn't in the current viewed study, we can add it
  if ( ! this.isInStudy(parseInt(this.selectedStudyDataItem)) ) {
    return true;
  }

  return false;
},

alternatively, you can also use :value which will convert it

  <select v-model="selectedStudyDataItem">
    <option :value="1">1</option>
    <option :value="2">2</option>
    <option :value="3">3</option>
    <option :value="4">4</option>
    <option :value="5">5</option>
    <option :value="6">6</option>
    <option :value="7">7</option>
    <option :value="8">8</option>
  </select>

👆 these will now be cast as Number instead of String, however I'd recommend that it is better to be explicit in the scripts and not rely on template magic for this.

Some other considerations

  • use computed instead of methods, they result is cached so you call them only as needed
  • if you only have add or remove as options, no need to calculate remove, it's just the opposite (you can use v-if and v-else)

new Vue({
  el: '#app',
    data() {
        return {
        'currentStudyId': 2,
      'selectedStudyDataItem': 1,
            'selectedStudyModel': '',       // dropdown model
      'studies': [
        {'id': 1, 'title': 'Study 1'},
        {'id': 2, 'title': 'Study 2'},
        {'id': 3, 'title': 'Study 3'},
        {'id': 4, 'title': 'Study 4'},
      ],
      'studyData': {
        'title': 'My Cool Study',
        'user_id': 1,
        'items': [
            1, 3
        ],
      },
    }
  },
  computed:{
    showAddButton() {
      // if it isnt the current study, we assume add and let api handle it
      if ( parseInt(this.selectedStudyModel) !== this.currentStudyId ) {
        return true;
      }

      // else, if it isn't in the current viewed study, we can add it
      if ( ! this.isInStudy(parseInt(this.selectedStudyDataItem)) ) {
        return true;
      }

      return false;
    },
  },
  methods: {
    isInStudy(something) {
        if ( this.studyData ) {
                const match = (item) => item === something;
                return this.studyData.items.some(match);
            }

            return false;
    }
  }
  
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  Study ID: {{ currentStudyId }}
  
  <br><br>

  Selected StudyData Item: 
  <select v-model="selectedStudyDataItem">
    <option value="1">1</option>
    <option value="2">2</option>
    <option value="3">3</option>
    <option value="4">4</option>
    <option value="5">5</option>
    <option value="6">6</option>
    <option value="7">7</option>
    <option value="8">8</option>
  </select>

  <br><br>

  <select v-model="selectedStudyModel" name="study_id" class="form-select">
    <option disabled value="">Choose a study...</option>
    <option v-for="item in studies" :value="item.id" :key="item.id">
      {{ item.title }}
    </option>
  </select>

  <button v-if="showAddButton" type="submit" class="btn btn-primary">Add</button>

  <button v-else type="submit" class="btn btn-danger">Remove</button>

  <p>
    <strong>How to reproduce:</strong> Choose the "Study 2" option from the 2nd dropdown. The button should be "Remove" because "Selected StudyData Item [1]" (dropdown above it) is in the "studyData". 1 and 3 are in the studyData for "Study 2".
  </p>
  <p>
    Then select a "studyData Item" that isn't in the study (not 1 or 3), ie: 8. It should change the button to "Add".
  </p>
  <p>
  Now, <u>here is the problem</u>. Swap the "studyData Item" back to 1 or 3 (items in the studyData). It is supposed to change the button back to "Remove" because 1 and 3 are both in the study (study 2).. However, it just does nothing lol.. 
  </p>
  selectedStudyDataItem: {{JSON.stringify(selectedStudyDataItem)}} | {{ typeof selectedStudyDataItem }}
  <br/>
  selectedStudyModel: {{JSON.stringify(selectedStudyModel)}} | {{ typeof selectedStudyModel}}
</div>

Upvotes: 1

Related Questions