ui
ui copied to clipboard
react hook forms, number input
Hello,
I'm trying to make a number input but it always being received as string when I enter the value, what I'm doing wrong?
<FormItem> <FormLabel>Pradinė kaina be PVM (€):</FormLabel> <FormControl> <Input type="number" min={100} {...field} /> </FormControl> <FormMessage /> </FormItem>
All HTML input elements values are a string
.
This library input components are written as controlled RHF inputs using Controller
, which means you need to convert your input value onChange
on your own before committing it.
So, you can
-
either create your own abstraction by updating your
Input
component based on thetype
prop however you want - or simply do
<Input
type="number"
min={100}
{...field}
+ onChange={event => field.onChange(+event.target.value)}
/>
Make sure to handle NaN
values too 😉
Thank you, when I try to validate datetime-local, I continually getting this error:
Uncaught ZodError: [ { "code": "invalid_type", "expected": "object", "received": "string", "path": [], "message": "Expected object, received string" } ]
I'm using this zod datetime validation for it: startTime: z.string().datetime({ precision: 0, offset: true }),
and here is my code:
<FormField control={form.control} name="startTime" render={({ field }) => ( <FormItem> <FormLabel> Aukciono pradžia:</FormLabel> <FormControl> <Input type="datetime-local" {...field} onChange={(e) => { console.log(formSchema.parse(e.target.value)); }} /> </FormControl> <FormMessage /> </FormItem> )} />
do you have any idea why zod, datetime is being received as object, when it's a string? I could not make it work...
You've given 2 different use-cases here with input type="number"
and type="datetime-local"
. Parsing the date string to number isn't right.
Can you create a reproduction sandbox with the expected behaviour instead?
But in my last example, there's only 1 type, datetime-local
In react-hook-form
you can utilize the valueAsNumber
option when registering your input. You would have to modify the Form component to allow for this option though I believe.
https://react-hook-form.com/api/useform/register#options
In
react-hook-form
you can utilize thevalueAsNumber
option when registering your input. You would have to modify the Form component to allow for this option though I believe.https://react-hook-form.com/api/useform/register#options
The valueAsNumber
option is strictly a register
API. It doesn't apply to Controller
, which is used in this library
But in my last example, there's only 1 type, datetime-local
So, the 2 examples are completely unrelated? You posted your issue with the number input & asked about the date type after, so I got confused 🤭
Thank you, when I try to validate datetime-local, I continually getting this error:
Uncaught ZodError: [ { "code": "invalid_type", "expected": "object", "received": "string", "path": [], "message": "Expected object, received string" } ]
I'm using this zod datetime validation for it: startTime: z.string().datetime({ precision: 0, offset: true }),
and here is my code:
<FormField control={form.control} name="startTime" render={({ field }) => ( <FormItem> <FormLabel> Aukciono pradžia:</FormLabel> <FormControl> <Input type="datetime-local" {...field} onChange={(e) => { console.log(formSchema.parse(e.target.value)); }} /> </FormControl> <FormMessage /> </FormItem> )} />
do you have any idea why zod, datetime is being received as object, when it's a string? I could not make it work...
Looking at your code, you're parsing the new input value
inside onChange
via the whole formSchema
which does indeed expect an object
as it's your form schema.
If you want to parse the input value with your already defined startTime
schema from your form formSchema
, you can access nested properties like this:
formSchema.shape.startTime.parse()
If you're simply after validating your form input on value change, you can do
useForm({
mode: "onChange"
})
You don't need to call your formSchema
methods yourself with React Hook Form (RHF).
Please, refer to React Hook Form resolvers documentation to learn more.
Thanks, I just want to validate my starttime datetime-local field that is it, instead of object to get the string value so the zod could validate it simply.
@arnasofc Note: you can use the coerce
helper on your Zod schema. Example:
const profileFormSchema = z.object({
age: z.coerce.number().min(18), // Zod will coerce age to a number.
})
// ...
<FormField
control={form.control}
name="age"
render={({ field }) => (
<FormItem>
<FormLabel>Age</FormLabel>
<Input type="number" placeholder="21" {...field} /> // <---- type is number.
<FormDescription>
You must be at least 18 years old to use this service.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
The following coercion is available:
z.coerce.string(); // String(input)
z.coerce.number(); // Number(input)
z.coerce.boolean(); // Boolean(input)
z.coerce.bigint(); // BigInt(input)
z.coerce.date(); // new Date(input)
See the docs for more: https://github.com/colinhacks/zod#coercion-for-primitives
@arnasofc Note: you can use the
coerce
helper on your Zod schema. Example:const profileFormSchema = z.object({ age: z.coerce.number().min(18), // Zod will coerce age to a number. }) // ... <FormField control={form.control} name="age" render={({ field }) => ( <FormItem> <FormLabel>Age</FormLabel> <Input type="number" placeholder="21" {...field} /> // <---- type is number. <FormDescription> You must be at least 18 years old to use this service. </FormDescription> <FormMessage /> </FormItem> )} />
The following coercion is available:
z.coerce.string(); // String(input) z.coerce.number(); // Number(input) z.coerce.boolean(); // Boolean(input) z.coerce.bigint(); // BigInt(input) z.coerce.date(); // new Date(input)
See the docs for more: https://github.com/colinhacks/zod#coercion-for-primitives
But how can I validate my specific code below?
<FormField
control={form.control}
name="startTime"
render={({ field }) => (
<FormItem>
<FormLabel> Aukciono pradžia:</FormLabel>
<FormControl>
<Input
type="datetime-local"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
The validation I'm using and is not working:
startTime: z.string().datetime({ precision: 0, offset: true }),
The HTML5 datetime-local
input only accepts specific date string formats. If you want to display your input in a custom format specified in your validation schema, you should use a text
input with a custom date format mask instead. Otherwise, you will need to format the input value both before updating the state in RHF's onChange
event and before passing it to the input's value property. Something along the lines of
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import { formatISO, parseISO } from "date-fns"
// ... Other implacable imports here ...
// INFO: an example of a function to format the HTML5 datetime-local input value to the `startTime` format in the validation schema
function formatDate(date: string): string {
const parsedDate = parseISO(date)
const formattedDate = formatISO(parsedDate, { representation: "complete" })
return formattedDate
}
const schema = z.object({
startTime: z.string().datetime({ precision: 0, offset: true }),
})
const defaultValues = {
startTime: "2023-06-20T10:36:00-04:00",
}
function MyForm () {
const form = useForm<ProfileFormValues>({
resolver: zodResolver(schema),
defaultValues,
mode: "onChange",
})
return (
{/* ... other components here ... */}
<FormField
control={form.control}
name="startTime"
render={({ field }) => (
<FormItem>
<FormLabel>Aukciono pradžia:</FormLabel>
<FormControl>
<Input
type="datetime-local"
{...field}
value={new Date(field.value).toISOString().slice(0, -1)}
onChange={(event) =>
field.onChange(formatDate(event.target.value))
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* ... other components here ... */}
)
}
@arnasofc Note: you can use the
coerce
helper on your Zod schema. Example:const profileFormSchema = z.object({ age: z.coerce.number().min(18), // Zod will coerce age to a number. }) // ... <FormField control={form.control} name="age" render={({ field }) => ( <FormItem> <FormLabel>Age</FormLabel> <Input type="number" placeholder="21" {...field} /> // <---- type is number. <FormDescription> You must be at least 18 years old to use this service. </FormDescription> <FormMessage /> </FormItem> )} />
The following coercion is available:
z.coerce.string(); // String(input) z.coerce.number(); // Number(input) z.coerce.boolean(); // Boolean(input) z.coerce.bigint(); // BigInt(input) z.coerce.date(); // new Date(input)
See the docs for more: https://github.com/colinhacks/zod#coercion-for-primitives
I did this but now I'm getting a weird bug. If I consistently enter an integer or float, everything works fine. But if I start with an integer then change it to float it will tell me the input is invalid. See for yourself. bug branch fixed
edit: Actually if I enter say 1.5 and then try to change it to 1.6 it doesn't work but 2.5 does. edit2: Okay, I figured out the problem. Setting undefined on form's defaultValues will cause this bug. I left the bug in the "bug-branch' in case you want to see. This is a little annoying because I rather the form show the placeholder value and not the default value.
Wanted to add on here that its important to be careful when using coerce as it will in some cases accept null
values. You can fix this by using pipe on the string types to ensure that your final value is always a number. Here is an example of this:
age: z
.number()
.positive({ message: "Value must be positive" })
.int({ message: "Value must be an integer" })
.or(z.string())
.pipe(
z.coerce
.number()
.positive({ message: "Value must be positive" })
.int({ message: "Value must be an integer" })
),
z.coerce.number()
is helpful, and it'll also be nice to deal with the leading '0's:
<Input
type='number'
{...field}
onChange={e => {
if (e.target.value == "0") e.target.value = ""
if (e.target.value != "") e.target.value = parseInt(e.target.value)
field.onChange(e)
}}
/>
Since z.coerce
could handle an empty string, and regards it as 0, so we can let e.target.value
to be an empty string.
All HTML input elements values are a
string
.This library input components are written as controlled RHF inputs using
Controller
, which means you need to convert your input valueonChange
on your own before committing it.So, you can
- either create your own abstraction by updating your
Input
component based on thetype
prop however you want- or simply do
<Input type="number" min={100} {...field} + onChange={event => field.onChange(+event.target.value)} />
Make sure to handle
NaN
values too 😉
All HTML input elements values are a
string
.This library input components are written as controlled RHF inputs using
Controller
, which means you need to convert your input valueonChange
on your own before committing it.So, you can
- either create your own abstraction by updating your
Input
component based on thetype
prop however you want- or simply do
<Input type="number" min={100} {...field} + onChange={event => field.onChange(+event.target.value)} />
Make sure to handle
NaN
values too 😉
Hi there,
To deal with NaN, i tried the below but whenever the input is focused and nothing is entered , I get an error from the console panel. My Try:
onChange={(event) => {
if (!Number.isNaN(event.target.value))
field.onChange(parseFloat(event.target.value));
}}
The error I get:
- on the console:
The specified value "NaN" cannot be parsed, or is out of range.
- from the input element( instead of the error message from my zod schema):
Expected number, received nan
My zod schema for the input:
amount: z
.number()
.positive({ message: "amount must be valid and positive" })
.or(z.string())
.pipe(
z.coerce
.number()
.positive({ message: "amount must be valid and positive" })
),
I am using Typescript and also tried this solution but keep getting the error:
Type 'number' is not assignable to type 'string'.
Clarifications on the above issues will be highly appreciated. Thank you in advance.
Schema and Types
import { z } from "zod";
export const FormSchema = z.object({
username: z.string().min(2, {
message: "Username must be at least 2 characters.",
}),
age: z
.number({ required_error: "What's your age?" })
.positive({ message: "Age must be positive" })
.int({ message: "Age must be integer" })
.or(z.string())
.pipe(
z.coerce
.number({ required_error: "What's your age?" })
.positive({ message: "Age must be positive" })
.int({ message: "Age must be integer" }),
),
});
export type FormType = z.infer<typeof FormSchema>;
Implementation with Zod + React Hook Forms
<FormField
control={form.control}
name="age"
render={({ field }) => (
<FormItem>
<FormLabel>Age</FormLabel>
<FormControl>
<Input
placeholder="How old are you?" // your defaultValue must be undefined
inputMode="numeric" // display numeric keyboard on mobile
{...field}
value={field.value || ""} // avoid errors of uncontrolled vs controlled
pattern="[0-9]*" // to receive only numbers without showing does weird arrows in the input
onChange={(e) =>
e.target.validity.valid && field.onChange(e.target.value) // e.target.validity.valid is required for pattern to work
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>;
@ziin Thank you so much. It worked like magic. If you don't mind, I've got a couple of questions:
- Does this
value={field.value || ""}
mean I no longer need to set defaultValue forage
? - What do you mean by
your defaultValue must be undefined
in the comment for the placeholder?
Thank you in advance.
@ziin Thank you so much. It worked like magic. If you don't mind, I've got a couple of questions:
- Does this
value={field.value || ""}
mean I no longer need to set defaultValue forage
?- What do you mean by
your defaultValue must be undefined
in the comment for the placeholder?Thank you in advance.
Thank you @mu-ab .
- Yes, age is a field that, in most cases, will be undefined by default. Everything I did was to make it happen properly.
- In order to display the placeholder, the value must be undefined.
I'm getting a problem where if I use coerce, then use form.getvalues()
, it give the wrong type. For example.
const formScheme = z.object({
width: z.coerce.number()
})
then when I use form.getValues('width')
, I will get type number but the real value will be a string, since input element only takes string. Anyway to correct this?
I'm getting a problem where if I use coerce, then use
form.getvalues()
, it give the wrong type. For example.const formScheme = z.object({ width: z.coerce.number() })
then when I use
form.getValues('width')
, I will get type number but the real value will be a string, since input element only takes string. Anyway to correct this?
After trying for a while to get the coerce
to work (which I thought didn't work), it turns out that if you do form.getValues('value')
, it will come with a string
value. But if you check the argument that comes with the onSubmit, in this case 'data'
, it will come with the actual numeric
value tha was converted with the coerce method.
const handleClickSubmit = async () => { //Just a function to check some things
form.clearErrors();
console.log(form.getValues('code')); // Output: code: '213131',
...
}
const onSubmit = async (data) => { //The actual onSubmit function
console.log(data); // Output: code: 213131,
...
I'm getting a problem where if I use coerce, then use
form.getvalues()
, it give the wrong type. For example.const formScheme = z.object({ width: z.coerce.number() })
then when I use
form.getValues('width')
, I will get type number but the real value will be a string, since input element only takes string. Anyway to correct this?After trying for a while to get the
coerce
to work (which I thought didn't work), it turns out that if you doform.getValues('value')
, it will come with astring
value. But if you check the argument that comes with the onSubmit, in this case'data'
, it will come with the actualnumeric
value tha was converted with the coerce method.const handleClickSubmit = async () => { //Just a function to check some things form.clearErrors(); console.log(form.getValues('code')); // Output: code: '213131', ... } const onSubmit = async (data) => { //The actual onSubmit function console.log(data); // Output: code: 213131, ...
Yeah, I know it works with onSubmit. However, there are situations where you want to check the value without submitting.
@Apestein Hope this will be helpful
I will return undefined
when the value is not a number and the value will be value={field.value ?? ''}
'use client';
import { Button } from '@/components/ui/button';
import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils/cn';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import z from 'zod';
const formSchema = z.object({
tel: z.coerce.number({ required_error: 'Telphone number required!', invalid_type_error: 'Telphone number required!' }),
});
export default function TelNumberForm() {
const waRef = useRef<HTMLAnchorElement>(null);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { tel: undefined }
});
function handleSubmit(data: z.infer<typeof formSchema>) {
// Update local-storage with the tel-number
const phone = `${data.tel}`;
}
return (
<>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className='space-y-5'>
<FormField
control={form.control}
name='tel'
render={({ field }) => (
<FormItem>
<FormControl>
<Input
className='font-aldrich placeholder:font-nunito shadow-none'
placeholder='Tel number'
type='number'
inputMode='numeric'
autoComplete='off'
{...field}
value={field.value ?? ''}
onChange={(e) => {
if (e.target.value === '') return field.onChange(undefined);
field.onChange(Number(e.target.value));
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button className='w-full font-bold' type='submit' size='lg'>
Open chat
</Button>
</form>
</Form>
</>
);
}
@Apestein Hope this will be helpful
I will return
undefined
when the value is not a number and the value will bevalue={field.value ?? ''}
'use client'; import { Button } from '@/components/ui/button'; import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; import { cn } from '@/lib/utils/cn'; import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import z from 'zod'; const formSchema = z.object({ tel: z.coerce.number({ required_error: 'Telphone number required!', invalid_type_error: 'Telphone number required!' }), }); export default function TelNumberForm() { const waRef = useRef<HTMLAnchorElement>(null); const form = useForm<z.infer<typeof formSchema>>({ resolver: zodResolver(formSchema) }); function handleSubmit(data: z.infer<typeof formSchema>) { // Update local-storage with the tel-number const phone = `${data.tel}`; } return ( <> <Form {...form}> <form onSubmit={form.handleSubmit(handleSubmit)} className='space-y-5'> <FormField control={form.control} name='tel' render={({ field }) => ( <FormItem> <FormControl> <Input className='font-aldrich placeholder:font-nunito shadow-none' placeholder='Tel number' type='number' inputMode='numeric' autoComplete='off' {...field} value={field.value ?? ''} onChange={(e) => { if (e.target.value === '') return field.onChange(undefined); field.onChange(Number(e.target.value)); }} /> </FormControl> <FormMessage /> </FormItem> )} /> <Button className='w-full font-bold' type='submit' size='lg'> Open chat </Button> </form> </Form> </> ); }
Got laid off so this is no longer a problem 👍 Might come in handy someday though, thanks
So, while the documentation only shows how to use the form components as controlled fields, these components do forward their refs which means they still work with the register
function.
I'll admit I haven't looked too deeply into any weird side effects this might cause, but for my use case of a simple form I find that this is sufficient to get the value of number inputs as numbers instead of strings:
<FormField
control={control}
name="price"
render={() => (
<FormItem>
<FormControl>
<Input
type="number"
step={0.01}
placeholder="Enter Price"
aria-label="Price"
{...register("price", { valueAsNumber: true })}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
Hello! I need an example that contains Select and DatePicker Please !
const formSchema = z.object({ temperature: z.coerce.number().min(1, "temperature cann't be zero."), })
@aynuayex you are using the zod schema to have the temperature with minimum of 1. You should change the value to less than 0.5 to start working.
const formSchema = z.object({
temperature: z.coerce.number().min(1, "temperature cann't be zero."),
})
@imopbuilder oh, what a shame, I don't know how I didn't notice that sorry.
@arnasofc Note: you can use the
coerce
helper on your Zod schema. Example:const profileFormSchema = z.object({ age: z.coerce.number().min(18), // Zod will coerce age to a number. }) // ... <FormField control={form.control} name="age" render={({ field }) => ( <FormItem> <FormLabel>Age</FormLabel> <Input type="number" placeholder="21" {...field} /> // <---- type is number. <FormDescription> You must be at least 18 years old to use this service. </FormDescription> <FormMessage /> </FormItem> )} />
The following coercion is available:
z.coerce.string(); // String(input) z.coerce.number(); // Number(input) z.coerce.boolean(); // Boolean(input) z.coerce.bigint(); // BigInt(input) z.coerce.date(); // new Date(input)
See the docs for more: https://github.com/colinhacks/zod#coercion-for-primitives
This seems like a hack no? since react-hook-form does provide a way with their register-api to set the correct input type.
I am using Typebox for validation. so. is there anyway can i achieve this?