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.
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);
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
- Creating Modules (Posts Example) - Module basics without fetch
- FormBuilder Documentation - Complete form builder reference
- TableBuilder Documentation - Complete table builder reference