Roles Forms Pattern¶
Overview¶
The Roles forms implementation provides a complete CRUD interface for managing roles with React/Inertia, following best practices for accessibility, validation, and user experience.
Components Structure¶
Pages¶
pages/roles/form.tsx
- RoleForm page used for both create and edit (rendered as Inertia viewroles/form
). It declaresRoleForm.layout = (page) => <AppLayout>{page}</AppLayout>
so the sidebar is shown consistently.- Note:
pages/roles/create.tsx
andpages/roles/edit.tsx
are not used in this boilerplate; the controller rendersroles/form
directly for both actions.
Components¶
components/pickers/role-picker.tsx
- Reusable role selector with search and quick-createcomponents/forms/field-error.tsx
- Field error display componentcomponents/forms/form-section.tsx
- Form section wrapper with title/descriptioncomponents/forms/form-actions.tsx
- Form actions bar with submit/cancel buttonscomponents/ui/switch.tsx
- Toggle switch component
Hooks¶
hooks/use-unsaved-changes.ts
- Detect and warn about unsaved form changes
RoleForm Component¶
The main form component that handles both create and edit modes:
interface RoleFormProps {
mode: 'create' | 'edit';
initial?: {
id?: number;
name?: string;
guard_name?: string;
is_active?: boolean;
permissions_ids?: number[];
updated_at?: string;
};
options: {
guards: Array<{ value: string; label: string }>;
permissions: Permission[];
};
can: Record<string, boolean>;
onSaved?: () => void;
}
Features¶
- Validation - Client-side and server-side validation with error display
- Partial Reloads - Permissions reload when guard changes
- Unsaved Changes - Warns users before navigating away with unsaved changes
- Accessibility - Proper ARIA attributes, labels, and focus management
- Optimistic Locking - Includes version field for update conflicts
- Error Focus - Automatically focuses first field with error
Optimistic Locking Details¶
The form includes a hidden version field (_version
) derived from initial.updated_at
on edit. On submit, _version
is sent back to the server and compared against the current model updated_at
using a timestamp-safe comparison. If they differ, the backend throws a domain exception to prevent overwriting concurrent changes.
Example (TypeScript + Inertia):
const { data, setData, processing } = useForm({
name: initial?.name ?? '',
guard_name: initial?.guard_name ?? 'web',
is_active: initial?.is_active ?? true,
permissions_ids: initial?.permissions_ids ?? [],
_version: initial?.updated_at ?? undefined, // keep original updated_at (ISO 8601)
});
function onSubmit(e: React.FormEvent) {
e.preventDefault();
router.put(route('roles.update', initial!.id), data, {
onSuccess: () => {
clearUnsavedChanges?.();
onSaved?.();
},
});
}
On the backend, HandlesForm::update()
passes _version
to BaseService::update()
, which normalizes both values to Unix timestamps before comparing them.
Form Fields¶
- Name - Required text input for role name
- Guard - Select dropdown for authentication guard
- Active Status - Toggle switch for role activation
- Permissions - Checkbox list filtered by selected guard
RolePicker Component¶
A reusable role selector component with advanced features:
interface RolePickerProps {
value?: number | number[];
onChange: (value: number | number[]) => void;
options?: Role[];
multiple?: boolean;
placeholder?: string;
className?: string;
disabled?: boolean;
allowCreate?: boolean;
canCreate?: boolean;
createOptions?: {
guards: Array<{ value: string; label: string }>;
permissions: Array<{ value: number; label: string; guard: string }>;
};
}
Features¶
- Search - Filter roles by name
- Single/Multiple - Support for single or multiple selection
- Quick Create - Optional modal to create new role inline
- Visual Feedback - Shows guard, permissions count, active status
- Selection Display - Shows selected roles as removable badges
Usage Examples¶
Create Page¶
export default function CreateRole({ can, options }: CreateRoleProps) {
return (
<AppLayout>
<Head title="Crear Rol" />
<RoleForm mode="create" options={options} can={can} />
</AppLayout>
);
}
Edit Page¶
export default function EditRole({ can, role, options }: EditRoleProps) {
return (
<AppLayout>
<Head title={`Editar Rol - ${role.name}`} />
<RoleForm mode="edit" initial={role} options={options} can={can} />
</AppLayout>
);
}
Using RolePicker¶
// Single selection
<RolePicker
value={selectedRoleId}
onChange={(id) => setSelectedRoleId(id)}
options={availableRoles}
placeholder="Select a role"
/>
// Multiple selection with create
<RolePicker
multiple
value={selectedRoleIds}
onChange={(ids) => setSelectedRoleIds(ids)}
options={availableRoles}
allowCreate
canCreate={can['roles.create']}
createOptions={roleFormOptions}
/>
Backend Integration¶
The forms integrate with Laravel backend through Inertia:
- Create: POST to
route('roles.store')
- Edit: PUT to
route('roles.update', id)
- Partial Reload: Reloads options when guard changes
- Flash Messages: Success/error messages via Laravel sessions
- Validation: Server-side validation with 422 responses
Best Practices¶
- Always include required field indicators with asterisk (*)
- Provide clear error messages below each field
- Use proper semantic HTML for accessibility
- Handle loading states with disabled inputs during submission
- Preserve scroll position when navigating between pages
- Focus management after validation errors
- Warn about unsaved changes before navigation
- Use optimistic locking for concurrent edit protection
UI/UX Enhancements¶
- Modern card layout with sensible max width for readability
- Header toolbar for permissions with pill counter and separators
- Tri-state group selection via group-level checkbox (checked/indeterminate/unchecked)
- Updated microcopy: “Define el nombre y el guard del rol”, “Buscar permisos”
- Required fields legend and consistent asterisk usage
- Consistent typography and spacing in
FormSection
- Layout assignment ensures the sidebar:
RoleForm.layout = (page) => <AppLayout>{page}</AppLayout>
- Clear unsaved changes on successful submit using
useUnsavedChanges
’clearUnsavedChanges()
- Edit form preselects permissions via
permissions_ids
provided by the backend
Accessibility Features¶
- Semantic HTML structure
- ARIA labels and descriptions
- Keyboard navigation support
- Focus indicators
- Screen reader announcements
- Error association with fields
- Required field indicators
Testing Considerations¶
When testing role forms:
- Verify validation messages appear correctly
- Test unsaved changes warnings
- Check partial reload functionality
- Ensure proper error focus
- Test permission filtering by guard
- Verify optimistic locking
- Test quick-create in RolePicker
- Check accessibility with screen readers