Table of Contents

Validation

CommandForm integrates seamlessly with the Arc command validation system to provide automatic validation feedback and error handling.

Overview

CommandForm automatically validates field inputs and displays errors based on:

  • Required Fields: Fields marked with required prop
  • Type Validation: Built-in HTML5 validation (email, URL, number ranges, etc.)
  • Command Validation Rules: Backend validation rules defined on your command
  • Custom Validation: Custom validators you define

Validation Timing

CommandForm provides flexible control over when validation occurs through the validateOn prop:

Validate on Blur (Default)

By default, validation occurs when a field loses focus (blur event). This provides a balance between immediate feedback and not interrupting the user while typing:

<CommandForm command={RegisterUser} validateOn="blur">
    <InputTextField<RegisterUser> value={c => c.email} type="email" title="Email" required />
    <InputTextField<RegisterUser> value={c => c.password} type="password" title="Password" required />
</CommandForm>

When to use: Most forms - provides feedback after the user completes a field without being intrusive.

Validate on Change

Validation runs immediately as the user types. This provides the fastest feedback but can be distracting:

<CommandForm command={RegisterUser} validateOn="change">
    <InputTextField<RegisterUser> value={c => c.email} type="email" title="Email" required />
    <InputTextField<RegisterUser> value={c => c.password} type="password" title="Password" required />
</CommandForm>

When to use: Forms where immediate validation is critical, like password strength meters or username availability checks.

Validate on Both

Validation runs on both change and blur events:

<CommandForm command={RegisterUser} validateOn="both">
    <InputTextField<RegisterUser> value={c => c.email} type="email" title="Email" required />
    <InputTextField<RegisterUser> value={c => c.password} type="password" title="Password" required />
</CommandForm>

When to use: Forms where continuous validation is important for complex rules.

Validation Scope

Control whether validation validates just the changed field or all fields:

Per-Field Validation (Default)

By default, only the field that changed is validated. This is more efficient and provides focused feedback:

<CommandForm 
    command={CreateUser} 
    validateOn="blur"
>
    <InputTextField<CreateUser> value={c => c.username} title="Username" required />
    <InputTextField<CreateUser> value={c => c.email} type="email" title="Email" required />
</CommandForm>

In this example, when the username field loses focus, only username validation runs.

Full Form Validation

Set validateAllFieldsOnChange to true to validate the entire form when any field changes:

<CommandForm 
    command={CreateUser} 
    validateOn="blur"
    validateAllFieldsOnChange={true}
>
    <InputTextField<CreateUser> value={c => c.username} title="Username" required />
    <InputTextField<CreateUser> value={c => c.email} type="email" title="Email" required />
    <InputTextField<CreateUser> value={c => c.confirmEmail} type="email" title="Confirm Email" required />
</CommandForm>

When to use: Forms with interdependent fields where one field's validity depends on another (e.g., password confirmation, start/end dates).

Silent Validation on Load

CommandForm always runs client validation silently on load, regardless of any other settings. This means isValid in the form context correctly reflects the real validity state from the very first render — before the user has interacted with any field.

This is useful for scenarios such as:

  • Disabling a submit button until the form is valid
  • Conditionally rendering actions based on form state
  • Knowing whether previously loaded data is valid before the user touches anything
function EditUserForm({ user }: { user: User }) {
    return (
        <CommandForm command={UpdateUser} currentValues={user}>
            <InputTextField<UpdateUser> value={c => c.username} title="Username" required />
            <InputTextField<UpdateUser> value={c => c.email} type="email" title="Email" required />
            <SubmitButton /> {/* can use isValid from useCommandFormContext */}
        </CommandForm>
    );
}

The silent validation does not display any error messages. Errors are only rendered after the user has interacted with a field (governed by validateOn) or when validateOnInit is set to true.

Showing Errors on Load (validateOnInit)

Set validateOnInit to true to show validation error messages immediately when the form renders:

<CommandForm 
    command={CreateUser} 
    validateOnInit={true}
    initialValues={{ username: '', email: '' }}
