Reputation: 825
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 )
<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
Reputation: 27242
As per my understanding you have below requirments :
isAcceptingFloatingPointNumbers
flag value (Only accept integers if flag is false
else field should accept the floating numbers).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
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
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;
}
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-
/^-?[0-9]+$/.test(num)
.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.
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
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;
}
}
Upvotes: 1