Как соединить react-input-mask и antd или какие есть альтернативы добавления маски к antd?
Всем доброго времени суток. Я много информации уже изучил по поводу соединения react-input-mask и antd.
использовал:
npm install react-input-mask@latest
npm i --save-dev @types/react-input-mask
Я пробовал все примеры от сюда https://github.com/sanniassin/react-input-mask/issues/139 Изучил https://github.com/ant-design/ant-design/issues/10580 Также просто все что мог находил на форумах. На них ответы были бесполезны.
Также пробовал npm install antd-mask-input --save
Но видимо из-за того, что библиотека уже не поддерживается - то не применяется измененный primaryColor для компонента-маски и не работает Compact для компонента-маски (и это только что я заметил, возможно больше еще нерабочих моментов).
Может у кого есть на текущий момент рабочее решение, чтобы хорошо стыковалось с antd. Поделитесь информацией, может ссылку на обсуждение или какие-то варианты добавления маски к antd или все-таки самому писать велосипед?
Ответы (2 шт):
Хочу поделиться успехами соединения react-input-mask и antd (хотя они не прям успешные)
Перечислю, что я сделал:
Скачал необходимые пакеты
npm install react-input-mask@latest
npm i --save-dev @types/react-input-mask
Пометка: на данный момент последней стабильной версией react-input-mask является версия 2.0.4. Я делал именно с ней. Есть еще версия 3.0.0-alpha.2, с ней мой код уже не работает.
Далее я сделал базовый компонент MaskedInput:
import { forwardRef, ReactNode } from 'react';
import { Input, InputProps } from 'antd';
import { Props } from 'react-input-mask';
import InputMask from 'react-input-mask';
type AllowedValue = string | number | readonly string[] | undefined;
type StrictOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export type MaskedInputProps = StrictOmit<InputProps, 'size' | 'prefix' | 'value' | 'onChange'> &
Props & {
mask: string;
size?: InputProps['size'];
prefix?: ReactNode;
value: AllowedValue;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
};
export const MaskedInput = forwardRef<HTMLInputElement, MaskedInputProps>(
({ mask, size, prefix, ...rest }, ref) => {
return (
<InputMask mask={mask} {...rest}>
{(inputProps) => <Input {...inputProps} size={size} prefix={prefix} ref={ref} />}
</InputMask>
);
},
);
Пометка: я сделал value и onChange обязательными, потому что без их определения маска работала совсем некорректно. Причину такого поведения я не выяснил.
Я планировал делать обертки конкретных масок над MaskedInput. Вот пример с номером телефона:
import { FC, useState } from 'react';
import { StrictOmit } from '@/shared/utils';
import { MaskedInput, MaskedInputProps } from './MaskedInput';
type StrictOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export interface PhoneInputProps
extends StrictOmit<MaskedInputProps, 'mask' | 'placeholder' | 'value' | 'onChange'> {
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
}
export const PhoneInput: FC<PhoneInputProps> = ({
defaultValue,
onChange,
...maskedInputProps
}) => {
const [value, setvalue] = useState(defaultValue);
return (
<MaskedInput
{...maskedInputProps}
mask="+7 (999) 999-99-99"
placeholder="+7 (___) ___-__-__"
value={value}
onChange={(event) => {
setvalue(event.target.value);
onChange?.(event);
}}
/>
);
};
В целом это работает, НО остались проблемы:
- при удалении символа, когда каретка стоит перед символом маски, происходит перемещение каретки назад на символ маски и + на символ значения. Например введено значение
+7 (123) 456-78-97
и при удержании backspace останется
+7 (368) ___-__-__
то есть как раз цифры перед символами маски: "3)", "6-" и "8-";
- когда маска уже заполнена полностью и если все равно еще набирать цифры, то визуально ничего не будет происходить, но в значении Input вводится лишнее число в конце и получается вместо +7 (123) 456-78-97 на одну цифру больше +7 (123) 456-78-977. Это и забирается Form из antd (неправильное значение).
С этими проблемами мне не удалось справиться. Если у кого-то получится их решить, то я думаю этим можно будет пользоваться.
Далее я перешел все-таки на antd-mask-input и попытался решить те проблемы, которые написал в вопросе и дополнить его еще функционалом.
Для работы с antd-mask-input я использовал следующее:
npm install antd-mask-input
npm antd-style
Пометка: "antd-mask-input" на данный момент версии "2.0.7". "antd-style" на данный момент версии "3.7.1"
Сделал свою обертку над MaskedInput из antd-mask-input, которая решает для меня следующие проблемы:
- Применяет измененные стили в конфигурации antd (с помощью useStylesMaskInput);
- Начинает корректно работать с Compact из antd (с помощью useStylesMaskInput);
- Если остается только маска без полезных данных, то возвращается пустое значение в форму (с помощью handleChange в котором обнуляется возвращаемое значение);
- Добавил
onChangeUnmaskedValueдля того, чтобы читать значение без маски извне. Также сделал параметрonChangeUnMaskкоторый для обработчикаonChangeбудет возвращать для формы значение без маски.
Код компонента:
import { FC } from 'react';
import MaskedInput from 'antd-mask-input';
import { MaskedInputProps } from 'antd-mask-input/build/main/lib/MaskedInput';
import { useStylesMaskInput } from './useStylesMaskInput';
type OnChangeEvent = Parameters<NonNullable<MaskedInputProps['onChange']>>[0];
interface MaskInputProps extends MaskedInputProps {
onChangeUnmaskedValue?: (value: string) => void;
onChangeUnMask?: boolean;
}
export const MaskInput: FC<MaskInputProps> = ({
onChangeUnmaskedValue,
onChangeUnMask,
onChange,
className,
...maskedInputProps
}) => {
const { styles, cx } = useStylesMaskInput();
const handleChange = (event: OnChangeEvent) => {
if (event.unmaskedValue.length === 0) {
const clearedEvent: OnChangeEvent = {
...event,
target: {
...event.target,
value: '',
},
maskedValue: '',
unmaskedValue: '',
};
onChange?.(clearedEvent);
onChangeUnmaskedValue?.('');
} else {
if (onChangeUnMask) {
const updateEvent: OnChangeEvent = {
...event,
target: {
...event.target,
value: event.unmaskedValue,
},
};
onChange?.(updateEvent);
} else {
onChange?.(event);
}
onChangeUnmaskedValue?.(event.unmaskedValue);
}
};
return (
<MaskedInput
{...maskedInputProps}
onChange={handleChange}
className={cx(styles.maskInput, className)}
/>
);
};
Код хука useStylesMaskInput:
import { createStyles } from 'antd-style';
export const useStylesMaskInput = createStyles(({ token }) => ({
maskInput: {
borderRadius: token.borderRadiusLG,
border: `${token.lineWidth}px ${token.lineType} ${token.colorBorder}`,
overflow: 'hidden',
transition: `border-color ${token.motionDurationMid} ${token.motionEaseInOut}`,
'&:hover': {
borderColor: token.colorPrimaryHover,
},
'&:focus-within': {
borderColor: token.colorPrimary,
boxShadow: `0 0 0 ${token.controlOutlineWidth}px ${token.controlOutline}`,
transition: `box-shadow ${token.motionDurationMid} ${token.motionEaseInOut}`,
},
'.ant-space-compact &': {
'&:not(:last-child)': {
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
marginRight: -1,
},
'&:first-child:not(:last-child)': {
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
},
'&:last-child': {
borderTopRightRadius: token.borderRadiusLG,
borderBottomRightRadius: token.borderRadiusLG,
},
'&:hover': {
zIndex: 2,
},
'&:focus': {
zIndex: 2,
},
},
},
}));
Также узнал про настройку lazy, которая мне видится полезной. Под капотом antd-mask-input использует библиотеку imask (можно обратиться к документации, чтобы лучше понимать как можно настраивать MaskedInput из antd-mask-input или обертку MaskInput). В библиотеке есть параметр lazy, который скрывает маску, если она пустая. Для использования в компонентах - этот параметр можно задать через maskOptions:
<MaskedInput
maskOptions={{ lazy: true }}
/>
Еще мне не нравился такой момент: при параметре lazy=false, когда поле находится не в фокусе и в поле не введены полезные значения (только маска), создается ощущение, что как будто бы поле не пустое. Для меня более предпочтительно выглядит, если полезных данных нет в поле, то и не стоит отображать маску (лучше отобразить placeHolder). Такого поведения тоже удалось достичь, немного изменив компонент:
import { FC, useEffect, useRef, useState } from 'react';
import MaskedInput from 'antd-mask-input';
import { MaskedInputProps } from 'antd-mask-input/build/main/lib/MaskedInput';
import { useStylesMaskInput } from './useStylesMaskInput';
type OnChangeEvent = Parameters<NonNullable<MaskedInputProps['onChange']>>[0];
interface MaskInputProps extends MaskedInputProps {
onChangeUnmaskedValue?: (value: string) => void;
onChangeUnMask?: boolean;
}
export const MaskInput: FC<MaskInputProps> = ({
onChangeUnmaskedValue,
onChangeUnMask,
mask,
onChange,
onBlur,
onFocus,
className,
...maskedInputProps
}) => {
const { styles, cx } = useStylesMaskInput();
const isLazy = maskedInputProps.maskOptions?.lazy ?? true;
const initialMask = useRef(mask);
const [currentMask, setCurrentMask] = useState(mask);
const unmaskedValueRef = useRef<string | undefined>(undefined);
useEffect(() => {
unmaskedValueRef.current = maskedInputProps.value || maskedInputProps.defaultValue;
if (!isLazy && !unmaskedValueRef.current) {
setCurrentMask('');
}
}, []);
const handleChange = (event: OnChangeEvent) => {
unmaskedValueRef.current = event.unmaskedValue;
if (event.unmaskedValue.length === 0) {
const clearedEvent: OnChangeEvent = {
...event,
target: {
...event.target,
value: '',
},
maskedValue: '',
unmaskedValue: '',
};
onChange?.(clearedEvent);
onChangeUnmaskedValue?.('');
} else {
if (onChangeUnMask) {
const updateEvent: OnChangeEvent = {
...event,
target: {
...event.target,
value: event.unmaskedValue,
},
};
onChange?.(updateEvent);
} else {
onChange?.(event);
}
onChangeUnmaskedValue?.(event.unmaskedValue);
}
};
const handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
if (!isLazy && !unmaskedValueRef.current) {
setCurrentMask('');
}
onBlur?.(event);
};
const handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
if (!isLazy && !unmaskedValueRef.current) {
setCurrentMask(initialMask.current);
}
onFocus?.(event);
};
return (
<MaskedInput
{...maskedInputProps}
mask={currentMask}
onChange={handleChange}
onBlur={handleBlur}
onFocus={handleFocus}
className={cx(styles.maskInput, className)}
/>
);
};
Буду рад вашей обратной связи по поводу багов или возможных улучшений этого компонента-обертки
что то сложно у тебя получается :)
Вот моя реализация на скорую руку, нужно допинать не без косяков
'use client'
import { Input, InputProps, InputRef } from 'antd'
import IMask, { InputMask } from 'imask'
import React, { useEffect, useRef } from 'react'
export interface MaskInputProps extends InputProps {
mask: string
}
export const MaskInput: React.FC<MaskInputProps> = (props) => {
const { mask, onChange, ...anyProps } = props
const inputRef = useRef<InputRef>(null)
const maskRef = useRef<InputMask>(null)
useEffect(() => {
if (inputRef.current?.input) {
const _mask = IMask(inputRef.current.input, { mask })
maskRef.current = _mask
return () => {
_mask.destroy()
}
}
}, [mask])
const handleChange: InputProps['onChange'] = (event) => {
if (maskRef.current) {
maskRef.current.updateValue()
}
// console.log('@@ maskRef.current', maskRef.current?.unmaskedValue)
onChange?.(event)
}
return <Input {...anyProps} ref={inputRef} onChange={handleChange} />
}