-
-
Save Sutil/5285f2e5a912dcf14fc23393dac97fed to your computer and use it in GitHub Desktop.
"use client"; | |
import { zodResolver } from "@hookform/resolvers/zod"; | |
import { useForm } from "react-hook-form"; | |
import MoneyInput from "src/components/custom/money-input"; | |
import { Button } from "src/components/ui/button"; | |
import { Form } from "src/components/ui/form"; | |
import * as z from "zod"; | |
const schema = z.object({ | |
value: z.coerce.number().min(0.01, "Required"), | |
}); | |
export default function PlanForm() { | |
const form = useForm<z.infer<typeof schema>>({ | |
resolver: zodResolver(schema), | |
defaultValues: { | |
value: 0, | |
}, | |
mode: "onTouched", | |
}); | |
function onSubmit(values: z.infer<typeof schema>) { | |
// handle submit | |
} | |
return ( | |
<Form {...form}> | |
<form | |
className="flex flex-col gap-8" | |
onSubmit={form.handleSubmit(onSubmit)} | |
> | |
<MoneyInput | |
form={form} | |
label="Valor" | |
name="value" | |
placeholder="Valor do plano" | |
/> | |
<Button type="submit" disabled={!form.formState.isValid}> | |
Submit | |
</Button> | |
</form> | |
</Form> | |
); | |
} |
"use client"; | |
import { useReducer } from "react"; | |
import { | |
FormControl, | |
FormField, | |
FormItem, | |
FormLabel, | |
FormMessage, | |
} from "../ui/form"; // Shadcn UI import | |
import { Input } from "../ui/input"; // Shandcn UI Input | |
import { UseFormReturn } from "react-hook-form"; | |
type TextInputProps = { | |
form: UseFormReturn<any>; | |
name: string; | |
label: string; | |
placeholder: string; | |
}; | |
// Brazilian currency config | |
const moneyFormatter = Intl.NumberFormat("pt-BR", { | |
currency: "BRL", | |
currencyDisplay: "symbol", | |
currencySign: "standard", | |
style: "currency", | |
minimumFractionDigits: 2, | |
maximumFractionDigits: 2, | |
}); | |
export default function MoneyInput(props: TextInputProps) { | |
const initialValue = props.form.getValues()[props.name] | |
? moneyFormatter.format(props.form.getValues()[props.name]) | |
: ""; | |
const [value, setValue] = useReducer((_: any, next: string) => { | |
const digits = next.replace(/\D/g, ""); | |
return moneyFormatter.format(Number(digits) / 100); | |
}, initialValue); | |
function handleChange(realChangeFn: Function, formattedValue: string) { | |
const digits = formattedValue.replace(/\D/g, ""); | |
const realValue = Number(digits) / 100; | |
realChangeFn(realValue); | |
} | |
return ( | |
<FormField | |
control={props.form.control} | |
name={props.name} | |
render={({ field }) => { | |
field.value = value; | |
const _change = field.onChange; | |
return ( | |
<FormItem> | |
<FormLabel>{props.label}</FormLabel> | |
<FormControl> | |
<Input | |
placeholder={props.placeholder} | |
type="text" | |
{...field} | |
onChange={(ev) => { | |
setValue(ev.target.value); | |
handleChange(_change, ev.target.value); | |
}} | |
value={value} | |
/> | |
</FormControl> | |
<FormMessage /> | |
</FormItem> | |
); | |
}} | |
/> | |
); | |
} |
Funciona muito bem quando estás a introduzir os dados, mas a apagar ele não funciona
@hspotted, It worked fine here. Let me know what behavior you expect on this.
Screen.Recording.2024-01-15.at.10.14.44.mov
Estou usando o componente e ele funciona perfeitamente, mas quando faço form.reset(data) para recuperar os dados, é o único campo que não atualiza
Estou usando o componente e ele funciona perfeitamente, mas quando faço form.reset(data) para recuperar os dados, é o único campo que não atualiza
Não previ este caso. A linha 51 sempre vai atrapalhar.
Vou ter que mudar a implementação
Eu consegui contornar a situação aqui, adicionei um useEffect no componente para atualizar o valor, e estou passando o value pelo formulário como uma props do componente:
useEffect(() => {
if (props.value) {
setValue((Number(props.value) * 100).toString());
}
}, [props.form, props.value]);
const [balanceValue, setBalanceValue] = useState('');
// setBalanceValue usei onde pego os dados da API
<MoneyInput
form={form}
value={balanceValue}
label="Saldo"
name="balance"
placeholder={!isDataLoading ? 'Saldo da carteira...' : 'Carregando...'}
/>
Eu consegui contornar a situação aqui, adicionei um useEffect no componente para atualizar o valor, e estou passando o value pelo formulário como uma props do componente:
useEffect(() => { if (props.value) { setValue((Number(props.value) * 100).toString()); } }, [props.form, props.value]);
const [balanceValue, setBalanceValue] = useState(''); // setBalanceValue usei onde pego os dados da API <MoneyInput form={form} value={balanceValue} label="Saldo" name="balance" placeholder={!isDataLoading ? 'Saldo da carteira...' : 'Carregando...'} />
Eu resolvi dessa forma e deixou o código um pouco mais limpo:
useEffect(() => { setValue(initialValue); }, [initialValue]);
Mas isso porque nao passo o value
como prop
e continuo utilizando apenas o form
como fonte de dado.
A little late, but here is how I managed the form.reset() outside the component:
const formData = form.watch(name);
useEffect(() => {
const formValue = moneyFormatter.format(Number(formData));
if (formValue !== value) {
setValue(formValue);
}
}, [formData, value])
Funciona muito bem quando estás a introduzir os dados, mas a apagar ele não funciona
Estou tendo esse problema, mas apenas quando pelo celular (chrome mobile).
Pra mim, ao clicar backspack ele apaga, mas move o cursor uma casa a esquerda.
WhatsApp.Video.2024-06-03.at.21.02.51.mp4
No pc está impecável.
Tenta colocar o attribute inputmode
= decimal
Valeu pessoal ajudou a dar um norte, vou deixar meu código aqui caso ajude mais alguém.
use client';
import { useEffect, useState } from 'react';
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '../ui/form';
import { Input } from '../ui/input';
import { UseFormReturn } from 'react-hook-form';
import { formatCurrency } from '@/lib/utils';
type MoneyInputProps = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
form: UseFormReturn<any>;
name: string;
label: string;
placeholder?: string;
value?: string | number;
};
export const MoneyInput = (props: MoneyInputProps) => {
const [inputValue, setInputValue] = useState<string>(props.value ? formatCurrency(Number(props.value)) : '');
useEffect(() => {
const formValue = props.form.getValues(props.name);
if (formValue && formValue !== inputValue) {
setInputValue(formatCurrency(Number(formValue)));
}
}, [props.form, props.name, props.value, inputValue]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const rawValue = e.target.value.replace(/\D/g, '');
const numericValue = (Number(rawValue) / 100).toFixed(2);
setInputValue(rawValue);
props.form.setValue(props.name, numericValue, { shouldValidate: true });
};
const handleBlur = () => {
if (inputValue) {
const formattedValue = formatCurrency(Number(inputValue) / 100);
setInputValue(formattedValue);
}
};
const handleFocus = () => {
const currentValue = props.form.getValues(props.name);
if (currentValue) {
setInputValue((Number(currentValue) * 100).toString());
}
};
return (
<FormField
control={props.form.control}
name={props.name}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>{props.label}</FormLabel>
<FormControl>
<Input
placeholder={props.placeholder}
type="text"
value={inputValue}
onChange={handleChange}
onBlur={handleBlur}
onFocus={handleFocus}
name={field.name}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
};
export const formatCurrency = (amount: number, currency: string = 'USD', locale: string = 'en-US'): string => {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency,
}).format(amount);
};
Based on the examples here, I did it this way using the formContext, with stronger typing and validation to prevent the field from showing R$ 0.00 when empty, displaying the placeholder instead. It works with reset()
import { useEffect, useReducer } from "react";
import { FieldValues, Path, useFormContext } from "react-hook-form";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "../ui/form";
import { Input, InputProps } from "../ui/input";
export type InputCurrencyProps<T extends FieldValues> = InputProps & {
name: Path<T>;
label?: string;
};
const toCurrency = (value: number) =>
new Intl.NumberFormat("pt-BR", {
currency: "BRL",
style: "currency",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(value);
function InputCurrency<T extends FieldValues>({
name,
label,
...props
}: InputCurrencyProps<T>) {
const { watch, control } = useFormContext();
const formValue = watch(name);
const [value, setValue] = useReducer((_: string, next: string) => {
if (!next) return "";
const numericValue = Number(next.replace(/\D/g, "")) / 100;
return numericValue ? toCurrency(numericValue) : "";
}, "");
useEffect(() => {
setValue(formValue ? toCurrency(formValue) : "");
}, [formValue]);
return (
<FormField
control={control}
name={name}
render={({ field }) => (
<FormItem>
{label && <FormLabel>{label}</FormLabel>}
<FormControl>
<Input
id={name}
type="text"
{...props}
{...field}
onChange={(ev) => {
const inputValue = ev.target.value;
setValue(inputValue);
const numericValue =
Number(inputValue.replace(/\D/g, "")) / 100;
field.onChange(numericValue || 0);
}}
value={value}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
}
export default InputCurrency;
Adding my two cents: I suggest using the currency type in the input field for better usability:
import { cn } from "@/lib/utils";
import * as React from "react";
type InputType = React.HTMLInputTypeAttribute | "currency";
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
currencyFormat?: Intl.NumberFormat;
type?: InputType;
}
const defaultCurrencyFormat = new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type = "text", currencyFormat, onChange, onFocus, ...props }, ref) => {
const isCurrency = type === "currency";
const inputType = isCurrency ? "text" : type;
const formatCurrency = (value: number) => {
return (currencyFormat ?? defaultCurrencyFormat).format(value);
};
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
if (isCurrency) {
const target = e.currentTarget;
target.setSelectionRange(target.value.length, target.value.length);
}
onFocus?.(e);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (isCurrency) {
const target = e.currentTarget;
const numericValue = Number(target.value.replace(/\D/g, "")) / 100;
target.value = formatCurrency(numericValue);
}
onChange?.(e);
};
return (
<input
type={inputType}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm",
"ring-offset-background file:border-0 file:bg-transparent file:text-sm",
"file:font-medium file:text-foreground placeholder:text-muted-foreground",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
isCurrency && "text-end",
className,
)}
maxLength={isCurrency ? 22 : undefined}
onFocus={handleFocus}
onChange={handleChange}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = "Input";
export { Input };
usage:
<FormField
control={form.control}
name="balance"
render={({ field }) => (
<FormItem className="grid grid-cols-4 items-center gap-4 space-y-0">
<FormLabel className="text-right">Balance</FormLabel>
<FormControl>
<Input type="currency" {...field} className="col-span-3" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
Who is using react-native can do
<Controller
name="price"
control={control}
render={({ field }) => (
<Input
ref={field.ref}
onChangeText={(text) => {
const digits = text.replace(/\D/g, '');
const realValue = Number(digits) / 100;
field.onChange(realValue);
}}
value={moneyFormatter.format(field.value)}
keyboardType="numeric"
placeholder="R$ 50"
className="border-gray-200/20 bg-white-100 p-5 max-w-40"
/>
)}
/>
Funciona muito bem quando estás a introduzir os dados, mas a apagar ele não funciona