How to easily sync with multiple v-models in Vue 3 using Composition API
This article was originally published at The Road To Enterprise.
Vue 3 has brought many new features, and the ability to use more than one v-model directive on the same element is one of them. I want to share with you a quick tip on how to handle updating the state of a parent component when using multiple v-models. It's especially useful when dealing with forms. You can find the full code example in this GitHub repository.
For this example, we will use a form shown in the image below.
Below you can find the code for it. We have two files - App.vue
, which has the form state and renders the Form
component. The Form.vue
component renders the form element with labels and inputs fields.
App.vue
<template>
<div :class="$style.container">
<Form
v-model:name="form.name"
v-model:surname="form.surname"
@submit="onSubmit"
/>
</div>
</template>
<script>
import { ref } from 'vue'
import Form from './components/Form.vue'
export default {
components: {
Form,
},
setup() {
const form = ref({
name: '',
surname: '',
})
const onSubmit = () => console.log(form)
return {
form,
onSubmit,
}
},
}
</script>
<style module>
.container {
max-width: 30rem;
@apply mx-auto py-8;
}
</style>
components/Form.vue
<template>
<form @submit.prevent="$emit('submit')">
<div :class="$style.formBlock">
<label :class="$style.label">Name</label>
<input
v-model="nameState"
:class="$style.input"
type="text"
aria-label="Name input"
/>
</div>
<div :class="$style.formBlock">
<label :class="$style.label">Surname</label>
<input
v-model="surnameState"
:class="$style.input"
type="text"
aria-label="Surname input"
/>
</div>
<div>
<button
class="float-right bg-blue-100 text-blue-900 px-4 py-3 rounded font-semibold"
type="submit"
>
Submit
</button>
</div>
</form>
</template>
<script>
import { useVModel } from '../composables/useVModel.js'
export default {
emits: ['update:name', 'update:surname', 'submit'],
props: {
name: String,
surname: String,
},
setup(props) {
return {
nameState: useVModel(props, 'name'),
surnameState: useVModel(props, 'surname'),
}
},
}
</script>
<style module>
.formBlock {
@apply flex flex-col mb-4;
}
.label {
@apply mb-2;
}
.input {
@apply px-4 py-3 shadow rounded border border-gray-300 bg-white;
}
</style>
To update the state in the parent, we need to emit an update:<modelValue>
event.
Here is the code for the useVModel
helper.
composables/useVModel.js
import { computed, getCurrentInstance } from 'vue'
export const useVModel = (props, propName) => {
const vm = getCurrentInstance().proxy
return computed({
get() {
return props[propName]
},
set(value) {
vm.$emit(`update:${propName}`, value)
},
})
}
We have to pass the props
object to keep the reactivity intact and the prop name which we want to sync with. Inside of the useVModel
we get access to the current instance via getCurrentInstance()
, as we need access to the $emit
method. The computed
receives an object with a getter and setter. The getter returns the value passed via props, whilst the setter emits an event to update the value. Thanks to this little helper, keeping the state passed through props via v-models is much cleaner and simpler.
I hope you enjoyed this article. If you would like to learn more tips, advanced patterns, techniques and best practices related to Vue, you might want to check out "Vue - The Road To Enterprise" book and subscribe to the newsletter.