Forms & Validation
@xond/ui provides a powerful form system built on React Hook Form, with automatic validation, computed fields, and seamless integration with Data Sources.
Form Component
The Form component generates forms dynamically from model definitions, handling validation, submission, and data management automatically.
Basic Usage
import { Form } from "@xond/ui";
import { useRemoteDataSource } from "@xond/ui";
function UserForm() {
const dataSource = useRemoteDataSource({
resourceName: "users",
entityName: "user",
endpoint: "/api/users",
});
return (
<Form
formModel={userFormModel}
dataSource={dataSource}
onClose={() => dataSource.startView(null)}
/>
);
}
Form Model Definition
Form models define the structure and behavior of forms. They're typically generated by @xond/api but can be customized.
Basic Form Model
const userFormModel: FormModel[] = [
{
fieldName: "name",
fieldType: "TextInput",
label: "Full Name",
isNullable: false,
isRequired: true,
placeholder: "Enter full name",
},
{
fieldName: "email",
fieldType: "EmailInput",
label: "Email Address",
isNullable: false,
isRequired: true,
validation: {
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "Invalid email address",
},
},
},
{
fieldName: "age",
fieldType: "NumberInput",
label: "Age",
isNullable: true,
validation: {
min: { value: 0, message: "Age must be positive" },
max: { value: 120, message: "Age must be less than 120" },
},
},
{
fieldName: "status",
fieldType: "SimpleSelectInput",
label: "Status",
options: ["Active", "Inactive", "Pending"],
isRequired: true,
},
];
Field Types
Supported field types map to input components:
TextInput– Single-line textLongTextInput– Multi-line textareaNumberInput– Numeric inputMoneyInput– Currency inputPercentInput– Percentage inputEmailInput– Email with validationPhoneInput– Phone number inputPasswordInput– Password fieldDateInput– Date pickerSimpleSelectInput– Basic select dropdownSearchSelect– Searchable select with DataSourceRemoteSelectInput– Select from API endpointRadioGroup– Radio button groupToggleInput– Toggle switchImageFileInput– Image uploadDocumentFileInput– Document uploadFileInput– Generic file uploadUUIDInput– UUID inputPinCodeInput– PIN code inputHiddenInput– Hidden field
Advanced Form Features
Computed Fields
Fields can compute their values based on other fields:
const formModel: FormModel[] = [
{
fieldName: "quantity",
fieldType: "NumberInput",
label: "Quantity",
},
{
fieldName: "unitPrice",
fieldType: "MoneyInput",
label: "Unit Price",
},
{
fieldName: "total",
fieldType: "MoneyInput",
label: "Total",
computedValue: async (formData) => {
const qty = formData.quantity || 0;
const price = formData.unitPrice || 0;
return qty * price;
},
dependsOn: ["quantity", "unitPrice"], // Recompute when these change
readOnly: true, // Computed fields are typically read-only
},
];
Conditional Fields
Show/hide fields based on other field values:
const formModel: FormModel[] = [
{
fieldName: "hasDiscount",
fieldType: "ToggleInput",
label: "Apply Discount",
},
{
fieldName: "discountPercent",
fieldType: "PercentInput",
label: "Discount Percentage",
showWhen: (formData) => formData.hasDiscount === true,
},
];
Field Dependencies
Fields can depend on other fields for options or validation:
const formModel: FormModel[] = [
{
fieldName: "country",
fieldType: "SimpleSelectInput",
label: "Country",
options: ["USA", "Canada", "Mexico"],
},
{
fieldName: "state",
fieldType: "SimpleSelectInput",
label: "State/Province",
options: async (formData) => {
// Fetch states based on selected country
const states = await fetchStates(formData.country);
return states;
},
dependsOn: ["country"],
},
];
Validation Rules
React Hook Form validation rules are supported:
const formModel: FormModel[] = [
{
fieldName: "email",
fieldType: "EmailInput",
label: "Email",
validation: {
required: "Email is required",
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "Invalid email address",
},
},
},
{
fieldName: "password",
fieldType: "PasswordInput",
label: "Password",
validation: {
required: "Password is required",
minLength: {
value: 8,
message: "Password must be at least 8 characters",
},
},
},
{
fieldName: "confirmPassword",
fieldType: "PasswordInput",
label: "Confirm Password",
validation: {
required: "Please confirm password",
validate: (value, formValues) => {
return value === formValues.password || "Passwords do not match";
},
},
},
];
Auto-Save
Forms support auto-save functionality for draft saving:
const [autoSaveStatus, setAutoSaveStatus] = useState<{
ready: boolean;
data: any;
} | null>(null);
<Form
formModel={userFormModel}
dataSource={dataSource}
autoSave={(status) => {
setAutoSaveStatus(status);
// Save draft to localStorage or API
if (status.ready) {
localStorage.setItem("draft", JSON.stringify(status.data));
}
}}
/>
Form Modes
Forms support three modes:
Add Mode
dataSource.startAdd(); // Opens form in add mode
// or
dataSource.startAdd({ name: "Default Name" }); // With initial data
Edit Mode
dataSource.startEdit(userRow); // Opens form in edit mode with user data
View Mode
dataSource.startView(userRow); // Opens form in read-only view mode
View Model
View models define how data is displayed in view mode:
const viewModel: ViewModel[] = [
{
fieldName: "name",
label: "Full Name",
component: "ValueView",
},
{
fieldName: "email",
label: "Email",
component: "ValueView",
},
{
fieldName: "avatar",
label: "Profile Picture",
component: "EncodedImageView",
},
{
fieldName: "bio",
label: "Biography",
component: "LongTextView",
},
];
Use view model with Form:
<Form
formModel={formModel}
viewModel={viewModel} // For view mode display
dataSource={dataSource}
/>
Form Integration with Data Sources
Forms automatically integrate with Data Sources:
const dataSource = useRemoteDataSource({
resourceName: "users",
entityName: "user",
endpoint: "/api/users",
onAfterSave: async (data) => {
// Form automatically calls this after successful save
notification.showNotification({
type: "success",
message: "User saved successfully",
});
dataSource.load(); // Reload data
},
});
// Open form
const handleAdd = () => {
dataSource.startAdd();
};
// Form handles submission automatically
<Form formModel={userFormModel} dataSource={dataSource} />
Custom Form Rendering
For advanced use cases, you can render forms manually:
import { useForm } from "react-hook-form";
import { TextInput, Button } from "@xond/ui";
function CustomForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
const onSubmit = async (data) => {
// Custom submission logic
await saveUser(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<TextInput
label="Name"
{...register("name", { required: "Name is required" })}
error={errors.name?.message}
/>
<Button type="submit">Save</Button>
</form>
);
}
Best Practices
- Use Form Models – Define forms declaratively using FormModel arrays
- Leverage Computed Fields – Use computed fields for calculated values
- Validate Early – Use React Hook Form validation for immediate feedback
- Handle Auto-Save – Implement auto-save for long forms
- Use View Models – Define view models for consistent read-only displays
- Integrate with Data Sources – Use Data Sources for automatic CRUD operations
Next Steps
- Learn about data sources
- Explore integration patterns
- See component examples