DEV Community

Michael Olofinjana
Michael Olofinjana

Posted on • Updated on

Multiple v-model for the rest of us

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"
/>
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
import Vue from "vue";
import Vue2MultipleVModels from "vue2-multiple-vmodels";

Vue.use(Vue2MultipleVModels);
Enter fullscreen mode Exit fullscreen mode

The library comes with 2 helpful vue directives.

  1. v-model-destruct
  2. 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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 multiple v-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>
Enter fullscreen mode Exit fullscreen mode

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 a models: 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>
Enter fullscreen mode Exit fullscreen mode

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!! 😉

Latest comments (2)

Collapse
 
sjoerd82 profile image
Sjoerd82

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)?

Collapse
 
sjoerd82 profile image
Sjoerd82

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:

models: [
    { data: "arrMyArray", event: "models:arrMyArray" },
],

watch: {
    arrMyArray(newVal) {
        if (!this.isLoaded) {
            this.isLoaded = true
        } else {
            this.$emit('models:arrMyArray',newVal)
        }
    },
},
Enter fullscreen mode Exit fullscreen mode