Browse docs
Browse docs
Build your first dynamic form with Dashforge in minutes.
The Dashforge Form System requires minimal setup. You wrap your form in DashForm, define your fields, and optionally add reactions for dynamic behavior.
import { DashForm } from '@dashforge/forms';
function MyForm() {
return (
<DashForm onSubmit={(data) => console.log(data)}>
{/* Your form fields go here */}
</DashForm>
);
}import { DashForm } from '@dashforge/forms';
import { TextField } from '@dashforge/tw';
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>
);
}Here it is running live — blur a field empty to trigger the required
rule, fill both and submit to see the payload. Use the StackBlitz
button on the toolbar to open it as a full project:
import { DashForm } from '@dashforge/forms';
import { TextField, Box, Stack, Button } from '@dashforge/tw';
export function SignupForm() {
return (
<Box sx="w-full max-w-md">
<DashForm
defaultValues={{ email: '', password: '' }}
mode="onBlur"
onSubmit={(data) => console.log(data)}
>
<Stack gap={2}>
<TextField
name="email"
label="Email"
rules={{ required: 'Email is required' }}
fullWidth
/>
<TextField
name="password"
label="Password"
type="password"
rules={{ required: 'Password is required' }}
fullWidth
/>
<Button type="submit" variant="solid" sx="self-start">
Submit
</Button>
</Stack>
</DashForm>
</Box>
);
}To make your form dynamic, pass a reactions array to DashForm:
import { DashForm } from '@dashforge/forms';
import { TextField, Select } from '@dashforge/tw';
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>
);
}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 } from '@dashforge/forms';
import { TextField, Select, Autocomplete } from '@dashforge/tw';
import { useState } from 'react';
// Mock API functions
async function fetchStates(country: string) {
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>
</div>
);
}This example demonstrates:
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.
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.
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.
The beginAsync / isLatest pattern prevents race conditions. If the user changes the country while states are loading, the system automatically discards the stale response.