10
votes

In my application I need to use a nested v-for to display a list of elements with a select-option.. This is the scenario

<div class="stuck" v-for="box in items">
  <p>Pick an option for this box:</p>
  <select v-model="box">
      <option v-for="package in packages" 
              :value="package.id">{{ package.name }} </option>
  </select>
</div>

The variable items come from Vuex store. In this way, i'm getting the error:

You are binding v-model directly to a v-for iteration alias. This will not be able to modify the v-for source array because writing to the alias is like modifying a function local variable. Consider using an array of objects and use v-model on an object property instead.

With this in mind, i'm going to change the code like so:

<div class="stuck" v-for="box in items">
  <p>Pick an option for this box:</p>
  <select v-model="box.id">
      <option v-for="package in packages" 
              :value="package.id">{{ package.name }} </option>
  </select>
</div>

I've just changed the select v-model from the alias box, to the right id: box.id

In this way, all works... or... half works. Because, if i'm going to pick an option from the select, i got another error:

[vuex] Do not mutate vuex store state outside mutation handlers.

This is correct, because the v-model is bind to box.id (that is not an alias but a real value). But, when i pick an option the v-model "try" to change box.id that come from Vuex store.

Now, in a simple scenario i will create a computed property for set/get to avoid vuex store mutation.

But... here i have a nested loop, so i cant create a computed on 'box.id'.

Do you have a solution for this ?

Thanks a lot!

2
Replace the v-model with :value="box.id" @input="$store.commit('boxUpdated', { id: box.id, value: $event.target.value })" or something like that, not sure about the syntax but That's what I would doAfikDeri
You might consider modifying some property other than box.id with the <select> element. Another developer might expect box.id to reliably distinguish boxes from one another, but it actually remembers a user's package selection. What if you used box.selected_package_id instead of box.id?Travis Hohl

2 Answers

2
votes

you could try a different data flow pattern. Your select listens to the store (but does not directly update it)

<div class="stuck" v-for="box in items">
  <p>Pick an option for this box:</p>
  <select :value="box.id" @change="updateBox">
    <option v-for="package in packages" :value="package.id">
      {{ package.name }}
    </option>
  </select>
</div>

Then you create a method that fires whenever the selected option changes

updateBox(e) {
  const id = e.target.value;
  this.$store.commit('changeYourBox', id);
},

This function should commit a vuex mutation that alteres the box id. So you'd need that mutation too. Once the store value updates, your components box object updates and the select that listens to that store will update it's selected value accordingly.

That way, you can alter the store value from anywhere and the selected value will change as well.

0
votes

with usage of mine library vuex-dot in this situation you can do so:

let's go with such state

  {
    state: {
      boxes: []
    },
    mutations: {
      editBox(state, {target, key, value}) {
        Vue.set(target, key, value);
      }
    }
  };

So let's create additional component BoxEdit:

<template>
  <div class="stuck">
    <p>Pick an option for this box:</p>
    <select v-model="id">
      <option v-for="package in packages"
              :value="package.id">{{ package.name }} </option>
    </select>
  </div>
</template>

<script>
  import { take } from 'vuex-dot'
  export default {
    props: ['box', 'packages'],
    computed: {
      ...take('box')
        .expose(['id'])
        .commit('editBox', true)
        .map()
    }
  }
</script>

and now you can make simply write

<box-edit v-for="box in boxes" :box="box" :packages="packages"></box-edit>

in your subject component template.

link to library site: https://github.com/yarsky-tgz/vuex-dot