Files
router-lists-ui/frontend/src/components/ValidatedInput.jsx
T

178 lines
5.2 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useRef } from 'react'
import { IconCheck, IconX, IconAlertCircle } from '@tabler/icons-react'
/**
* ValidatedInput - input с real-time валидацией
* @param {function} validate - функция валидации, возвращает { valid: boolean, message: string }
* @param {number} debounce - задержка перед валидацией в ms
*/
function ValidatedInput({
value,
onChange,
validate,
debounce = 300,
label,
placeholder,
hint,
required = false,
type = 'text',
className = '',
showValidIcon = true,
validateOnChange = true,
...props
}) {
const [validationState, setValidationState] = useState({
valid: null,
message: '',
isValidating: false
})
const [isTouched, setIsTouched] = useState(false)
const timeoutRef = useRef(null)
useEffect(() => {
// Не валидируем, пока пользователь не начал вводить
if (!isTouched || !validateOnChange) return
if (!validate) {
setValidationState({ valid: null, message: '', isValidating: false })
return
}
// Очищаем предыдущий таймер
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
// Показываем индикатор валидации
setValidationState(prev => ({ ...prev, isValidating: true }))
// Запускаем валидацию с debounce
timeoutRef.current = setTimeout(async () => {
try {
const result = validate(value)
// Поддержка как синхронной, так и асинхронной валидации
const validationResult = result instanceof Promise ? await result : result
setValidationState({
valid: validationResult.valid,
message: validationResult.message || '',
isValidating: false
})
} catch (error) {
setValidationState({
valid: false,
message: 'Ошибка валидации',
isValidating: false
})
}
}, debounce)
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
}
}, [value, validate, debounce, isTouched, validateOnChange])
const handleChange = (e) => {
if (!isTouched) setIsTouched(true)
onChange(e)
}
const handleBlur = () => {
setIsTouched(true)
// Запускаем валидацию сразу при потере фокуса
if (validate && !validateOnChange) {
const result = validate(value)
const validationResult = result instanceof Promise ? result.then(r => {
setValidationState({
valid: r.valid,
message: r.message || '',
isValidating: false
})
}) : setValidationState({
valid: result.valid,
message: result.message || '',
isValidating: false
})
}
}
const getInputClass = () => {
const classes = ['form-control']
if (isTouched && validationState.valid === true) {
classes.push('is-valid')
} else if (isTouched && validationState.valid === false) {
classes.push('is-invalid')
}
return classes.join(' ')
}
return (
<div className={`mb-3 ${className}`}>
{label && (
<label className="form-label">
{label}
{required && <span className="text-danger ms-1">*</span>}
</label>
)}
<div className="input-group">
<input
type={type}
className={getInputClass()}
value={value}
onChange={handleChange}
onBlur={handleBlur}
placeholder={placeholder}
aria-invalid={validationState.valid === false}
aria-describedby={`${props.id}-feedback ${props.id}-hint`}
{...props}
/>
{/* Индикатор валидации */}
{showValidIcon && isTouched && !validationState.isValidating && (
<span className="input-group-text">
{validationState.valid === true && (
<IconCheck size={18} className="text-success" />
)}
{validationState.valid === false && (
<IconX size={18} className="text-danger" />
)}
</span>
)}
{/* Индикатор процесса валидации */}
{validationState.isValidating && (
<span className="input-group-text">
<span className="spinner-border spinner-border-sm" role="status"></span>
</span>
)}
</div>
{/* Подсказка */}
{hint && !validationState.message && (
<small id={`${props.id}-hint`} className="form-hint text-muted">
{hint}
</small>
)}
{/* Сообщение об ошибке */}
{isTouched && validationState.message && (
<div
id={`${props.id}-feedback`}
className={`invalid-feedback d-block ${validationState.valid ? 'text-success' : 'text-danger'}`}
>
<IconAlertCircle size={14} className="me-1" />
{validationState.message}
</div>
)}
</div>
)
}
export default ValidatedInput