Form System Quick Start | Dashforge-UI
DocsStarter Kits
v0.1.0-alpha

Quick Start

Build your first dynamic form with Dashforge in minutes.

Basic Setup

Three steps to get started

The Dashforge Form System requires minimal setup. You wrap your form in DashForm, define your fields, and optionally add reactions for dynamic behavior.

Step 1: Wrap your form

import { DashForm } from '@dashforge/ui';

function MyForm() {
  return (
    <DashForm onSubmit={(data) => console.log(data)}>
      {/* Your form fields go here */}
    </DashForm>
  );
}

Step 2: Add form fields

import { DashForm, TextField, Select } from '@dashforge/ui';

function MyForm() {
  return (
    <DashForm onSubmit={(data) => console.log(data)}>
      <TextField 
        name="email" 
        label="Email"
        rules={{ required: 'Email is required' }}
      />
      
      <TextField 
        name="password" 
        label="Password" 
        type="password"
        rules={{ required: 'Password is required' }}
      />
    </DashForm>
  );
}

Step 3: Add dynamic behavior (optional)

To make your form dynamic, pass a reactions array to DashForm:

import { DashForm, TextField, Select } from '@dashforge/ui';

function AddressForm() {
  const reactions = [
    {
      id: 'load-states',
      watch: ['country'],
      run: async (ctx) => {
        const country = ctx.getValue<string>('country');
        
        if (!country) {
          ctx.setRuntime('state', { status: 'idle', data: null });
          return;
        }

        const requestId = ctx.beginAsync('fetch-states');
        ctx.setRuntime('state', { status: 'loading' });

        const states = await fetchStatesByCountry(country);

        if (ctx.isLatest('fetch-states', requestId)) {
          ctx.setRuntime('state', {
            status: 'ready',
            data: { options: states }
          });
        }
      }
    }
  ];

  return (
    <DashForm 
      reactions={reactions}
      onSubmit={(data) => console.log(data)}
    >
      <Select
        name="country"
        label="Country"
        options={['United States', 'Canada', 'Mexico']}
        rules={{ required: 'Country is required' }}
      />

      <Select
        name="state"
        label="State / Province"
        optionsFromFieldData
        visibleWhen={(engine) => 
          engine.getNode('country')?.value != null
        }
        rules={{ required: 'State is required' }}
      />

      <TextField name="city" label="City" />
    </DashForm>
  );
}

That's it! You now have a form where the state dropdown automatically loads options when a country is selected, with loading states and stale response handling built in.


Complete Working Example

A realistic form with chained dependencies

This example shows what makes Dashforge powerful: three cascading dropdowns (Country → State → City), with async loading, automatic stale response protection, and conditional visibility—all without useEffect or manual state management.

import { DashForm, TextField, Select, Autocomplete } from '@dashforge/ui';
import { useState } from 'react';

// Mock API functions
async function fetchStates(country: string) {
  // Simulate API delay
  await new Promise(resolve => setTimeout(resolve, 500));
  
  const statesByCountry = {
    'United States': ['California', 'Texas', 'New York', 'Florida'],
    'Canada': ['Ontario', 'Quebec', 'British Columbia', 'Alberta'],
    'Mexico': ['Jalisco', 'Nuevo León', 'Yucatán', 'Quintana Roo']
  };
  
  return statesByCountry[country as keyof typeof statesByCountry] || [];
}

async function fetchCities(state: string) {
  await new Promise(resolve => setTimeout(resolve, 500));
  
  const citiesByState: Record<string, string[]> = {
    'California': ['Los Angeles', 'San Francisco', 'San Diego'],
    'Texas': ['Houston', 'Austin', 'Dallas'],
    'Ontario': ['Toronto', 'Ottawa', 'Hamilton'],
    'Quebec': ['Montreal', 'Quebec City', 'Laval']
  };
  
  return citiesByState[state] || [];
}

export function DynamicAddressForm() {
  const [submittedData, setSubmittedData] = useState<any>(null);

  const reactions = [
    {
      id: 'load-states',
      watch: ['country'],
      run: async (ctx) => {
        const country = ctx.getValue<string>('country');
        
        if (!country) {
          ctx.setRuntime('state', { status: 'idle', data: null });
          return;
        }

        const requestId = ctx.beginAsync('fetch-states');
        ctx.setRuntime('state', { status: 'loading' });

        const states = await fetchStates(country);

        if (ctx.isLatest('fetch-states', requestId)) {
          ctx.setRuntime('state', {
            status: 'ready',
            data: { options: states }
          });
        }
      }
    },
    {
      id: 'load-cities',
      watch: ['state'],
      run: async (ctx) => {
        const state = ctx.getValue<string>('state');
        
        if (!state) {
          ctx.setRuntime('city', { status: 'idle', data: null });
          return;
        }

        const requestId = ctx.beginAsync('fetch-cities');
        ctx.setRuntime('city', { status: 'loading' });

        const cities = await fetchCities(state);

        if (ctx.isLatest('fetch-cities', requestId)) {
          ctx.setRuntime('city', {
            status: 'ready',
            data: { options: cities }
          });
        }
      }
    }
  ];

  return (
    <div>
      <DashForm
        reactions={reactions}
        onSubmit={(data) => setSubmittedData(data)}
      >
        <Select
          name="country"
          label="Country"
          options={['United States', 'Canada', 'Mexico']}
          rules={{ required: 'Please select a country' }}
        />

        <Select
          name="state"
          label="State / Province"
          optionsFromFieldData
          visibleWhen={(engine) => 
            engine.getNode('country')?.value != null
          }
          rules={{ required: 'Please select a state' }}
        />

        <Autocomplete
          name="city"
          label="City"
          optionsFromFieldData
          visibleWhen={(engine) => 
            engine.getNode('state')?.value != null
          }
          rules={{ required: 'Please enter a city' }}
        />

        <TextField
          name="postalCode"
          label="Postal Code"
          visibleWhen={(engine) => 
            engine.getNode('city')?.value != null
          }
        />

        <button type="submit">Submit</button>
      </DashForm>

      {submittedData && (
        <div>
          <h3>Submitted Data:</h3>
          <pre>{JSON.stringify(submittedData, null, 2)}</pre>
        </div>
      )}
    </div>
  );
}

This example demonstrates:

  • Chained dependencies: Country → States → Cities
  • Async data loading: Fetching options from APIs
  • Loading states: Automatic loading indicators
  • Stale response protection: Prevents race conditions
  • Conditional visibility: Fields appear based on previous selections
  • Form validation: Standard React Hook Form validation

ℹ️

Notice how the component code is clean and declarative. All the orchestration logic lives in the reactions array, not scattered throughout the component.


Key Concepts

Understanding what makes this work

Reactions

Declarative side effects that run when watched fields change. They can read values, fetch data, and update runtime state. The system handles execution timing and async coordination automatically.

Runtime State

Separate storage for field metadata like loading status, dynamic options, and async errors. This keeps your form values clean while providing rich UI feedback. Fields read from runtime state using optionsFromFieldData.

Conditional Visibility

Fields can appear or disappear based on other field values using the visibleWhen prop. This prop receives the reactive engine and can read any field's current state to determine visibility.

Stale Response Protection

The beginAsync / isLatest pattern prevents race conditions. If the user changes the country while states are loading, the system automatically discards the stale response.


Next Steps

Continue learning

Now that you understand the basics, explore more advanced concepts:

  • Reactions: Deep dive into reaction lifecycle, conditions, and patterns
  • Dynamic Forms: Learn all the ways to build adaptive forms
  • Patterns: Best practices for structuring complex forms
  • API Reference: Complete reference for all form system APIs

On This Page