John D.
John D.

Reputation: 2599

Vue 3 - Two-Way Data Binding Trouble With Child Component

I'm having trouble getting two-way data binding to work in Vue 3 between a parent component and its child component, which is a form.

parent.vue

<template>
  <h5>{{ record.title }}</h5>
  <ChildForm v-model:record="record"></ChildForm>
</template>

<script lang="ts">
export default {
  data() {
    record: undefined
  },
  created() {
    //code to fetch record
    this.$data.record = new Record(responseJson);
  },
  watch: {
    record: function(newRecord, oldRecord) {
      alert("change detected!");
      //perform appropriate logic when record is changed in child
    }
  }
}
</script>

child.vue

<template>
  <InputText v-model="record.title"></InputText>
  <InputText v-model="record.description"></InputText>
</template>

<script lang="ts">
export default {
  props: {
    record: {
      type: Object as () => RecordModel
    }
  },
  emits: ["update:record"]
}

If I change the title in the ChildForm's title input box, the parent accurately reflects the change in the h5 header. But, the watch function never gets fired.

What do I have to do to get the parent's watch method to fire when the child changes the model object's values?

Upvotes: 1

Views: 1918

Answers (2)

John D.
John D.

Reputation: 2599

The answer Steven B linked to was correct but it took some work to adapt to Vue 3 and my case:

parent.vue

<template>
  <h5>{{ record.title }}</h5>
  <ChildForm v-model:record="record" v-on:update:record="parentRecordUpdate"></ChildForm>
</template>

<script lang="ts">
export default {
  data() {
    record?: undefined
  },
  created() {
    //code to fetch record
    this.$data.record = new Record(responseJson);
  },
  methods: {
    parentRecordUpdate() {
      alert("change detected!");
      //perform appropriate logic when record is changed in child
    }
  }
}
</script>

child.vue

<template>
  <InputText v-bind:value="record.title" @input="childUpdateRecord('title', $event.target.value)"></InputText>
  <InputText v-bind:value="record.status" @input="childUpdateRecord('status', $event.target.value)"></InputText>
</template>

<script lang="ts">
export default {
  props: {
    record: {
      type: Object as () => RecordModel
    }
  },
  emits: ["update:record"],
  methods: {
    childUpdateRecord(field: string, value: string) {
      //Update with whatever happened in the InputText box
      this.$props.record.setAttribute(field, value);
      //calling $emit triggers the parent's update method
      this.$emit('update:record', this.$props.record);
    }
  }
}

Upvotes: 1

Steven B.
Steven B.

Reputation: 9362

A couple notes:

  1. You're modifying the prop directly, which is frowned upon, and never technically emitting update:record so the v-model is in essence, useless. If you were emitting, then the watch would fire as you would expect. If you want an example on how to do this, see this answer that shows the jist of it for vue2, you'll just need to modify it to use the new update:prop event names.

  2. If you want to keep modifying the prop directly, then you don't need the v-model:record at all. Just use :record="record" and make the watcher deep

watch: {
    record: {
        handler(newRecord, oldRecord) {
            alert("change detected!");
        },
        deep: true
    }
}

Upvotes: 1

Related Questions