react-form-validator-plus
v1.0.0
Published
A powerful, extensible React form validation library with built-in validators and easy customization
Maintainers
Readme
React Form Validator Plus
A powerful, extensible React form validation library with built-in validators, custom validation support, and seamless integration patterns.
🚀 Features
- Extensible Architecture: Easy to extend with custom validators and logic
- Rich Built-in Validators: 25+ pre-built validators for common use cases
- React Hooks: Powerful hooks for form and field validation
- React Components: Ready-to-use validated input components
- TypeScript Support: Full TypeScript declarations included
- Async Validation: Built-in support for server-side validation
- Cross-field Validation: Validate fields based on other field values
- Real-time Validation: Configurable validation timing (onChange, onBlur, onSubmit)
- Accessibility: ARIA attributes and screen reader support
- Performance Optimized: Debounced validation and efficient re-renders
📦 Installation
npm install react-form-validator-plusyarn add react-form-validator-pluspnpm add react-form-validator-plus🏁 Quick Start
Basic Form with Validation
import React from 'react';
import { useForm, V, ValidatedInput, FormActions } from 'react-form-validator-plus';
function ContactForm() {
const form = useForm({
initialValues: {
name: '',
email: '',
message: ''
},
validationSchema: {
name: [V.required(), V.minLength(2)],
email: [V.required(), V.email()],
message: [V.required(), V.minLength(10, 'Message must be at least 10 characters')]
},
onSubmit: async (values) => {
console.log('Form submitted:', values);
// Submit to API
}
});
return (
<form {...form.formProps}>
<div>
<label>Name:</label>
<ValidatedInput
{...form.getFieldProps('name')}
placeholder="Enter your name"
/>
{form.hasError('name') && (
<div className="error">{form.getError('name')}</div>
)}
</div>
<div>
<label>Email:</label>
<ValidatedInput
{...form.getFieldProps('email')}
type="email"
placeholder="Enter your email"
/>
{form.hasError('email') && (
<div className="error">{form.getError('email')}</div>
)}
</div>
<div>
<label>Message:</label>
<ValidatedTextarea
{...form.getFieldProps('message')}
placeholder="Enter your message"
rows={4}
/>
{form.hasError('message') && (
<div className="error">{form.getError('message')}</div>
)}
</div>
<FormActions
submitText="Send Message"
disabled={!form.isValid}
/>
</form>
);
}Using the ValidatedForm Component
import { ValidatedForm, FieldGroup, ValidatedInput, V } from 'react-form-validator-plus';
function UserForm() {
const schema = {
username: [V.required(), V.minLength(3), V.regex(/^[a-zA-Z0-9_]+$/)],
email: [V.requiredEmail()],
password: [V.strongPassword()],
confirmPassword: [V.required(), V.confirmPassword('password')]
};
return (
<ValidatedForm
validationSchema={schema}
onSubmit={(values) => console.log(values)}
className="user-form"
>
{(form) => (
<>
<FieldGroup name="username" label="Username" required>
<ValidatedInput name="username" />
</FieldGroup>
<FieldGroup name="email" label="Email Address" required>
<ValidatedInput name="email" type="email" />
</FieldGroup>
<FieldGroup name="password" label="Password" required>
<ValidatedInput name="password" type="password" />
</FieldGroup>
<FieldGroup name="confirmPassword" label="Confirm Password" required>
<ValidatedInput name="confirmPassword" type="password" />
</FieldGroup>
<FormActions submitText="Create Account" showReset />
</>
)}
</ValidatedForm>
);
}📚 Built-in Validators
String Validators
V.required('This field is required')
V.minLength(5, 'Minimum 5 characters')
V.maxLength(100, 'Maximum 100 characters')
V.length(5, 50, 'Must be 5-50 characters')
V.regex(/^[A-Za-z]+$/, 'Letters only')Numeric Validators
V.numeric('Must be a number')
V.integer('Must be an integer')
V.min(0, 'Must be at least 0')
V.max(100, 'Must be at most 100')
V.range(1, 10, 'Must be between 1 and 10')Format Validators
V.email('Invalid email address')
V.url('Invalid URL')
V.phone('Invalid phone number')Password Validators
V.password({
minLength: 8,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialChars: true
})
V.confirmPassword('password', 'Passwords must match')Date Validators
V.date('Invalid date')
V.minDate(new Date(), 'Date must be in the future')
V.maxDate('2025-12-31', 'Date must be before 2026')
V.age(18, 65, 'Must be 18-65 years old')File Validators
V.fileType(['image/jpeg', 'image/png'], 'Only JPEG and PNG allowed')
V.fileSize(5 * 1024 * 1024, 'File must be less than 5MB')
V.fileSizeMB(10, 'File must be less than 10MB')Selection Validators
V.oneOf(['red', 'green', 'blue'], 'Must select a valid color')
V.arrayLength(1, 3, 'Select 1-3 items')Cross-field Validators
V.equals('password', 'Must match password')
V.different('username', 'Must be different from username')🔧 Creating Custom Validators
1. Function Validators (Quick & Easy)
import { V } from 'react-form-validator-plus';
// Simple custom validator
const isEven = V.custom(
(value) => parseInt(value) % 2 === 0,
'Number must be even'
);
// Advanced custom validator with context
const uniqueUsername = V.custom(
(value, context) => {
const existingUsers = context.existingUsers || [];
return !existingUsers.includes(value);
},
'Username already exists'
);
// Usage
const form = useForm({
validationSchema: {
favoriteNumber: [V.required(), V.numeric(), isEven],
username: [V.required(), uniqueUsername]
},
context: {
existingUsers: ['john', 'jane', 'bob']
}
});2. Class-based Validators (Full Control)
import { Validator } from 'react-form-validator-plus';
class CreditCardValidator extends Validator {
constructor(message = 'Invalid credit card number') {
super(message);
}
validate(value, context = {}) {
if (!this.shouldValidate(value, context)) {
return this.createResult(true);
}
const cleanValue = String(value).replace(/\s+/g, '');
// Luhn algorithm validation
const isValid = this.luhnCheck(cleanValue);
return this.createResult(isValid);
}
luhnCheck(cardNumber) {
let sum = 0;
let alternate = false;
for (let i = cardNumber.length - 1; i >= 0; i--) {
let digit = parseInt(cardNumber.charAt(i), 10);
if (alternate) {
digit *= 2;
if (digit > 9) digit = (digit % 10) + 1;
}
sum += digit;
alternate = !alternate;
}
return sum % 10 === 0;
}
}
// Usage
const creditCardValidator = new CreditCardValidator();3. Async Validators
import { AsyncValidator } from 'react-form-validator-plus';
class UsernameAvailabilityValidator extends AsyncValidator {
constructor(apiEndpoint) {
super('Username is not available');
this.apiEndpoint = apiEndpoint;
this.debounce(500); // Wait 500ms before validating
}
async validateAsync(value, context = {}) {
try {
const response = await fetch(
`${this.apiEndpoint}?username=${encodeURIComponent(value)}`,
{ signal: context.signal } // Support cancellation
);
const result = await response.json();
return this.createResult(
result.available,
result.available ? null : 'Username is already taken'
);
} catch (error) {
if (error.name === 'AbortError') throw error;
return this.createResult(false, 'Unable to check availability');
}
}
}
// Usage
const form = useForm({
validationSchema: {
username: [
V.required(),
V.minLength(3),
new UsernameAvailabilityValidator('/api/check-username')
]
}
});🎯 Advanced Usage Patterns
Form with Dynamic Fields
import { FieldArray, ConditionalField } from 'react-form-validator-plus';
function DynamicForm() {
const form = useForm({
initialValues: {
contacts: [{ name: '', email: '' }],
hasCompany: false,
company: ''
},
validationSchema: {
'contacts[].name': [V.required()],
'contacts[].email': [V.required(), V.email()],
company: [V.when((value, context) => context.formData.hasCompany, V.required())]
}
});
return (
<ValidatedForm {...form}>
<FieldArray
name="contacts"
maxLength={5}
addText="Add Contact"
>
{({ index, name, remove, canRemove }) => (
<div key={index}>
<ValidatedInput
name={`${name}.name`}
placeholder="Contact Name"
/>
<ValidatedInput
name={`${name}.email`}
type="email"
placeholder="Contact Email"
/>
{canRemove && (
<button type="button" onClick={remove}>Remove</button>
)}
</div>
)}
</FieldArray>
<label>
<ValidatedCheckbox
name="hasCompany"
label="I represent a company"
/>
</label>
<ConditionalField condition={(values) => values.hasCompany}>
<FieldGroup name="company" label="Company Name" required>
<ValidatedInput name="company" />
</FieldGroup>
</ConditionalField>
<FormActions />
</ValidatedForm>
);
}Custom Composite Validators
import { V, CompositeValidator } from 'react-form-validator-plus';
// Create reusable validator combinations
const strongEmail = () => V.all(
V.required(),
V.email(),
V.custom((email) => {
const domain = email.split('@')[1];
const blockedDomains = ['tempmail.com', '10minutemail.com'];
return !blockedDomains.includes(domain);
}, 'Temporary email addresses not allowed')
);
const securePassword = () => V.all(
V.required(),
V.minLength(12),
V.password({
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialChars: true
}),
V.custom((password) => {
const commonPasswords = ['password123', 'admin123'];
return !commonPasswords.includes(password.toLowerCase());
}, 'Password is too common')
);
// Usage
const validationSchema = {
email: strongEmail(),
password: securePassword(),
confirmPassword: [V.required(), V.confirmPassword('password')]
};Multi-step Form Validation
import { FormProgress } from 'react-form-validator-plus';
function MultiStepForm() {
const [currentStep, setCurrentStep] = useState(0);
const steps = [
{
name: 'Personal Info',
fields: ['firstName', 'lastName', 'email']
},
{
name: 'Address',
fields: ['street', 'city', 'state', 'zip']
},
{
name: 'Preferences',
fields: ['newsletter', 'notifications']
}
];
const form = useForm({
validationSchema: {
firstName: [V.required(), V.minLength(2)],
lastName: [V.required(), V.minLength(2)],
email: [V.requiredEmail()],
street: [V.required()],
city: [V.required()],
state: [V.required()],
zip: [V.required(), V.regex(/^\d{5}$/)],
newsletter: [],
notifications: []
}
});
const canProceed = () => {
const currentFields = steps[currentStep].fields;
return currentFields.every(field => !form.hasError(field));
};
return (
<ValidatedForm {...form}>
<FormProgress
steps={steps}
currentStep={currentStep}
/>
{currentStep === 0 && (
<div>
<FieldGroup name="firstName" label="First Name" required>
<ValidatedInput name="firstName" />
</FieldGroup>
{/* ... other step 1 fields */}
</div>
)}
{/* ... other steps */}
<div>
{currentStep > 0 && (
<button type="button" onClick={() => setCurrentStep(currentStep - 1)}>
Previous
</button>
)}
{currentStep < steps.length - 1 ? (
<button
type="button"
onClick={() => setCurrentStep(currentStep + 1)}
disabled={!canProceed()}
>
Next
</button>
) : (
<FormActions submitText="Complete Registration" />
)}
</div>
</ValidatedForm>
);
}🔌 API Reference
Hooks
useForm(options)
Main form management hook.
Options:
initialValues- Initial form valuesvalidationSchema- Validation rules for fieldsvalidateOnChange- Validate on input change (default: true)validateOnBlur- Validate on field blur (default: true)validateOnSubmit- Validate on form submit (default: true)debounceMs- Debounce validation (default: 300ms)onSubmit- Form submit handleronSubmitSuccess- Success callbackonSubmitError- Error callback
Returns:
values- Current form valueserrors- Validation errorstouched- Touched field statusisValid- Overall form validityisSubmitting- Submit statussetFieldValue(name, value)- Set field valuegetFieldProps(name)- Get input props for fieldhandleSubmit- Form submit handlerresetForm- Reset form to initial state
useField(name, validators, options)
Individual field validation hook.
useValidation(validators, options)
Generic validation hook for any value.
Components
<ValidatedForm>
Form wrapper with built-in validation.
<ValidatedInput>
Input component with validation.
<FieldGroup>
Field wrapper with label and error display.
<ErrorMessage>
Displays validation errors.
<FormActions>
Common form buttons (submit, reset).
Validators
All validators support:
.withMessage(message)- Custom error message.required()- Mark as required.when(condition)- Conditional validation
🎨 Styling
The library provides minimal styling and CSS classes for customization:
/* Basic validation styles */
.validated-input-wrapper {
position: relative;
}
.validated-input-wrapper input.error {
border-color: #dc3545;
box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.25);
}
.validated-input-wrapper input.valid {
border-color: #28a745;
box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.25);
}
.error-message {
color: #dc3545;
font-size: 0.875rem;
margin-top: 0.25rem;
}
.validation-icon {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
}
.valid-icon {
color: #28a745;
}
.error-icon {
color: #dc3545;
}
.loading-icon {
color: #6c757d;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: translateY(-50%) rotate(0deg); }
to { transform: translateY(-50%) rotate(360deg); }
}
/* Form styles */
.field-group {
margin-bottom: 1.5rem;
}
.field-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.required-indicator {
color: #dc3545;
margin-left: 0.25rem;
}
.form-actions {
margin-top: 2rem;
text-align: right;
}
.form-actions button {
margin-left: 0.75rem;
}🧪 Testing
The library includes comprehensive tests using React Testing Library and Vitest. Run tests with:
npm test # Run tests in watch mode
npm run test:run # Run tests once
npm run test:coverage # Run tests with coverage
npm run verify # Run tests + build (CI)Testing Forms with Validation
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useForm, V } from 'react-form-validator-plus';
// Test component
function TestForm() {
const form = useForm({
validationSchema: {
email: [V.required(), V.email()]
}
});
return (
<form {...form.formProps}>
<input {...form.getFieldProps('email')} data-testid="email" />
{form.hasError('email') && (
<div data-testid="email-error">{form.getError('email')}</div>
)}
<button type="submit">Submit</button>
</form>
);
}
// Test cases
test('shows validation error for invalid email', async () => {
const user = userEvent.setup();
render(<TestForm />);
const emailInput = screen.getByTestId('email');
await user.type(emailInput, 'invalid-email');
await user.tab(); // Trigger blur
await waitFor(() => {
expect(screen.getByTestId('email-error')).toBeInTheDocument();
});
});
test('clears error when valid email is entered', async () => {
const user = userEvent.setup();
render(<TestForm />);
const emailInput = screen.getByTestId('email');
await user.type(emailInput, '[email protected]');
await user.tab();
await waitFor(() => {
expect(screen.queryByTestId('email-error')).not.toBeInTheDocument();
});
});Testing Custom Validators
import { CreditCardValidator } from './validators/CreditCardValidator';
describe('CreditCardValidator', () => {
let validator;
beforeEach(() => {
validator = new CreditCardValidator();
});
test('validates correct credit card number', () => {
const result = validator.validate('4532015112830366');
expect(result.isValid).toBe(true);
});
test('rejects invalid credit card number', () => {
const result = validator.validate('1234567890123456');
expect(result.isValid).toBe(false);
expect(result.message).toBe('Invalid credit card number');
});
test('handles empty values when not required', () => {
const result = validator.validate('');
expect(result.isValid).toBe(true);
});
test('rejects empty values when required', () => {
validator.required();
const result = validator.validate('');
expect(result.isValid).toBe(false);
});
});📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
🆓 Free to Use
This library is completely free and open source under the MIT License, which means:
✅ Commercial Use: Use in commercial projects without restrictions
✅ Modification: Modify and customize the code for your needs
✅ Distribution: Distribute the original or modified versions
✅ Private Use: Use in private/internal projects
✅ No Attribution Required: While appreciated, not legally required
✅ No Warranty: Provided "as-is" without warranty
You can use this library in any project - personal, commercial, or open source - without any licensing fees or restrictions!
🤝 Contributing
We welcome contributions! Please see our Contributing Guide for details.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
📊 Bundle Size
| Format | Size (gzipped) | |--------|----------------| | ESM | ~12KB | | CommonJS | ~12KB | | UMD | ~15KB |
🔗 Links
🌟 Why Choose React Form Validator Plus?
- ✅ Extensible: Easy to extend with custom validators
- ✅ TypeScript Ready: Full type safety out of the box
- ✅ Performance Focused: Optimized for minimal re-renders
- ✅ Accessible: Built-in ARIA support for screen readers
- ✅ Flexible: Works with any UI library or custom components
- ✅ Well Tested: Comprehensive test suite
- ✅ Production Ready: Used in production applications
- ✅ Active Maintenance: Regular updates and bug fixes
- ✅ Great DX: Excellent developer experience with IntelliSense
Made with ❤️ for the React community
