ui icon indicating copy to clipboard operation
ui copied to clipboard

react hook forms, number input

Open arnasofc opened this issue 1 year ago • 31 comments

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?

Pradinė kaina be PVM (€):

arnasofc avatar May 22 '23 19:05 arnasofc

<FormItem> <FormLabel>Pradinė kaina be PVM (€):</FormLabel> <FormControl> <Input type="number" min={100} {...field} /> </FormControl> <FormMessage /> </FormItem>

arnasofc avatar May 22 '23 19:05 arnasofc

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 the type 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 😉

Moshyfawn avatar May 22 '23 22:05 Moshyfawn

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...

arnasofc avatar May 23 '23 06:05 arnasofc

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?

Moshyfawn avatar May 23 '23 18:05 Moshyfawn

But in my last example, there's only 1 type, datetime-local

arnasofc avatar May 23 '23 20:05 arnasofc

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

Rykuno avatar May 23 '23 22:05 Rykuno

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

The valueAsNumber option is strictly a register API. It doesn't apply to Controller, which is used in this library

Moshyfawn avatar May 23 '23 22:05 Moshyfawn

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 🤭

Moshyfawn avatar May 23 '23 22:05 Moshyfawn

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.

Moshyfawn avatar May 24 '23 04:05 Moshyfawn

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 avatar May 24 '23 06:05 arnasofc

@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

shadcn avatar May 24 '23 12:05 shadcn

@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 }),

arnasofc avatar May 24 '23 12:05 arnasofc

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 ... */}
  )
}

Moshyfawn avatar May 24 '23 14:05 Moshyfawn

@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.

Apestein avatar Jun 05 '23 03:06 Apestein

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" })
		),

Zod Docs Ref

Lermatroid avatar Jul 03 '23 19:07 Lermatroid

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.

x-tropy avatar Oct 26 '23 14:10 x-tropy

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 the type 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 value onChange on your own before committing it.

So, you can

  • either create your own abstraction by updating your Input component based on the type 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:

  1. on the console: The specified value "NaN" cannot be parsed, or is out of range.
  2. 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.

mu-ab avatar Nov 05 '23 16:11 mu-ab

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 avatar Nov 09 '23 15:11 ziin

@ziin Thank you so much. It worked like magic. If you don't mind, I've got a couple of questions:

  1. Does this value={field.value || ""} mean I no longer need to set defaultValue for age ?
  2. What do you mean by your defaultValue must be undefined in the comment for the placeholder?

Thank you in advance.

mu-ab avatar Nov 12 '23 06:11 mu-ab

@ziin Thank you so much. It worked like magic. If you don't mind, I've got a couple of questions:

  1. Does this value={field.value || ""} mean I no longer need to set defaultValue for age ?
  2. What do you mean by your defaultValue must be undefined in the comment for the placeholder?

Thank you in advance.

Thank you @mu-ab .

  1. Yes, age is a field that, in most cases, will be undefined by default. Everything I did was to make it happen properly.
  2. In order to display the placeholder, the value must be undefined.

ziin avatar Nov 13 '23 01:11 ziin

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?

Apestein avatar Nov 16 '23 19:11 Apestein

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,
  ...

DilbertRV avatar Nov 24 '23 11:11 DilbertRV

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,
  ...

Yeah, I know it works with onSubmit. However, there are situations where you want to check the value without submitting.

Apestein avatar Nov 26 '23 23:11 Apestein

@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>
		</>
	);
}



imopbuilder avatar Jan 13 '24 09:01 imopbuilder

@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)
	});

	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

Apestein avatar Jan 16 '24 05:01 Apestein

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>
    )}
  />

RomanBaiocco avatar Mar 05 '24 14:03 RomanBaiocco

Hello! I need an example that contains Select and DatePicker Please !

stonestecnologia avatar Mar 18 '24 04:03 stonestecnologia

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 avatar Mar 29 '24 15:03 imopbuilder

@imopbuilder oh, what a shame, I don't know how I didn't notice that sorry.

aynuayex avatar Mar 30 '24 09:03 aynuayex

@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.

Christophvh avatar Apr 03 '24 12:04 Christophvh

I am using Typebox for validation. so. is there anyway can i achieve this?

amjed-ali-k avatar May 09 '24 14:05 amjed-ali-k