Milk Admin

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.

Important: This documentation covers custom validation for complex scenarios. For standard field validation (required, min/max length, data types, etc.), use Model validation rules instead.

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

  1. First validation: Runs when user submits the form
  2. Re-validation: After first submit, fields are re-validated each time the user modifies them
  3. 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
})
Important: Dynamic forms need 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

  1. The validation listens to the fieldValidation event on the end_datetime field
  2. When triggered, it compares the end datetime with the start datetime
  3. If invalid, it sets a custom error message using setCustomValidity()
  4. If valid, it clears the error by calling setCustomValidity('')
  5. Bootstrap automatically displays the error message below the field with red styling
✓ Result: The form validates that end_datetime is greater than start_datetime, showing error feedback in Bootstrap style.

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

  1. Validation fails: MessagesHandler::addError() marks fields as invalid
  2. Form reloads: The form is returned with invalid field styling (red border)
  3. User modifies field: The invalid state is cleared automatically when the user types
  4. 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 message
  • MessagesHandler::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

Loading...