Skip to content

Form with Conditional Fields

Problem

Implement form with conditional fields in a production-friendly way with @vielzeug/formit while keeping setup and cleanup explicit.

Runnable Example

The snippet below is copy-paste runnable in a TypeScript project with @vielzeug/formit installed.

Form with fields that show/hide based on other field values.

typescript
import { createForm } from '@vielzeug/formit';

const profileForm = createForm({
  defaultValues: {
    accountType: 'personal' as 'personal' | 'business',
    name: '',
    email: '',
    // Business-only fields
    companyName: '',
    vatNumber: '',
    businessEmail: '',
  },
  validators: {
    name: (v) => (!v ? 'Name is required' : undefined),
    email: [
      (v) => (!v ? 'Email is required' : undefined),
      (v) => (v && !String(v).includes('@') ? 'Invalid email' : undefined),
    ],
  },
});

// Conditional validation based on account type
profileForm.subscribe(() => {
  const accountType = profileForm.get<string>('accountType');

  if (accountType === 'business') {
    if (!profileForm.get('companyName')) {
      profileForm.setError('companyName', 'Company name is required');
    } else {
      profileForm.setError('companyName');
    }
    if (!profileForm.get('vatNumber')) {
      profileForm.setError('vatNumber', 'VAT number is required');
    } else {
      profileForm.setError('vatNumber');
    }
    const businessEmail = profileForm.get<string>('businessEmail');
    if (!businessEmail) {
      profileForm.setError('businessEmail', 'Business email is required');
    } else if (!businessEmail.includes('@')) {
      profileForm.setError('businessEmail', 'Invalid email');
    } else {
      profileForm.setError('businessEmail');
    }
  } else {
    profileForm.setError('companyName');
    profileForm.setError('vatNumber');
    profileForm.setError('businessEmail');
  }
});

// Submit
async function submitProfile() {
  await profileForm.submit(async (values) => {
    const payload: Record<string, unknown> = {
      accountType: values['accountType'],
      name: values['name'],
      email: values['email'],
    };

    if (values['accountType'] === 'business') {
      payload['companyName'] = values['companyName'];
      payload['vatNumber'] = values['vatNumber'];
      payload['businessEmail'] = values['businessEmail'];
    }

    const response = await fetch('/api/profile', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });

    return response.json();
  });
}

Expected Output

  • The example runs without type errors in a standard TypeScript setup.
  • The main flow produces the behavior described in the recipe title.

Common Pitfalls

  • Forgetting cleanup/dispose calls can leak listeners or stale state.
  • Skipping explicit typing can hide integration issues until runtime.
  • Not handling error branches makes examples harder to adapt safely.