Open stass-1 opened 5 months ago
Please send the full usage code, what initial value are you passing?
Having the similar issue
The initial value comes from the API, let's say the phone value 4799999999 which will mask to (47)999-9999
// Function to modify the input mask based on the value
export const phoneModify = (input: string) => {
if ((input.length >= 6 && input[5] === '9') || input.length === 11) {
return {
mask: '(__) _____-____',
}
}
return {
mask: '(__) ____-_____',
}
}
// Mask logic
const phone01InputRef = useMask({
mask: '(__) ____-_____',
replacement: { _: /\d/ },
modify: phoneModify,
onMask: (event) => {
setValue('phone_01', event.detail.value)
},
})
<Input
className="mt-2"
{...register('phone_01')}
ref={phone01InputRef}
/>
It uses React hook for and react query, when the component is mounted the data is fetched from API to refill the fields and allow users to edit the field.
I tried using setValue() and reset() inside useEffect, from hook form with no luck, the console.log using watch shows the value is set to field but it is still blank, the defaultValue() field is filled but the mask is not triggered.
I created a page / component that gives a better idea:
'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { MaskEventHandler, useMask } from '@react-input/mask'
import { useQuery } from '@tanstack/react-query'
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
interface ApiResponse {
zip_code: string
city: string
}
async function simulateApiCall() {
await new Promise((resolve) => setTimeout(resolve, 3000))
return {
zip_code: '12345678',
city: 'São Paulo',
} as ApiResponse
}
const formSchema = z.object({
zip_code: z
.string({
message: 'CEP inválido',
})
.min(8, {
message: 'CEP inválido',
})
.max(9, {
message: 'CEP inválido',
}),
city: z.string().min(3, {
message: 'Cidade é obrigatória',
}),
})
export type CompanyFormType = z.infer<typeof formSchema>
export default function Page() {
// Handle the search zip code logic Todo: Implement the loading status
const handleMaskZip: MaskEventHandler = async (event) => {
console.log('event', event)
}
// Mask logic
const ZipInputRef = useMask({
mask: '_____-___',
replacement: { _: /\d/ },
onMask: handleMaskZip,
})
const { data } = useQuery({
queryKey: ['loja', 1],
queryFn: () => {
return simulateApiCall()
},
})
async function handleUserUpdateSubmit(data: CompanyFormType) {
console.log('data', data)
}
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<CompanyFormType>({
resolver: zodResolver(formSchema),
})
useEffect(() => {
if (data) {
reset({
zip_code: data.zip_code,
city: data.city,
})
}
}, [data, reset])
return (
<div>
<form
onSubmit={handleSubmit(handleUserUpdateSubmit)}
className="w-full p-4"
>
<input className="border" {...register('zip_code')} ref={ZipInputRef} />
{errors.zip_code && <span>{errors.zip_code.message}</span>}
<input className="border" {...register('city')} />
{errors.city && <span>{errors.city.message}</span>}
</form>
</div>
)
}
You can see only one field is populated ( the one without mask )
@patrickfreitasdev you are overriding the ref
property, thus preventing the element from being registered for the form, since react-hook-form
itself expects the ref
callback returned by form.register
to be called.
You need to store references to the element for @react-input/mask
and react-hook-form
like this:
import { useState, useEffect } from 'react';
import { useMask } from '@react-input/mask';
import { useForm } from 'react-hook-form';
interface ApiResponse {
zip_code: string;
city: string;
}
function useQuery() {
const [data, setData] = useState<ApiResponse | null>(null);
useEffect(() => {
setTimeout(() => {
setData({ zip_code: '12345-678', city: 'São Paulo' });
}, 3000);
}, []);
return data;
}
export default function Page() {
const data = useQuery();
const maskRef = useMask({ mask: '_____-___', replacement: { _: /\d/ } });
const form = useForm();
const zipCodeRegistration = form.register('zip_code');
const cityRegistration = form.register('city');
useEffect(() => {
if (data) {
form.setValue('zip_code', data.zip_code);
form.setValue('city', data.city);
}
}, [data]);
const ref = (element: HTMLInputElement | null) => {
zipCodeRegistration.ref(element);
maskRef.current = element;
};
return (
<form onSubmit={form.handleSubmit(console.log)}>
<input {...zipCodeRegistration} ref={ref} />
<input {...cityRegistration} />{' '}
</form>
);
}
Note that a masked input will be initialized with exactly the value you give it, so you must ensure a masked value for initialization or format it manually (if the initialized value is not masked) using the format
utility (https://github.com/GoncharukBro/react-input/tree/main/packages/mask#format):
const value = format(data.zip_code, { mask, replacement });
form.setValue('zip_code', value);
More.
Starting with @react-input/mask@2.0.0
, we removed the input-mask
event and the onMask
method, focusing only on using native React events and methods such as onChange
, since the input-mask
event cannot be explicitly coordinated with React events and methods, making such usage and event firing order non-obvious.
To use the useful data from the detail
property of the input-mask
(onMask
) event object, you can also use the utilities described in the «Utils» section.
The release of @react-input/mask@2.0.0
is expected next week.
Hi @GoncharukBro
Thanks, that makes sense, the code you provided is working, though when I receive the data using React query, the field is filled but the mask isn't triggered so it will fill as 12345678 rather than 12345-678, it triggers fine on type,
You can see it with
'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { MaskEventHandler, useMask } from '@react-input/mask'
import { useQuery } from '@tanstack/react-query'
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
interface ApiResponse {
zip_code: string
city: string
}
async function simulateApiCall() {
await new Promise((resolve) => setTimeout(resolve, 3000))
return {
zip_code: '12345678',
city: 'São Paulo',
} as ApiResponse
}
const formSchema = z.object({
zip_code: z
.string({
message: 'CEP inválido',
})
.min(8, {
message: 'CEP inválido',
})
.max(9, {
message: 'CEP inválido',
}),
city: z.string().min(3, {
message: 'Cidade é obrigatória',
}),
})
export type CompanyFormType = z.infer<typeof formSchema>
export default function Page() {
const { data } = useQuery({
queryKey: ['loja', 1],
queryFn: () => {
return simulateApiCall()
},
})
async function handleUserUpdateSubmit(data: CompanyFormType) {
console.log('data', data)
}
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<CompanyFormType>({
resolver: zodResolver(formSchema),
})
const maskRef = useMask({ mask: '_____-___', replacement: { _: /\d/ } })
const zipCodeRegistration = register('zip_code')
const ref = (element: HTMLInputElement | null) => {
zipCodeRegistration.ref(element)
maskRef.current = element
}
useEffect(() => {
if (data) {
reset({
zip_code: data.zip_code,
city: data.city,
})
}
}, [data, reset])
return (
<div>
<form
onSubmit={handleSubmit(handleUserUpdateSubmit)}
className="w-full p-4"
>
<input {...zipCodeRegistration} ref={ref} />
{errors.zip_code && <span>{errors.zip_code.message}</span>}
<input className="border" {...register('city')} />
{errors.city && <span>{errors.city.message}</span>}
</form>
</div>
)
}
Console error returns
Error: An invalid character was found in the initialized property value
value
ordefaultValue
(index: 5). Check the correctness of the initialized value in the specified property.
I will keep digging here, thank you in advance.
Ok, console error is gone with
setValue('zip_code', data.zip_code)
I was using reset() so all good on that, sill the mask doesn't trigger.
@patrickfreitasdev
As the documentation and my previous comment suggest, @react-input/mask
does not change the value passed in the value
or defaultValue
properties of the input
element, it only changes the input process, for obvious reasons. So set the initialized value to something that can match the masked value at any point in the input. If you make a mistake, you will see a warning in the console about it.
If you have a non-masked value, use the format
utility to initialize the value (see «Utils»).
import { useMask, format } from '@react-input/mask';
// ...
useEffect(() => {
if (data) {
const zipCodeMaskedValue = format(data.zip_code, { mask, replacement });
form.setValue('zip_code', zipCodeMaskedValue);
form.setValue('city', data.city);
}
}, [data]);
@GoncharukBro Awesome, thanks, all clear now.
Initial value loaded by setValue method of react-hook-form is not formatting. But keyboard input formatting works correctly. Programmatically triggering onChange event and onKeyDown event wont help.