Recipe: Building a form
Goal: collect input and run an Arc command — with the confirm button disabled until the form is valid and while it executes, validation wired up, and no manual fetch.
Prerequisites: the providers and styles from Getting started, and a generated command proxy. The examples use the Arc tutorial’s RegisterAuthor, which has a required id and a name.
Use CommandDialog
Section titled “Use CommandDialog”CommandDialog takes your generated command constructor, instantiates it, renders the fields you supply plus the OK/Cancel footer, and executes on confirm:
import { useState } from 'react';import { Guid } from '@cratis/fundamentals';import { CommandDialog } from '@cratis/components/CommandDialog';import { InputTextField } from '@cratis/components/CommandForm';import { RegisterAuthor } from './Authors/RegisterAuthor'; // generated proxy
export const AddAuthor = () => { const [id] = useState(() => Guid.create()); return ( <CommandDialog<RegisterAuthor> command={RegisterAuthor} title='Add author' okLabel='Add' initialValues={{ id }} validateOn='change' onSuccess={() => console.info('Author registered')} > <InputTextField<RegisterAuthor> value={(command) => command.name} title='Name' placeholder='Sample Author' /> </CommandDialog> );};The value={(command) => command.name} accessor is typed against the command — rename Name in C#, rebuild, and this line stops compiling until you fix it. Every field component in CommandForm binds the same way.
What happens at each stage:
- While editing — Add stays disabled until client validation passes: every required property has a value and every rule the backend shared with the proxy is satisfied. Field messages appear on blur by default;
validateOn='change'shows them as the user types, and'both'does both. - On confirm — every button is disabled while the command executes.
- On success —
onSuccessreceives the command’s response and the dialog closes. - On rejection — the dialog stays open; server validation messages appear against their fields, and
onValidationFailure,onFailed,onException, andonUnauthorizedreport the result.
Show the dialog
Section titled “Show the dialog”Open it from a button with Arc’s useDialog, and await the result if the caller needs to know how it closed:
import { DialogResult, useDialog } from '@cratis/arc.react/dialogs';import { AddAuthor } from './AddAuthor';
export const AuthorsToolbar = () => { const [AddAuthorDialog, showAddAuthor] = useDialog(AddAuthor); return ( <> <button type='button' onClick={async () => { const [result] = await showAddAuthor(); if (result === DialogResult.Ok) { // The command succeeded and the dialog closed. } }} > Add author </button> <AddAuthorDialog /> </> );};CommandDialog closes itself through the useDialog context: with DialogResult.Ok after a successful command, and with DialogResult.Cancelled from Cancel, the close button, or Escape. You don’t need to call closeDialog yourself. useDialog mounts the dialog only while it is open, so the id created in AddAuthor is fresh for every registration.
- Injected (non-input) values — set required values the user doesn’t type, like a new id or a parent id, via
initialValues, notonBeforeExecute.onBeforeExecuteruns only on submit, after validation, so a required value set there arrives too late and the confirm button stays disabled. UseonBeforeExecuteonly for transforms that don’t affect validity, and return the values from it. - Multi-step forms — reach for
StepperCommandDialogwhen one command needs to be gathered across several stages. - Inline forms — use
CommandFormdirectly when the fields belong on the page rather than in a dialog. - Use the Cratis dialog wrappers so execution, validation timing, focus behavior, and footers stay consistent.
- Displaying data — render the results
- CommandDialog — execution callbacks, busy behavior, dialog options, and context
- CommandForm — form binding, field discovery, and available field components