Vue 3 has been around for a minute and one of my favourite features has been the addition of multiple v-model bindings for custom components.
See here on how to use new feature.
https://dev.to/snehalk/multiple-v-model-in-vue-3-31lc
In summary - with multiple v-models, you can create custom components like this.
<CustomMultipleInput
v-model:title="heading"
v-model:subHeading="pageSubHeading"
/>
The benefit here is that you can abstract common but complex components and bind to the specific parts you're interested in.
That's nice and all. But what about Vue 2?
Not everyone can easily bump up or upgrade to Vue 3.
A classic yet simple example I always run into is in building a basic SelectInput
component that makes very little assumptions and allow you bind any kind of select options and text input.
Lets take a look.
The SelectInput component at its barest looks like this.
it's simply a select and input element mash up.
The code for the above image could look something like this.
SelectInput.vue
<template>
<div class="select-input">
<select class="select-input__select">
<slot />
</select>
<input class="select-input__input" />
</div>
</template>
<style>
.select-input {
border: solid 1px #e2e2e2;
max-width: 250px;
font-size: 14px;
padding: 5px 6px;
}
.select-input__select {
border: none;
background: none;
max-width: 69px;
}
.select-input__input {
border: none;
width: 180px;
border: none;
outline: none;
}
</style>
Not the most complex component. But helps illustrate a real world use case.
Emitting Objects
A popular approach when dealing with complex components in vue 2 is to emit javascript objects that contain all the fields you want to bind to.
So something like this.
SelectInput.vue
<template>
<div class="select-input">
<select v-model="value.select" @change="emit" class="select-input__select">
<slot />
</select>
<input v-model="value.input" @input="emit" class="select-input__input" />
</div>
</template>
<script>
export default {
model: {
prop: "value",
event: "change"
},
props: {
value: {
type: Object,
default: () => ({
select: undefined,
input: undefined,
}),
},
},
methods: {
emit() {
this.$emit('change', this.value);
}
},
};
</script>
Here, we're taking advantage of the fact that Vue allows direct mutation of prop fields of objects for a quick 2-way binding component.
Simple and easy (Don't use this code in production 🌚).
The custom component can now be used as below.
Changes made within the component will reflect in App.vue and changes made in App.vue will reflect in the component.
App.vue
<template>
<SelectInput v-model="amount">
<option value="NGN">NGN</option>
<option value="GHS">GHS</option>
<option value="USD">USD</option>
</SelectInput>
</template>
<script>
export default {
data () {
return {
amount: {
select: "NGN",
input: 0,
}
}
}
}
</script>
This works and helps you abstract complex components, forms etc and emit only the parts you need as fields in the object.
A downside of this approach is that it can be very brittle.
The SelectInput
component has to dictate the structure of the amount
object. If we mutate it in a way the SelectInput
component cannot accomodate, we can easily break things.
This means we must keep track and maintain that object structure and as a consequence, the code can be less readable (especially with larger objects).
Vue2 Multiple V-Models
A solution!!! 🕺🏻
I wrote this library to solve these problems. https://github.com/michaelolof/vue2-multiple-vmodels
First lets install!
$ npm install --save vue2-multiple-vmodels
import Vue from "vue";
import Vue2MultipleVModels from "vue2-multiple-vmodels";
Vue.use(Vue2MultipleVModels);
The library comes with 2 helpful vue directives.
v-model-destruct
v-models
Destructuring v-model objects with v-model-destruct
Consider the earlier example where we emited objects.
What if the object was a lot more complex but you were only interested in one field in the object?
What if in the SelectInput
component, You were only interested in the amount field?
With
v-model-destruct
you can destruct an incoming v-model object and bind only to the fields you're interested in.
So in App.vue
we could write...
<template>
<SelectInput v-model-destruct:input="amount">
<option value="NGN">NGN</option>
<option value="GHS">GHS</option>
<option value="USD">USD</option>
</SelectInput>
</template>
<script>
export default {
data () {
return {
amount: 300,
}
}
}
</script>
In the above snippet, we are destructing the v-model object and binding (2-way binding), only the input field of the v-model object to the amount
variable.
And just in the same way we could specifially bind the select
field to the currency
variable E.g
<template>
<SelectInput v-model-destruct:select="currency" v-model-destruct:input="amount">
<option value="NGN">NGN</option>
<option value="GHS">GHS</option>
<option value="USD">USD</option>
</SelectInput>
</template>
<script>
export default {
data () {
return {
currency: "GHS",
amount: 300,
}
}
}
</script>
Multiple v-model with v-models
The earlier example with v-model-destruct
is great for already existing components designed to emit objects. But it isn't really a multiple v-model
alternative.
The
v-models
directive simply put is my Vue2 response to the new multiplev-model
feature in Vue 3.
Lets see how it works.
Using the same SelectInput.vue
component we can design our component to support v-models
like this...
SelectInput.vue
<template>
<div class="select-input">
<select v-model="select" class="select-input__select">
<slot />
</select>
<input v-model="input" class="select-input__input" />
</div>
</template>
<script>
export default {
models: [
{ data: "select", event: "models:select" },
{ data: "input", event: "models:input" },
],
data() {
return {
select: undefined,
input: "",
}
},
watch: {
select(newVal) {
this.$emit("models:select", newVal);
},
input(newVal) {
this.$emit("models:input", newVal);
},
},
}
</script>
The structure of this component is quite different from the earlier example - but its very straight forward.
- We define our select and input elements and bind a data field to them respectively.
- Then we say - anytime the data field changes we want to emit a
models:<xxx>
event. Prefixing the emitted event with amodels:
is required for this feature to work. The directive checks for it. - We then register the data field and the emitted event in the
models
section.
Thats it!!
This is what the App.vue
component will look like utilizing this directive.
App.vue
<template>
<SelectInput v-models:select="currency" v-models:input="amount">
<option value="NGN">NGN</option>
<option value="GHS">GHS</option>
<option value="USD">USD</option>
</SelectInput>
</template>
<script>
export default {
data () {
return {
currency: "GHS",
amount: 300,
}
}
}
</script>
As you might have noticed, we didn't define any props in the component nor did we pass any to the component.
The v-models
directive doesn't need props to work. You could still define your prop and update the data field whenever the prop changes - but v-models
doesn't rely on props.
It relies on data. This is because props are immutatble.
We also have a models
array section instead of model
for normal v-model. Here you define the data and corresponding event for all the fields you want v-models to work on.
As mentioned earlier, the event must be preceeded with models:
for the directive to work.
That's it.
Not too shabby yeah? 😂
And that's how you do multiple v-model in Vue 2.
Thanks for reading!! 😉
Top comments (2)
Just took it for a quick test drive and it seems to work fine, and is nice for us people that are going to migrate to Vue3 somewhere soonish, giving us the opportunity to already start incorporating some of the new features using plugins like these.
Without having looked too closely yet at Vue3, it seems that not much change is needed when migrating from your plugin to Vue3. Could it be as little as uninstalling and renaming v-models:select to v-model:select, and v-models:input to v-model:input (keeping with the example)?
One odd thing I just came across when using this on arrays (and I imagine on objects as well). Normally I would in a component, in the created()-phase, wait on the $nextTick to set an isLoaded variable to true, this way I'm sure everything is populated and ready, however, the variables set via this multiple v-model take a long time to initialize, and aren't ready yet on $nextTick.
I found no other way to wait for the first time the watcher got triggered on the array. This feels like it's the wrong way around: