taiga-family / maskito

Collection of libraries to create an input mask which ensures that user types value according to predefined format.
https://maskito.dev
Apache License 2.0
1.42k stars 33 forks source link

📚 - Using maskito with react-hook-form #1189

Closed ilterugur closed 1 month ago

ilterugur commented 7 months ago

What is the affected URL?

No response

Description

Hello, first of all, thanks for the library. If I want to use use this library's react adapter with react-hook-form, what should I do? They both utilize ref props and we can give only one ref value to inputs. Or if I want to use a regular ref object on an input and also use maskito on that, how can I pass my own ref value while using maskito's ref callback value? Only way I found is giving maskito ref to a parent element and using elementPredicate option on useMaskito hook but it doesn't feel like it's the right way

Which browsers have you used?

Which operating systems have you used?

nextZed commented 6 months ago

Hey there! Since ref prop is a callback, you can do something like this:

<YourAmazingInput
  ref={node => {
    maskitoRefCallback(node)
    myInputRef.current = node
  }}
/>
mauriciocrecencio commented 6 months ago

Hey guys,

Is there some way to display default value with mask applied when using react-hook-form?

I'm trying to use Maskito with currency mask and react-hook-form for manage my form state, but initial state is the raw number, and the input is only numbers without $ or . ,

Example: my input has initial value 12000 and it really display 12000, but only when start typing that mask are applied CleanShot 2024-04-23 at 20 58 25@2x

CleanShot 2024-04-23 at 20 57 41@2x

nsbarsukov commented 6 months ago

Hey guys,

Is there some way to display default value with mask applied when using react-hook-form?

I'm trying to use Maskito with currency mask and react-hook-form for manage my form state, but initial state is the raw number, and the input is only numbers without $ or . ,

Example: my input has initial value 12000 and it really display 12000, but only when start typing that mask are applied CleanShot 2024-04-23 at 20 58 25@2x

CleanShot 2024-04-23 at 20 57 41@2x

@mauriciocrecencio

Use maskitoInitialCalibrationPlugin or maskitoTransform

wladpaiva commented 6 months ago

Worked like a charm

const maskitoOptions: MaskitoOptions = {
  mask: /^\d+$/,
}

function Input({onChange, ...props}: React.ComponentProps<'input'>){
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    event.currentTarget.value = maskitoTransform(
      event.currentTarget.value,
      maskitoOptions,
    )

    onChange?.(event)
  }

  return <input onChange={handleChange} {...props} />
}
mykola-kolomoyets commented 2 months ago

Faced the refs conflict issue (such a delicate case for react XD ), because the useMaskito hooks returns ref. And using react-hook-form means accepting the ref as well for acceptable form state management.

Here is the thing.

I have the form input field (shadcn-ui form layer is being used, but does not matter, taking into account that we use react-hook-form):

<FormField
    control={form.control}
    name="purchasePrice"
    render={({ field }) => (
        <FormItem className="col-span-2">
            <div className="inline-flex items-center gap-1">
                <FormLabel>Purchase Price</FormLabel>
                <HelpTooltip Icon={CircleAlertIcon}>
                    // ...
                </HelpTooltip>
            </div>
            <FormControl>
                <CurrencyInput
                    placeholder="e.g $ 1.650"
                    value={field.value}
                    onChange={(event) => {
                        // react-hook-form schema requires number here, so I am parsing it and convert to numeric value
                        field.onChange(parseInt(event.target.value) || 0);
                    }}
                />
            </FormControl>
            <FormMessage />
        </FormItem>
    )}
/>

The <CurrencyInput/> components has such code:

const CurrencyInput: React.ForwardRefRenderFunction<HTMLInputElement, React.ComponentPropsWithoutRef<'input'>> = (
    props,
   // Forwarded ref here
    ref
) => {
   // gnerated ref here
    const currencyInputRef = useMaskito({
        options: CURRENCY_INPUT_MASKITO_OPTIONS,
    });

    return (
        <Input
            ref={
                // What should i pass here with my 2 refs, and both are important?
            }
            {...props}
        />
    );
};

So it is impossible to use the library in this way. WOuld it be possible to return not the ref as an option, but the props slice for this?

waterplea commented 2 months ago

Man, if only react had directives...

nextZed commented 1 month ago

@mykola-kolomoyets hey! Did you try solution that I provided above? Did it not work for some reason? In your case it would look something like this:

ref={node => {
  currencyInputRef(node)
  ref.current = node
}}
leverage-analytics commented 1 month ago

