Custom Forms
Custom forms allow you to create specialized UI components for workflow nodes that aren't covered by built-in form types.
Registering Custom Forms
Custom forms must be registered in FormRegistryContext:
import { FormRegistryContext } from "@xond/workflow";
import { PersonalDataForm } from "./components/forms/PersonalDataForm";
import { ReviewDataForm } from "./components/forms/ReviewDataForm";
function App() {
const customForms = {
PersonalDataForm: PersonalDataForm,
ReviewDataForm: ReviewDataForm,
// Add more custom forms here
};
return (
<FormRegistryContext customForms={customForms}>
{/* Your app */}
</FormRegistryContext>
);
}
Creating a Custom Form
Custom forms must follow a specific interface:
import React from "react";
import { CustomFormProps } from "@xond/workflow";
export const PersonalDataForm: React.FC<CustomFormProps> = ({
returnValue,
inputData,
}) => {
const [formData, setFormData] = useState(inputData || {});
const handleSubmit = () => {
// Validate data
if (!formData.name || !formData.email) {
returnValue({ ready: false, data: formData });
return;
}
// Return ready state
returnValue({ ready: true, data: formData });
};
return (
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
<input
value={formData.name || ""}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Name"
/>
<input
type="email"
value={formData.email || ""}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder="Email"
/>
<button type="submit">Next</button>
</form>
);
};
Custom Form Interface
interface CustomFormProps {
returnValue: (value: { ready: boolean; data: any }) => void;
inputData: any; // Initial data from workflow instance
}
returnValue
Call returnValue to update workflow state:
// When form is not ready (validation failed, incomplete)
returnValue({ ready: false, data: formData });
// When form is ready (valid, complete)
returnValue({ ready: true, data: formData });
The ready flag controls whether action buttons are enabled.
inputData
inputData contains:
- Initial values from workflow instance
- Values from previous nodes
- Default values from node configuration
Using @xond/ui Components
Custom forms can use @xond/ui components:
import { TextInput, EmailInput, Button, useForm } from "@xond/ui";
import { CustomFormProps } from "@xond/workflow";
export const PersonalDataForm: React.FC<CustomFormProps> = ({
returnValue,
inputData,
}) => {
const { register, handleSubmit, watch, formState: { errors, isValid } } = useForm({
defaultValues: inputData || {},
});
const formData = watch();
useEffect(() => {
returnValue({
ready: isValid && formData.name && formData.email,
data: formData,
});
}, [formData, isValid]);
return (
<form onSubmit={handleSubmit(() => {})}>
<TextInput
label="Full Name"
{...register("name", { required: "Name is required" })}
error={errors.name?.message}
/>
<EmailInput
label="Email"
{...register("email", { required: "Email is required" })}
error={errors.email?.message}
/>
</form>
);
};
Advanced Custom Form
Here's a more advanced example with file upload:
import React, { useState, useEffect } from "react";
import { ImageFileInput, Button, useNotification } from "@xond/ui";
import { useForm } from "react-hook-form";
import { CustomFormProps } from "@xond/workflow";
export const KKUploadForm: React.FC<CustomFormProps> = ({
returnValue,
inputData,
}) => {
const { showNotification } = useNotification();
const { register, handleSubmit, watch, setValue, formState: { errors, isValid } } = useForm({
defaultValues: inputData || {},
});
const formData = watch();
const [uploading, setUploading] = useState(false);
useEffect(() => {
returnValue({
ready: isValid && formData.kkPhoto,
data: formData,
});
}, [formData, isValid]);
const handleFileChange = async (file: File) => {
setUploading(true);
try {
// Upload file
const formData = new FormData();
formData.append("file", file);
const response = await fetch("/api/upload", {
method: "POST",
body: formData,
});
const result = await response.json();
setValue("kkPhoto", result.url);
showNotification({
title: "Success",
statusIcon: "success",
description: "File uploaded successfully",
});
} catch (error) {
showNotification({
title: "Error",
statusIcon: "error",
description: "Failed to upload file",
});
} finally {
setUploading(false);
}
};
return (
<form onSubmit={handleSubmit(() => {})}>
<div className="mb-4">
<h2 className="text-xl font-bold">Upload KK (Family Card)</h2>
<p className="text-gray-600">Please upload a photo of your family card</p>
</div>
<ImageFileInput
label="KK Photo"
{...register("kkPhoto", { required: "KK photo is required" })}
error={errors.kkPhoto?.message}
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
handleFileChange(file);
}
}}
disabled={uploading}
/>
{uploading && <div>Uploading...</div>}
</form>
);
};
Accessing Workflow Context
Custom forms can access workflow instance data:
import { CustomFormProps } from "@xond/workflow";
export const CustomForm: React.FC<CustomFormProps & { workflowInstance?: any }> = ({
returnValue,
inputData,
workflowInstance,
}) => {
// Access workflow instance data
const personId = workflowInstance?.contextEntityId;
const workflowCode = workflowInstance?.workflow?.code;
// Use in form logic
return (
// Form JSX
);
};
Form Validation
Implement validation in custom forms:
const validateForm = (data: any): { valid: boolean; errors: string[] } => {
const errors: string[] = [];
if (!data.name) {
errors.push("Name is required");
}
if (!data.email) {
errors.push("Email is required");
} else if (!/\S+@\S+\.\S+/.test(data.email)) {
errors.push("Email is invalid");
}
return {
valid: errors.length === 0,
errors,
};
};
// Use in form
const { valid, errors } = validateForm(formData);
returnValue({ ready: valid, data: formData });
Best Practices
- Use returnValue properly – Call it whenever form state changes
- Validate data – Ensure data is valid before setting
ready: true - Handle loading states – Show loading indicators during async operations
- Use @xond/ui components – Leverage existing UI components
- Access inputData – Use initial data from workflow instance
- Handle errors – Show user-friendly error messages
- Test forms – Verify forms work in workflow context
Example: Complete Registration Form
import React, { useState, useEffect } from "react";
import { TextInput, EmailInput, PhoneInput, DateInput, Button } from "@xond/ui";
import { useForm } from "react-hook-form";
import { CustomFormProps } from "@xond/workflow";
export const RegistrationForm: React.FC<CustomFormProps> = ({
returnValue,
inputData,
}) => {
const { register, handleSubmit, watch, formState: { errors, isValid } } = useForm({
defaultValues: inputData || {},
});
const formData = watch();
useEffect(() => {
returnValue({
ready: isValid,
data: formData,
});
}, [formData, isValid]);
return (
<form onSubmit={handleSubmit(() => {})} className="space-y-4">
<h2 className="text-2xl font-bold">Registration</h2>
<TextInput
label="Full Name"
{...register("name", { required: "Name is required" })}
error={errors.name?.message}
/>
<EmailInput
label="Email"
{...register("email", { required: "Email is required" })}
error={errors.email?.message}
/>
<PhoneInput
label="Phone"
{...register("phone", { required: "Phone is required" })}
error={errors.phone?.message}
/>
<DateInput
label="Birth Date"
{...register("birthDate", { required: "Birth date is required" })}
error={errors.birthDate?.message}
/>
</form>
);
};
Next Steps
- Learn about workflow execution
- Explore integration patterns
- See node configuration