Custom Component v-model attribute with Vue 3
The core of VueJS and why it’s so awesome is the ease of making reactive interfaces. To watch input on an element you can use the v-model
attribute. This attribute will automatically update when the user changes a value. However, when you start to create more advanced VueJS functionality, you will want to abstract that functionality into components that you can use throughout the app. You also want these complex components to be able to accept a v-model
attribute and react to what the user enters.
In this tutorial, we will create a component that searches our transaction categories in Financial Freedom. This component will contain a decent amount of logic that makes sense to abstract into a re-usable piece that we will use in multiple places. When the user selects a category through a variety of methods, we will update the v-model
attribute. This allows us to access the data from a parent component.
v-model
Documentation
While this functionality is fully documented on VueJS Documentation, I wanted to make a more consumable version with an example. I feel this is a super powerful way to make re-usable elements for your app or as open source projects. Hopefully this quick tutorial helps a little bit!
What We Are Building
Just for a proof of concept, we are creating a separate VueJS component called CategorySelect.vue
. In this component, we will be allowing the user to search through a list of provided categories, autocomplete the results that match, and select a category. These categories will be for financial transactions, such as “Groceries”, “Gas”, “Rent”.
The reason we abstracted this functionality is so we can re-use this CategorySelect.vue
component anywhere we import or add a transaction. This means that we need to allow v-model
to bind to what the user selects.
If you are like me, and just want to see the final code, see below. Otherwise keep reading and I’ll step you through the process. In the example we are also using InertiaJS. You will see reference to this.$page.props.categories
which is a global array of the categories present in our app.
CategorySelect.vue
<template>
<div class="relative">
<input type="text"
@focus="prepareInput()"
@keypress="filter()"
@keyup.down="incrementActiveIndex()"
@keyup.up="decrementActiveIndex()"
@keydown.enter.prevent="selectCategory()"
v-model="search"
class="flex-1 min-w-0 block w-full px-3 py-2 rounded-md border focus:ring-blue-500 focus:border-blue-500 sm:text-sm border-gray-300"/>
<div v-if="showResults" class="absolute max-h-60 w-96 overflow-y-scroll bg-white z-50 shadow-sm rounded-md border border-gray-300">
<div class="p-2 group cursor-pointer flex items-center hover:bg-blue-600 hover:text-white"
v-for="( category, categoryIdx ) in filteredCategories"
v-bind:key="category.id"
v-bind:class="{
'bg-blue-600': activeIndex == categoryIdx,
'text-white': activeIndex == categoryIdx
}"
@click="selectCategory( categoryIdx )">
<svg v-show="category.parent_id"
v-bind:class="{
'fill-white': activeIndex == categoryIdx,
'fill-gray-500': activeIndex != categoryIdx
}"
class="ml-5 mr-1 group-hover:fill-white" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="16" height="16" viewBox="0 0 32 32"><path d="M 5 5 L 5 22 L 24.0625 22 L 19.78125 26.28125 L 21.21875 27.71875 L 27.21875 21.71875 L 27.90625 21 L 27.21875 20.28125 L 21.21875 14.28125 L 19.78125 15.71875 L 24.0625 20 L 7 20 L 7 5 Z"></path></svg>
{{ category.name }}
</div>
</div>
</div>
</template>
<script>
export default {
props: {
modelValue: String
},
emits: ['update:modelValue'],
data(){
return {
search: '',
showResults: false,
filteredCategories: [],
activeIndex: 0,
selectedCategory: {}
}
},
mounted(){
this.filteredCategories = this.$page.props.categories
},
watch: {
search(){
// Handles deletion from the field
if( this.search == '' ){
this.selectedCategory = {};
this.filter();
}
}
},
methods: {
selectCategory( index = null ){
if( !index ){
this.selectedCategory = this.filteredCategories[ this.activeIndex ];
}else{
this.selectedCategory = this.filteredCategories[ index ];
}
this.search = this.selectedCategory.name;
this.filteredCategories = [];
this.activeIndex = 0;
this.showResults = false;
this.$emit('update:modelValue', this.selectedCategory.id);
},
prepareInput(){
this.activeIndex = 0;
this.showResults = true;
this.search = '';
this.filter();
this.$emit('update:modelValue', "");
},
filter(){
this.activeIndex = 0;
if( this.search != '' ){
this.filteredCategories = this.filteredCategories.filter( function( category ){
let name = category.name.toLowerCase();
return name.search( this.search.toLowerCase() ) > -1;
}.bind(this));
}else{
this.filteredCategories = this.$page.props.categories;
}
},
incrementActiveIndex(){
if( this.activeIndex + 1 < this.filteredCategories.length ){
this.activeIndex = this.activeIndex + 1;
}
},
decrementActiveIndex(){
if( this.activeIndex - 1 >= 0 ){
this.activeIndex--;
}
},
}
}
</script>
Using The Component
<category-select v-model="transaction.category"/>
Step 1: Add modelValue
Prop to Component
There are only three steps to make allow for v-model
usage on a custom component. Step 1 is to add the modelValue
prop the type of a String
to your component:
props: {
modelValue: String
},
This will allow you to accept the v-model
attribute on your custom component.
Step 2: Register The Events Emitted By Your Component
Next, add the following property:
emits: ['update:modelValue'],
This property registers what events are emitted and listened to by the parent component. When the property we want to set as the model changes, we will emit the update:modelValue
event.
Step 3: Emit Event When Value Updates
Finally, whenever we update the value, in this case when the user selects the appropriate category, we need to emit the update:modelValue
event:
this.$emit('update:modelValue', this.selectedCategory.id);
This event will update the model property so the parent can two-way sync and read the value of the component. Now you can encapsulate complex code and logic into reusuable components and directly bind the value from a parent component.
Conclusion
Even though this is a smaller tutorial, it’s extremely helpful to order to break out functionality into smaller parts. Especially with complex form fields and selectors. Let me know if you have any questions by leaving a comment or reaching out to me on Twitter (@danpastori).