Skip to content

StepperCommandDialog - Advanced Features

All advanced features described here apply to every step in the wizard, because all steps share a single underlying command instance.

StepperCommandDialog supports typed command responses and provides callbacks for different execution outcomes:

import { StepperCommandDialog } from '@cratis/components/CommandDialog';
import { StepperPanel } from 'primereact/stepperpanel';
import { ValidationResult } from '@cratis/arc/validation';
type CreateProjectResponse = {
projectId: string;
message: string;
};
<StepperCommandDialog<CreateProject, CreateProjectResponse>
command={CreateProject}
title="Create Project"
onSuccess={(response) => {
// Handle successful creation - response is fully typed
console.log(`Project created with ID: ${response.projectId}`);
showNotification(response.message);
navigate(`/projects/${response.projectId}`);
}}
onFailed={(commandResult) => {
// Handle any failure - includes all failure details
console.error('Command failed:', commandResult);
}}
onException={(messages, stackTrace) => {
// Handle exceptions specifically
console.error('Exception occurred:', messages.join(', '));
}}
onUnauthorized={() => {
// Handle authorization failures
showNotification('You are not authorized to perform this action');
}}
onValidationFailure={(validationResults) => {
// Handle validation failures
const errors = validationResults.map(r => r.message).join(', ');
showNotification(`Validation failed: ${errors}`);
}}
>
<StepperPanel header="Basic Info">
<InputTextField<CreateProject> value={c => c.name} title="Name" />
</StepperPanel>
<StepperPanel header="Details">
<TextAreaField<CreateProject> value={c => c.description} title="Description" />
</StepperPanel>
</StepperCommandDialog>

Multiple callbacks may fire for the same command execution:

  1. onSuccess: Only fires when commandResult.isSuccess is true
  2. onFailed: Fires for any failure (validation, exception, unauthorized, etc.)
  3. onException: Fires specifically when an exception occurs
  4. onUnauthorized: Fires specifically when authorization fails
  5. onValidationFailure: Fires specifically when validation fails

For example, a validation failure will trigger both onFailed and onValidationFailure.

The response type parameter is optional and defaults to object:

// Explicit response type
<StepperCommandDialog<CreateProject, CreateProjectResponse>
command={CreateProject}
onSuccess={(response) => {
// response is CreateProjectResponse
}}
>
{/* steps */}
</StepperCommandDialog>
// Default object response type
<StepperCommandDialog<CreateProject>
command={CreateProject}
onSuccess={(response) => {
// response is object
}}
>
{/* steps */}
</StepperCommandDialog>

Provide custom validation logic for individual fields:

const validateField = (command, fieldName, oldValue, newValue) => {
if (fieldName === 'email' && !newValue.includes('@')) {
return 'Invalid email address';
}
return undefined;
};
<StepperCommandDialog
onFieldValidate={validateField}
// ... other props
>
<StepperPanel header="Contact">
<InputTextField<CreateProject> value={c => c.email} title="Email" />
</StepperPanel>
</StepperCommandDialog>

Transform command values before execution. The transformation runs just before Submit is clicked on the last step:

const transformBeforeExecute = (values) => {
return {
...values,
timestamp: new Date()
};
};
<StepperCommandDialog
onBeforeExecute={transformBeforeExecute}
// ... other props
>

When steps contain custom controls instead of CommandForm fields, keep the command instance synchronized with the custom input values. StepperCommandDialog still uses command-form validity as the source of truth, and isValid is only an additional external gate.

Prefer currentValues for externally managed values so client validation sees the same command values that will be submitted. onBeforeExecute can still perform final transformations, but values populated only in onBeforeExecute are not visible to client validation before the final Submit button is clicked:

<StepperCommandDialog
command={UpdateProject}
currentValues={projectDraft}
isValid={projectDraft.stepsComplete}
>
{/* steps */}
</StepperCommandDialog>

React to field value changes across any step:

const handleFieldChange = (command, fieldName, oldValue, newValue) => {
console.log(`${fieldName} changed from ${oldValue} to ${newValue}`);
};
<StepperCommandDialog
onFieldChange={handleFieldChange}
// ... other props
>

Combining multiple validation patterns across fields:

const validateField = (command, fieldName, oldValue, newValue) => {
switch (fieldName) {
case 'email':
if (!newValue || !newValue.includes('@')) {
return 'Valid email address is required';
}
break;
case 'budget':
if (newValue <= 0) {
return 'Budget must be greater than zero';
}
if (newValue > 10_000_000) {
return 'Budget exceeds the maximum allowed value';
}
break;
case 'password':
if (newValue.length < 8) {
return 'Password must be at least 8 characters';
}
if (!/[A-Z]/.test(newValue)) {
return 'Password must contain an uppercase letter';
}
if (!/[0-9]/.test(newValue)) {
return 'Password must contain a number';
}
break;
}
return undefined;
};

Because all steps share the same command instance, you can validate a field on one step against a value entered on a different step:

const validateField = (command, fieldName, oldValue, newValue) => {
// endDate is on step 3, startDate was entered on step 1 — command has both
if (fieldName === 'endDate') {
if (newValue < command.startDate) {
return 'End date must be after start date';
}
}
if (fieldName === 'confirmPassword') {
if (newValue !== command.password) {
return 'Passwords do not match';
}
}
return undefined;
};

The step indicator circles reflect per-step validation state, so errors in step 1 remain visible (red circle) even after the user navigates to step 3.

Update other fields when one field changes, regardless of which step they are on:

const handleFieldChange = (command, fieldName, oldValue, newValue) => {
if (fieldName === 'country' && newValue === 'USA') {
// Could trigger state updates or side effects
console.log('Country changed to USA, update state list');
}
if (fieldName === 'quantity') {
// Calculate derived values from fields on other steps
const total = newValue * command.pricePerUnit;
console.log('New total:', total);
}
};

For validation that requires API calls:

const validateField = async (command, fieldName, oldValue, newValue) => {
if (fieldName === 'username') {
const isAvailable = await checkUsernameAvailability(newValue);
if (!isAvailable) {
return 'Username is already taken';
}
}
return undefined;
};

Common transformation scenarios run just before Submit:

const transformBeforeExecute = (values) => {
return {
...values,
// Add metadata
timestamp: new Date(),
userId: getCurrentUserId(),
// Normalize data
email: values.email.toLowerCase().trim(),
// Convert formats
startDate: new Date(values.startDate),
// Remove UI-only fields
confirmPassword: undefined,
// Calculate derived values gathered across steps
totalPrice: values.quantity * values.pricePerUnit
};
};