ebbishop
ebbishop

Reputation: 1983

V-model with props & computed properties

I have a checkbox component that tracks whether or not an item has been saved by the user as a favorite. This information is passed in as a prop.

Because we can't/shouldn't mutate props passed in from a parent component, I am using v-model on a computed property.

<template>
  <input class="favorite" type="checkbox" v-model="checked">
</template>
<script>
  module.exports = {
    props: ['favorite'],
    computed: {
      checked: {
        get: function getChecked() {
          return this.favorite;
        },
        set: function setChecked(newVal) {
          this.$emit('update:favorite', newVal);
        }
      }
    }
  };
</script>

The parent component controls sending requests to the favorites api & updating the state of each entity if/when the request is successful.

<template>
  <input-favorite
    @update:favorite="toggleFavorite" 
    :favorite="entity.favorite"
  ></input-favorite>
</template>
<script>
  module.exports = {
    methods: {
      toggleFavorite: function toggleFavorite(val) {
        if (val) {
          this.$store.dispatch('postFavorite', { id: this.entity.id, name: this.entity.name });
        } else {
          this.$store.dispatch('deleteFavorite', this.entity.id);
        }
      }
    }
  };
</script>

If the request fails, however, is it possible to prevent the checkbox from getting checked in the first place? Both this.favorite and this.checked stay in sync, but the state of the checkbox does not.

Because the data & props stay correct, I'm also having trouble figuring out how I could trigger a re-render of the checkbox to get it back to the correct state.

Upvotes: 0

Views: 5090

Answers (1)

Roy J
Roy J

Reputation: 43881

I suspect the problem is that favorite never changes, so Vue doesn't see a need to update. You should update it to true upon receiving the checked value (so state is consistent) and then update it again to false when the request fails.

Vue.component('inputFavorite', {
  template: '#input-favorite',
  props: ['favorite'],
  computed: {
    checked: {
      get: function getChecked() {
        return this.favorite;
      },
      set: function setChecked(newVal) {
        this.$emit('update:favorite', newVal);
      }
    }
  }
});

new Vue({
  el: '#app',
  data: {
    entity: {
      favorite: false
    }
  },
  methods: {
    toggleFavorite: function toggleFavorite(val) {
      if (val) {
        console.log("Post");
        this.entity.favorite = true;
        // Mock up a failure
        setTimeout(() => {
          console.log("Failed");
          this.entity.favorite = false;
        }, 250);
      } else {
        console.log("Delete");
      }
    }
  }
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<template id="input-favorite">
  <input class="favorite" type="checkbox" v-model="checked">
</template>

<div id="app">
  <input-favorite @update:favorite="toggleFavorite" :favorite="entity.favorite"></input-favorite>
</div>

The way you have set this up lends itself to the recently-reintroduced .sync modifier, which would simplify your HTML a bit:

  <input-favorite :favorite.sync="entity.favorite"></input-favorite>

Then you do away with toggleFavorite and instead add a watch:

watch: {
  'entity.favorite': function (newValue) {
    console.log("Updated", newValue);
    if (newValue) {
      setTimeout(() => {
        console.log("Failed");
        this.entity.favorite = false;
      }, 250);
    }
  }
}

Upvotes: 2

Related Questions