Browsers give events different execution priority. Here’s our take at using Vue’s custom events on input components.
tldr: Here’s a fiddle.
That sounds trivial, but trying to catch the native event in an app execution can be a pain, especially when dealing with user interaction on input fields.
Here’s how we used Vue.js reactivity handle the change on a child component input and sync the data with its parent using a custom watcher.
//app structure: outerData is synced via v-model <div id="app"> <checkyb v-model="outerData" label="inner data"></checkyb> <div> Outer data: {{outerData}} </div> </div>
// the component instance Vue.component('checkyb', { props:['label'], data: function(){ return { innerData: [] } }, watch: { innerData: function(){ //when this data changes emit the value to the parent this.$emit('input', this.innerData) } }, template: ' <div> <label> <input type="checkbox" id="check1" v-model="innerData" :value="1"> <input type="checkbox" id="check2" v-model="innerData" :value="2"> {{ label }}</label> <span>{{innerData}}</span></div>', })
Finally create the Vue instance:
new Vue({ el: '#app', data: { outerData: [], }, watch: { outerData: function(){ //an user has interacted with our data console.log('data has changed a query will start') } } })
Background story
When a client commissioned us a single page application that will have to handle multiple input affecting the page behavior real time, we were faced with a tough choice: tackle the problem with JS+ JQuery or try a more modern front-end framework.
Vue is a promising rising stars into the vast javascript front end frameworks echo system and offers a solid MVVM control of the page, so the decision came naturally.
We decided to set up a parent component that holds all the different information, and then have some child components that handles the user interaction.
This way we delegate the parent to handle the communication between child-user interaction and the back-end server.
// a visualized idea of how the app is composed. -Parent: dataA dataB dataC --ChildA: handles dataA --ChildB: handles dataB --ChildC: handles dataC
Turns out that Vue lets you use v-model
on custom input.
Our first Approach looked something like this fiddle.
//html <div id="app"> <checkyb v-model="outerData" :outerData="outerData" label="inner data"> </checkyb> <div> Outer data: {{outerData}} </div> </div>
//Vue js Vue.component('checkyb', { props: { label: String, value: null, outerData: Array }, data: function(){ return { innerData: [] } }, methods: { emitValue(){ // this won't get triggered either in Safari/Firefox this.$emit('input', this.innerData); } }, template: ' <div> <label> <input v-model="innerData" value=1 type="checkbox" @click="emitValue()"> <input v-model="innerData" value=2 type="checkbox" @change="emitValue()"> {{ label }} </label> <span>{{innerData}}</span> </div>', }) new Vue({ data: { outerData: [], }, watch: { // when outerData changes a function will fire outerData: function(){ console.log('data has changed a query will start') } } }).$mount('#app')
What’s this?
If you are not familiar with Vue don’t worry: playing around with the fiddle will makes everything clearer.
The input has its own set of value innerData
that thanks to v-model="innerData"
will stay in sync.
Then on a DOM event on the input call the method emitValue()
to emit the new set of values to the parent that, thanks to v-model="outerData"
will “pick up” the event and change its data accordingly.
This way, in theory, we can delegate the input to performs different actions if needed, and when ready emit the data to the parent.
The problem.
Unfortunately this arise a problem: different browsers prioritize events differently.
That’s why one input listen for click
and the other for change
; so we can clearly see the difference.
If you try it into different browsers you’ll see that the behavior change.
So how can we expect consistency among different browser when events are picked up in different orders?
Our solution.
We decided to let Vue’s reactivity handle the functionality, avoiding entirely the usage of click
or change.
Vue allows the usage of computed properties, as well as watchers, so we can set one on the property that the v-model
is watching and let it emit that an input has changed.
Vue.component('checkyb', { props:['label'], data: function(){ return { innerData: [] } }, watch: { innerData: function(){ this.$emit('input', this.innerData) } }, template: ' <div> <label> <input type="checkbox" id="check1" v-model="innerData" :value="1"> <input type="checkbox" id="check2" v-model="innerData" :value="2"> {{ label }}</label> <span>{{innerData}}</span></div>', })
You can see and play around with our solution on this fiddle.
If you have any questions, comments or have a more elegant solution please let us know in the comment.
Happy coding.