GoncharukOrg / react-input

100 stars 7 forks source link

Creating a generic MUI TextField that accepts mask and replacement #7

Closed lisandro52 closed 1 year ago

lisandro52 commented 1 year ago

Hi! First of all, thank you for your library, it's the first one that I see that has a simple yet powerful solution for masking an input.

I'm having a problem, though, with using the MUI TextField with react-input/mask. I'm sorry I don't have a codesandbox, but I couldn't install @react-input/mask there. I've tried using your example from the docs, where you pass a CustomInputMask, but the problem is that I also need to pass the properties mask and replacement, because I don't want my dev-user to directly work with InputMask, I just want them to give me the mask and I'll handle the rest.

I tried defining the function right there but it's obviously re-rendering every time and it's impossible to type. I've also tried using a useMemo to create the CustomInputMask only when either the mask or replacement changes (using json-deterministic-stringify to avoid passing an object as a dependency), but that didn't work either.

Finally I tried simply using the useMask hook and passing down the inputRef to the TextField, and that's working ok! But for some reason, there is a console.error saying that the inputRef is null, and it doesn't look good...

This is my latest working code

export const TextFieldWithRef = forwardRef(
  (
    { loading, mask, replacement, ...inputProps }: TextFieldProps,
    ref: ForwardedRef<HTMLInputElement>
  ) => {
    const maskRef = useMask({
      mask,
      replacement,
    });

    return (
      <MuiTextField
        variant="standard"
        fullWidth
        {...inputProps}
        ref={ref}
        inputRef={maskRef}
        autoComplete="off"
        disabled={inputProps.disabled || loading}
        InputProps={{
          ...inputProps.InputProps,
          readOnly: inputProps.readOnly || loading,
        }}
      />
    );
  }
);

If you need more info, just let me know. Thanks!

GoncharukBro commented 1 year ago

I reproduced the hook example and didn't see an error in the console, but you can use the following Material UI integrations to be able to set the mask in an external component:

1) Hook

import { forwardRef } from "react";

import { useMask, MaskProps } from "@react-input/mask";
import TextField, { TextFieldProps } from "@mui/material/TextField";

type InputProps = TextFieldProps &
   Pick<MaskProps, "mask" | "replacement"> & { loading?: boolean };

const Input = forwardRef<HTMLDivElement, InputProps>(
   (
     {
       loading=false
       disabled=false
       InputProps,
       mask,
       replacement,
       ...props
     },
     ref
   ) => {
     const maskRef = useMask({ mask, replacement });

     return (
       <TextField
         ref={ref}
         fullWidth
         variant="standard"
         autocomplete="off"
         disabled={disabled || loading}
         inputRef={maskRef}
         InputProps={{
           ...InputProps,
           readOnly: InputProps?.readOnly || loading
         }}
         {...props}
       />
     );
   }
);

export default function App() {
   return <Input mask="___-___" replacement="_" />;
}

2) Input Component

import { forwardRef } from "react";

import { InputMask, MaskProps } from "@react-input/mask";
import TextField, { TextFieldProps } from "@mui/material/TextField";

type InputProps = TextFieldProps &
   Pick<MaskProps, "mask" | "replacement"> & { loading?: boolean };

const Input = forwardRef<HTMLDivElement, InputProps>(
   (
     {
       loading=false
       disabled=false
       InputProps,
       mask,
       replacement,
       ...props
     },
     ref
   ) => {
     return (
       <TextField
         ref={ref}
         fullWidth
         variant="standard"
         autocomplete="off"
         disabled={disabled || loading}
         InputProps={{
           ...InputProps,
           readOnly: InputProps?.readOnly || loading,
           inputComponent: InputMask,
           inputProps: { mask, replacement }
         }}
         {...props}
       />
     );
   }
);

export default function App() {
   return <Input mask="___-___" replacement="_" />;
}

The organization of properties is up to you, but the principle of use remains the same.

Thanks for highlighting the inaccurate point from the documentation, I'll be sure to update the README to make using the library easier.

Please provide feedback if possible.

lisandro52 commented 1 year ago

Hi! Thank you so much for replying to my issue. When I saw that you couldn't reproduce the error, I tried creating a clean repo to showcase the error but... I can't actually replicate it, so I'm guessing there is something else here.

One crucial bit of info I missed on my original issue is that I'm also using react-hook-form together with the rest, and RHF is the one sending a ref... but again, it's odd, because the ref from RHF is the one that gets passed to the TextField and it has nothing to do with the inputRef or the useMask hook...

I think it has to do with the React.StrictMode and some other library, like react-router for example - while mounting/unmounting, the current ref gets lost somehow and it triggers that warning . Thankfully, it won't appear on production but it still is odd.

Anyway, while trying to replicate the error I found another thing: if you make the parameters mask and replacement optionals, and you pass undefined, i.e. you don't use the props, the input stops working completely. I had to do a hack to stop using the maskRef in case there is no mask defined.

inputRef={mask === undefined ? undefined : maskRef}

Is this the intended functionality?

lisandro52 commented 1 year ago

I've now found the problem. I think it was, partially, due to my hack above. The ref is never set, so it never leaves the 'null' state and it throws the warning on the console. If I remove my hack, the warning doesn't appear, but the input stops working because there is no mask... It was all connected after all.

GoncharukBro commented 1 year ago

Please send me a complete code example so I can reproduce the problem.

lisandro52 commented 1 year ago

Please send me a complete code example so I can reproduce the problem.

I could finally reproduce it on my brand new repo. Here is the link, and thank you again for taking the time. https://github.com/lisandro52/react-input-bug

yarn && yarn dev

GoncharukBro commented 1 year ago

I looked at your code and there is no error in it. The component or hook only uses formatted input, which means that if you don't pass in a mask value, then the (default) mask will be an empty string, which provides no replacement characters, meaning no input will be taken.

If you have a need to enter any character without passing a mask, you must check for the presence of the mask and substitute either the mask input component or the arbitrary input component. You have already used a similar approach in HackedTextWithHook.

Tell me, do you see the implementation, in which arbitrary input will be carried out without a mask, useful?

lisandro52 commented 1 year ago

If you have a need to enter any character without passing a mask, you must check for the presence of the mask and substitute either the mask input component or the arbitrary input component. You have already used a similar approach in HackedTextWithHook.

Yes, exactly, I think the solution should be this, but in development mode, there is an error popping up on the console and it's distracting. It's exactly this line that pops up.

Just making the console.error into a console.warn would be enough.

GoncharukBro commented 1 year ago

Perhaps using warn instead of error would actually be more appropriate. Thanks for the feedback!