>
    <InputTextField<CreateUser> value={c => c.username} title="Username" required />
    <InputTextField<CreateUser> value={c => c.email} type="email" title="Email" required />
</CommandForm>

When to use:

  • Edit forms where existing data might have validation issues
  • Forms where you want to show all errors upfront
  • Step-by-step wizards showing validation state of upcoming steps

For automatic server-side validation as users type, see Auto Server Validation.

Validation Options Summary

Prop Type Default Description
validateOn 'blur' \| 'change' \| 'both' 'blur' When to trigger validation
validateAllFieldsOnChange boolean false Validate all fields or just the changed field
validateOnInit boolean false Show validation error messages on form initialization (validation itself always runs silently on load)

Examples

Gentle validation (recommended for most forms):

<CommandForm command={T} validateOn="blur" />

Note: isValid in context is already accurate on first render due to silent validation on load, even though no errors are displayed yet.

Aggressive validation (real-time feedback):

<CommandForm command={T} validateOn="change" validateAllFieldsOnChange={true} />

Show all errors immediately:

<CommandForm command={T} validateOn="blur" validateOnInit={true} />

Required Fields

Mark fields as required using the required prop:

<CommandForm command={RegisterUser}>
    <InputTextField<RegisterUser> value={c => c.email} type="email" title="Email" required />
    <InputTextField<RegisterUser> value={c => c.password} type="password" title="Password" required />
    <CheckboxField<RegisterUser> value={c => c.agreeToTerms} title="Terms" label="I agree" required />
</CommandForm>

Required fields:

  • Show visual indicator when invalid
  • Prevent form submission when empty
  • Display error messages when validation fails

Automatic Error Display

By default, CommandForm displays error messages below each invalid field:

// Errors shown automatically for invalid/required fields
<CommandForm command={CreateAccount}>
    <InputTextField<CreateAccount> value={c => c.username} title="Username" required />
    {/* Error appears here if username is empty or invalid */}
    
    <InputTextField<CreateAccount> value={c => c.email} type="email" title="Email" required />
    {/* Error appears here if email is invalid format */}
</CommandForm>

Disabling Error Display

Disable automatic errors to implement custom error rendering:

<CommandForm command={CreateAccount} showErrors={false}>
    <InputTextField<CreateAccount> value={c => c.username} title="Username" required />
    {/* No automatic error rendering */}
</CommandForm>

See Customization for custom error rendering patterns.

HTML5 Validation

Field components leverage HTML5 validation attributes:

<CommandForm command={UpdateProfile}>
    {/* Email format validation */}
    <InputTextField<UpdateProfile>
        value={c => c.email} 
        type="email" 
        title="Email" 
        required 
    />
    
    {/* URL format validation */}
    <InputTextField<UpdateProfile>
        value={c => c.website} 
        type="url" 
        title="Website" 
        placeholder="https://example.com"
    />
    
    {/* Number range validation */}
    <NumberField<UpdateProfile>
        value={c => c.age} 
        title="Age" 
        min={18} 
        max={120} 
        required 
    />
    
    {/* Pattern matching */}
    <InputTextField<UpdateProfile>
        value={c => c.phone} 
        type="tel" 
        title="Phone Number"
    />
</CommandForm>

Backend Validation

CommandForm automatically propagates validation results from backend command handlers:

