baitendbidz
baitendbidz

Reputation: 825

How to create a numeric integer input using Vuetify?

Based on Is there specific number input component in Vuetify? I'm trying to create a numeric input.

The input and output value is unknown so it could be undefined or null because one might want to clear the field so it should not respond with 0.

The input component should not have "up"/"down" buttons if possible.

If the user passes in a flag isAcceptingFloatingPointNumbers = false this input should only accept integer values ( it should not be possible to type floats )

Reproduction link

<template>
  <v-app>
    <v-main>
      <v-text-field 
        type="number"
        label="number input" 
        :clearable="true"
        :model-value="num"
        @update:modelValue="num = $event"
      />
    </v-main>
  </v-app>
</template>

<script setup lang="ts">
import { ref, watch, Ref } from 'vue'

const num: Ref<unknown> = ref(undefined)

watch(num, () => console.log(num.value))
</script>

How can I make sure the user can only type integer values if the flag isAcceptingFloatingPointNumbers returns false? The only thing coming to my mind is to append a custom rule like

v => Number.isInteger(v) || 'Must be integer'

but AFAIK this rule would trigger even if the value could be undefined. Is there a way to prevent the user input instead?


Based on yoduh's answer I tried this ( reproduction link )

NumberField.vue

<template>
  <v-text-field 
    type="number"
    label="number input" 
    :clearable="true"
    :model-value="num"
    @update:modelValue="emit('update:modelValue', $event)"
    @keypress="filterInput"
  />
</template>

<script setup lang="ts">
const props = defineProps<{
  num: unknown;
  isAcceptingFloatingPointNumbers: boolean;
}>();

const emit = defineEmits<{
  (e: "update:modelValue", newValue: unknown): void;
}>();

function filterInput(inputEvent) {
  if(props.isAcceptingFloatingPointNumbers.value) {
    return true;
  }
  
  const inputAsString = inputEvent.target.value.toString() + inputEvent.key.toString();
  const inputValue = Number(inputAsString);
  
  if(!Number.isInteger(inputValue)) {
    inputEvent.preventDefault();
  }
  
  return true;
}
</script>

I'm consuming the component like so

<template>
  <number-field :num="num" :isAcceptingFloatingPointNumbers="false" @update:model-value="num = $event" />
</template>

<script setup lang="ts">
import { ref, watch } from 'vue'
import NumberField from "./NumberField.vue";

const num: Ref<unknown> = ref(undefined);
  
watch(num, () => console.log(num.value));
</script>

The problem is that my filter function is wrong. It's still possible to type "12.4" because the filter ignores "12." and then converts "12.4" to 124.

Does someone got any ideas how to fix this?

Upvotes: 2

Views: 2122

Answers (4)

Roh&#236;t J&#237;ndal
Roh&#236;t J&#237;ndal

Reputation: 27242

As per my understanding you have below requirments :

  • To prevent the user input based on the isAcceptingFloatingPointNumbers flag value (Only accept integers if flag is false else field should accept the floating numbers).
  • No up/down arrows in the input field.
  • Input field should accept the 0 value.

If my above understandings are correct, You can simply achieve this requirement by normal text field and on every keyup event, You can replace the input value with an empty string if it's not matched with passed valid regEx.

Live Demo :

const { ref } = Vue;
const { createVuetify } = Vuetify;
const vuetify = createVuetify();

let options = {
    setup: function () {
    let num = ref('');
    let isAcceptingFloatingPointNumbers = ref(false);
    
    const validateInput = () => {
      const numbersRegEx = !isAcceptingFloatingPointNumbers.value ? /[^-\d]/g : /[^-\d.]/g;
      num.value = num.value.replace(numbersRegEx, '');
    }

    return {
      num,
      validateInput
    };
  }
};

let app = Vue
.createApp(options)
.use(vuetify)
.mount('#app');
<script src="https://unpkg.com/vue@next/dist/vue.global.js"></script>
<script src="https://unpkg.com/@vuetify/nightly@3.1.1/dist/vuetify.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@vuetify/nightly@3.1.1/dist/vuetify.css"/>

<div id="app">
  <v-text-field label="Numper Input" v-model="num" v-on:keyup="validateInput"></v-text-field>
</div>

Upvotes: 2

Igor Moraru
Igor Moraru

Reputation: 7739

Since an integer is made only of digits, you can test only if each pressed key is a digit, no need to check the whole input value.

