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
>

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
};
};