17

According to the documentation I should be able to use computed properties as v-model in Vue as long as I define get/set methods, but in my case it doesn't work:

export default{

    template: `
      <form class="add-upload" @submit.prevent="return false">
        <label><input type="checkbox" v-model="options.test" /> test </label>
      </form>
    `,

    computed: {

      options: {

        get(){
          console.log('get');
          return {test: false};
        },

        set(value){
          console.log('set');
        },

      },

    }

}

Apparently set is not called when I check/uncheck the input. But get is called when the component is displayed...

9
  • 5
    Here option is reactive not the options.test Commented Jul 9, 2020 at 16:59
  • 1
    Does that mean I need to define computed properties with get/set for each key? That would force me to write a lot of duplicate code. I only have test here but I am planning to add much more input fields.. Commented Jul 9, 2020 at 17:01
  • 1
    I don't understand? Commented Jul 9, 2020 at 17:13
  • 1
    So why do you want to use computed property with v-model ? what's wrong with defining data ? The issue here is that v-model tries to set value on options.test but your computed property is setting new values on options. Not sure if that will work automatically. Commented Jul 9, 2020 at 17:21
  • 1
    @Stark Buttowski comment is the correct answer. Setter is only invoked directly. Not when calling a property of an object. I was scratching my head over this one for awhile. So v-model="test" setter will work on computed setter for test. But v-model="obj.test" setter will NOT fire on computed setter obj Commented Oct 13, 2020 at 19:00

6 Answers 6

5

Edit: After reading in the comments that you rely on the localstorage, I can only suggest you to take the Vuex approach and use a persistence library to handle the localstorage. (https://www.npmjs.com/package/vuex-persist) This way, your localstorage will always be linked to your app and you don't have to mess with getItem/setItem everytime.

Looking at your approach, I assume you have your reasons to use a computed property over a data property.

The problem happens because your computed property returns an object defined nowhere but in the get handler. Whatever you try, you won't be able to manipulate that object in the set handler.

The get and set must be linked to a common reference. A data property, as many suggested, or a source of truth in your app (a Vuex instance is a very good example).

this way, your v-model will work flawlessly with the set handler of your computed property.

Here's a working fiddle demonstrating the explanation:

With Vuex

const store = new Vuex.Store({
  state: {
    // your options object is predefined in the store so Vue knows about its structure already
    options: {
      isChecked: false
    }
  },
  mutations: {
    // the mutation handler assigning the new value
    setIsCheck(state, payload) {
      state.options.isChecked = payload;
    }
  }
});

new Vue({
  store: store,
  el: "#app",
  computed: {
    options: {
      get() {
        // Here we return the options object as depicted in your snippet
        return this.$store.state.options;
      },
      set(checked) {
        // Here we use the checked property returned by the input and we commit a Vuex mutation which will mutate the state
        this.$store.commit("setIsCheck", checked);
      }
    }
  }
})
body {
  background: #20262E;
  padding: 20px;
  font-family: Helvetica;
}

#app {
  background: #fff;
  border-radius: 4px;
  padding: 20px;
  transition: all 0.2s;
}

h2 {
  font-weight: bold;
  margin-bottom: 15px;
}
<div id="app">
  <h2>isChecked: {{ options.isChecked }}</h2>
  <input type="checkbox" v-model="options.isChecked" />
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://unpkg.com/[email protected]"></script>

With a data property

new Vue({
  el: "#app",
  data: {
    options: {
      isChecked: false
    }
  },
  computed: {
    computedOptions: {
      get() {
        return this.options;
      },
      set(checked) {
        this.options.isChecked = checked;
      }
    }
  }
})
body {
  background: #20262E;
  padding: 20px;
  font-family: Helvetica;
}

#app {
  background: #fff;
  border-radius: 4px;
  padding: 20px;
  transition: all 0.2s;
}

h2 {
  font-weight: bold;
  margin-bottom: 15px;
}
<div id="app">
  <h2>isChecked: {{ computedOptions.isChecked }}</h2>
  <input type="checkbox" v-model="computedOptions.isChecked" />
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>

Your approach is a bit special IMHO but, again, you must have your reasons to do so.

Sign up to request clarification or add additional context in comments.

Comments

4

The very simple explanation here in code. computed properties are dependent on other data/reactive variables. If only when the reactive properties changed their values and if same property used to compute some other computed properties then the computed property would become reactive.

this way we must set values and get in setter and getter methods.

new Vue({
  el: '#app',
  data: {
    message: 'Use computed property on input',
    foo:0,
    isChecked:true
  },
  computed:{
   bar:{
    get: function(){
        return this.foo;
    },
   set: function(val){
     this.foo = val;
    }
   },
   
    check:{
    get: function(){
        return this.isChecked;
    },
   set: function(val){
     this.isChecked = val;
    }
   }
  }
})
<script src="https://unpkg.com/vue"></script>

