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​