I'm having a hard time with using v-model
in a chained fashion.
Essentially I have a Parent
component that can have many Child
instances. Child works fine alone and I want Parent to keep track of all values of Child and spit out an array of the values with some custom formatting.
It kind of works, but removing a child has some weird behaviour.
When I remove the first entry, the data is the expected ['']
. However visually I see the wrong child being removed. Any ideas what is going on?
Child.vue
<script setup>
import { ref, watch, defineProps } from 'vue';
const { modelValue } = defineProps(['modelValue']);
const emitUpdate = defineEmits(['update:modelValue']);
const category = ref(0);
const foods = ref(0);
// Emit new value when user picks a new food
watch(foods, () => {
emitUpdate('update:modelValue', `${category.value} ${foods.value}`);
});
</script>
<template>
<div>
<select v-model="category">
<option value="Fruits">Fruits</option>
<option value="Pasta">Pasta</option>
</select>
<select v-model="foods">
<option value="oranges">Oranges</option>
<option value="lasagna">Lasagna</option>
</select>
</div>
</template>
Parent.vue
<script setup>
import Child from './Child.vue';
import { ref, defineProps } from 'vue';
const props = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);
const children = ref([]);
function addChild() {
children.value.push({ value: '' });
}
function removeChild(index) {
children.value.splice(index, 1);
emit('update:modelValue', children.value.map(child => child.value));
}
function updateChildValue(index, value) {
children.value[index].value = value;
emit('update:modelValue', children.value.map(child => child.value));
}
</script>
<template>
<div>
<button @click="addChild">Add child</button>
<div v-for="(child, index) in children" :key="index">
<Child v-model="child.value" @update:modelValue="updateChildValue(index, $event)" />
<button @click="removeChild(index)">Remove child</button>
</div>
</div>
</template>
Usage
import Parent from '@/components/Parent.vue';
const myParentValue = ref('')
<template>
...
<p>PARENT</p>
<Parent v-model="myParentValue"></Parent>
<p>myParentValue: {{ myParentValue }}</p>
...
</template>
...
Do not use index as your v-for
key. The key should be a unique value. When you have an array where elements are being added and removed, the item at each index can change, meaning the index is not a unique identifier for the given item. This is the reason the UI is not updating correctly. Vue is not able to properly diff the changes to the array based on the key.
I suggest adding a unique id
field when adding a new child. This is one possible simple way of doing it:
let count = 0
function addChild() {
children.value.push({ id: count++, value: '' });
}
Then just update your v-for:
<div v-for="(child, index) in children" :key="child.id">
Two other non-critical issues spotted:
First, in Child.vue:
const { modelValue } = defineProps(['modelValue']);
You shouldn't destructure defineProps
. You can lose reactivity this way. Always opt to assign the return value of defineProps
to a variable, e.g. const props = defineProps
. This isn't an issue in your code at the moment because you don't actually use modelValue in your Child component... You can remove this prop and the v-model
on the <Child>
element in the parent if you want to since they're not technically doing anything. What's really doing all the work is the emits.
Second, and not really an issue, but you don't need to import defineProps
, it is automatically available inside script setup
All corrections can be seen in this Vue Playground example