Command Definition (C#)

public record CreateUser(string Email, string Username);

public class CreateUserHandler : ICommandHandler<CreateUser>
{
    public async Task<CommandResult> Execute(CreateUser command)
    {
        // Backend validation
        if (await UserExists(command.Email))
        {
            return CommandResult.Failed("A user with this email already exists");
        }
        
        if (command.Username.Length < 3)
        {
            return CommandResult.Failed("Username must be at least 3 characters");
        }
        
        // Process command...
        return CommandResult.Success();
    }
}

Form Usage

<CommandForm command={CreateUser}>
    <InputTextField<CreateUser> value={c => c.email} type="email" title="Email" required />
    <InputTextField<CreateUser> value={c => c.username} title="Username" required />
</CommandForm>

When the form is submitted:

  1. Frontend validation runs first (required, type checking)
  2. Command is sent to backend if frontend validation passes
  3. Backend validation rules execute
  4. Validation errors are returned and displayed in the form
  5. The form remains interactive for corrections

Accessing Validation State

Use the useCommandFormContext hook to access validation state programmatically:

import { useCommandFormContext } from '@cratis/applications-react/commands';

function MyForm() {
    const { getFieldError, commandResult } = useCommandFormContext();
    const emailError = getFieldError('email');
    const hasAnyErrors = commandResult?.validationResults && commandResult.validationResults.length > 0;
    
    return (
        <CommandForm command={CreateAccount} showErrors={false}>
            <InputTextField<CreateAccount> value={c => c.email} type="email" title="Email" required />
            
            {/* Check for specific field errors */}
            {emailError && (
                <div className="error">
                    {emailError}
                </div>
            )}
            
            <InputTextField<CreateAccount> value={c => c.username} title="Username" required />
            
            {/* Check for any errors */}
            {hasAnyErrors && (
                <div className="error-summary">
                    Please fix the errors above before submitting.
                </div>
            )}
        </CommandForm>
    );
}

Progressive Validation

Validate as users interact with the form using the useCommandInstance hook:

import { useCommandInstance } from '@cratis/applications-react/commands';
import { useEffect } from 'react';

function MyForm() {
    const command = useCommandInstance(CreateAccount);
    const [canSubmit, setCanSubmit] = useState(false);
    
    useEffect(() => {
        // Validate whenever command changes
        const validate = async () => {
            const result = await command.validate();
            setCanSubmit(result.isValid);
        };
        
        if (command.hasChanges) {
            validate();
        }
    }, [command.hasChanges]);
    
    return (
        <CommandForm command={CreateAccount}>
            <InputTextField<CreateAccount> value={c => c.email} type="email" title="Email" required />
            <InputTextField<CreateAccount> value={c => c.username} title="Username" required />
            
            <button type="submit" disabled={!canSubmit}>
                Create Account
            </button>
        </CommandForm>
    );
}

Field-Level Validation

Validate individual fields on blur for immediate feedback:

function RegistrationForm() {
    const command = useCommandInstance(RegisterUser);
    const [emailError, setEmailError] = useState<string>();
    
    const handleEmailBlur = async () => {
        // Validate just the email field
        const result = await command.validate();
        
        if (result.hasErrors('email')) {
            setEmailError(result.getErrorsFor('email')[0]);
        } else {
            setEmailError(undefined);
        }
    };
    
    return (
        <CommandForm command={RegisterUser} showErrors={false}>
            <InputTextField<RegisterUser>
                value={c => c.email} 
                type="email" 
                title="Email" 
                required 
                onBlur={handleEmailBlur}
            />
            {emailError && <div className="error">{emailError}</div>}
        </CommandForm>
    );
}

Validation Results

The command validate() method returns a CommandResult with:

interface CommandResult {
    isSuccess: boolean;
    isValid: boolean;
    isAuthorized: boolean;
    validationResults: ValidationResult[];
    errors: Record<string, string[]>;
    
    hasErrors(property?: string): boolean;
    getErrorsFor(property: string): string[];
}

Example

const result = await command.validate();

if (!result.isValid) {
    console.log('Validation failed');
    console.log('All errors:', result.errors);
    console.log('Email errors:', result.getErrorsFor('email'));
}

if (!result.isAuthorized) {
    console.log('User not authorized to execute this command');
}

Best Practices

  1. Use Required Fields: Mark essential fields with required for client-side validation
  2. Type-Specific Fields: Use appropriate input types (email, url, number) for built-in validation
  3. Backend Validation: Always validate on the server for security and data integrity
  4. Progressive Feedback: Consider validating on field blur for better UX
  5. Clear Messages: Provide clear, actionable error messages
  6. Accessible Errors: Ensure error messages are associated with their fields for screen readers
  7. Visual Feedback: Use consistent visual styling for invalid fields

Command Validation System

For comprehensive details on the command validation system:

See Also