CommandDialog
The CommandDialog component provides a dialog interface for executing commands with built-in form handling and validation.
Purpose
Section titled “Purpose”CommandDialog simplifies the process of presenting a command form to users within a modal dialog. It handles command execution, validation, and result management automatically.
Key Features
Section titled “Key Features”- Binds
CommandFormfield children to one command instance - Keeps the confirm button disabled until the command passes client validation
- Field-level change tracking and custom field validation
- Pre-execution transformation of values and optional approval before execution
- Success and cancellation handling through the Arc dialog context
- Busy state management during command execution (buttons and fields disabled, spinner shown)
- Integration with Cratis Arc command system
Recommended Usage Pattern
Section titled “Recommended Usage Pattern”For new implementations, use the same dialog pattern as other typed dialogs:
- Open dialogs through
useDialog<TResult>() - Close from inside the dialog through
useDialogContext<TRequest, TResult>()when you need to return a value awaitthe dialog at the call site and handle[dialogResult, value]
useDialogContext takes the request (input props) type first and the result type second. When the dialog returns the command’s response, use the response type as TResult and pass it to closeDialog from onSuccess.
Basic Usage
Section titled “Basic Usage”The example assumes CreateProject is the proxy Arc generates from a C# [Command] whose Handle() returns a CreateProjectResponse, and that the app renders <Arc> around CratisComponentsProvider.
import { DialogResult, useDialog, useDialogContext } from '@cratis/arc.react/dialogs';import { CommandDialog } from '@cratis/components/CommandDialog';import { InputTextField } from '@cratis/components/CommandForm';import { CreateProject } from './CreateProject';
type CreateProjectResponse = { projectId: string;};
const CreateProjectDialog = () => { const { closeDialog } = useDialogContext<object, CreateProjectResponse>();
return ( <CommandDialog<CreateProject, CreateProjectResponse> command={CreateProject} title='Create project' okLabel='Create' onSuccess={(response) => closeDialog(DialogResult.Ok, response)} > <InputTextField<CreateProject> value={(c) => c.name} title='Name' /> <InputTextField<CreateProject> value={(c) => c.email} title='Contact email' type='email' /> </CommandDialog> );};
export const Projects = () => { const [CreateProjectDialogWrapper, showCreateProjectDialog] = useDialog<CreateProjectResponse>(CreateProjectDialog);
const createProject = async () => { const [result, response] = await showCreateProjectDialog(); if (result === DialogResult.Ok && response) { console.log('Created project', response.projectId); } };
return ( <> <button type='button' onClick={createProject}> Create project </button> <CreateProjectDialogWrapper /> </> );};What happens when the user clicks Create:
- The command runs. While it is in flight the dialog is busy (see Busy State).
- On failure the matching result callbacks fire, field errors from the server appear on the form, and the dialog stays open.
- On success
onSuccessreceives the typed response. Here it callscloseDialog(DialogResult.Ok, response), which closes the dialog and resolves the caller’sawaitwith the response.CommandDialogthen also closes through the dialog context withDialogResult.Ok;useDialoghas already resolved, so that second close changes nothing.
Cancel, the header close (X), Escape, and a backdrop click close the dialog with DialogResult.Cancelled without any callback. You only need onSuccess here because the caller wants the response; a dialog whose caller only needs to know that it succeeded can leave out every callback.
Initialize command values
Section titled “Initialize command values”A generated command proxy starts with every property unset. Arc’s client validation reports any non-optional property that is still undefined or null, so the confirm button stays disabled until each one has a value. Values that no field renders, such as the id of the item being edited, must be seeded explicitly.
| Prop | When it applies | Counts for validation before confirm |
|---|---|---|
initialValues | Seeds the command when the form mounts and becomes the change-tracking baseline | Yes |
currentValues | Overlays values the parent keeps changing; the form re-validates when they change | Yes |
onBeforeExecute | Transforms the values only after the user clicks confirm, immediately before execution | No |
Pass required ids and the values being edited through initialValues:
import { useDialog } from '@cratis/arc.react/dialogs';import { CommandDialog } from '@cratis/components/CommandDialog';import { InputTextField } from '@cratis/components/CommandForm';import { Guid } from '@cratis/fundamentals';import { RenameProject } from './RenameProject';
type RenameProjectDialogProps = { projectId: Guid; name: string;};
const RenameProjectDialog = ({ projectId, name }: RenameProjectDialogProps) => ( <CommandDialog<RenameProject> command={RenameProject} title='Rename project' initialValues={{ projectId, name }} > <InputTextField<RenameProject> value={(c) => c.name} title='Name' /> </CommandDialog>);
export const RenameProjectButton = (props: RenameProjectDialogProps) => { const [RenameProjectDialogWrapper, showRenameProjectDialog] = useDialog(RenameProjectDialog);
return ( <> <button type='button' onClick={() => showRenameProjectDialog(props)}> Rename </button> <RenameProjectDialogWrapper /> </> );};For a client-generated id, create it once and seed it the same way, for example const [projectId] = useState(() => Guid.create()); followed by initialValues={{ projectId }}. Keeping the value stable avoids generating a new id every time the component renders.
Field display defaults are not command values either. A CheckboxField shows an unchecked box and a NumberField shows 0 while the bound property is still unset, so a non-optional boolean or number keeps confirm disabled until the user edits it. Seed those values, for example initialValues={{ isActive: false }}, when the default is an acceptable answer.
Validation and when confirm enables
Section titled “Validation and when confirm enables”The confirm button is enabled when all of the following hold:
- Arc’s client validation passes: every non-optional property has a value and the command’s client-side validation rules pass. The form validates on mount and after every change.
- The
isValidprop is notfalse. It can only disable a valid form;isValid={true}never enables an invalid one. - No command is executing.
The form is treated as invalid until its first validation finishes, so confirm starts out disabled for a moment even when every value is already present.
validateOn ('blur' by default, or 'change' / 'both') and validateOnInit only control when error messages appear. They do not change when confirm enables. Set autoServerValidate to also ask the server’s validate endpoint once client validation passes (throttled by autoServerValidateThrottle, 500 ms by default); its result also enables or disables confirm.
onFieldValidate messages appear on the field, but they do not disable confirm. Use a command validator, autoServerValidate, or the isValid prop for rules that must block submission. See Advanced Features.
Required Props
Section titled “Required Props”command: Constructor for the command typetitle: Dialog title text
Optional Props
Section titled “Optional Props”visible: Boolean controlling dialog visibility (defaults totrue). See Controlled visibilityinitialValues: Initial values for the command form. See Initialize command valuescurrentValues: Values that the parent keeps in sync with the commandpopulateFromQuery,populateFromObservableQuery,populateFromQueryArgs: Seed the form from a query result (ArcCommandFormprops)onSuccess: Callback invoked on successful command execution with the typed responseonFailed: Callback invoked when command execution fails with the fullCommandResult<TResponse>onException: Callback invoked when the command throws an exception with error messages and stack traceonUnauthorized: Callback invoked when authorization failsonValidationFailure: Callback invoked on validation errors with the validation resultsonConfirm: Runs only after successful command execution; returntrueto close the dialogonCancel: Runs for Cancel, the header close (X),Escape, and a backdrop click; returntrueto close the dialogonClose: Fallback used for whichever ofonConfirm/onCancelis absent; closes unless it returnsfalseokLabel: Custom text for confirm button (default: the provider’smessages.dialog.ok, then'Ok')cancelLabel: Custom text for cancel button (default: the provider’smessages.dialog.cancel, then'Cancel')yesLabel,noLabel: Labels forYesNoandYesNoCancelbutton modesbuttons:DialogButtonsvalue or custom footer content (default:DialogButtons.OkCancel). A custom footer does not run the command; keep the predefined buttons unless you execute the command yourselfinitialFocus: Where keyboard focus lands when the dialog opens — forwarded toDialog(see below)dismissable: Whether the header close (X),Escape, and a backdrop click are offered. Defaults totruefor predefinedbuttonscloseAriaLabel: Accessible name for the header close buttonresizable: Accepted for source compatibility; the viewport-bounded Cratis dialog has no resize handleisValid: Additional validity gate combined with command form validityvalidateOn,validateOnInit,validateAllFieldsOnChange: When validation messages are shownautoServerValidate,autoServerValidateThrottle: Server-side validation while the user editsonFieldValidate: Custom validation function for fieldsonFieldChange: Callback when field values changeonBeforeExecute: Transform command values before execution. It must return the values to run with and may be asyncconfirmBeforeExecute(values): Optional synchronous or async guard called with the transformed values after validation. Returntrueto execute orfalseto stay open without executingstyle: Custom CSS stylescontentStyle: Custom CSS styles for the dialog content areawidth: Dialog width (default:'450px')className,pt: Styling hooks for the Cratis-owned dialog root and stable partsptOptions,unstyled: Retained temporarily for source compatibility; ignored because Cratis part attributes always merge and styling is CSS-owned
Callback Behavior
Section titled “Callback Behavior”Result Callbacks
Section titled “Result Callbacks”CommandDialog supports the following result callbacks that are invoked based on the command execution outcome:
onSuccess(response: TResponse): Invoked when the command executes successfully. Receives the typed response.onFailed(commandResult: CommandResult<TResponse>): Invoked when command execution fails for any reason.onException(messages: string[], stackTrace: string): Invoked when the command throws an exception.onUnauthorized(): Invoked when authorization fails.onValidationFailure(validationResults: ValidationResult[]): Invoked on validation errors.
Multiple callbacks may fire for the same execution. For example, both onFailed and onValidationFailure will be invoked for validation errors. If onBeforeExecute throws, the command does not execute, no result callback runs, and the dialog stays open. A rejection from confirmBeforeExecute also keeps the dialog open and skips execution; it calls onException with the error message and stack trace (or logs the error if no callback is supplied), not onFailed.
Dialog Callbacks
Section titled “Dialog Callbacks”“Closes” below means that the dialog calls closeDialog from the surrounding useDialog context.
| User action | onConfirm / onCancel present | Only onClose present | No callback |
|---|---|---|---|
| Confirm, after the command succeeds | onConfirm() runs; closes only if it returns true | onClose(DialogResult.Ok) runs; closes unless it returns false | Closes with DialogResult.Ok |
Confirm, confirmBeforeExecute returns false | Not called; dialog stays open | Not called; dialog stays open | Dialog stays open |
| Confirm, command fails | Not called; dialog stays open | Not called; dialog stays open | Dialog stays open |
Cancel, X, Escape, backdrop | onCancel() runs; closes only if it returns true | onClose(DialogResult.Cancelled) runs; closes unless it returns false | Closes with DialogResult.Cancelled |
A callback that calls closeDialog(...) itself closes the dialog regardless of its return value. Use closeDialog(DialogResult.Ok, value) when the caller needs a value; the automatic close never passes one.
Confirm before executing
Section titled “Confirm before executing”Use confirmBeforeExecute for a second, explicit decision. Unlike onConfirm, which runs after successful execution to decide whether to close, the guard runs before execution. A false result leaves the command dialog open, does not execute, and calls neither onSuccess nor onFailed. If you also provide onBeforeExecute, the guard receives its returned values, not the original form values. Client validation still happens first. If the values change while confirmation is pending (for example, through currentValues), the command is not executed and no result callback runs; confirm again with the new values.
Register ConfirmationDialog in DialogComponents above the component using the hook. The hook must run in a child of that provider, not in the component that creates the provider. The confirmation opens on a higher dialog z-index tier and its No result leaves the command form ready to edit or retry:
import { DialogButtons, DialogComponents, DialogResult, useConfirmationDialog } from '@cratis/arc.react/dialogs';import { CommandDialog } from '@cratis/components/CommandDialog';import { ConfirmationDialog } from '@cratis/components/Dialogs';import { UpdateProject } from './UpdateProject';
function UpdateProjectDialog() { const [showConfirmation] = useConfirmationDialog( 'Save changes?', 'Apply these changes?', DialogButtons.YesNo, ); return ( <CommandDialog<UpdateProject> command={UpdateProject} title='Update project' initialValues={{ name: 'Example Project' }} confirmBeforeExecute={async (_values) => (await showConfirmation()) === DialogResult.Yes} /> );}
export function ProjectDialogs() { return ( <DialogComponents confirmation={ConfirmationDialog}> <UpdateProjectDialog /> </DialogComponents> );}The equivalent runnable, type-checked example is the WithPreExecutionConfirmation story in Source/CommandDialog/CommandDialog.stories.tsx. In an app using useDialog, render the command dialog’s wrapper inside DialogComponents instead of rendering it permanently as above.
Controlled visibility
Section titled “Controlled visibility”Without useDialog there is no dialog context, so nothing closes automatically. Close the dialog yourself from onSuccess and onCancel:
import { useState } from 'react';import { CommandDialog } from '@cratis/components/CommandDialog';import { InputTextField } from '@cratis/components/CommandForm';import { Guid } from '@cratis/fundamentals';import { RenameProject } from './RenameProject';
export const RenameProjectInline = ({ projectId, name }: { projectId: Guid; name: string }) => { const [visible, setVisible] = useState(false);
return ( <> <button type='button' onClick={() => setVisible(true)}> Rename </button> {visible && ( <CommandDialog<RenameProject> command={RenameProject} title='Rename project' initialValues={{ projectId, name }} onSuccess={() => setVisible(false)} onCancel={() => setVisible(false)} > <InputTextField<RenameProject> value={(c) => c.name} title='Name' /> </CommandDialog> )} </> );};Rendering the dialog only while it is open gives the user a fresh form each time. If you keep it mounted and toggle visible instead, the command form stays mounted while hidden, so values typed before a cancel are still there when it opens again. useDialog unmounts the dialog when it closes.
Destructive Commands and Initial Focus
Section titled “Destructive Commands and Initial Focus”The confirm button is focused when the dialog opens, and a focused native button
fires click from the keydown of Enter. A command whose form has required
fields is protected from a held or double-tapped Enter for free, because the
form’s validity keeps confirm disabled until something is filled in. A command
that takes no input — the typical “delete this, permanently” command — has
no such gate, so its confirm button is armed the instant the dialog appears.
Pass initialFocus for those. It is forwarded straight to
Dialog and changes nothing else — the
footer, the close (X), Escape, and the confirm wiring that runs the command
all stay intact.
import { CommandDialog } from '@cratis/components/CommandDialog';import { DialogInitialFocus } from '@cratis/components/Dialogs';import { Guid } from '@cratis/fundamentals';import { DeletePersonalData } from './DeletePersonalData';
export const DeletePersonalDataDialog = ({ personId }: { personId: Guid }) => ( <CommandDialog<DeletePersonalData> command={DeletePersonalData} title='Delete personal data?' okLabel='Delete' initialValues={{ personId }} initialFocus={DialogInitialFocus.Cancel} > This cannot be undone. </CommandDialog>);Pressing Enter inside a text field does not submit the command. The footer buttons are plain type="button" buttons, so the user confirms by activating the confirm button.
Busy State
Section titled “Busy State”CommandDialog automatically manages a busy state from the start of onBeforeExecute, through confirmBeforeExecute, until command execution settles:
- The header close and secondary buttons are disabled and the primary button shows a loading spinner. The primary button stays focusable but is
aria-disabledand ignores activation, so focus stays inside the dialog while the command runs. - The form fields are disabled and inert, so values cannot change while the command runs.
- Escape and backdrop dismissal are ignored while work is in flight.
- Once execution completes (success or failure), the buttons return to their normal state.
- After a refused submit (validation failure or business rule) the dialog stays open, focus returns to the confirm button, and a polite live region announces
messages.dialog.refused(default'The action was not completed. Review the dialog and try again.'). A successful submit closes the dialog as before. - This prevents duplicate submissions and accidental dismissal while giving users clear visual feedback.
Context
Section titled “Context”CommandDialog is built on top of CommandForm and Dialog, and uses command form context internally for values, validation, and execution state.
Field children are bound to the dialog’s own command instance. Do not nest another CommandForm or an AutoCommandForm inside a CommandDialog: the inner form has its own command instance, so its fields never reach the command the dialog executes.
Integration
Section titled “Integration”CommandDialog integrates with:
@cratis/arc/commandsfor command execution@cratis/arc.react/commandsfor form handling@cratis/arc.react/dialogsforuseDialog,useDialogContext,DialogResult, andDialogButtons- React Aria modal/focus behavior behind Cratis-owned markup
See Also
Section titled “See Also”- Advanced Features - Field validation, transformation, and change tracking
- CommandForm - The field components you place inside the dialog
- StepperCommandDialog - Split one command across several steps
- Dialog - The underlying dialog and its close semantics