function filterInput(inputEvent) {
  
  if(props.isAcceptingFloatingPointNumbers.value) {
    return true;
  }

  if(!inputEvent.target.value.length && inputEvent.key === '-'){
    return true;
  }
  
  if(!Number.isInteger(Number(inputEvent.key))) {
    // Of course, you can choose any other method to check if the key 
    // pressed was a number key, for ex. check if the event.keyCode is 
    // in range 48-57.
    inputEvent.preventDefault();
  }
  
  return true;
}

Concerning the arrows, it is not a Vuetify specific element, but elements added by the browser to inputs of type number. You can disable them like this.

Upvotes: 3

Neha Soni
Neha Soni

Reputation: 4704

As per your comment on @yoduh's answer, if you want to stick with type="number" (good to reduce the step to validate the non-numeric characters), then hide the arrows using following CSS-

/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

/* Firefox */
input[type=number] {
  -moz-appearance: textfield;
}

Logic 1-

On the keyup event, check if isAcceptingFloatingPointNumbers is false and the typed input is not an integer, empty the input field's value. To check if the input value is an integer or not-

  1. You can use a regex pattern, /^-?[0-9]+$/.test(num).
  2. You can use the JS method Number.isInteger(num).

Though, in the second method the input value will always be of type string (why?). To resolve this, use the built-in Vue.js directive v-model.number to recast the input value's type to a number.

Demo-

const { ref } = Vue;
const { createVuetify } = Vuetify;
const vuetify = createVuetify();

let options = {
  setup: function() {
    let num = ref(null);
    let error = ref('');
    let isAcceptingFloatingPointNumbers = ref(false);
    const validateInput = () => {
      // If floats not allowed and input is not a integer, clean it.
      if (
        !isAcceptingFloatingPointNumbers.value &&
        !Number.isInteger(num.value)
      ) {
        num.value = null;
        error.value = "Only integers are allowed."
      } else {
        error.value = '';
      }
    };
    return {
      num,
      error,
      validateInput,
    };
  },
};

let app = Vue.createApp(options)
  .use(vuetify)
  .mount("#app");
.error {
  color: red;
}
<script src="https://unpkg.com/vue@next/dist/vue.global.js"></script>
<script src="https://unpkg.com/@vuetify/nightly@3.1.1/dist/vuetify.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@vuetify/nightly@3.1.1/dist/vuetify.css"/>
<div id="app">
  <v-text-field 
    type="number"
    label="number input" 
    :clearable="true"
    v-model.number="num"
    @keyup="validateInput"
    >
  </v-text-field>
  <label class="error">{{ error }}</label>
</div>

The only glitch here is if the user types 123. and stops typing then the dot will be visible because of the type="number" but if you use this value, it will always be decoded as 123.
If you want to restrict the typing of the dot, detect the key on the keypress event and prevent further execution.

EDIT------------------

Logic 2

If a user tries to input the float number, you can return the integer part of that floating-point number by removing the fractional digits using Math.trunc(num) method.

Demo-

const { ref } = Vue;
const { createVuetify } = Vuetify;
const vuetify = createVuetify();

let options = {
  setup: function() {
    let num = ref(null);
    let error = ref('');
    let isAcceptingFloatingPointNumbers = ref(false);
    const validateInput = () => {
      if (!isAcceptingFloatingPointNumbers.value && !Number.isInteger(num.value)) {
        error.value = "Only integer is allowed.";
        // Keep only integer part.
        num.value = Math.trunc(num.value);
      } else {
        error.value = ''
      }

    };
    return {
      num,
      error,
      validateInput,
    };
  },
};

let app = Vue.createApp(options)
  .use(vuetify)
  .mount("#app");
.error {
  color: red;
}
<script src="https://unpkg.com/vue@next/dist/vue.global.js"></script>
<script src="https://unpkg.com/@vuetify/nightly@3.1.1/dist/vuetify.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@vuetify/nightly@3.1.1/dist/vuetify.css"/>
<div id="app">
  <v-text-field 
    type="number"
    label="number input" 
    :clearable="true"
    v-model.number="num"
    @keyup="validateInput"
    >
  </v-text-field>
  <label class="error">{{ error }}</label>
</div>

Upvotes: 1

yoduh
yoduh

Reputation: 14739

I think the best way would be to create a custom filter function that runs on keypress. With your own custom filter you can also remove the type="number" since it's no longer necessary and will remove the up/down arrows on the input.

<v-text-field 
        label="number input" 
        :clearable="true"
        :model-value="num"
        @update:modelValue="num = $event"
        @keypress="filter(event)"
      />
const filter = (e) => {
  e = (e) ? e : window.event;
  const input = e.target.value.toString() + e.key.toString();

  if (!/^[0-9]*$/.test(input)) {
    e.preventDefault();
  } else {
    return true;
  }
}

updated sandbox

Upvotes: 1

Related Questions