简体   繁体   中英

V-model is not listening to value change for an input (vuejs)

I have an object property which could listen to the user input or could be changed by the view. With the snipped below :

  • if I typed something the value of my input is updated and widget.Title.Name is updated.
  • if I click on the button "External Update", the property widget.Title.Name is updated but not the value in my field above.

Expected result : value of editable text need to be updated at the same time when widget.Title.Name change.

I don't understand why there are not updated, if I inspect my property in vue inspector, all my fields ( widget.Title.Name and Value ) are correctly updated, but the html is not updated.

 Vue.component('editable-text', { template: '#editable-text-template', props: { value: { type: String, default: '', }, contenteditable: { type: Boolean, default: true, }, }, computed: { listeners() { return { ...this.$listeners, input: this.onInput }; }, }, mounted() { this.$refs["editable-text"].innerText = this.value; }, methods: { onInput(e) { this.$emit('input', e.target.innerText); } } }); var vm = new Vue({ el: '#app', data: { widget: { Title: { Name: '' } } }, async created() { this.widget.Title.Name = "toto" }, methods: { externalChange: function () { this.widget.Title.Name = "changed title"; }, } }) 
 button{ height:50px; width:100px; } 
 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script> <div id="app"> <editable-text v-model="widget.Title.Name"></editable-text> <template>Name : {{widget.Title.Name}}</template> <br> <br> <button v-on:click="externalChange">External update</button> </div> <template id="editable-text-template"> <p ref="editable-text" v-bind:contenteditable="contenteditable" v-on="listeners"> </p> </template> 

I searched a lot of subject about similar issues but they had reactivity problem, I think I have a specific problem with input. Have you any idea of what's going on ? I tried to add a listener to change event but it was not triggered on widget.Title.Name change.

To anwser to this problem, you need to do 3 differents things.

  1. Add watch property with the same name as your prop (here value)
  2. Add debounce function from Lodash to limit the number of request
  3. Add a function to get back the cursor (caret position) at the good position when the user is typing

For the third point : when you change the value of widget.Title.Name , the component will re-render, and the caret position will be reinitialize to 0, at the beginning of your input. So, you need to re-update it at the last position or you will just write from right to left.

I have updated the snippet above with my final solution. I hope this will help other people coming here.

 Vue.component('editable-text', { template: '#editable-text-template', props: { value: { type: String, default: '', }, contenteditable: { type: Boolean, default: true, }, }, //Added watch value to watch external change <-> enter here by user input or when component or vue change the watched property watch: { value: function (newVal, oldVal) { // watch it // _.debounce is a function provided by lodash to limit how // often a particularly expensive operation can be run. // In this case, we want to limit how often we update the dom // we are waiting for the user finishing typing his text const debouncedFunction = _.debounce(() => { this.UpdateDOMValue(); }, 1000); //here your declare your function debouncedFunction(); //here you call it //not you can also add a third argument to your debounced function to wait for user to finish typing, but I don't really now how it works and I didn't used it. } }, computed: { listeners() { return { ...this.$listeners, input: this.onInput }; }, }, mounted() { this.$refs["editable-text"].innerText = this.value; }, methods: { onInput(e) { this.$emit('input', e.target.innerText); }, UpdateDOMValue: function () { // Get caret position if (window.getSelection().rangeCount == 0) { //this changed is made by our request and not by the user, we //don't have to move the cursor this.$refs["editable-text"].innerText = this.value; } else { let selection = window.getSelection(); let index = selection.getRangeAt(0).startOffset; //with this line all the input will be remplaced, so the cursor of the input will go to the //beginning... and you will write right to left.... this.$refs["editable-text"].innerText = this.value; //so we need this line to get back the cursor at the least position setCaretPosition(this.$refs["editable-text"], index); } } } }); var vm = new Vue({ el: '#app', data: { widget: { Title: { Name: '' } } }, async created() { this.widget.Title.Name = "toto" }, methods: { externalChange: function () { this.widget.Title.Name = "changed title"; }, } }) /** * Set caret position in a div (cursor position) * Tested in contenteditable div * @@param el : js selector to your element * @@param caretPos : index : exemple 5 */ function setCaretPosition(el, caretPos) { var range = document.createRange(); var sel = window.getSelection(); if (caretPos > el.childNodes[0].length) { range.setStart(el.childNodes[0], el.childNodes[0].length); } else { range.setStart(el.childNodes[0], caretPos); } range.collapse(true); sel.removeAllRanges(); } 
 button{ height:50px; width:100px; } 
 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script> <div id="app"> <editable-text v-model="widget.Title.Name"></editable-text> <template>Name : {{widget.Title.Name}}</template> <br> <br> <button v-on:click="externalChange">External update</button> </div> <template id="editable-text-template"> <p ref="editable-text" v-bind:contenteditable="contenteditable" v-on="listeners"> </p> </template> 

you can use $root.$children[0]

 Vue.component('editable-text', { template: '#editable-text-template', props: { value: { type: String, default: '', }, contenteditable: { type: Boolean, default: true, }, }, computed: { listeners() { return {...this.$listeners, input: this.onInput }; }, }, mounted() { this.$refs["editable-text"].innerText = this.value; }, methods: { onInput(e) { this.$emit('input', e.target.innerText); } } }); var vm = new Vue({ el: '#app', data: { widget: { Title: { Name: '' } } }, async created() { this.widget.Title.Name = "toto" }, methods: { externalChange: function(e) { this.widget.Title.Name = "changed title"; this.$root.$children[0].$refs["editable-text"].innerText = "changed title"; }, } }) 
 <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <div id="app"> <editable-text v-model="widget.Title.Name"></editable-text> <template>Name : {{widget.Title.Name}}</template> <br> <br> <button v-on:click="externalChange">External update</button> </div> <template id="editable-text-template"> <p ref="editable-text" v-bind:contenteditable="contenteditable" v-on="listeners"> </p> </template> 

or use Passing props to root instances

 Vue.component('editable-text', { template: '#editable-text-template', props: { value: { type: String, default: '', }, contenteditable: { type: Boolean, default: true, }, }, computed: { listeners() { return {...this.$listeners, input: this.onInput }; }, }, mounted() { this.$refs["editable-text"].innerText = this.value; this.$root.$on("titleUpdated",(e)=>{ this.$refs["editable-text"].innerText = e; }) }, methods: { onInput(e) { this.$emit('input', e.target.innerText); } } }); var vm = new Vue({ el: '#app', data: { widget: { Title: { Name: '' } } }, async created() { this.widget.Title.Name = "toto" }, methods: { externalChange: function(e) { this.widget.Title.Name = "changed title"; this.$root.$emit("titleUpdated", this.widget.Title.Name); }, } }) 
 <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <div id="app"> <editable-text v-model="widget.Title.Name"></editable-text> <template>Name : {{widget.Title.Name}}</template> <br> <br> <button v-on:click="externalChange">External update</button> </div> <template id="editable-text-template"> <p ref="editable-text" v-bind:contenteditable="contenteditable" v-on="listeners"> </p> </template> 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM