4

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.

Karakayn
  • 159
  • 1
  • 4
  • 16
  • `v-model` is shorthand for [v-bind:value and v-on:input](https://vuejs.org/v2/guide/forms.html) - contenteditable p's don't have a value. You might need to use a watcher or a computed value instead. Some more information on this can be found in [this stackoverflow question](https://stackoverflow.com/questions/42260233/vue-js-difference-between-v-model-and-v-bind). – Lewis Jul 18 '19 at 09:25
  • It has the value from component, it may be strange but this is how it work with contenteditable. It comes from here :https://stackoverflow.com/a/53899854/8237280 – Karakayn Jul 18 '19 at 09:33
  • I tried to use a computed properties with getters and setters but it didn't work and finished in a stackoverflow. I use now a watcher, but it is not perfect for now, because the value is updated at each input, and the cursor come back to position 0 (so you write in the wrong direction...) I am investigating... Feel free to add an exemple / code snippet with your anwser :) – Karakayn Jul 18 '19 at 15:25

2 Answers2

0

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>
Karakayn
  • 159
  • 1
  • 4
  • 16
0

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>
  • I didn't think to that ! It could work but in my real app I can't use ```this.$root.$children[0]``` but thank for sharing – Karakayn Jul 25 '19 at 08:29