Skip to content

Form Lifecycle

Control form behavior throughout its lifecycle with hooks, state management, callbacks, and auto-save functionality.

Handle command execution results with dedicated callbacks. These callbacks are invoked automatically after command execution based on the result state:

import { CommandForm } from '@cratis/arc/commands';
import { ValidationResult } from '@cratis/arc/validation';
interface CreateUserResponse {
userId: string;
message: string;
}
function UserForm() {
const handleSuccess = (response: CreateUserResponse) => {
console.log('User created with ID:', response.userId);
// Navigate to user profile, show success message, etc.
};
const handleFailed = (result: CommandResult<CreateUserResponse>) => {
console.error('Command failed:', result);
// Handle general failure
};
const handleException = (messages: string[], stackTrace: string) => {
console.error('Exception occurred:', messages);
// Log exception, show error dialog, etc.
};
const handleUnauthorized = () => {
console.warn('User is not authorized');
// Redirect to login, show authorization message, etc.
};
const handleValidationFailure = (validationResults: ValidationResult[]) => {
console.warn('Validation failed:', validationResults);
// Additional validation failure handling beyond automatic field errors
};
return (
<CommandForm<CreateUser, CreateUserResponse>
command={CreateUser}
onSuccess={handleSuccess}
onFailed={handleFailed}
onException={handleException}
onUnauthorized={handleUnauthorized}
onValidationFailure={handleValidationFailure}
>
<InputTextField<CreateUser> value={c => c.name} title="Name" required />
<InputTextField<CreateUser> value={c => c.email} type="email" title="Email" required />
<button type="submit">Create User</button>
</CommandForm>
);
}
CallbackParametersWhen Invoked
onSuccess(response: TResponse) => voidCommand executed successfully
onFailed(commandResult: CommandResult<TResponse>) => voidCommand execution failed (any failure type)
onException(messages: string[], stackTrace: string) => voidCommand threw an exception
onUnauthorized() => voidUser is not authorized to execute the command
onValidationFailure(validationResults: ValidationResult[]) => voidCommand failed validation

When a command fails, multiple callbacks may be invoked:

  1. onFailed - Always called when isSuccess is false
  2. One or more specific callbacks based on failure type:
    • onException if hasExceptions is true
    • onUnauthorized if isAuthorized is false
    • onValidationFailure if isValid is false

CommandForm supports generic type parameters for type-safe responses:

// Define response type
interface OrderResponse {
orderId: string;
orderNumber: string;
totalAmount: number;
}
// Use generic type parameters
<CommandForm<CreateOrder, OrderResponse>
command={CreateOrder}
onSuccess={(response) => {
// response is strongly typed as OrderResponse
console.log(`Order ${response.orderNumber} created with ID ${response.orderId}`);
}}
>
{/* Form fields */}
</CommandForm>

Common pattern for navigating after successful command execution:

import { useNavigate } from 'react-router-dom';
function CreateProjectForm() {
const navigate = useNavigate();
const handleSuccess = (response: ProjectResponse) => {
// Navigate to the newly created project
navigate(`/projects/${response.projectId}`);
};
return (
<CommandForm<CreateProject, ProjectResponse>
command={CreateProject}
onSuccess={handleSuccess}
>
<InputTextField<CreateProject> value={c => c.name} title="Name" required />
<TextAreaField<CreateProject> value={c => c.description} title="Description" />
<button type="submit">Create Project</button>
</CommandForm>
);
}

Execute code before the command is submitted:

function OrderForm() {
const handleBeforeExecute = async (command: CreateOrder): Promise<boolean> => {
// Confirm before submitting
const confirmed = window.confirm('Submit this order?');
if (!confirmed) {
return false; // Cancel submission
}
// Add calculated fields
command.totalAmount = calculateTotal(command.items);
command.submittedAt = new Date();
return true; // Proceed with submission
};
return (
<CommandForm
command={CreateOrder}
beforeExecute={handleBeforeExecute}
>
<InputTextField<CreateOrder> value={c => c.customerName} title="Customer" required />
{/* More fields... */}
</CommandForm>
);
}

The beforeExecute callback:

  • Receives the command instance
  • Returns true to proceed, false to cancel
  • Can modify the command before submission
  • Can perform async operations

Track form state for enhanced UX:

function SmartForm() {
const command = useCommandInstance(UpdateProfile);
const [isSubmitting, setIsSubmitting] = useState(false);
const [lastSaved, setLastSaved] = useState<Date | null>(null);
const handleExecute = async () => {
setIsSubmitting(true);
try {
const result = await command.execute();
if (result.isSuccess) {
setLastSaved(new Date());
}
} finally {
setIsSubmitting(false);
}
};
return (
<div>
<CommandForm command={UpdateProfile}>
<InputTextField<UpdateProfile> value={c => c.name} title="Name" />
<InputTextField<UpdateProfile> value={c => c.email} type="email" title="Email" />
</CommandForm>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '1rem' }}>
<button
onClick={handleExecute}
disabled={!command.hasChanges || isSubmitting}
>
{isSubmitting ? 'Saving...' : 'Save'}
</button>
{lastSaved && (
<span style={{ fontSize: '0.875rem', color: '#6b7280' }}>
Last saved: {lastSaved.toLocaleTimeString()}
</span>
)}
{command.hasChanges && (
<span style={{ fontSize: '0.875rem', color: '#f59e0b' }}>
Unsaved changes
</span>
)}
</div>
</div>
);
}

Implement auto-save functionality:

function AutoSaveForm() {
const command = useCommandInstance(UpdateDraft);
const timeoutRef = useRef<NodeJS.Timeout>();
useEffect(() => {
if (command.hasChanges) {
// Clear previous timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
// Set new timeout to auto-save after 2 seconds
timeoutRef.current = setTimeout(async () => {
await command.execute();
console.log('Auto-saved');
}, 2000);
}
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, [command.hasChanges]);
return (
<CommandForm command={UpdateDraft}>
<InputTextField<UpdateDraft> value={c => c.title} title="Title" />
<TextAreaField<UpdateDraft> value={c => c.content} title="Content" rows={10} />
<div style={{ fontSize: '0.875rem', color: '#6b7280', marginTop: '0.5rem' }}>
Changes are saved automatically
</div>
</CommandForm>
);
}