Vue – v-model with two-way data binding in custom component

Vue offers very nice binding and events system. We can create component, set some data using props, and also emit events from that component. If you read official Vue documentation, you can see examples for some standard form inputs. These examples using v-model and allows two-way binding without any additional code and without creating methods for events in parents – of course, we can still watch data from v-model. Can we use v-model way also in our custom components? Yes, it’s possible and very simple to do. See some examples.

Problems with packages

I work with some custom components, that uses external libraries. For example, use Cleave.js for currency/numbers formatting in inputs or one of available datepickers to set dates. Both from NPM, both offer their specific components. It’s ok, but in many cases, we don’t want to use “pure” package components, but also made some operations for our application. If it’s only on one place, no problem, we can use component directly and set all data and handle all events. Advantage: most of components available on NPM offers v-model two-way binding. You don’t have to create any methods for events.

<!-- PARENT COMPONENT -->
<template>
	<div>
		<!-- SOME ELEMENTS -->

		<!-- v-model, you dont have to use event to get modified parentData -->
		<custom-component-from-npm v-model="parentData"></custom-component-from-npm>
	</div>
</template>

<script>
	import CustomComponentFromNpm from 'npm-component'

    export default {
        components: {
            CustomComponentFromNpm 
        },
        data () {
        	// Will be always in good state
        	parentData: null
        }
    }
</script>

If we want to use it on many places with some additional options specific for our application, it isn’t good approach. Better option is to create custom, “middle” component and then set props to this component and move to “original” component from package. With such approach, we can modify data before we send it to original component and also modify before we receive this data on parent. Examples from previous paragraph: I can cast my default value to float, and also return float from Cleave. I can also always use one time format in application, but prepare data before I display it in datepicker. It’s the best option, because it’s very elastic. But… if we do that, we will lose v-model option, yep? Not exactly, we can still use it, but must prepare our middle component for this.

Solution: emit input

The most important thing is to emit input event with modified data. And it’s the only one requirement, really. See an example of simple middle component. I put original data in props, then modify data and send to child component. When child modified data, I also do it and send emit. Parent can then use v-model and will not see all this internal logic. For parent side, we just set variable for two-way data binding, nothing else. Very simple and very, very clear:

<!-- MIDDLE COMPONENT -->
<template>
	<custom-component-from-npm v-model="value"></custom-component-from-npm>
</template>

<script>
	import CustomComponentFromNpm from 'npm-component'

    export default {
        components: {
            CustomComponentFromNpm 
        },
        props: {
        	// It's v-model from parent
            value: {
                type: Number,
                required: true
            }
            // Additional options from parent, to control middle component
            options: {
            	type: Object,
            	required: false
            }
        },
        data () {
        	someDefaultOptions: {
        		// Global options for this component
        	}
        },
        // Let's say, we must multiply value from parent by 100
        created () {
        	this.value = value * 100
        }
        watch (value) {
        	// We want to always emit number
        	value = parseFloat(value)
        	if (typeof value !== 'number' || Number.isNaN(value)) {
        		value = 0
        	} else {
        		value = value / 100
        	}
        	// This causes, that v-model on parent will work!
 			this.$emit('input', this.value)
        }
    }
</script>

Of course, you can use this solution also on other custom components, not only for “middle” usage, but even if you want to use v-model on parent. I think we should use such approach always if it’s possible. With that, our custom components are isolated, do only few things and also parent doesn’t have to observe many events. If you have any observations or ideas, just add comment!