One of my favorite VueJS feature is the ability to do two-way data-binding with form inputs. However, if we want to wrap a native input component with our own logic, we will still have to write some boiler-plate code i.e. specific props and specific event names to make them work. You can view the official docs here on how to do it. However, in this post, we will expand the idea from Thorsten Lünborg’s post on this issue with TypeScript.
modelWrapper
composable
Here is the code for the useModelWrapper()
composable function.
import { computed, getCurrentInstance } from "vue";
export function useModelWrapper<T>(propName = "modelValue") {
const instance = getCurrentInstance();
return computed({
get: () => instance?.props[propName] as T,
set: (value: T) => instance?.emit(`update:${propName}`, value),
});
}
The above code is more concise than Thorsten’s version as we are using the
getCurrentInstance()
method to access theprops
andemit
which removes the need to pass it into the composable function.
Using useModelWrapper()
Here we will see how we use the composable in our custom TextboxDemo.vue
component. As an example, the TextboxDemo.vue
component has an extra functionality to clear the textbox.
<script setup lang="ts">
import { useModelWrapper } from "@/composables/modelWrapper";
defineProps<{
modelValue?: string;
}>();
defineEmits<{
(e: "update:modelValue", value: string): void;
}>();
const value = useModelWrapper();
</script>
<template>
<input v-model="value" type="text" />
<button type="button" @click="value = ''">×</button>
</template>
Using TextboxDemo.vue
Straightforward using the component. Just like a native textbox element.
<script setup lang="ts">
import { ref } from "vue";
import TextboxDemo from "./TextboxDemo.vue";
const msg = ref("hello world");
</script>
<template>
<TextboxDemo v-model="msg"></TextboxDemo>
</template>
Component with multiple v-models
Let’s say we are creating a range control that needs to do 2-way data-binding with the from
and to
inputs, we can do so like this.
<script setup lang="ts">
import { useModelWrapper } from "@/composables/modelWrapper";
defineProps<{
from: number;
to: number;
}>();
defineEmits<{
(e: "update:from", value: number): void;
(e: "update:to", value: number): void;
}>();
const from = useModelWrapper("from");
const to = useModelWrapper("to");
</script>
<template>
<input v-model.number="from" type="number" />
<input v-model.number="to" type="number" />
</template>
Using RangeDemo.vue
And to use it, it becomes really intuitive to define the props on the component.
<script setup lang="ts">
import { ref } from "vue";
import RangeInput from "./components/RangeInput.vue";
const from = ref(0);
const to = ref(1);
</script>
<template>
<RangeInput v-model:from="from" v-model:to="to" />
</template>
10 Apr 2023 UPDATE: This was written before my knowledge of VueUse’s
useVModel()
composable. So please check out this page if you are already using@vueuse/core
library in your project. That being said, my version is still a little more concise than that from@vueuse/core
.