[Vuejs]-Updating nested v-model with computed propertes instead of watcher in VueJS 2

2👍

Indeed, watchers should be only used if there is no other solution, as it can cause side-effects and makes debugging a mess. In your case it’s probably not necessary.

A computed property is never used to update values directly. So it’s not an option neither. You should use them where possible, but only for converting values into a different format, as helper values for methods or other computed properties or in the template.

In general you should think about your component architecture. The problematic bit here is that you use an object as v-model value, which can cause side-effects too. Of course you can do it like this, but it more complex to maintain and you have to implement it correctly. Imagine you have multiple components using this object as v-model value. In the worst case, the components would override values they should not override. To prevent this you usually use only single, primitive values as v-model, instead of objects.

Actually, the way you implemented it works by accident. In Vue 2 v-model requires that you emit an input event from the child component to the parent component, so the value gets updated in the parent component (check here for more details: https://v2.vuejs.org/v2/guide/components.html#Using-v-model-on-Components). In your case it works, because your v-model is an object – means the reference to the object is passed to the child component and you are updating a property of this object reference.

So, let’s refactor your code a bit (not tested, but the concept should be clear):

1. Assuming you only have a handful of values to update in the child component

In your case v-model is not an option, because you have 2 model values to update in the same component. Vue 3 would support it (https://v3.vuejs.org/guide/component-custom-events.html#multiple-v-model-bindings), but as you use Vue 2 I would suggest to do it by hand:

<template>
  <user-component
    :nationality="apiData.user.nationality"
    :stateless="apiData.user.stateless"
    @update:nationality="onUpdateNationality"
    @update:stateless="onUpdateStateless"
  />
</template>

<script>
export default {
  name: "MyForm",
  components: {
    UserComponent
  },
  data() {
    return {
      apiData: {},
    };
  },
  async mounted() {
    const response = await ApiService.getData();
    this.apiData = response.data;   
  },

  methods: {
    onUpdateNationality (value) {
      this.apiData.user.nationality = value
    },

    onUpdateStateless (value) {
      this.apiData.user.stateless = value

      if (value) {
        this.apiData.user.nationality = null // You also could handle it in your child component
      }
    }
  }
};
</script>
<template>
  <b-form-group
    label="Nationality"
  >
    <b-form-input
      :value="nationality"
      @input="onUpdateNationality($event.target.value)"
    />
    <b-form-checkbox
      :checked="stateless"
      @click="onClickStateless"
    >
      Stateless
    </b-form-checkbox>
  </b-form-group>
</template>

<script>
export default {
  name: "UserCompoent",
  props: {
    nationality: {
      type: String,
      required: true
    },

    stateless: {
      type: Boolean,
      required: true
    }
  },

  methods: {
    onClickStateless () {
      this.$emit('update:stateless', !this.stateless)
    },

    onUpdateNationality (nationality) {
      this.$emit('update:nationality', nationality)
    }
  }
};
</script>

2. Assuming your child component updates many props

<template>
  <user-component
    :user="apiData.user"
    @update:user="onUpdateUser"
  />
</template>

<script>
export default {
  name: "MyForm",
  components: {
    UserComponent
  },
  data() {
    return {
      apiData: {},
    };
  },
  async mounted() {
    const response = await ApiService.getData();
    this.apiData = response.data;   
  },

  methods: {
    onUpdateUser ({ nationality, stateless }) {
      this.apiData.user.nationality = nationality
      this.apiData.user.stateless = stateless

      if (stateless) {
        this.apiData.user.nationality = null // You also could handle it in your child component
      }
    }
  }
};
</script>
<template>
  <b-form-group
    label="Nationality"
  >
    <b-form-input
      :value="form.nationality"
      @input="onUpdateNationality($event.target.value)"
    />
    <b-form-checkbox
      :checked="form.stateless"
      @click="onClickStateless"
    >
      Stateless
    </b-form-checkbox>
  </b-form-group>
</template>

<script>
export default {
  name: "UserCompoent",
  props: {
    user: {
      type: Object,
      requird: true
    }
  },

  data () {
    return {
      form: {
        nationality: this.user.nationality,
        stateless: this.user.stateless
      }
    }
  }

  methods: {
    onClickStateless () {
      this.form.stateless = !this.form.stateless
      this.$emit('update:user', this.form)
    },

    onUpdateNationality (nationality) {
      this.form.nationality = nationality
      this.$emit('update:user', this.form)
    }
  }
};
</script>

1👍

You could let the computed property be about the user-property directly.

Then use get & set:

https://v2.vuejs.org/v2/guide/computed.html#Computed-Setter


[edit]

In response to the question by OP in the comments:

Actually – now that you’ve updated your question with more details, I don’t think computed properties are the answer at all.

The accepted answer by @tho-masn is clearly the way to go.

Computed properties with getters and setters are a nice way to create v-model for something, even though that something is computed. I use it in combination with vuex, for instance.

But that’s not what you’re doing. You want to update property A, when property B changes. That’s an event, not a computed property.

I’d delete my answer if you hadn’t already asked a question on it.

[/edit]

Leave a comment