Home > Mobile >  Each change on state destroys user input if one-way bound
Each change on state destroys user input if one-way bound

Time:11-03

Consider a simple clock display and an input which I bound one-way to keep control over old/new state:

<div>{{ time }}</div>
<input ref="text" type="text" :value="text" style="width:95%">
<button type="button" @click="saveOnDiff">Save</button>
createApp({
  ...,
  methods: {
    saveOnDiff() {
      const current = this.$refs.text.value;
      // Compare current with old text   update if it changed.
      ...
    }
  },
  mounted() {
    const instance = this;
    setInterval(() => instance.time = new Date(), 1000);
  }
}).mount('#app');

The clock is updated each second. Unfortunately, this update spoils the input. Try it here: https://jsfiddle.net/dL78tsh9

How can I reduce binding updates to the absolute necessary ones? Some extra switch on one-way bindings like :value.lazy="text" would be helpful...

CodePudding user response:

Changing time on each and every second will cause the whole template to be re-run after every 1 second. Which results everything in that template getting updated.

When a user types into <input> element, You aren't storing that value anywhere. You've got a :value to poke the value in but you aren't updating it when the value changes. The result will be that when Vue re-renders everything it will jump back to its original value.

Possible solution : Kindly ensure that your data is kept in sync with what the user types in. This could be done using v-model and watcher to get new and old values and based on that you can achieve this requirmeent.

You can try something like this (This is not a perfect solution but it will give you an idea) :

const {
  createApp
} = Vue

const characterWiseDiff = (left, right) => right
  .split("")
  .filter(function(character, index) {
    return character != left.charAt(index);
  })
  .join("");


createApp({
  data() {
    return {
      result: "",
      text: "Try to change me here",
      time: new Date(),
      oldVal: null
    }
  },
  watch: {
    text(newVal, oldVal) {
      this.oldVal = oldVal;
    }
  },
  methods: {
    saveOnDiff() {
      if (!this.oldVal) this.oldVal = this.text
      const current = this.$refs.text.value;
      console.log(current, this.oldVal)
      if (current === this.oldVal) {
        this.result = "No changes have been made!";
      } else {
        this.result = `Saved! Your changes were: "${characterWiseDiff(current, this.oldVal)}"`;
      }
    }
  },
  mounted() {
    const instance = this;
    setInterval(() => instance.time = new Date(), 1000);
  }
}).mount('#app');
<script src="https://unpkg.com/[email protected]/dist/vue.global.js"></script>
<div id="app">

  <div>{{ time }}</div>
  <input ref="text" type="text" v-model="text" style="width:95%">
  <button type="button" @click="saveOnDiff">Save</button>
  {{ result }}

</div>

CodePudding user response:

As far as I know, there's no way to trick VueJs to not re-render a specific field.

When the time changes, your existing virtual DOM has a value for "text" and the newly generated virtual DOM has a different value so... VueJS re renders it.

UPDATE:

Based on @Tolbxela comment, looks like you could use v-once to only render the field once, and ignore the future DOM updates.

https://vuejs.org/api/built-in-directives.html#v-once

Alternative

If you want to control old/new state, why don't you just use two-way binding and save both states?

Something like this:

const {
  createApp
} = Vue

const characterWiseDiff = (left, right) => right
  .split("")
  .filter(function(character, index) {
    return character != left.charAt(index);
  })
  .join("");


createApp({
  data() {
    return {
      result: "",
      text: "Try to change me here",
      previousText: "Try to change me here",
      time: new Date(),
    }
  },
  methods: {
    saveOnDiff() {
      if (this.text === this.previousText) {
        this.result = "No changes have been made!";
      } else {
        this.result = `Saved! Your changes were: "${characterWiseDiff(this.previousText, this.text)}"`;
        this.previousText = this.text;
      }
    }
  },
  mounted() {
    const instance = this;
    setInterval(() => instance.time = new Date(), 1000);
  }
}).mount('#app');
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">

  <div>{{ time }}</div>
  <div>
    <input id="mockId" ref="text" type="text" v-model="text" style="width:95%">
    <button type="button" @click="saveOnDiff">Save</button>
  </div>
  {{ result }}

</div>

https://jsfiddle.net/dmxLuf9w/6/

CodePudding user response:

The best solution is to not use the Vue reactivity for timer updates at all. See my UPDATE 2 below.

The simplest way to fix it is to replace :value with v-model

UPDATE 1:

We need an other data field to store the input value.

<input ref="text" type="text" v-model="input" style="width:95%">

Check the sample below.

But it is not a good solution for complex apps, since every second you whole app HTML is refreshed. This can cause problems with rendering and lags.

UPDATE 1:

I have missed the other logic of comparing values. Here is the well working solution

UPDATE 2:

This question helped me to understand the whole problem with template rendering in Vue.

TransitionGroup lag when using requestAnimationFrame

And here is a good article about Improve Vue Performance with v-once v-memo

CODE:

const placeholder = "Try to change me here"

const {
  createApp
} = Vue

const characterWiseDiff = (left, right) => right
  .split("")
  .filter(function(character, index) {
    return character != left.charAt(index);
  })
  .join("");


createApp({
  data() {
    return {
      result: "",
      text: placeholder,
      input: placeholder,
      time: new Date(),
    }
  },
  methods: {
    saveOnDiff() {
      const current = this.input
      if (current === this.text) {
        this.result = "No changes have been made!";
      } else {
        this.result = `Saved! Your changes were: "${characterWiseDiff(this.text, current)}"`;
        this.text = current;
      }
    }
  },
  mounted() {
    const instance = this;
    setInterval(() => instance.time = new Date(), 1000);
  }
}).mount('#app');
<div id="app">

  <div>{{ time }}</div>
  <input ref="text" type="text" v-model="input" style="width:95%">
  <button type="button" @click="saveOnDiff">Save</button>
  {{ result }}
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

  • Related