Custom Form Validation
This guide shows how to implement custom client-side and server-side validation for specific business logic that cannot be handled by standard Model validation rules.
When to Use Custom Validation
Use custom validation for:
- Comparing multiple field values (e.g., start date vs end date)
- Complex business rules (e.g., conditional requirements)
- Custom regex patterns or domain-specific validations
- Cross-field dependencies
Client-Side Validation Events
The framework provides JavaScript events for custom validation:
| Event | When Triggered | Usage |
|---|---|---|
fieldValidation |
On submit, then on each field change after first submit | Add custom validation for specific fields |
customValidation |
On submit, before checking validity | Form-level validation involving multiple fields |
beforeFormSubmit |
After validation passes, before actual submit | Perform actions before submission (e.g., show loading) |
Validation Flow
- First validation: Runs when user submits the form
- Re-validation: After first submit, fields are re-validated each time the user modifies them
- Error clearing: When user modifies an invalid field, the error message is cleared automatically
Where to Place Validation Code
For Static Forms (Loaded on Page Load)
Use DOMContentLoaded for forms that are present when the page loads:
document.addEventListener('DOMContentLoaded', function() {
// Your validation code here
})
For Dynamic Forms (Loaded via AJAX/Modal)
Use updateContainer for forms loaded dynamically after the page loads:
document.addEventListener('updateContainer', function(event) {
// Your validation code here
})
updateContainer because they are loaded after the page's DOMContentLoaded event has already fired.
Client-Side Validation Example: Date Range
This example validates that an end datetime is greater than a start datetime. This validation is used in the Events module.
JavaScript Code
Place this in your module's JavaScript file (e.g., milkadmin/Modules/Events/assets/events.js):
// For dynamic forms loaded via AJAX/modal
document.addEventListener('updateContainer', function(event) {
const end_datetime = document.querySelector('[name="data[end_datetime]"]')
const start_datetime = document.querySelector('[name="data[start_datetime]"]')
if (end_datetime && start_datetime) {
end_datetime.addEventListener('fieldValidation', function(e) {
if (end_datetime.value < start_datetime.value) {
end_datetime.setCustomValidity('End datetime must be greater than start datetime')
} else {
end_datetime.setCustomValidity('')
}
});
}
})
How It Works
- The validation listens to the
fieldValidationevent on theend_datetimefield - When triggered, it compares the end datetime with the start datetime
- If invalid, it sets a custom error message using
setCustomValidity() - If valid, it clears the error by calling
setCustomValidity('') - Bootstrap automatically displays the error message below the field with red styling
Server-Side Validation
Server-side validation is essential for data integrity, as client-side validation can be bypassed. Use MessagesHandler to display validation errors.
Backend Validation Example: Date Range
This example from the Events module (milkadmin/Modules/Events/EventsController.php) validates that the start date is not later than the end date:
public function saveEvent($form_builder, $request) {
$this->model->fill($request);
// --- Date range validation ---
if (!empty($request['start_datetime']) && !empty($request['end_datetime'])) {
$start = strtotime($request['start_datetime']);
$end = strtotime($request['end_datetime']);
if ($start > $end) {
// Add error message for specific fields
\App\MessagesHandler::addError(
'The start date cannot be later than the end date',
['start_datetime', 'end_datetime']
);
// Return error response with updated form
return $this->jsonModalError(
'There are validation errors in the form',
$form_builder
);
}
}
// --- Model validation ---
if (!$this->model->validate()) {
return $this->jsonModalError('Error saving event', $form_builder);
}
// --- Saving ---
if (!$this->model->save()) {
return $this->jsonModalError(
'Error saving event: ' . $this->model->getLastError(),
$form_builder
);
}
// --- Success response ---
Response::json([
'success' => true,
'message' => 'Event saved successfully!',
'modal' => ['action' => 'hide'],
'calendar' => ['id' => 'calendar_events', 'action' => 'reload']
]);
}
Helper Method for Error Responses
The jsonModalError method returns the form with validation errors highlighted:
private function jsonModalError($message, $form_builder)
{
Response::json([
'success' => false,
'message' => $message,
'modal' => [
'title' => 'Edit Event',
'body' => $form_builder->getForm(), // Re-render form with errors
'size' => 'lg'
]
]);
}
How Backend Validation Works
- Validation fails:
MessagesHandler::addError()marks fields as invalid - Form reloads: The form is returned with invalid field styling (red border)
- User modifies field: The invalid state is cleared automatically when the user types
- No re-validation: Client-side validation doesn't run again until the user submits the form
MessagesHandler Methods
MessagesHandler::addError($message, $field)- Adds an error message for one or more fields (accepts string or array)MessagesHandler::addFieldError($field)- Marks a field as invalid without a messageMessagesHandler::getErrorAlert()- Returns an alert div with all error messages
Best Practices
1. Always Clear Custom Validity
// Always include an else statement to clear the error
if (condition_fails) {
field.setCustomValidity('Error message')
} else {
field.setCustomValidity('') // Critical: must clear when valid!
}
2. Use Descriptive Error Messages
// Good - Specific and actionable
field.setCustomValidity('End date must be greater than start date')
// Bad - Too vague
field.setCustomValidity('Invalid date')
3. Validate on Both Client and Server
Always implement critical validations on both sides:
- Client-side: Provides immediate feedback to users
- Server-side: Ensures data integrity (client-side can be bypassed)
4. Link JavaScript File in Module
Don't forget to link your validation JavaScript file in your module configuration:
protected function configure($rule): void
{
$rule->page('events')
->title('Events')
->setJs('Modules/Events/assets/events.js') // Link validation file
->access('registered');
}
Related Documentation
- Model Validation Rules: Standard field validation using Model rules
- Field Management: Adding, removing, and configuring form fields
- Conditional Field Visibility: Show/hide fields based on other field values
- Organizing Fields with Containers: Grouping fields into columns and sections