UNPKG

@syncfusion/react-inputs

Version:

Syncfusion React Input package is a feature-rich collection of UI components, including Textbox, Textarea, Numeric-textbox and Form, designed to capture user input in React applications.

562 lines (561 loc) 20.4 kB
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime"; import * as React from 'react'; import { forwardRef, useEffect, useCallback, useRef, useImperativeHandle, createContext } from 'react'; import { L10n, preRender, useProviderContext } from '@syncfusion/react-base'; const VALIDATION_REGEX = { EMAIL: /^(?!.*\.\.)[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/, // eslint-disable-next-line security/detect-unsafe-regex URL: /^(https?:\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(\/[^\s]*)?$/, DATE_ISO: /^([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/, DIGITS: /^[0-9]*$/, PHONE: /^[+]?[0-9]{9,13}$/, CREDIT_CARD: /^\d{13,16}$/ }; const FormContext = createContext(null); const FormProvider = FormContext.Provider; /** * Provides a form component with built-in validation functionality. Manages form state tracking, * field validation, and submission handling. * * ```typescript * import { Form, FormField, FormState } from '@syncfusion/react-inputs'; * * const [formState, setFormState] = useState<FormState>(); * * <Form * rules={{ username: { required: [true, 'Username is required'] } }} * onSubmit={data => console.log(data)} * onFormStateChange={setFormState} > * <FormField name="username"> * <input * name="username" * value={(formState?.values.username || '') as string} * onChange={(e) => formState?.onChange('username', { value: e.target.value })} * onBlur={() => formState?.onBlur('username')} * onFocus={() => formState?.onFocus('username')} * /> * {formState?.errors?.username && (<div className="error">{formState.errors.username}</div>)} * </FormField> * <button type="submit">Submit</button> * </Form> * ``` */ export const Form = forwardRef((props, ref) => { const { rules, onSubmit, onReset, children, onFormStateChange, initialValues = {}, validateOnChange = false, className = '', ...otherProps } = props; const formRef = useRef(null); const { locale, dir } = useProviderContext(); const stateRef = useRef({ values: { ...initialValues }, errors: {}, touched: {}, visited: {}, modified: {}, submitted: false, validated: {} }); const notifyStateChange = useCallback(() => { if (onFormStateChange) { formStateRef.current = getFormState(); onFormStateChange(formStateRef.current); } }, [onFormStateChange]); const setFieldValue = useCallback((field, value) => { stateRef.current = { ...stateRef.current, values: { ...stateRef.current.values, [field]: value }, modified: { ...stateRef.current.modified, [field]: true } }; }, []); const setFieldTouched = useCallback((field) => { stateRef.current = { ...stateRef.current, touched: { ...stateRef.current.touched, [field]: true } }; }, []); const setFieldVisited = useCallback((field) => { stateRef.current = { ...stateRef.current, visited: { ...stateRef.current.visited, [field]: true } }; }, []); const setFieldError = useCallback((field, error) => { const errors = { ...stateRef.current.errors }; if (error) { errors[field] = error; } else { delete errors[field]; } stateRef.current = { ...stateRef.current, errors }; }, []); const setSubmitted = useCallback((value) => { stateRef.current = { ...stateRef.current, submitted: value }; }, []); const resetForm = useCallback((values) => { stateRef.current = { values, errors: {}, touched: {}, visited: {}, modified: {}, submitted: false, validated: {} }; notifyStateChange(); }, [notifyStateChange]); const touchAllFields = useCallback(() => { const allTouched = {}; Object.keys(stateRef.current.values).forEach((field) => { allTouched[field] = true; }); stateRef.current = { ...stateRef.current, touched: allTouched }; }, []); const visitAllFields = useCallback(() => { const allVisited = {}; Object.keys(stateRef.current.values).forEach((field) => { allVisited[field] = true; }); stateRef.current = { ...stateRef.current, visited: allVisited }; }, []); const setBulkErrors = useCallback((errors) => { const newErrors = { ...stateRef.current.errors }; Object.entries(errors).forEach(([field, error]) => { if (error) { newErrors[field] = error; } else { delete newErrors[field]; } }); stateRef.current = { ...stateRef.current, errors: newErrors }; notifyStateChange(); }, [notifyStateChange]); const rulesRef = useRef(rules); const formStateRef = useRef(null); const l10nRef = useRef(null); const defaultErrorMessages = { required: 'This field is required.', email: 'Please enter a valid email address.', url: 'Please enter a valid URL.', date: 'Please enter a valid date.', dateIso: 'Please enter a valid date (ISO).', creditCard: 'Please enter valid card number.', number: 'Please enter a valid number.', digits: 'Please enter only digits.', maxLength: 'Please enter no more than {0} characters.', minLength: 'Please enter at least {0} characters.', rangeLength: 'Please enter a value between {0} and {1} characters long.', range: 'Please enter a value between {0} and {1}.', max: 'Please enter a value less than or equal to {0}.', min: 'Please enter a value greater than or equal to {0}.', regex: 'Please enter a correct value.', tel: 'Please enter a valid phone number.', equalTo: 'Please enter the same value again.' }; const registeredFields = useRef({}); const registerField = (fieldName) => { registeredFields.current[fieldName] = true; }; useEffect(() => { l10nRef.current = L10n('formValidator', defaultErrorMessages, locale); return () => { l10nRef.current = null; }; }, [locale]); useEffect(() => { notifyStateChange(); validateInitialValues(); preRender('formValidator'); return () => { l10nRef.current = null; rulesRef.current = {}; registeredFields.current = {}; if (formRef.current) { formRef.current = null; } formStateRef.current = null; if (onFormStateChange) { onFormStateChange(formStateRef.current); } }; }, []); useEffect(() => { rulesRef.current = rules; }, [rules]); const validateInitialValues = () => { if (Object.keys(initialValues).length > 0) { const errors = {}; for (const fieldName in initialValues) { if (Object.prototype.hasOwnProperty.call(initialValues, fieldName) && Object.prototype.hasOwnProperty.call(rulesRef.current, fieldName)) { const error = validateFieldValue(fieldName, initialValues[fieldName]); if (error) { errors[fieldName] = error; } else { errors[fieldName] = null; } } } if (Object.keys(errors).length > 0) { setBulkErrors(errors); } } }; const formatErrorMessage = (ruleName, params) => { let formattedMessage = l10nRef.current?.getConstant(ruleName); if (Array.isArray(params)) { params.forEach((value, index) => { const placeholder = `{${index}}`; if (formattedMessage.includes(placeholder)) { formattedMessage = formattedMessage.replace(placeholder, String(value)); } }); } else { formattedMessage = formattedMessage.replace('{0}', String(params)); } return formattedMessage; }; const validateCreditCard = (value) => { if (!VALIDATION_REGEX.CREDIT_CARD.test(value)) { return false; } const cardNumber = value.replace(/[\s-]/g, ''); let sum = 0; let shouldDouble = false; for (let i = cardNumber.length - 1; i >= 0; i--) { let digit = parseInt(cardNumber.charAt(i), 10); if (shouldDouble) { digit *= 2; if (digit > 9) { digit -= 9; } } sum += digit; shouldDouble = !shouldDouble; } return (sum % 10) === 0; }; const validateFieldValue = (fieldName, value) => { const fieldRules = rulesRef.current[fieldName]; if (!fieldRules || !registeredFields.current[fieldName]) { return null; } const isValueEmpty = value === undefined || value === null || value.toString().trim() === ''; const isRequired = fieldRules.required != null && fieldRules.required[0] !== false; if (isValueEmpty && !isRequired) { return null; } for (const ruleName in fieldRules) { if (Object.prototype.hasOwnProperty.call(fieldRules, ruleName)) { const ruleValue = fieldRules[ruleName]; if (!ruleValue) { continue; } let isValid = true; let param = null; let errorMessage; if (ruleName === 'customValidator' && typeof ruleValue === 'function') { const customError = ruleValue(value); if (customError) { return customError; } continue; } if (Array.isArray(ruleValue)) { param = ruleValue[0]; errorMessage = ruleValue[1]; if (ruleName === 'required' && param === false) { continue; } } if (ruleName !== 'required' && (value === '' || value === null || value === undefined)) { continue; } switch (ruleName) { case 'required': isValid = !isValueEmpty; break; case 'email': isValid = VALIDATION_REGEX.EMAIL.test(value); break; case 'url': isValid = VALIDATION_REGEX.URL.test(value); break; case 'date': isValid = !isNaN(Date.parse(value)); break; case 'dateIso': isValid = VALIDATION_REGEX.DATE_ISO.test(value); break; case 'number': isValid = !isNaN(Number(value)) && String(value).indexOf(' ') === -1; break; case 'digits': isValid = VALIDATION_REGEX.DIGITS.test(value); break; case 'creditCard': isValid = validateCreditCard(value); break; case 'minLength': isValid = String(value).length >= Number(param); break; case 'maxLength': isValid = String(value).length <= Number(param); break; case 'rangeLength': if (Array.isArray(param)) { isValid = String(value).length >= param[0] && String(value).length <= param[1]; } break; case 'min': isValid = Number(value) >= Number(param); break; case 'max': isValid = Number(value) <= Number(param); break; case 'range': if (Array.isArray(param)) { isValid = Number(value) >= param[0] && Number(value) <= param[1]; } break; case 'regex': if (param instanceof RegExp) { isValid = param.test(value); } else if (typeof param === 'string') { // eslint-disable-next-line security/detect-non-literal-regexp isValid = new RegExp(param).test(value); } break; case 'tel': isValid = VALIDATION_REGEX.PHONE.test(value); break; case 'equalTo': if (typeof param === 'string') { isValid = value === stateRef.current.values[param]; } break; } if (!isValid) { if (errorMessage) { return errorMessage; } else { return formatErrorMessage(ruleName, param); } } } } return null; }; const validateForm = () => { const errors = {}; const fields = Object.keys(rulesRef.current); for (const field of fields) { const error = validateFieldValue(field, stateRef.current.values[field]); if (!stateRef.current.values[field]) { setFieldValue(field, stateRef.current.values[field]); } if (error) { errors[field] = error; } } return errors; }; const validate = () => { const formErrors = validateForm(); for (const field in formErrors) { if (Object.prototype.hasOwnProperty.call(formErrors, field)) { setFieldError(field, formErrors[field]); } } const fields = Object.keys(rulesRef.current); for (const field of fields) { if (!formErrors[field]) { setFieldError(field, null); } } if (Object.keys(formErrors).length === 0) { stateRef.current = { ...stateRef.current, errors: {}, touched: {}, visited: {}, modified: {}, submitted: false, validated: {} }; } notifyStateChange(); return Object.keys(formErrors).length === 0; }; const handleSubmit = (event) => { event?.preventDefault(); const isValid = validate(); touchAllFields(); visitAllFields(); setSubmitted(true); notifyStateChange(); if (isValid) { onSubmit?.(stateRef.current.values); } }; const handleChange = (name, { value }) => { setFieldValue(name, value); if (validateOnChange) { const error = validateFieldValue(name, value); setFieldError(name, error); } notifyStateChange(); }; const handleBlur = (fieldName) => { setFieldTouched(fieldName); const error = validateFieldValue(fieldName, stateRef.current.values[fieldName]); setFieldError(fieldName, error); notifyStateChange(); }; const handleFormReset = (args) => { resetForm({ ...initialValues }); onReset?.(args); }; const reset = () => { handleFormReset(); }; const getFormState = () => { const state = stateRef.current; return { values: state.values, errors: state.errors, submitted: state.submitted, touched: state.touched, visited: state.visited, modified: state.modified, valid: Object.keys(rulesRef.current).reduce((acc, fieldName) => { acc[fieldName] = !state.errors[fieldName]; return acc; }, {}), allowSubmit: Object.keys(state.errors).length === 0, onChange: handleChange, onBlur: handleBlur, onFocus: handleFocus, onFormReset: handleFormReset, onSubmit: handleSubmit, fieldNames: Object.keys(registeredFields.current).reduce((acc, fieldName) => { acc[fieldName] = fieldName; return acc; }, {}) }; }; const validateField = (fieldName) => { const error = validateFieldValue(fieldName, stateRef.current.values[fieldName]); setFieldError(fieldName, error); notifyStateChange(); return !error; }; const publicAPI = React.useMemo(() => ({ rules, initialValues, validateOnChange }), [rules, initialValues, validateOnChange]); useImperativeHandle(ref, () => ({ ...publicAPI, validate, reset, validateField, element: formRef.current }), [publicAPI]); const formClassName = React.useMemo(() => { return [ 'sf-control sf-form-validator', dir === 'rtl' ? 'sf-rtl' : '', className ].filter(Boolean).join(' '); }, [dir, className]); const handleFocus = (fieldName) => { setFieldVisited(fieldName); notifyStateChange(); }; const formContextValue = { registerField: registerField }; return (_jsx(FormProvider, { value: formContextValue, children: _jsx("form", { ref: formRef, className: formClassName, onSubmit: handleSubmit, onReset: handleFormReset, noValidate: true, ...otherProps, children: children }) })); }); Form.displayName = 'Form'; /** * Specifies a component that connects form inputs with validation rules. The FormField component provides * an easy way to integrate form controls with the Form validation system, handling state management and * validation automatically. * * ```typescript * const [formState, setFormState] = useState<FormState>(); * * <Form * rules={{ * username: { required: [true, 'Username is required'] } * }} * onSubmit={data => console.log(data)} * onFormStateChange={setFormState} * > * <FormField name="username"> * <input * name="username" * value={formState?.values.username || ''} * onChange={(e) => formState?.onChange('username', { value: e.target.value })} * onBlur={() => formState?.onBlur('username')} * onFocus={() => formState?.onFocus('username')} * /> * {formState?.touched?.username && formState?.errors?.username && ( * <div className="error">{formState.errors.username}</div> * )} * </FormField> * <button type="submit">Submit</button> * </Form> * ``` * * @param {IFormFieldProps} props - Specifies the form field configuration properties * @returns {React.ReactNode} - Returns the children with access to form validation context */ export const FormField = (props) => { const { name, children } = props; if (!name) { return null; } const formContext = React.useContext(FormContext); if (!formContext) { return null; } if (formContext.registerField) { formContext.registerField(name); } return _jsx(_Fragment, { children: children }); }; FormField.displayName = 'FormField';