Milk Admin

Fetch-Based Modules

Create modules that use AJAX/Fetch for dynamic content updates without page reloads.


Overview

MilkAdmin supports modules with forms that submit via fetch instead of traditional page reload. This pattern is used for forms displayed in offcanvas, modals, or inline editing.

Key Mechanism: The third parameter of FormBuilder::create() controls the behavior: false enables fetch mode (JSON responses), while true or omitting it uses page reload.

FormBuilder Configuration

The third parameter of FormBuilder::create() controls the submission behavior:

// Fetch mode: returns JSON, no page reload
$form_builder = FormBuilder::create($this->model, 'tasks', false);

// Page reload mode: redirects after save
$form_builder = FormBuilder::create($this->model);
Parameter Type Description
$model AbstractModel The model instance
$page string Page identifier (optional)
$url_success_or_json string|bool false = fetch mode (returns JSON), string = success redirect URL
$url_error string Error redirect URL (optional)

Controller Implementation

A fetch-based controller uses Response::htmlJson() to return JSON responses that control the UI:

#[RequestAction('edit')]
public function taskEdit() {
    $response = ['page' => $this->page, 'title' => $this->title];

    // Third parameter = false enables fetch mode
    $form_builder = FormBuilder::create($this->model, 'tasks', false)
        ->addStandardActions(true); // Adds save, delete, cancel buttons

    $form_html = $form_builder->getForm();
    $action = $form_builder->getPressedAction();

    if ($action == 'save' || $action == 'delete') {
        // Form submitted: check for errors
        if (!MessagesHandler::hasErrors()) {
            // Success: hide offcanvas and reload table
            $response['offcanvas_end'] = ["action" => "hide"];
            $response['table'] = ["id" => "idTableTasks", "action" => "reload"];
        } else {
            // Errors: re-display form with error messages
            $response['offcanvas_end'] = [
                "title" => "Edit Task",
                "body" => $form_html,
                "action" => "show"
            ];
        }
    } elseif ($action == 'cancel') {
        // Cancel: just close offcanvas
        $response['offcanvas_end'] = ["action" => "hide"];
    } else {
        // Initial load: show form
        $response['offcanvas_end'] = [
            "title" => "Edit Task",
            "body" => $form_html,
            "action" => "show"
        ];
    }

    // Trigger JavaScript hook for form initialization
    $response['hook'] = ["name" => "update-form"];

    Response::htmlJson($response);
}

TableBuilder Integration

Configure table actions to trigger fetch requests:

TableBuilder::create($this->model, 'idTableTasks')
    // fetchLink: makes column clickable with fetch request
    ->fetchLink('title', '?page='.$this->page.'&action=edit&id=%id%')

    // setActions: configure row actions
    ->setActions([
        'edit' => [
            'label' => 'Edit',
            'link' => '?page='.$this->page.'&action=edit&id=%id%',
            'fetch' => true  // Triggers fetch request instead of navigation
        ],
        'delete' => [
            'label' => 'Delete',
            'action' => [$this, 'actionDelete'],
            'confirm' => 'Are you sure?'
        ]
    ])
    ->render();

JSON Response Structure

Response::htmlJson() accepts an array that controls various UI elements:

Key Description Example
offcanvas_end Control offcanvas sidebar ['action' => 'show', 'title' => 'Edit', 'body' => $html]
table Reload table ['id' => 'tableId', 'action' => 'reload']
hook Trigger JavaScript hook ['name' => 'update-form']
toast Show notification ['message' => 'Success', 'type' => 'success']
redirect Navigate to URL '?page=tasks'

JavaScript Hooks

Use JavaScript hooks to add custom client-side behavior when forms are loaded or updated.

Setup

1. Create JavaScript file in Modules/Tasks/Assets/task.js:

registerHook('update-form', function() {
    const title = document.querySelector('[name="title"]');

    if (title) {
        title.addEventListener('fieldValidation', function(e) {
            const field = e.detail.field;

            if (field.value.length < 3) {
                field.setCustomValidity('Title must be at least 3 characters');
            } else {
                field.setCustomValidity('');
            }
        });
    }
});

2. Register in module configuration:

protected function configure($rule): void {
    $rule->page('tasks')
         ->setJs('Assets/task.js')
         ->version(20251021);
}

3. Trigger in controller response:

$response['hook'] = ["name" => "update-form"];
Response::htmlJson($response);
Include the hook in every response that contains form HTML to ensure it runs on both initial load and after validation errors.

Key Concepts Summary

  • FormBuilder third parameter: false = fetch mode, returns JSON instead of redirecting
  • getPressedAction(): Returns which button was clicked ('save', 'delete', 'cancel')
  • addStandardActions(true): Automatically adds save, delete, and cancel buttons
  • Response::htmlJson(): Returns JSON to control UI elements (offcanvas, tables, hooks)
  • fetchLink(): Makes table columns trigger fetch requests instead of navigation
  • fetch: true: Makes table row actions use fetch instead of page reload
  • hook: Triggers JavaScript code registered with registerHook()

See Also

Loading...