<div id="app">
  <p>{{ message }} Text</p>
  <input type="text" v-model="bar" /> 
  {{bar}}

<br/>
 <p>{{ message }} Checkbox</p>
    <input type="checkbox" v-model="check" /> 
    
    {{check}}
</div>

Comments

4

Instead of a computed getter/setter, use a local data prop, initialized to the target localStorage item; and a deep watcher (which detects changes on any subproperty) that sets localStorage upon change. This allows you to still use v-model with the local data prop, while observing changes to the object's subproperties.

Steps:

  1. Declare a local data prop (named options) that is initialized to the current value of localStorage:
export default {
  data() {
    return {
      options: {}
    }
  },
  mounted() {
    const myData = localStorage.getItem('my-data')
    this.options = myData ? JSON.parse(myData) : {}
  },
}
  1. Declare a watch on the data prop (options), setting deep=true and handler to a function that sets localStorage with the new value:
export default {
  watch: {
    options: {
      deep: true,
      handler(options) {
        localStorage.setItem('my-data', JSON.stringify(options))
      }
    }
  },
}

demo

Comments

3
+250

It seems the problem is both in the presence of options and the return value of the getter.

You could try this:

let options;

try {
  options = JSON.parse(localStorage.getItem("options"));
}
catch(e) {
  // default values
  options = { test: true };
}

function saveOptions(updates) {
  localStorage.setItem("options", JSON.stringify({ ...options, ...updates }));
}

export default{
  template: `
    <form class="add-upload" @submit.prevent="return false">
      <label><input type="checkbox" v-model="test" /> test </label>
    </form>`,
  computed: {
    test: {
      get() {
        console.log('get');
        return options.test;
      },
      set(value) {
        console.log('set', value);
        saveOptions({ test: value });
      },
    },
  }
}

Hope this helps.

Comments

2

I'm not familiar if there's a computed set method that could work here, but there's a few other approaches to solving the problem.

If you want a singular getter for mutating the data, you can use an event based method for setting the data. This method is my favorite:

export default {
  template: `
      <form class="add-upload" @submit.prevent="">
        <label for="test"> test </label>
        {{options.test}}
        <input id="test" type="checkbox" v-model="options.test" @input="setOptions({test: !options.test})"/>
      </form>
    `,
  data() {
    return {
      optionsData: {
        test: false
      }
    }
  },
  computed: {
    options: {
      get() {
        return this.optionsData;
      },
    },
  },
  methods: {
    setOptions(options) {
      this.$set(this, "optionsData", { ...this.optionsData, ...options })
    }
  }
}

If you're not really doing anything in the get/set you can just use the data option

export default {
  template: `
      <form class="add-upload" @submit.prevent="">
        <label for="test"> test </label>
        {{options.test}}
        <input id="test" type="checkbox" v-model="options.test" />
      </form>
    `,
  data() {
    return {
      options: {
        test: false
      }
    }
  }
}

Then there's also the option of get/set for every property

export default {
  template: `
      <form class="add-upload" @submit.prevent="">
        <label for="test"> test </label>
        {{test}}
        <input id="test" type="checkbox" v-model="test" />
      </form>
    `,
  data() {
    return {
      optionsData: {
        test: false
      }
    }
  },
  computed: {
    test: {
      get() {
        return this.optionsData.test;
      },
      set(value) {
        this.optionsData.test = value
      }
    },
  },
}

Comments

2

The return value of Vue computed properties are not automatically made reactive. Because you are returning a plain object, and because you're assigning to a property within the computed property, the setter will not trigger.

You have two problems you need to solve, one problem's solution is to store a reactive version of your computed property value (see Vue.observable()). The next problem is a bit more nuanced, I'd need to know why you want to hook in to the setter. My best guess without more information would be that you're actually looking to perform side-effects. In that case, you should watch the value for changes (see vm.$watch()).

Here's how I'd write that component based on the assumptions above.

export default {
  template: `
      <form class="add-upload" @submit.prevent="return false">
        <label><input type="checkbox" v-model="options.test" /> test </label>
      </form>
    `,
  computed: {
    options(vm) {
      return (
        vm._internalOptions ||
        (vm._internalOptions = Vue.observable({ test: false }))
      )
    },
  },
  watch: {
    "options.test"(value, previousValue) {
      console.log("set")
    },
  },
}

If you need to trigger side-effects based on anything changing on options, You can deeply watch it. The biggest caveat though is that the object must be reactive (solved by Vue.observable() or defining it in the data option).

export default {
  watch: {
    options: {
      handler(value, previousValue) {
        console.log("set")
      },
      deep: true,
    },
  },
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.