uNmAnNeR / imaskjs

vanilla javascript input mask
https://imask.js.org
MIT License
4.94k stars 256 forks source link

Is there a way to have IMaskMixin accept custom prop types in TypeScript? #654

Closed Asghwor closed 2 years ago

Asghwor commented 2 years ago

I'm trying to create a masked component around Material UI's TextField, using IMaskMixin, like so:

import { TextField } from "@mui/material";
import { IMaskMixin } from "react-imask";

export const MaskedTextField = IMaskMixin((props) => (
  <TextField {...props} />
))
<MaskedTextField mask="aaa" helperText="Error!" />

However, I get type errors in the component passed to IMaskMixin because there seems to be no way to get its props to include the type of the internal component.

image

I also get type errors when trying to pass props to the resulting component that belong to the internal component, even though it passes it properly when TypeScript is disabled using @ts-ignore:

image

Is there a way to have IMaskMixin and its resulting component accept generic types for props in addition to its defaults?

bradbyte commented 2 years ago

@Asghwor I have the exact same issue.. here's my temporary work around...

import { AnyMaskedOptions, MaskElement } from "imask";
import { ComponentType,} from "react";
import { IMaskMixin, IMask } from "react-imask";

// these are your MUI props or whatever custom component props you want.
type InputProps = {};

// these seem to be the defaults, you could extend as needed
type IMaskProps = IMaskInputProps<
  AnyMaskedOptions,
  false,
  string,
  MaskElement | HTMLTextAreaElement | HTMLInputElement
>;

export const MaskedInput: ComponentType<IMaskProps & InputProps> = IMaskMixin(({ inputRef, ...props }) => (
  <Input ref={inputRef} {...props} />
));
Asghwor commented 2 years ago

@bradbyte I also managed to create a workaround using nested components:

import { TextField, TextFieldProps } from "@mui/material";
import { ComponentProps } from "react";
import { IMaskMixin } from "react-imask";

const InternalMaskTextField = IMaskMixin((props) => (
  <TextField {...props as any}/>
))

type MaskProps = ComponentProps<typeof InternalMaskTextField>

export const MaskTextField = (props: TextFieldProps & MaskProps) => {
  return <InternalMaskTextField {...props} />
}

I did try your version, but I ended up getting a linting error on the ref prop.

It would be nice to have something less hacky and not having to use any, though.

RyKilleen commented 2 years ago

This is a pretty painful setup, and likely pretty common for anyone trying to integrate with react-hook-form useController.

I'd love to not have to mangle types while abstracting a MaskedInput component

hakankaan commented 2 years ago

@bradbyte I also managed to create a workaround using nested components:

import { TextField, TextFieldProps } from "@mui/material";
import { ComponentProps } from "react";
import { IMaskMixin } from "react-imask";

const InternalMaskTextField = IMaskMixin((props) => (
  <TextField {...props as any}/>
))

type MaskProps = ComponentProps<typeof InternalMaskTextField>

export const MaskTextField = (props: TextFieldProps & MaskProps) => {
  return <InternalMaskTextField {...props} />
}

I did try your version, but I ended up getting a linting error on the ref prop.

It would be nice to have something less hacky and not having to use any, though.

I get the error in the below. And react-hook-form does not work with this item. Do you have any suggestions?

react_devtools_backend.js:4026 Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

Check the render method of `EditHotel`.
    at MaskTextField

This is how i used it:

<MaskTextField
    label="Phone"
    variant="standard"
    error={errors.phone ? true : false}
    mask={"+{00}(000)000-0000"}
    overwrite
    {...register("phone")}
  />
EduardoLopes commented 2 years ago

After a lot of tests, i tried using hooks and i end up with this working code:

PS: Input here is a styled input using stitches

export type MaskedInputProps = {
  maskOptions: IMask.AnyMaskedOptions;
} & InputProps;

export const MaskedInput = forwardRef<HTMLInputElement, MaskedInputProps>(
  (props, ref) => {
    const { maskOptions, ...rest } = props;

    const [opts, setOpts] = useState<IMask.AnyMaskedOptions>(maskOptions);
    const { ref: inputMaskRef } = useIMask(opts);

    useEffect(() => {
      setOpts(maskOptions);
    }, [maskOptions]);

    // tick to make it work with react-hook-forms and useIMask
    function handleRefs(instance: HTMLInputElement | null) {
      if (ref) {
        if (typeof ref === "function") {
          ref(instance);
        } else {
          ref.current = instance;
        }
      }

      if (instance) {
        inputMaskRef.current = instance;
      }
    }

    return <Input ref={handleRefs} {...rest} />;
  }
);

