Criando mascara para input com React e useForm
Fala galera!
Recentemente eu precisei trabalhar com máscara no React com TypeScript e tive alguns problemas ao usar a lib ReactInputMask.
Recebi o Error
Overload 1 of 2, '(props: Props | Readonly<Props>): ReactInputMask', gave the following error. Type '(inputProps: any) => JSX.Element' is not assignable to type 'ReactI18NextChild | Iterable<ReactI18NextChild>'. Overload 2 of 2, '(props: Props, context: any): ReactInputMask', gave the following error.Type '(inputProps: any) => JSX.Element' is not assignable to type 'ReactI18NextChild | Iterable<ReactI18NextChild>'.
Então decidi postar aqui a solução que encontrei utilizando o regex e acredito que poderá ajudá-los em alguma ocasião.
Primeiro vamos criar uma pasta chamada MASKS e dentro dela criaremos um arquivo chamado MASK.TS, para criarmos a função que fará a máscara.
Agora dentro do arquivo mask.ts vamos criar a duas funções que serão nossas mascaras “cnpj, telefone e cep” , no caso do TypeScript, precisaremos passar um type para o parâmetro da nossa função, nesse caso passaremos a tipagem com String ou undefined.
No início da função iremos verificar se existe algum valor dentro do nosso parâmetro, caso não exista daremos um return vazio e sairemos da função, caso exista valor faremos todo o regex até termos o resultado que gostaríamos
export const normalizePhoneNumber = (value: String | undefined) => {
if (!value) return ''
return value.replace(/[\D]/g, '')
.replace(/(\d{2})(\d)/, '($1) $2')
.replace(/(\d{5})(\d)/, '$1-$2')
.replace(/(-\d{4})(\d+?)/, '$1')
}
export const normalizeCnpjNumber = (value: String | undefined) => {
if (!value) return ''
return value.replace(/[\D]/g, '')
.replace(/(\d{2})(\d)/, '$1.$2')
.replace(/(\d{3})(\d)/, '$1.$2')
.replace(/(\d{3})(\d)/, '$1/$2')
.replace(/(\d{4})(\d)/, '$1-$2')
.replace(/(-\d{2})\d+?$/, '$1')
}
export const normalizeCepNumber = (value: String | undefined) => {
if (!value) return ''
return value.replace(/\D/g, "")
.replace(/^(\d{5})(\d{3})+?$/, "$1-$2")
.replace(/(-\d{3})(\d+?)/, '$1')
}
O próximo passo será importa a função onde precisaremos usar a máscara. No meu caso estava usando a lib react-use-form então precisei utilizar a função watch do proprio useForm para conseguir escutar quando esse campo recebe um valor e a função setValue para setar o valor para a mesma variável dentro do useForm.
import {useForm} from 'react-hook-form'
import { normalizePhoneNumber, normalizeCnpjNumber, normalizeCepNumber } from '../../Mask/mask'
const App = () => {
const {handleSubmit, watch,setValue } = useForm<FormValues>()
const onSubmit = handleSubmit((data) => console.log(data))
const phoneValue = watch("phone")
const cnpjValue = watch("cnpj")
const cepValue = watch("cep")
useEffect(() => {
setValue("phone", normalizePhoneNumber(phoneValue))
},[phoneValue])
useEffect(() => {
setValue("cnpj", normalizeCnpjNumber(cnpjValue))
},[cnpjValue])
useEffect(() => {
setValue("cep", normalizeCepNumber(cepValue))
},[cepValue])
return (
<form onSubmit={onSubmit}>
<input {...register("cnpj", {required: true})} />
<input {...register("phone", {required: true})} />
<input {...register("cep", {required: true})} />
</form>
)
}
Esse post salvou minha task, obrigado OP. Fiz alguns ajustes, e implementei essa lógica no React Native, com mascara para CPF e CNPJ no mesmo input.
shared/utils/maskUtils.ts
export function maskCpfOrCnpj(value: string): string {
value = value.replace(/\D/g, '')
if (value.length <= 11) {
// Aplica máscara de CPF
return value
.replace(/(\d{3})(\d)/, '$1.$2')
.replace(/(\d{3})(\d)/, '$1.$2')
.replace(/(\d{3})(\d{1,2})$/, '$1-$2')
} else {
// Aplica máscara de CNPJ
return value
.replace(/^(\d{2})(\d)/, '$1.$2')
.replace(/^(\d{2})\.(\d{3})(\d)/, '$1.$2.$3')
.replace(/\.(\d{3})(\d)/, '.$1/$2')
.replace(/(\d{4})(\d)/, '$1-$2')
}
}
export function unMaskCpfOrCnpj(value: string): string {
return value.replace(/\D/g, '') // Remove tudo que não for número, pra enviar pra api
}
Meu componente personalizado de input
import { useEffect, forwardRef } from 'react'
import { TextInput, type TextInputProps } from 'react-native'
import type { Control, FieldValues, Path } from 'react-hook-form'
import { Controller } from 'react-hook-form'
interface ControlProps<T extends FieldValues> {
control?: Control<T>
name?: Path<T>
}
interface MaskProps {
mask?: (value: string) => string
}
export type FieldProps<T extends FieldValues> = TextInputProps &
ControlProps<T> &
MaskProps
export const Field = forwardRef(function Field<T extends FieldValues>(
{ control, name, value, mask, ...props }: FieldProps<T>,
ref: React.LegacyRef<TextInput> | undefined,
) {
useEffect(() => {
if (value) {
handleFocus(false, !!placeholder)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<Controller
control={control}
name={name}
render={({ field: { onChange } }) => (
<TextInput
ref={ref}
value={value}
onChangeText={(text) => onChange(mask ? mask(text) : text)}
{...props}
/>
)}
/>
}) as <T extends FieldValues>(
props: FieldProps<T> & { ref?: React.Ref<TextInput> },
) => JSX.Element
No arquivo da rota
const handleLogin = async () => {
setIsLoading(true)
const { cpfOrCnpj, password } = control._formValues
const cpfOrCnpjUnmasked = unMaskCpfOrCnpj(cpfOrCnpj)
const response: Response = await AuthService.login(
cpfOrCnpjUnmasked,
password,
)
}
<Input error={errors.cpfOrCnpj?.message}>
<Input.Field
placeholder='CPF ou CNPJ'
control={control}
name='cpfOrCnpj'
keyboardType='number-pad'
inputMode='numeric'
collapsable
mask={maskCpfOrCnpj}
/>
</Input>
Boa noite, parabéns pela postagem. Só um adendo, a lib no caso seria a React Hook Form. o useForm é apenas um método/função dessa lib. Abraços
Muito bom! Utilizei o imask ao invés de utilizar as funções próprias. É uma lib que utilizo a muito tempo, ela é bem simples e deixa o código mais fácil de entender.
https://github.com/uNmAnNeR/imaskjs
Usei dessa forma para uma mascara de cep por exemplo:
import IMask from 'imask'
const cepPipe = IMask.createPipe({
mask: '00000-000',
})
<input
{...register('cep', {
onChange: (event) => {
event.target.value = cepPipe(event.target.value)
},
})}
/>
Se alguém precisar do código pro cpf, é só seguir a lógica:
export const normalizeCpfNumber = (value: String | undefined) => {
if (!value) return "";
return value
.replace(/[\D]/g, "")
.replace(/(\d{3})(\d)/, "$1.$2")
.replace(/(\d{3})(\d)/, "$1.$2")
.replace(/(\d{3})(\d)/, "$1-$2")
.replace(/(-\d{2})\d+?$/, "$1");
};
const cpfValue = watch("cpf");
useEffect(() => {
setValue("cpf", normalizeCpfNumber(cpfValue));
}, [cpfValue]);
Ola man, top seu artigo. precisei de algo simple e que resolverce meu problema e seu artigo me ajudou. eu peguei seu codigo acresentei o que eu precisava e deu sucesso.
if (!value) return ''
return value
.replace(/[\D]/g, '')
.replace(/(\d{2})(\d)/, '$1/$2')
.replace(/(\d{2})(\d)/, '$1/$2')
.replace(/(\d{4})(\d)/, '$1')
}
useEffect(() => {
form.setValue('datadosorteio', normalizeDataString(dateString))
}, [dateString])
Excelente postagem, muito obrigado, ajudou bastante!
Cara, tu estás de brincadeira... perdi horas testando libs e nada funcionava até achar seu código. Simples, objetivo e funcional. Tu é fera !!
Obrigada, Rodrigo. Passei um dia inteiro testando bibliotecas que pudesse usar com o react hook form para criar uma máscara de telefone, nem vídeo achei. Encontrei outros tipos de máscaras e explicações, mas sempre dava algum erro. Sua explicação me salvou.
Top de mais cara. Tava precisando de algo extamente assim. Eu não tava enentendendo como eu iria manipular o valor, não conhecia esse watch do useForm.
Eu so acrecentaria para mascara de telefone o numero do pais (+55 ...), pois ai podemos pega telefone de qualquer lugar.
Mas em fim, ta show o código. TabNews ficando melhor que o stackoverflow hehhehe
Valeu pela publicação! Estava atrás de uma biblioteca para fazer isso, e da forma com tu fez, ficou fácil de aplicar e simples de usar. Obrigado!
Olá, alguma dica para formularios maiores, no caso tenho um formulario com remetente e destinatario, fora outras dados, e tem dois cpf/cpnj, dois celulares e telefones, vários selects, cep validando se é válido, e um card se usar o watch, acaba ficando muito pesado, alguma solução para não usar o watch?
Obrigado pela ajuda.
Ficou muito bom! Valeu pela ajuda.