Skip to main content

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 text
  • LongTextInput – Multi-line textarea
  • NumberInput – Numeric input
  • MoneyInput – Currency input
  • PercentInput – Percentage input
  • EmailInput – Email with validation
  • PhoneInput – Phone number input
  • PasswordInput – Password field
  • DateInput – Date picker
  • SimpleSelectInput – Basic select dropdown
  • SearchSelect – Searchable select with DataSource
  • RemoteSelectInput – Select from API endpoint
  • RadioGroup – Radio button group
  • ToggleInput – Toggle switch
  • ImageFileInput – Image upload
  • DocumentFileInput – Document upload
  • FileInput – Generic file upload
  • UUIDInput – UUID input
  • PinCodeInput – PIN code input
  • HiddenInput – 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

  1. Use Form Models – Define forms declaratively using FormModel arrays
  2. Leverage Computed Fields – Use computed fields for calculated values
  3. Validate Early – Use React Hook Form validation for immediate feedback
  4. Handle Auto-Save – Implement auto-save for long forms
  5. Use View Models – Define view models for consistent read-only displays
  6. Integrate with Data Sources – Use Data Sources for automatic CRUD operations

Next Steps