MaskedInput.displayName = "MaskedInput";
RyKilleen commented 2 years ago

Here was my approach without hooks, though I very much like your approach:

import { ComponentProps, forwardRef } from 'react'
import { FieldValues, UseControllerProps } from 'react-hook-form'
import { IMaskInput } from 'react-imask'

import { styled } from '../stitches-config'

const StyledMaskedInput = styled(IMaskInput, { /** your styles here */})

export type MaskedInputProps = {
  mask: ComponentProps<typeof IMaskInput>['mask'] 
  unmask?: 'typed' | boolean
  placeholder?: string
} & UseControllerProps<FieldValues, any> &
  typeof StyledMaskedInput

const MaskedInput = forwardRef<
  HTMLInputElement,
  typeof StyledMaskedInput
>((props, ref) => {
  // @ts-ignore
  return <StyledMaskedInput ref={ref} {...props} />
})

export default MaskedInput
uNmAnNeR commented 2 years ago

you can do now

const MyInput = IMaskMixin<
  IMask.AnyMaskedOptions, // Mask options
  false, // Unmask?
  string, // value type
  HTMLInputElement, // wrapped element type
  { placeholder: string }, // here is your custom props
>(({ placeholder }) => (
  <input placeholder={placeholder} />
));

i have an idea to rearrange parameters of the template in v7.0 to do it like:

  1. element type
  2. mask type
  3. additional props
  4. Unmask?
  5. Value type
LuizHAP commented 1 year ago

After a lot of tests, i tried using hooks and i end up with this working code:

PS: Input here is a styled input using stitches

export type MaskedInputProps = {
  maskOptions: IMask.AnyMaskedOptions;
} & InputProps;

export const MaskedInput = forwardRef<HTMLInputElement, MaskedInputProps>(
  (props, ref) => {
    const { maskOptions, ...rest } = props;

    const [opts, setOpts] = useState<IMask.AnyMaskedOptions>(maskOptions);
    const { ref: inputMaskRef } = useIMask(opts);

    useEffect(() => {
      setOpts(maskOptions);
    }, [maskOptions]);

    // tick to make it work with react-hook-forms and useIMask
    function handleRefs(instance: HTMLInputElement | null) {
      if (ref) {
        if (typeof ref === "function") {
          ref(instance);
        } else {
          ref.current = instance;
        }
      }

      if (instance) {
        inputMaskRef.current = instance;
      }
    }

    return <Input ref={handleRefs} {...rest} />;
  }
);

MaskedInput.displayName = "MaskedInput";

I founded a fixed on typescript in your solution with casting a mutable ref on inputMaskRef

import IMask from 'imask'
import {
  ComponentProps,
  forwardRef,
  MutableRefObject,
  useEffect,
  useState,
} from 'react'
import { useIMask } from 'react-imask'
import { Input } from '../Input'

export type MaskedInputProps = {
  maskOptions: IMask.AnyMaskedOptions
} & ComponentProps<typeof Input>

export const MaskedInput = forwardRef<HTMLInputElement, MaskedInputProps>(
  (props, ref) => {
    const { maskOptions, ...rest } = props

    const [opts, setOpts] = useState<IMask.AnyMaskedOptions>(maskOptions)
    const { ref: IMaskInput } = useIMask(opts)
    const inputMaskRef = IMaskInput as MutableRefObject<HTMLInputElement>

    useEffect(() => {
      setOpts(maskOptions)
    }, [maskOptions])

    function handleRefs(instance: HTMLInputElement | null) {
      if (ref) {
        if (typeof ref === 'function') {
          ref(instance)
        } else {
          ref.current = instance
        }
      }

      if (instance) {
        inputMaskRef.current = instance
      }
    }

    return <Input ref={handleRefs} {...rest} />
  },
)
LuizHAP commented 1 year ago

I have a question, when I used this implementation above when I paste or use a browser history value on the field, the input doesn't recognize the value, only leaving the input. Why this happened @uNmAnNeR ?

I provided some codesandbox for this problem https://codesandbox.io/s/awesome-hooks-5i0r9d?file=/src/styles.css

Sunset-Kim commented 4 months ago

I solve ts error below code.

export const MaskNumberField: ComponentType<
  IMaskInputProps<HTMLInputElement> & NumberInputProps
> = IMaskMixin(({ inputRef, ...props }) => {
  return <NumberInput ref={inputRef} {...props} />
})