Search code examples
javascriptvue.jsvuex

How to use a dynamic property from an object from store as a v-model?


I've been wrapping my head around this one and I can't quite figure out the way to do this.

How can I have input data-binding to a store object property? Meaning: my component only knows the store object, but it does not know in advance which properties the object contains.

Take this example:

Vue.component('os-choice', {
  template: `
    <div>
      <h5>Select OS</h5>
  
      <div>
        <label class="checkbox">
          <input type="checkbox" value="linux" v-model="linux">
            Linux
        </label>
      </div>

      <div>
        <label class="checkbox">
          <input type="checkbox" value="macos" v-model="macos">
            MacOS
        </label>
      </div>

      <div>
        <label class="checkbox">
          <input type="checkbox" value="windows" v-model="windows">
            Windows
        </label>
      </div>

    </div>
  `,
  computed: {
    linux: {
      get() {
        return store.state.linux;
      },
      set(val) {
        store.commit('updateLinux', {
          linux: val
        });
      }
    },
    macos: {
      get() {
        return store.state.macos;
      },
      set(val) {
        store.commit('updateMacos', {
          macos: val
        });
      }
    },
    windows: {
      get() {
        return store.state.windows;
      },
      set(val) {
        store.commit('updateWindows', {
          windows: val
        });
      }
    },
  }  
});


Vue.component('os-choice2', {
  template: `
    <div>
      <h5>Select OS</h5>
  
        <div v-for="(value,index) in os" :key="index">
          <label class="checkbox">
            <input type="checkbox" value="index" v-model="value">
              {{index}}
          </label>
        </div>

      </div>

    </div>
  `,
  computed: {
    os: {
      get(){ return store.state.os; },
      set(event){
        var name = event.target.value;
        var value = event.target.checked;
        store.commit('updateOs', {name: name, value: value});
      }
    },
  }  
});


 const store = new Vuex.Store({
  state: {
    // initial state
    linux: true,
    macos: true,
    windows: false,
    os: {
      linux: true,
      macos: true,
      windows: false,
    }
  },
  mutations: {
    updateLinux(state, payload) {
      console.log('STORE: updated linux');
      state.linux = payload.linux;
    },
    updateMacos(state, payload) {
      console.log('STORE: updated MacOS');
      state.macos = payload.macos;
    },
    updateWindows(state, payload) {
      console.log('STORE: updated Windows');
      state.windows = payload.windows;
    },
    updateOs(state, payload) {
      console.log('STORE: updated OS');
      state.os[payload.name] = payload.value;
    }
  }
});



var app = new Vue({
  el: "#app",


  
})
.row{
  width: 100%;
}
.column{
  width: 50%;
}
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script src="https://unpkg.com/[email protected]/dist/vuex.js"></script>

<div id="app" style="text-align:center;">
  <div class="row">
    <div class="column">
      <os-choice></os-choice>
    </div>
    <div class="column">
      <os-choice2></os-choice2>
    <div>
  </div>
</div>

The first component works great. The second component does not update the store: it generates error "[vuex] Do not mutate vuex store state outside mutation handlers."

I believe I understand why it is happening: Since the computed property of the component points to the object (and not the property being changed), v-model will try to change the deep property, thus forcing a change on the store which will throw an error before the component even calls the set() method on the object, right?

So what I would like to know is how would be the proper way to go about this?

My real-world scenario is that I'm building a dynamic filter with checkboxes and at runtime I do not know how many options it will have. This means I'll loop through my options to create the checkboxes, but then the data-binding would have to be in the form of "values[]". And this is where my headaches start.

Thank you all in advance


Solution

  • From VueJs documentation:

    Custom events can also be used to create custom inputs that work with v-model. Remember that:

     <input v-model="searchText">
    

    does the same thing as:

     <input   v-bind:value="searchText"   v-on:input="searchText =
     $event.target.value"
     >
    

    When used on a component, v-model instead does this:

     <custom-input   v-bind:value="searchText"   v-on:input="searchText =
     $event"
     ></custom-input>
    

    So, instead of using v-model, you could use value as the v-bind:value="value" and map the event to update the store appropriately, something like: v-on:input="store.commit('store.commit('updateOs', event.target.value)"

    The other component works since you are following exactly what Vuex recommend doing: https://vuex.vuejs.org/guide/forms.html

    But on the v-for, you are using the store model directly.