Type-safe form state management and validation for React applications.
OpenSite Page Speed Forms is a high-performance library designed to streamline form state management, validation, and submission handling in React applications. This library is part of OpenSite AI's open-source ecosystem, built for performance and open collaboration. By emphasizing type safety and modularity, it aligns with OpenSite's goal to create scalable, open, and developer-friendly performance tooling.
Learn more at OpenSite.ai Developers.
- Type-safe form state management with TypeScript.
- Flexible validation schemas supporting both synchronous and asynchronous validation.
- Modular useForm and useField hooks for complete form and field control.
- Built-in support for form submission and error handling.
- Configurable validation modes:
onChange,onBlur, andonSubmit.
To install OpenSite Page Speed Forms, ensure you have Node.js and npm installed, then run:
npm install @page-speed/forms
Dependencies:
- React
Here is a basic example to get started with OpenSite Page Speed Forms in your React application:
import React from 'react';
import { useForm, Form } from '@page-speed/forms';
function MyForm() {
const form = useForm({
initialValues: { email: '' },
onSubmit: (values) => {
console.log('Form Submitted:', values);
}
});
return (
<Form form={form}>
<input
name="email"
value={form.values.email}
onChange={(e) => form.setFieldValue('email', e.target.value)}
onBlur={() => form.setFieldTouched('email', true)}
/>
<button type="submit">Submit</button>
</Form>
);
}OpenSite Page Speed Forms can be customized with various options:
const form = useForm({
initialValues: { email: '' },
validationSchema: {
email: (value) => value.includes('@') ? undefined : 'Invalid email'
},
validateOn: 'onBlur',
revalidateOn: 'onChange',
onSubmit: (values) => console.log(values),
onError: (errors) => console.error(errors),
debug: true
});Validate fields that depend on other field values using the crossFieldValidator utility or by accessing allValues in your validator:
import { useForm, crossFieldValidator } from '@page-speed/forms/validation';
// Method 1: Using crossFieldValidator helper
const form = useForm({
initialValues: { password: '', confirmPassword: '' },
validationSchema: {
confirmPassword: crossFieldValidator(
['password', 'confirmPassword'],
(values) => {
if (values.password !== values.confirmPassword) {
return 'Passwords must match';
}
return undefined;
}
)
}
});
// Method 2: Direct access to allValues
const form = useForm({
initialValues: { password: '', confirmPassword: '' },
validationSchema: {
confirmPassword: (value, allValues) => {
if (value !== allValues.password) {
return 'Passwords must match';
}
return undefined;
}
}
});Optimize async validators (like API calls) with built-in debouncing to prevent excessive requests:
import { useForm, asyncValidator } from '@page-speed/forms/validation';
const checkUsernameAvailability = async (username: string) => {
const response = await fetch(`/api/check-username?username=${username}`);
const { available } = await response.json();
return available ? undefined : 'Username already taken';
};
const form = useForm({
initialValues: { username: '' },
validationSchema: {
// Debounce async validation by 500ms
username: asyncValidator(
checkUsernameAvailability,
{ delay: 500, trailing: true }
)
}
});Debounce Options:
delay: Milliseconds to wait (default: 300ms)leading: Validate immediately on first change (default: false)trailing: Validate after delay expires (default: true)
The asyncValidator wrapper also includes automatic race condition prevention, ensuring only the latest validation result is used.
Use pre-built, tree-shakable validation rules for common scenarios:
import {
required,
email,
url,
phone,
minLength,
maxLength,
min,
max,
pattern,
matches,
oneOf,
creditCard,
postalCode,
alpha,
alphanumeric,
numeric,
integer,
compose
} from '@page-speed/forms/validation/rules';
const form = useForm({
initialValues: {
email: '',
password: '',
confirmPassword: '',
age: 0,
username: '',
cardNumber: ''
},
validationSchema: {
email: compose(
required({ message: 'Email is required' }),
email({ message: 'Invalid email format' })
),
password: compose(
required(),
minLength(8, { message: 'Password must be at least 8 characters' })
),
confirmPassword: matches('password', { message: 'Passwords must match' }),
age: compose(
required(),
numeric({ message: 'Age must be a number' }),
min(18, { message: 'Must be 18 or older' })
),
username: compose(
required(),
alphanumeric({ message: 'Only letters and numbers allowed' }),
minLength(3),
maxLength(20)
),
cardNumber: creditCard({ message: 'Invalid credit card number' })
}
});Available Validators:
| Validator | Description | Example |
|---|---|---|
required() |
Field must have a value | required({ message: 'Required' }) |
email() |
Valid email format (RFC 5322) | email() |
url() |
Valid URL format | url() |
phone() |
US phone number format | phone() |
minLength(n) |
Minimum string/array length | minLength(3) |
maxLength(n) |
Maximum string/array length | maxLength(100) |
min(n) |
Minimum numeric value | min(0) |
max(n) |
Maximum numeric value | max(100) |
pattern(regex) |
Custom regex pattern | pattern(/^[A-Z]+$/) |
matches(field) |
Match another field | matches('password') |
oneOf(values) |
Value in allowed list | oneOf(['a', 'b', 'c']) |
creditCard() |
Valid credit card (Luhn) | creditCard() |
postalCode() |
US ZIP code format | postalCode() |
alpha() |
Alphabetic characters only | alpha() |
alphanumeric() |
Letters and numbers only | alphanumeric() |
numeric() |
Valid number | numeric() |
integer() |
Whole number | integer() |
compose(...) |
Combine multiple validators | compose(required(), email()) |
Customize error messages globally for internationalization support:
import { setErrorMessages } from '@page-speed/forms/validation/utils';
// Set custom messages (e.g., Spanish translations)
setErrorMessages({
required: 'Este campo es obligatorio',
email: 'Por favor ingrese un correo electrΓ³nico vΓ‘lido',
minLength: ({ min }) => `Debe tener al menos ${min} caracteres`,
maxLength: ({ max }) => `No debe exceder ${max} caracteres`,
phone: 'Por favor ingrese un nΓΊmero de telΓ©fono vΓ‘lido'
});
// Use with validation rules
import { required, email, minLength } from '@page-speed/forms/validation/rules';
const form = useForm({
initialValues: { email: '', password: '' },
validationSchema: {
email: compose(required(), email()),
password: compose(required(), minLength(8))
}
});Message Template Functions:
Error messages support template functions with parameter interpolation:
setErrorMessages({
minLength: ({ min }) => `Must be at least ${min} characters`,
max: ({ max }) => `Cannot exceed ${max}`,
matches: ({ field }) => `Must match ${field}`
});Per-Field Custom Messages:
Override global messages on a per-field basis:
const form = useForm({
initialValues: { email: '' },
validationSchema: {
email: required({ message: 'Please provide your email address' })
}
});Validate fields only when certain conditions are met:
import { when, required, minLength } from '@page-speed/forms/validation';
const form = useForm({
initialValues: { accountType: 'personal', companyName: '' },
validationSchema: {
// Only require company name for business accounts
companyName: when(
(allValues) => allValues.accountType === 'business',
compose(
required({ message: 'Company name is required for business accounts' }),
minLength(3)
)
)
}
});@page-speed/forms includes a comprehensive set of accessible, production-ready input components that work seamlessly with the form hooks.
Standard text input with support for various types (text, email, password, etc.):
import { TextInput } from '@page-speed/forms/inputs';
<Field name="email" label="Email">
{({ field }) => <TextInput {...field} type="email" placeholder="Enter email" />}
</Field>Multi-line text input:
import { TextArea } from '@page-speed/forms/inputs';
<Field name="description" label="Description">
{({ field }) => <TextArea {...field} rows={5} placeholder="Enter description" />}
</Field>Single checkbox or group of checkboxes:
import { Checkbox, CheckboxGroup } from '@page-speed/forms/inputs';
// Single checkbox
<Field name="terms" label="Terms">
{({ field }) => <Checkbox {...field} label="I agree to the terms" />}
</Field>
// Checkbox group
<Field name="interests" label="Interests">
{({ field }) => (
<CheckboxGroup
{...field}
options={[
{ label: 'Sports', value: 'sports' },
{ label: 'Music', value: 'music' },
{ label: 'Travel', value: 'travel' }
]}
/>
)}
</Field>Radio button group:
import { Radio } from '@page-speed/forms/inputs';
<Field name="plan" label="Select Plan">
{({ field }) => (
<Radio
{...field}
options={[
{ label: 'Basic', value: 'basic' },
{ label: 'Pro', value: 'pro' },
{ label: 'Enterprise', value: 'enterprise' }
]}
/>
)}
</Field>Dropdown select with support for single and multi-select:
import { Select } from '@page-speed/forms/inputs';
// Single select
<Field name="country" label="Country">
{({ field }) => (
<Select
{...field}
options={[
{ label: 'United States', value: 'us' },
{ label: 'Canada', value: 'ca' },
{ label: 'United Kingdom', value: 'uk' }
]}
searchable
clearable
/>
)}
</Field>
// Multi-select
<Field name="skills" label="Skills">
{({ field }) => (
<Select
{...field}
multiple
options={[
{ label: 'JavaScript', value: 'js' },
{ label: 'TypeScript', value: 'ts' },
{ label: 'React', value: 'react' }
]}
searchable
/>
)}
</Field>Date selection with calendar popup:
import { DatePicker } from '@page-speed/forms/inputs';
<Field name="birthdate" label="Birth Date">
{({ field }) => (
<DatePicker
{...field}
placeholder="Select date"
dateFormat="MM/dd/yyyy"
minDate={new Date(1900, 0, 1)}
maxDate={new Date()}
clearable
/>
)}
</Field>Props:
dateFormat: Date display format (default: "MM/dd/yyyy")minDate,maxDate: Restrict selectable datesisDateDisabled: Custom function to disable specific datesclearable: Show clear buttonshowTodayButton: Show "Today" button
Time selection with hour/minute/period selectors:
import { TimePicker } from '@page-speed/forms/inputs';
<Field name="appointmentTime" label="Appointment Time">
{({ field }) => (
<TimePicker
{...field}
placeholder="Select time"
use24Hour={false}
minuteStep={15}
clearable
/>
)}
</Field>Props:
use24Hour: Use 24-hour format (default: false)minuteStep: Minute increment (default: 1)clearable: Show clear button
Date range selection with start and end dates:
import { DateRangePicker } from '@page-speed/forms/inputs';
<Field name="dateRange" label="Date Range">
{({ field }) => (
<DateRangePicker
{...field}
placeholder="Select date range"
separator=" - "
minDate={new Date()}
clearable
/>
)}
</Field>Props:
separator: String between start and end dates (default: " - ")minDate,maxDate: Restrict selectable datesisDateDisabled: Custom function to disable specific datesclearable: Show clear button
WYSIWYG and Markdown editor with toolbar:
import { RichTextEditor } from '@page-speed/forms/inputs';
<Field name="content" label="Content">
{({ field }) => (
<RichTextEditor
{...field}
placeholder="Enter content..."
minHeight="200px"
maxHeight="600px"
allowModeSwitch
defaultMode="wysiwyg"
/>
)}
</Field>Props:
defaultMode: "wysiwyg" or "markdown" (default: "wysiwyg")allowModeSwitch: Enable mode toggle buttonminHeight,maxHeight: Editor height constraintscustomButtons: Add custom toolbar buttons
Features:
- WYSIWYG mode: Bold, Italic, Underline, Headings, Lists, Links
- Markdown mode: Direct markdown editing
- Automatic HTML β Markdown conversion
File upload with drag-and-drop, progress indicators, and image cropping:
import { FileInput } from '@page-speed/forms/inputs';
<Field name="avatar" label="Profile Picture">
{({ field }) => (
<FileInput
{...field}
accept="image/*"
maxSize={5 * 1024 * 1024} // 5MB
maxFiles={1}
showProgress
uploadProgress={uploadProgress}
enableCropping
cropAspectRatio={1}
onCropComplete={(file) => console.log('Cropped:', file)}
/>
)}
</Field>Props:
accept: File type filter (e.g., "image/*", ".pdf")multiple: Allow multiple filesmaxFiles: Maximum number of filesmaxSize: Maximum file size in bytesshowPreview: Show file previewsshowProgress: Display upload progress barsuploadProgress: Object mapping filenames to progress percentagesenableCropping: Enable image cropping for image filescropAspectRatio: Crop aspect ratio (e.g., 16/9, 1 for square)onCropComplete: Callback when cropping is complete
Features:
- Drag-and-drop support
- File type and size validation
- Image previews with thumbnails
- Upload progress indicators with percentage
- Interactive image cropping with zoom
- Multiple file support
- Accessible file selection
The FileInput component uses a two-phase upload process optimized for Rails API integration. Files are uploaded immediately to temporary storage and return unique tokens, which are then associated with your form submission.
Quick Example:
const [uploadTokens, setUploadTokens] = useState<string[]>([]);
const handleFileUpload = async (files: File[]) => {
const formData = new FormData();
formData.append("contact_form_upload[file_upload]", files[0]);
const response = await fetch("/api/contact_form_uploads", {
method: "POST",
body: formData,
});
const data = await response.json();
setUploadTokens([data.token]);
};
// In your form submission:
onSubmit: async (values) => {
await submitForm({
...values,
contact_form_upload_tokens: uploadTokens,
});
}Comprehensive Guide:
For complete file upload documentation, including:
- Two-phase upload process and flow diagrams
- Rails API integration with endpoint specifications
- Multiple working examples (resume uploads, image galleries, document forms)
- Progress tracking and error handling patterns
- Image cropping implementation
- File validation strategies
- Best practices and common patterns
- Troubleshooting guide
See the File Upload Guide for detailed information.
All components in @page-speed/forms are intentionally unstyled to provide maximum flexibility and framework-agnostic design. Components use predictable BEM class names (e.g., .text-input, .select-trigger, .field-label) as styling hooks, allowing you to apply any design system or custom styles.
Quick Example:
/* Your custom CSS */
.text-input {
height: 2.25rem;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
padding: 0.5rem 0.75rem;
}
.text-input:focus {
outline: none;
border-color: #3b82f6;
}
.text-input--error {
border-color: #ef4444;
}Comprehensive Guide:
For complete styling documentation, including:
- BEM class reference for all components
- Multiple styling approaches (Vanilla CSS, Tailwind, CSS Modules, CSS-in-JS)
- Complete examples (shadcn/ui, Material Design, custom brands)
- Best practices and common patterns
- Dark mode support
See the Styling Guide for detailed information.
Performance is a core facet of everything we build at OpenSite AI. The library is optimized for minimal re-renders and efficient form state updates, ensuring your applications remain responsive and fast.
We welcome contributions from the community to enhance OpenSite Page Speed Forms. Please refer to our GitHub repository for guidelines and more information on how to get involved.
Licensed under the BSD 3-Clause License. See the LICENSE file for details.
- Domain Extractor
- Page Speed Hooks
- Visit opensite.ai for more tools and information.
