@ttoss/forms
@ttoss/forms provides React form components built on React Hook Form and Yup, with integrated i18n support and theme styling.
Installation
pnpm i @ttoss/forms @ttoss/react-i18n @ttoss/ui @emotion/react
pnpm i --save-dev @ttoss/i18n-cli
Note: This package is ESM only. I18n configuration is required—see @ttoss/react-i18n for setup details.
Quick Start
import { Button } from '@ttoss/ui';
import {
Form,
FormFieldCheckbox,
FormFieldInput,
useForm,
yup,
yupResolver,
} from '@ttoss/forms';
import { I18nProvider } from '@ttoss/react-i18n';
const schema = yup.object({
firstName: yup.string().required('First name is required'),
age: yup.number().required('Age is required'),
receiveEmails: yup.boolean(),
});
export const FormComponent = () => {
const formMethods = useForm({
mode: 'all',
resolver: yupResolver(schema),
});
const onSubmit = (data) => {
console.log(data);
};
return (
<I18nProvider>
<Form {...formMethods} onSubmit={onSubmit}>
<FormFieldInput name="firstName" label="First Name" />
<FormFieldInput name="age" label="Age" type="number" />
<FormFieldCheckbox name="receiveEmails" label="Receive Emails" />
<Button type="submit">Submit</Button>
</Form>
</I18nProvider>
);
};
React Hook Form Integration
All React Hook Form APIs are re-exported from @ttoss/forms, including hooks like useForm, useController, useFieldArray, and useFormContext. See the React Hook Form documentation for complete API details.
Yup Validation
Import yup and yupResolver directly from @ttoss/forms:
import { Form, FormFieldInput, useForm, yup, yupResolver } from '@ttoss/forms';
const schema = yup.object({
firstName: yup.string().required(),
});
const MyForm = () => {
const formMethods = useForm({
resolver: yupResolver(schema),
});
return (
<Form {...formMethods} onSubmit={(data) => console.log(data)}>
<FormFieldInput name="firstName" label="First Name" defaultValue="" />
<Button type="submit">Submit</Button>
</Form>
);
};
Validation Messages
Invalid fields display default error messages like "Field is required". These messages are defined using i18n and can be customized for each locale.
Default Yup Messages
The package provides internationalized default messages for common Yup validation errors. These are automatically extracted when you run pnpm run i18n:
- Required field: "Field is required"
- Type mismatch: "Invalid Value for Field of type (type)"
- Minimum length: "Field must be at least (min) characters"
To customize these messages for your locale, extract the i18n messages and translate them in your application's i18n files (e.g., i18n/compiled/pt-BR.json). See the i18n-CLI documentation for more details.
Custom Schema Messages
You can also provide custom error messages directly in your Yup schemas using i18n patterns:
import { useI18n } from '@ttoss/react-i18n';
import { useMemo } from 'react';
const MyForm = () => {
const {
intl: { formatMessage },
} = useI18n();
const schema = useMemo(
() =>
yup.object({
name: yup.string().required(
formatMessage({
defaultMessage: 'Name must not be null',
description: 'Name required constraint',
})
),
age: yup.number().min(
18,
formatMessage(
{
defaultMessage: 'You must be {age} years old or more',
description: 'Min age constraint message',
},
{ age: 18 }
)
),
}),
[formatMessage]
);
// ... rest of form implementation
};
Validation Approaches
There are two ways to validate form fields in @ttoss/forms: schema-based validation using Yup schemas with yupResolver, and field-level validation using the rules prop on individual form fields.
IMPORTANT: You cannot mix both validation methods for the same field—choose either schema-based or field-level validation per field.
When to use schema validation:
- Cross-field validation
- Complex business logic
- Reusable validation patterns
- Type-safe validation with TypeScript
When to use rules:
- Simple, field-specific validations
- Dynamic validation based on component state
- Quick prototyping
- Single-field conditional logic
1. Schema-based Validation (Recommended)
Use Yup schemas with yupResolver for complex validation logic:
const schema = yup.object({
email: yup.string().email().required(),
age: yup.number().min(18).max(100).required(),
});
const formMethods = useForm({
resolver: yupResolver(schema),
});
Advantages:
- Centralized validation logic
- Type-safe with TypeScript
- Reusable schemas
- Complex validation patterns
- Schema composition
2. Field-level Validation
Use the rules prop on individual form fields for simpler validations:
<FormFieldInput
name="username"
label="Username"
rules={{
required: 'Username is required',
minLength: {
value: 3,
message: 'Username must be at least 3 characters',
},
pattern: {
value: /^[a-zA-Z0-9_]+$/,
message: 'Only letters, numbers, and underscores allowed',
},
}}
/>
<FormFieldInput
name="email"
label="Email"
rules={{
required: 'Email is required',
validate: (value) => {
return value.includes('@') || 'Invalid email format';
},
}}
/>
Available validation rules:
required: Field is required (string message or boolean)min: Minimum value (for numbers)max: Maximum value (for numbers)minLength: Minimum string lengthmaxLength: Maximum string lengthpattern: RegExp patternvalidate: Custom validation function or object of functions
Form Field Components
All form field components share common props:
name(required): Field name in the formlabel: Field label textdisabled: Disables the fielddefaultValue: Initial field valuetooltip: Label tooltip configurationwarning: Warning message displayed below the fieldsx: Theme-UI styling object
Disabling Form Fields
You can disable form fields in two ways:
1. Disable the entire form:
Set disabled: true in useForm to disable all fields at once:
const formMethods = useForm({
disabled: true, // Disables all fields
});
This is particularly useful for preventing user interaction during asynchronous operations:
const [isSubmitting, setIsSubmitting] = useState(false);
const formMethods = useForm({
disabled: isSubmitting, // Disable form during submission
});
const onSubmit = async (data) => {
setIsSubmitting(true);
await saveData(data);
setIsSubmitting(false);
};
2. Disable individual fields:
Use the disabled prop on specific form field components:
<FormFieldInput name="email" label="Email" disabled />
Field-level disabled props override the form-level setting:
const formMethods = useForm({
disabled: false,
});
// This field will be disabled even though the form is enabled
<FormFieldInput name="id" label="ID" disabled />;
FormFieldInput
Text input field supporting all HTML input types.
<FormFieldInput
name="email"
label="Email"
type="email"
placeholder="Enter your email"
/>
FormFieldPassword
Password input with show/hide toggle.
<FormFieldPassword name="password" label="Password" />
FormFieldTextarea
Multi-line text input.
<FormFieldTextarea name="description" label="Description" rows={4} />
FormFieldCheckbox
Single checkbox or checkbox group.
<FormFieldCheckbox name="terms" label="I accept the terms" />
FormFieldSwitch
Toggle switch component.
<FormFieldSwitch name="notifications" label="Enable notifications" />
FormFieldRadio
Radio button group.
const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
];
<FormFieldRadio name="choice" label="Choose one" options={options} />;
FormFieldRadioCard
Radio buttons styled as cards.
<FormFieldRadioCard name="plan" label="Select Plan" options={options} />
FormFieldRadioCardIcony
Radio cards with icon support.
const options = [
{ value: 'card', label: 'Credit Card', icon: 'credit-card' },
{ value: 'bank', label: 'Bank Transfer', icon: 'bank' },
];
<FormFieldRadioCardIcony
name="payment"
label="Payment Method"
options={options}
/>;
FormFieldSelect
Dropdown select field.
const options = [
{ value: 'ferrari', label: 'Ferrari' },
{ value: 'mercedes', label: 'Mercedes' },
{ value: 'bmw', label: 'BMW' },
];
<FormFieldSelect name="car" label="Choose a car" options={options} />;
FormFieldSelect Default Values
FormFieldSelect defaults to the first option or a specified defaultValue:
// Defaults to "Ferrari"
<FormFieldSelect name="car" label="Cars" options={options} />
// Defaults to "Mercedes"
<FormFieldSelect
name="car"
label="Cars"
options={options}
defaultValue="Mercedes"
/>
When using a placeholder, an empty option is automatically added if not present:
<FormFieldSelect
name="car"
label="Cars"
options={options}
placeholder="Select a car"
/>
Note: placeholder and defaultValue cannot be used together.
For dynamic options from async data, reset the field after loading:
const { resetField } = useForm();
const [options, setOptions] = useState([]);
useEffect(() => {
// Fetch options
const fetchedOptions = await fetchData();
setOptions(fetchedOptions);
resetField('car', { defaultValue: 'Ferrari' });
}, []);
FormFieldNumericFormat
Numeric input with formatting support (decimals, thousands separators).
<FormFieldNumericFormat
name="price"
label="Price"
thousandSeparator=","
decimalScale={2}
/>
FormFieldCurrencyInput
Currency input with locale-based formatting. The decimal and thousand separators are automatically determined by the locale set in the I18nProvider.
<FormFieldCurrencyInput name="amount" label="Amount" prefix="$" />
Customizing Separators per Locale
The component uses i18n messages to determine the decimal and thousand separators based on the current locale. You can customize these for each locale in your application:
- First, extract the i18n messages by running
pnpm run i18nin your package - In your application's i18n files (e.g.,
i18n/compiled/pt-BR.json), add the custom separators:
{
"JnCaDG": ",", // Decimal separator (default: ".")
"0+4wTp": "." // Thousand separator (default: ",")
}
This approach allows each locale to define its own number formatting rules, which will be automatically applied to all currency inputs.
For more information about the i18n workflow, see the i18n-CLI documentation.
FormFieldPatternFormat
Input with custom format patterns.
<FormFieldPatternFormat
name="phone"
label="Phone"
format="+1 (###) ###-####"
mask="_"
/>
FormFieldCreditCardNumber
Credit card input with automatic formatting.
<FormFieldCreditCardNumber name="cardNumber" label="Card Number" />
Brazil-Specific Fields
Import from @ttoss/forms/brazil:
import {
FormFieldCEP,
FormFieldCNPJ,
FormFieldPhone,
} from '@ttoss/forms/brazil';
FormFieldCEP
Brazilian postal code (CEP) input with automatic formatting.
<FormFieldCEP name="cep" label="CEP" />
FormFieldCNPJ
Brazilian tax ID (CNPJ) input with validation and formatting.
<FormFieldCNPJ name="cnpj" label="CNPJ" />
The package also exports isCnpjValid(cnpj: string) for standalone validation.
FormFieldPhone
Brazilian phone number input with formatting.
<FormFieldPhone name="phone" label="Phone" />
FormGroup
Groups related fields with optional label and layout direction.
<FormGroup label="Personal Information" direction="row">
<FormFieldInput name="firstName" label="First Name" />
<FormFieldInput name="lastName" label="Last Name" />
</FormGroup>
<FormGroup label="Address">
<FormFieldInput name="street" label="Street" />
<FormFieldInput name="city" label="City" />
</FormGroup>
Props:
label: Group labeldirection: Layout direction ('row'|'column')name: Group name for error messages
Multistep Forms
Import from @ttoss/forms/multistep-form:
import { MultistepForm } from '@ttoss/forms/multistep-form';
import { FormFieldInput, yup } from '@ttoss/forms';
const steps = [
{
label: 'Step 1',
question: 'What is your name?',
fields: <FormFieldInput name="name" label="Name" />,
schema: yup.object({
name: yup.string().required('Name is required'),
}),
},
{
label: 'Step 2',
question: 'How old are you?',
fields: <FormFieldInput type="number" name="age" label="Age" />,
defaultValues: { age: 18 },
schema: yup.object({
age: yup
.number()
.min(18, 'Must be at least 18')
.required('Age is required'),
}),
},
];
const MyForm = () => {
return (
<MultistepForm
steps={steps}
onSubmit={(data) => console.log(data)}
footer="© 2024 Company"
header={{
variant: 'logo',
src: '/logo.png',
onClose: () => console.log('closed'),
}}
/>
);
};
MultistepForm Props
steps: Array of step configurationsonSubmit: Handler called with complete form datafooter: Footer textheader: Header configuration (see below)
Step Configuration
Each step object contains:
label: Step label for navigationquestion: Question or instruction textfields: React element(s) containing form fieldsschema: Yup validation schemadefaultValues: Optional default values for step fields
Header Types
Logo Header:
{
variant: 'logo',
src: '/path/to/logo.png',
onClose: () => console.log('Close clicked')
}
Titled Header:
{
variant: 'titled',
title: 'Form Title',
leftIcon: 'arrow-left',
rightIcon: 'close',
onLeftIconClick: () => console.log('Back'),
onRightIconClick: () => console.log('Close')
}