ui icon indicating copy to clipboard operation
ui copied to clipboard

How to add mask on input field?

Open gabrieldesiderio opened this issue 1 year ago • 2 comments

I'm trying to add masks to inputs using the shadcn/ui forms integration with react hook form and zod. However, the libraries I've found so far aren't working very well.

Has anyone implemented this?

gabrieldesiderio avatar Nov 01 '23 19:11 gabrieldesiderio

I came into a solution using maskito: https://maskito.dev/frameworks/react

And applying with the current Shadcn components would be something like: image

The masked fields on your schema should be strings to make this work (you could process those values after the handleSubmit or something)

DeveloperMatheus avatar Nov 15 '23 14:11 DeveloperMatheus

Here is an example for money mask https://gist.github.com/Sutil/5285f2e5a912dcf14fc23393dac97fed

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

And in your form:

<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}>
         save
        </Button>
      </form>
    </Form>

Sutil avatar Jan 12 '24 03:01 Sutil

This issue has been automatically closed because it received no activity for a while. If you think it was closed by accident, please leave a comment. Thank you.

shadcn avatar Feb 11 '24 23:02 shadcn

you can use: https://github.com/eduardoborges/use-mask-input

ramontavarez avatar Mar 16 '24 13:03 ramontavarez

you can use: https://github.com/eduardoborges/use-mask-input

Do you have an example? In my case, trying to use it on an Input with the ref={useMask('')} syntax says the types of ref are not compatible.

bduff9 avatar Mar 25 '24 19:03 bduff9

I made a wrapper.

https://gist.github.com/Sutil/5285f2e5a912dcf14fc23393dac97fed

Sutil avatar Mar 25 '24 22:03 Sutil

thanks @Sutil , based on your solution i built my own just extending the input with a callback.

import * as React from 'react';
import { cn } from '@/lib/utils';
import { Input } from './ui/input';

const moneyFormatter = Intl.NumberFormat("pt-BR", {
  currency: "BRL",
  currencyDisplay: "symbol",
  currencySign: "standard",
  style: "currency",
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
});

type CurrencyInputProps = {
  className?: string;
  initialValue?: string;
  onCallback?: Function;
};

export type InputProps = React.InputHTMLAttributes<HTMLInputElement> & CurrencyInputProps;

const CurrencyInput = React.forwardRef<HTMLInputElement, InputProps>(
  ({ className, initialValue = "", onCallback, ...props }, ref) => {
    const [value, setValue] = React.useReducer(
      (_: any, next: string) => {
        const digits = next.replace(/\D/g, "");
        return moneyFormatter.format(Number(digits) / 100);
      },
      initialValue
    );

    function handleChange(formattedValue: string) {
      const digits = formattedValue.replace(/\D/g, "");
      const realValue = Number(digits) / 100;
      onCallback && onCallback(realValue);
    }

    return (
      <Input
        ref={ref}
        className={cn('w-full', className)}
        {...props}
        value={value}
        onChange={(ev) => {
          setValue(ev.target.value);
          handleChange(ev.target.value);
        }}
      />
    );
  }
);

CurrencyInput.displayName = 'CurrencyInput';

export { CurrencyInput };

using

<FormField
            control={form.control}
            name="value"
            render={({ field }) => (
              <FormItem className="col-span-12 md:col-span-6 lg:col-span-4">
                <FormLabel>Valor</FormLabel>
                <FormControl>
                  <CurrencyInput {...field} onCallback={field.onChange} />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />

Gabrieldsantos96 avatar Aug 11 '24 23:08 Gabrieldsantos96