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?
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.
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>
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
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-
You can use a regex pattern, /^-?[0-9]+$/.test(num).
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>
Related
I have two inputs and an array with two positions in which both are numbers, in each input its v-model will make a value of the array in each position, writing in each input changes the heat in the array and this is fine.
I am trying to write in each input to format it and add commas of thousands and millions but keeping the value in the array without format and of type number, I have a function that formats it well in the console, but I cannot show it in the input, how can I achieve this?
<template>
<input
type="text"
class="text-center"
v-model="rangePrice[0]"
/>
<input
type="text"
class="text-center"
v-model="rangePrice[1]"
/>
</template>
<script setup>
const rangePrice = ref([0, 100000000])
// this function displays the formatted value to the console
const format = (e) => {
console.log(rangePrice.value[0].toString().replace(/\D/g, "").replace(/\B(?=(\d{3})+(?!\d))/g, ","))
}
</script>
I am using vue form slider where I have two scroll points and these two minimum and maximum values are the values of the array, which when moving the bar converts the values to a number:
<Slider
class="slider"
v-model="rangePrice"
:lazy="false"
:min="0"
:max="100000000"
:tooltips="false"
/>
Try with #input and :value instead v-model:
const { ref } = Vue
const app = Vue.createApp({
setup() {
const rangePrice = ref([0, 100000000])
const format = (e) => {
return e.toString().replace(/\D/g, "").replace(/\B(?=(\d{3})+(?!\d))/g, ",")
}
const saveInput = (e, i) => {
rangePrice.value[i] = parseInt(e.target.value.replaceAll(',', ''))
}
return {
rangePrice, format, saveInput
};
},
})
app.mount('#demo')
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="demo">
<input
type="text"
class="text-center"
:value="format(rangePrice[0])"
#input="saveInput($event, 0)"
>
<input
type="text"
class="text-center"
:value="format(rangePrice[1])"
#input="saveInput($event, 1)"
>
{{rangePrice}}
</div>
I am following a tutorial exercise and I got the following error
Objects are not valid as a React child
I know this error is related to the object as I am trying to access the object but it needs an individual item of an object but not sure.
Why cannot the map loop over each item in the array?
Following is my code
var template = <h1>Indecision App</h1>;
var app = {
title: 'Indecision App',
subtitle: 'yo',
options: []
}
let count = 0;
function checkSubtitles (subtitle){
if(subtitle){
return <p>{subtitle}</p>
}else{
return undefined
}
}
function reset(){
count = 0;
reRenderApp();
}
function increaseCount(){
count++;
reRenderApp();
}
function onSubmitHandle(e){
e.preventDefault();
const options = e.target.elements.options;
app.options.push(options);
reRenderApp();
e.target.elements.options.value = ''
}
function removeAll(){
app.options = [];
reRenderApp();
}
function reRenderApp(){
var templateTwo = (
<div>
<h1>{app.title}</h1>
{checkSubtitles(app.subtitle)}
<p>Count: {count}</p>
<p>Array Length: {app.options.length > 0 ? app.options.length : '0 Items'}</p>
<ol>
{app.options.map((item)=>{
return <li key={item}>{item}</li>
})}
</ol>
<hr></hr>
<form onSubmit={onSubmitHandle}>
<input type="text" name="options" />
<input type="submit" value="Push to the Array" />
<input type="reset" value="Empty my list" onClick={removeAll} />
</form>
<button onClick={()=>{
increaseCount();
}}>Increase Count</button>
<button onClick={()=>{
reset();
}}>Reset Count</button>
</div>
)
ReactDOM.render(templateTwo, appRoot)
}
var appRoot = document.getElementById('app');
reRenderApp();
<body>
<div id="app"></div>
<script src="https://unpkg.com/react#16.0.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.0.0/umd/react-dom.development.js"></script>
<script src="./app.js"></script>
</body>
</html>
The main problem is, as you mentioned: Objects are not valid as a React child
But, what is happening?
If we go into:
function onSubmitHandle(e){
e.preventDefault();
// Line 1
const options = e.target.elements.options;
app.options.push(options);
reRenderApp();
// Line 2
e.target.elements.options.value = ''
}
So in Line 1, you're pushing options into the options array.
But, then in Line 2, we can notice options has an attribute (so, it's an object)
So, if you change Line 1, from:
const options = e.target.elements.options;
To this:
const options = e.target.elements.options.value;
It'd work.
Also, to check what I'm saying you have 2 options:
option 1: console.log
function onSubmitHandle(e){
e.preventDefault();
const options = e.target.elements.options;
console.log({ options })
app.options.push(options);
reRenderApp();
e.target.elements.options.value = ''
}
option 2: make that option a valid child of react with JSON.stringify()
<ol>
{app.options.map((item, index)=>{
return <li key={index}>{JSON.stringify(item)}</li>
})}
</ol>
You can do
{app.options.length && app.options.map((item)=>{
return <li key={item}>{item}</li>
})}
But you must be sure that "item" here is not an object as you can't render an object
The reason for this is that your options array is going to be filled with elements as you're pushing the input element with the name of "option" into your array - this elements are objects in JS which you can't render out as list items.
Use React State to store anything that's going to change in the UI - in this case your list of options> So rather than doing
var app = {
title: 'Indecision App',
subtitle: 'yo',
options: []
}
let count = 0;
Do:
const [options, setOptions] = React.useState([]);
const [count, setCount] = React.useState(0);
Title and subtitle are probably not going to change, so just put them in h1 & h2 elements - if they are, then use the state pattern again.
Get rid of the two inputs with types of "submit" & "reset" just use normal button elements instead.
You'll also need an onchange event on your input where the text will go in and each time the onchange event is fired (i.e, when a user types) you'll need to save the input text
const [inputText, setInputText] = React.useState('');
const handleChange = (e) => {
const {value} = e.target;
setInputText(value)
}
<input type="text" value={inputText} onChange={handleChange}/>
Then in your onHandleSubmit function, just have
const onHandleSubmit = () => {
setOptions([...options, inputText]);
setInputText('')
}
This should work
I am attempting to set up a number input field in Vue 3 that prevents the user from entering a value below 1. So far I have the following input with min = 1 to prevent clicking the input arrows below 1:
<input min="1" type="number" />
However, the user can still manually enter 0 or a negative number. How can I prevent the user entering a number below 1?
You can check value on keyup:
const { ref } = Vue
const app = Vue.createApp({
setup() {
const numValue = ref(null)
const setMin = () => {
if(numValue.value < 1) numValue.value = null
}
return { numValue, setMin }
},
})
app.mount('#demo')
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="demo">
<input #keyup="setMin" min="1" v-model="numValue" type="number" />
</div>
I'm trying to make a input box component that has instant feedback using Formik. I want the input box to turn green when the user input matches a predefined string (the "answer"), gray if the input matches the prefix of the answer (including the empty string) and red otherwise. This string is stored as a property of the initial values, values.answer. The Formik validate function checks if the input equals values.answer and sets values.correct = true. I then created a css class corresponding to a green input box and set the className of the input conditional on the value of values.correct. The problem is it only seems to update (i.e turn green with a correct input) when I click out of focus of the input box (i.e onBlur). I would like it to work onChange. How would I do this?
Here is the relevant code sandbox: https://codesandbox.io/s/instant-feedback-box-lub0g?file=/src/Frame.js
Cool problem, but you've overcomplicated your code a little bit 😉 Some feedback:
touched is set to true during onBlur by default. You can override this by using setTouched(), but I found it simpler to just use values instead of touched in your form
try to keep values as minimal as possible, it's only meant to access input values so there's no need for hint and answer to be assigned to it
the purpose of the validation function is to return an errors object and not to set values, so remove assignments like values.correct = true
You don't need to store isDisabled in state, you can derive it from formik.submitCount and formik.isSubmitting
const Note = () => {
const [showFrame, setShowFrame] = useState({ 1: true });
const onCorrectSubmission = (frameId) => {
setShowFrame({ ...showFrame, [frameId]: true });
};
const text =
"What is the sum of the first three natural numbers? (give answer as a word, i.e one, two etc.)";
const hint = "The first three natural numbers are 1, 2, and 3";
const answer = "six";
return (
<div>
<h1>Induction</h1>
{showFrame[1] ? (
<Frame
id={1}
text={text}
hint={hint}
answer={answer}
onCorrectSubmission={onCorrectSubmission}
/>
) : null}
{showFrame[2] ? (
<Frame
id={2}
text={text}
hint={hint}
answer={answer}
onCorrectSubmission={onCorrectSubmission}
/>
) : null}
</div>
);
};
const Frame = ({
id,
text,
hint,
answer,
values,
onCorrectSubmission,
...props
}) => {
const validate = (values) => {
const errors = {};
if (!answer.startsWith(values.cloze)) {
errors.cloze = hint;
} else if (values.cloze !== answer) {
errors.cloze = true;
}
return errors;
};
const formik = useFormik({
initialValues: {
cloze: ""
},
validate,
onSubmit: (values) => {
onCorrectSubmission(id + 1);
}
});
const isFinished = formik.isSubmitting || formik.submitCount > 0;
return (
<form enablereinitialize={true} onSubmit={formik.handleSubmit}>
<p>{text}</p>
<input
id="cloze"
name="cloze"
type="text"
autoComplete="off"
{...formik.getFieldProps("cloze")}
disabled={isFinished}
className={`input
${!answer.startsWith(formik.values.cloze) ? "invalid-input" : ""}
${formik.values.cloze && !formik.errors.cloze ? "valid-input" : ""}
`}
/>
{formik.values.cloze && formik.errors.cloze ? (
<div>{formik.errors.cloze}</div>
) : null}
<button disabled={!!formik.errors.cloze || isFinished} type="submit">
Submit
</button>
</form>
);
};
export default Frame;
Live Demo
I'm trying to prevent my TextInput from getting values like $,%,^,&,(,) etc. Basically my TextInput should allow letters only. My approach is as follows. But still i'm able to input these other characters. How can i prevent special characters from the TextInput
restrict(event) {
const regex = new RegExp("^[a-zA-Z]+$");
const key = String.fromCharCode(!event.charCode ? event.which : event.charCode);
if (!regex.test(key)) {
event.preventDefault();
return false;
}
}
<TextInput
underlineColorAndroid='transparent'
allowFontScaling={false}
style={styles.questionText}
onKeyPress={e => this.restrict(e)}
value={firstNameState}
/>
the onKeyPress event on android does not work very well.
That is why I have chosen to use a method that eliminates these characters and then save it wherever you want, just as it might change the state of your field.
restrict = text => text.replace(/[`~0-9!##$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, '')
I have to block special characters by this line of code.
var format = /[!##$%^&*()_+-=[]{};':"\|,.<>/?]+/;
if(format.test(string)){ }
You may define your OnChange event handler using your regex, where you will check if the input string matches your regex with /^[^!-\/:-#\[-`{-~]+$/.test(text):
const {useState} = React;
const App = () => {
const [value, setValue] = useState("");
const onChange = e => {
const input = e.currentTarget.value;
if (/^[^!-\/:-#\[-`{-~]+$/.test(input) || input === "") {
setValue(input);
}
};
return (
<div className="App">
<input
value={value}
onChange={onChange}
underlineColorAndroid='transparent'
allowFontScaling={false}
/>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>