@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
JavaScript
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';