Hi, I'm trying to accomplish something similar but using the Controller implementation of react-hook-form with Maskito and Material UI.

I have the mask working when I am interacting with the field, but as soon as I tab away to the next field, the mask disappears, and the value returns to 0. It also does not appear when the form is first loaded. Any ideas?

Here is my implementation:

import { TextField } from "@mui/material";
import { Controller, RegisterOptions } from "react-hook-form";
import { MaskitoOptions } from "@maskito/core";
import { useMaskito } from "@maskito/react";

type InputTextProps = {
  name: string;
  label: string;
  control: any;
  mask: MaskitoOptions;
  options?: RegisterOptions;
  value?: string | number;
  required?: boolean;
  type?: "text"
};

export const FormMaskedInputText = ({
  name,
  label,
  control,
  mask,
  options,
  required,
}: InputTextProps): JSX.Element => {
  const maskitoRef = useMaskito({ options: mask });

  return (
    <Controller
      name={name}
      control={control}
      rules={options}
      render={({
        field: { onChange, onBlur, ref, value, name },
        fieldState: { error },
      }) => (
        <TextField
          helperText={error ? error.message : null}
          error={!!error}
          name={name}
          onChange={onChange}
          onBlur={onBlur}
          inputRef={(el: HTMLElement | null) => {
            maskitoRef(el);
            ref(el);
          }}
          label={label}
          required={required || true}
          value={value}
        />
      )}
    />
  );
};

And then rendering with:

import { SubmitHandler, useForm } from "react-hook-form";
import Box from "@mui/material/Box";
import Grid2 from "@mui/material/Grid2";
import { maskitoNumberOptionsGenerator } from "@maskito/kit";

import { FormMaskedInputText } from "./FormMaskedInputText";

const type Demo = {
  latitude: number;
  longitude: number;
}

export const DemoForm = ({
  data,
  onSubmit,
}: {
  data: Demo;
  onSubmit: SubmitHandler<Demo>;
}): JSX.Element => {
  const form = useForm<Demo>({
    defaultValues: data,
  });

  const latitudeMask = maskitoNumberOptionsGenerator({
    precision: 4,
    decimalZeroPadding: true,
    min: -90,
    max: 90,
  });

  const longitudeMask = maskitoNumberOptionsGenerator({
    precision: 4,
    decimalZeroPadding: true,
    min: -180,
    max: 180,
  });

  return (
    <Box component="form" onSubmit={form.handleSubmit(onSubmit)}>
      <Grid2 container>
        <Grid2>
          <FormMaskedInputText
            name="latitude"
            label="Latitude"
            control={form.control}
            mask={latitudeMask}
          />
        </Grid2>
        <Grid2>
          <FormMaskedInputText
            name="longitude"
            label="Longitude"
            control={form.control}
            mask={longitudeMask}
          />
        </Grid2>
        <input type="submit" />
      </Grid2>
    </Box>
  );
};
leverage-analytics commented 1 month ago

@nextZed Hey - did you have any ideas on the use case I shared? Thanks!

nextZed commented 1 month ago

@leverage-analytics could you please provide a stackblitz with example above?

leverage-analytics commented 1 month ago

@nextZed sure! Here is a demo!. Let me know if you have any questions. I appreciate your help!

somativa-victor-formisano commented 1 month ago

I'm also facing this issue, did anyone made any progress?

nextZed commented 1 month ago

@leverage-analytics @somativa-victor-formisano When you use a controlled input, you need to pass onInput instead of onChange. More on the docs "Controlled masked input". Here's working example.

Hope this helps!

waterplea commented 1 month ago

Should we add a big ass warning at the top of React page to use proper onInput in 2k24 if using the onChange is still so prominent in React community?

leverage-analytics commented 1 month ago

@nextZed that's great! Thank you so much for your help.

Just one quick follow-up question. Based on the discussion above, should the implementation we're discussing now mask the default form values? That's the only part that's still not working as desired for me.

waterplea commented 1 month ago

No, it only masks the value as user edits it by default. You can use maskitoTransform helper function or a maskitoInitialCalibrationPlugin: https://maskito.dev/core-concepts/plugins#initial-calibration

The reason we have it like that is imagine the following scenario: A user is applying for a loan and enters a date within the limits of the form. Then they save it as a draft and open it in a few days. The limits would be different and the date user entered might fall out of them. If we automatically change it to the first available date (which min/max params do in date mask as you type it) — we would have altered the users input without them noticing. That could be very bad if the data is sensitive. So by default we do not change any data unless triggered by user input, but it's easy to enable by adding the plugin provided out of the box.