Question3r
Question3r

Reputation: 3802

text input only takes one digit or multiple identical digits

I'm using Vue.js and would like to create a input field component that only takes positive integers (0 is fine too). I would like to avoid using regex to keep it human readable :)

My consuming parent component passes in the current value and uses the shorthand sync directive

<template>
  <div>
    <form>
      <positive-integer-input :value.sync="currentValue" />
    </form>
  </div>
</template>

<script>
import PositiveIntegerInput from "../components/PositiveIntegerInput.vue";

export default {
  components: {
    "positive-integer-input": PositiveIntegerInput
  },
  data() {
    return {
      currentValue: 0
    };
  }
};
</script> 

The input component itself is just a basic text input (for better CSS styling)

<template>
  <input :id="_uid" type="text" :value="value" @input="onInput" />
</template>

<script>
export default {
  props: {
    value: {
      type: Number,
      required: true
    }
  },
  methods: {
    onInput({ data }) {
      const inputNumber = Number(data);

      if (!Number.isInteger(inputNumber) || inputNumber < 0) {
        const input = document.getElementById(this._uid);
        input.value = this.value;
      } else {
        this.$emit("update:value", inputNumber);
      }
    }
  }
};
</script>

Whenever the input changes I'm validating the incoming input for positive integers. If that validation fails I want to reset the current input field content (maybe there is a more userfriendly solution?)

The input field itself only takes one digit or multiple identical digits. So only these are possible:

but not:

Does someone know how to fix the component?

Thanks in advance

Upvotes: 1

Views: 344

Answers (3)

Question3r
Question3r

Reputation: 3802

For the sake of completeness I tried to improve Skirtles answer. So the component consumer remains the same, only the input component changes a little bit. Now it's possible to leave the input empty but the value won't trigger an update then. Also the input field adds some visual styles based on the validation.

<template>
  <input
    type="text"
    :value="value"
    @input="onInput"
    :class="[inputValueIsValid ? 'valid-input' : 'invalid-input']"
  />
</template>

<script>
export default {
  props: {
    value: {
      type: Number,
      required: true
    }
  },
  data() {
    return {
      digitsOnlyRegex: /^\d*$/,
      inputValueIsValid: false
    };
  },
  mounted() {
    this.validateInputValue(this.value);
  },
  methods: {
    onInput(inputEvent) {
      const currentInputValue = inputEvent.target.value;
      this.validateInputValue(currentInputValue);

      if (this.inputValueIsValid) {
        const inputNumber = parseInt(currentInputValue, 10);
        this.$emit("update:value", inputNumber);
      }
    },
    validateInputValue(currentInputValue) {
      const isNotEmpty = currentInputValue.toString().length > 0;
      const containsDigitsOnly = this.digitsOnlyRegex.test(currentInputValue);
      this.inputValueIsValid = isNotEmpty && containsDigitsOnly;
    }
  }
};
</script>

<style scoped>
.valid-input {
  background: green;
}

.invalid-input {
  background: red;
}
</style>

Upvotes: 0

skirtle
skirtle

Reputation: 29122

I'm still not entirely sure I understand the requirements but I think this is somewhere close to what you're looking for:

const PositiveIntegerInput = {
  template: `
    <input
      ref="input"
      type="text"
      :value="value"
      @input="onInput"
    >  
  `,

  props: {
    value: {
      type: Number,
      required: true
    }
  },

  methods: {
    onInput(ev) {
      const value = ev.target.value;
      const inputNumber = parseInt(value, 10);

      if (inputNumber >= 0 && String(inputNumber) === value) {
        this.$emit('update:value', inputNumber);
      } else {
        // Reset the input if validation failed
        this.$refs.input.value = this.value;
      }
    }
  }
};

new Vue({
  el: '#app',
  
  components: {
    PositiveIntegerInput
  },
  
  data () {
    return {
      currentValue: 0
    }
  }
})
<script src="https://unpkg.com/[email protected]/dist/vue.js"></script>

<div id="app">
  <div>
    <form>
      <positive-integer-input :value.sync="currentValue" />
    </form>
  </div>
</div>

I've used $refs instead of document.getElementById to perform the reset.

To check the input is an integer I've simply converted it to an integer using parseInt, then converted it back to a string and compared that to the original string.

There are several edge cases that could cause problems. A particularly obvious one is that the input can never be empty, which makes changing an existing value quite difficult. However, as far as I can tell, this does meet the requirements given in the question.

Update:

A few notes on number parsing.

Using Number(value) will return NaN if it can't parse the entire string. There are some quirks with using Number too. e.g. Try Number('123e4'), Number('0x123') or Number('0b11').

Using parseInt(value, 10) will parse as many characters as it can and then stop. parseInt also has some quirks but by explicitly specifying a radix of 10 they mostly go away.

If you try to parse '53....eeöööööö' using parseInt you'll get 53. This is an integer, so it'll pass a Number.isInteger test.

Personally I'd like to have used a RegExp to ensure the string only contains digits 0-9 before doing any further manipulation. That quickly helps to eliminate a whole load of possible edge cases such that they wouldn't need any further consideration. Something like /^\d*$/.test(value), or the converse !/\D/.test(value).

Upvotes: 1

webprogrammer
webprogrammer

Reputation: 2477

Your problem is here:

const input = document.getElementById(this._uid);
input.value = this.value;

Use v-model instead or try to replace code by this:

<template>
  <input :id="_uid" type="text" :value="inputValue" @input="onInput($event.target.value, $event)" />
</template>

<script>
export default {
  data(){
    return inputValue: 0,
  },
  props: {
    value: {
      type: Number,
      required: true
    }
  },
  created(){
    this.inputValue = this.value;
  },
  methods: {
    onInput(value, event) {
      const inputNumber = Number(value);

      if (!Number.isInteger(inputNumber) || inputNumber < 0) {
        this.inputValue = this.value;
      } else {
        this.$emit("update:value", inputNumber);
      }
    }
  }
};
</script>

Upvotes: 0

Related Questions