VueJS : update input value without losing cursor position

07 Nov 2018

In my recent project I came across a requirement which need to format the input value while typing. More preceisly I need to format the number into comma seperated format while the user types. In the first glance it seems to be easy, but when we tried one specific issue caught us.

The issue is when you format the value in input the cursor jumps to the end of input which gives bad experience for the user.

demo cursor jump issue

This blog post will explain how we solved it and gave our users better experience. Since the blog post is regarding cursor postion, we won’t go into the details on formatting the input value.

Capture current cursor position

To start first we will captiure the current position of cursor on every input change and keep this in state.

<template>
  <input
    :value="formatedValue"
    @input="handleInput"
  />
</template>


<script>

import formatNumber from 'accounting-js/lib/formatNumber';
import unformat from 'accounting-js/lib/unformat';

export default {
  name: "CommaFormattedNumber",
  props: {
    value: {
      type: String,
      default: "",
      required: true,
    }
  },
  data() {
    return {
      formatedValue: this.processFormatting(this.value),
      position: 0,
    };
  },
  watch: {
    value() {
      this.formatedValue = this.processFormatting(this.value);
    }
  },
  methods: {
    handleInput(e) {
      this.prevValue = e.target.value;
      let targetValue = unformat(e.target.value);
      this.position = e.target.selectionStart;
      this.formatedValue = formatNumber(targetValue)
      this.$emit("input", this.formatedValue);
    },
    processFormatting(value) {
        // process formatting
    }
  }
};
</script>

The CommaFormattedNumber component will accept the value as prop, format as comma seperated and render in input. On input change we will get the cursor position using e.target.selectionStart and seti it in the state.

Using custom directives

Now we have the current position of the cursor in the state, Next we need to set the cursor postion on input using selectionEnd after the VNode update. This can be achieved using custom directives in VueJS.

The VueJs directives have update hook function which we use for this. But there is a catch. we can’t access the this object inside the update. It will receive the element which is updated as the first argument. Since there is no this we can’t get the this.position in update. To by-pass this we decided to set the position as data attribute to input element.

<template>
  <input
    :value="formatedValue"
    @input="handleInput"
    :data-position="position"
  />
</template>

Now we have the position of cursor available inside the update method and can be accessed as e.dataset.position.

<script>

import formatNumber from 'accounting-js/lib/formatNumber';
import unformat from 'accounting-js/lib/unformat';

export default {
  name: "CommaFormattedNumber",
  props: {
    value: {
      type: String,
      default: "",
      required: true,
    }
  },
  data() {
    return {
      formatedValue: this.processFormatting(this.value),
      position: 0,
    };
  },
  directives: {
    formatWithComma: {
      update(e) {
        if (e.selectionEnd !== e.dataset.position) {
          e.selectionEnd = Number(e.dataset.position);
        }
      }
    }
  },
  // other methods and watch
};
</script>

This will give basic fix, but needed some corner case handling etc which I skipped here. The full code is available on codesandbox

demo cursor jump fixed

Edit Vue Template

This is now published as a node module vue-comma-formatted-number

If you find my work helpful, You can buy me a coffee.