Creating a Fetch-Based Form with Offcanvas
Revision: 2025/12/11
Learn how to create forms that work via AJAX fetch requests, opening in offcanvas panels without page reloads.
- How to use FormBuilder methods for fetch-based forms
- How to open forms in offcanvas panels
- How to automatically reload tables after save
- How to use
activeFetch()to enable fetch mode - Dynamic title handling for new/edit modes
Overview: The Recipe Module
We'll examine a complete working module that demonstrates fetch-based forms. The Recipe module includes a table with add/edit functionality, all working via fetch requests.
File Structure
milkadmin_local/Modules/Recipe/
├── RecipeModel.php # Database model
├── RecipeModule.php # Controller with actions
└── Views/
└── list_page.php # View template
Database Schema
| Field | Type | Description |
|---|---|---|
id |
int | Primary key |
name |
varchar(255) | Recipe name (displayed as title) |
ingredients |
text | Recipe ingredients (textarea) |
difficulty |
varchar(50) | Select: Easy, Medium, Hard |
Step 1: Create the Model
The model defines the database structure and field types.
File: milkadmin_local/Modules/Recipe/RecipeModel.php
<?php
namespace Local\Modules\Recipe;
use App\Abstracts\AbstractModel;
class RecipeModel extends AbstractModel
{
protected function configure($rule): void
{
$rule->table('#__recipes')
->id()
->title('name')->index()
->text('ingredients')->formType('textarea')
->select('difficulty', ['Easy', 'Medium', 'Hard']);
}
}
Field Explanations:
->id()- Auto-increment primary key->title('name')- String field with automatic title display->text('ingredients')->formType('textarea')- Text field rendered as textarea->select('difficulty', [...])- Dropdown select field
Step 2: Create the Module (Controller)
The module contains two actions: one for displaying the table, and one for the edit form.
File: milkadmin_local/Modules/Recipe/RecipeModule.php
<?php
namespace Local\Modules\Recipe;
use App\Abstracts\AbstractModule;
use App\Attributes\{RequestAction, AccessLevel};
use Builders\{TableBuilder, FormBuilder};
use App\Response;
class RecipeModule extends AbstractModule
{
protected function configure($rule): void {
$rule->page('recipes')
->title('My Recipes')
->menu('Recipes', '', 'bi bi-book', 10)
->access('registered')
->version(251101);
}
#[RequestAction('home')]
public function recipesList() {
$tableBuilder = TableBuilder::create($this->model, 'idTableRecipes')
->activeFetch()
->field('name')->link('?page='.$this->page.'&action=edit&id=%id%')
->setDefaultActions();
$response = array_merge($this->getCommonData(), $tableBuilder->getResponse());
Response::render(__DIR__ . '/Views/list_page.php', $response);
}
#[RequestAction('edit')]
public function recipeEdit() {
$response = ['page' => $this->page, 'title' => $this->title];
$response = array_merge($response, FormBuilder::create($this->model, $this->page)
->activeFetch()
->asOffcanvas()
->setTitle('New Recipe', 'Edit Recipe')
->dataListId('idTableRecipes')
->getResponse());
Response::json($response);
}
}
Action 1: recipesList() - Display the Table
This action creates a table with fetch-based interactions:
TableBuilder::create($this->model, 'idTableRecipes')- Create table builder->activeFetch()- Converts all table action buttons and links to fetch calls->field('name')->link(...)- Makes the name field clickable->setDefaultActions()- Adds edit and delete buttons
Action 2: recipeEdit() - Show Form in Offcanvas
This action demonstrates the FormBuilder method chain:
FormBuilder::create($this->model, $this->page)
->activeFetch() // Enable fetch mode
->asOffcanvas() // Display in offcanvas panel
->setTitle('New Recipe', 'Edit Recipe') // Set titles for new/edit
->dataListId('idTableRecipes') // Auto-reload table on success
->getResponse() // Generate JSON response
Each method is explained in detail in the next section.
Step 3: Create the View
The view contains a card with the table and an "Add New" button.
File: milkadmin_local/Modules/Recipe/Views/list_page.php
<?php
namespace Modules\Posts\Views;
use Builders\TitleBuilder;
!defined('MILK_DIR') && die(); // Avoid direct access
?>
<div class="card">
<div class="card-header">
<?php
$title = TitleBuilder::create($title)->addButton('Add New', '?page='.$page.'&action=edit', 'primary', '', 'post');
echo (isset($search_html)) ? $title->addRightContent($search_html) : $title->addSearch('idTableRecipes', 'Search...', 'Search');
?>
</div>
<div class="card-body">
<p class="text-body-secondary mb-3"><?php _pt('This is a sample module to show how to create a basic module on Milk Admin. Go to the modules/posts folder to see the code.') ?></p>
<?php _ph($html); ?>
</div>
</div>
TitleBuilder with "Add New" Button
The addButton() method creates a button that triggers a fetch request:
->addButton('Add New', '?page='.$page.'&action=edit', 'primary', '', 'post')
Parameters:
'Add New'- Button text'?page=...'- URL to call'primary'- Bootstrap button style''- Icon (empty in this case)'post'- Converts the link to a fetch POST request
FormBuilder Methods for Fetch-Based Forms
These methods enable fetch-based form handling with automatic offcanvas/modal display and table reloading.
activeFetch()
Enables fetch mode for the form. This method converts form submission and action buttons into fetch calls:
- Form submission becomes an AJAX request (no page reload)
- Submit buttons trigger fetch calls instead of traditional form submission
- The response must be JSON
->activeFetch()
activeFetch(), the form will still open in the offcanvas, but the page will reload when you save or cancel.
asOffcanvas()
Sets the response type to offcanvas panel. The form will appear in a sliding panel from the right side of the screen.
->asOffcanvas()
asModal()
Alternative to asOffcanvas(). Displays the form in a centered modal dialog instead of a side panel.
->asModal()
asDom($id)
Alternative display method. Renders the form directly in a DOM element with the specified ID.
->asDom('contentWrapper')
setTitle($new, $edit = null)
Sets dynamic titles for new and edit modes. The system automatically determines which title to use based on whether the record has an ID.
->setTitle('New Recipe', 'Edit Recipe')
Parameters:
$new- Title when creating a new record$edit- Title when editing existing record (optional, defaults to$new)
dataListId($id)
Enables automatic table reload on successful save or delete operations.
->dataListId('idTableRecipes')
When set, after a successful save:
- The table with the specified ID is automatically reloaded
- The offcanvas/modal is automatically closed
size($size)
Sets the size of the offcanvas or modal. Available options:
->size('lg') // or 'sm', 'xl', 'fullscreen'
| Size | Width | Best For |
|---|---|---|
sm |
Small | Simple forms, confirmations |
lg |
Large | Forms with multiple fields |
xl |
Extra large | Complex forms, editors |
fullscreen |
Full screen | Very complex forms |
getResponse()
Generates the complete JSON response array based on the configured response type (offcanvas, modal, or dom).
->getResponse()
Returns an array ready to be sent as JSON, containing:
- Action tracking (executed_action, action_success)
- List reload instructions (if dataListId is set)
- Offcanvas/modal/dom configuration
Understanding the JSON Response
The getResponse() method generates a JSON response with this structure:
{
"executed_action": "save", // or null if no action
"action_success": true, // or false
"list": { // only if dataListId is set and success
"id": "idTableRecipes",
"action": "reload"
},
"offcanvas_end": { // or "modal" or "element"
"title": "Edit Recipe",
"action": "show", // or "hide" if success
"body": "",
"size": "lg" // optional
}
}
Response Keys:
executed_action- Name of the action that was executed (save, delete, etc.)action_success- Whether the action completed successfullylist- Instructions to reload a table (only appears ifdataListIdis set and action succeeded)offcanvas_end/modal/element- Display configuration based on response type
For complete documentation on JSON response handling, see: JSON Actions (MilkActions)
Page Reload vs Fetch-Based Approach
MilkAdmin supports two approaches for handling forms.
Page Reload Approach
Traditional approach where each action triggers a full page refresh.
- The form is displayed on a dedicated page
- Submit redirects to a success or error page
- Entire page HTML is transferred on each request
- Better for complex pages with multiple sections and heavy context
Fetch-Based Approach (This Tutorial)
Modern approach using AJAX fetch requests with no page reloads.
- Forms open in offcanvas/modal overlays
- Only JSON data is transferred
- Tables auto-reload after save/delete
- Smoother user experience with no page flicker
- Better for simple forms and CRUD operations
Code Comparison
Page Reload Approach
public function recipeEdit() {
$form = FormBuilder::create($this->model, $this->page)->getForm();
Response::render(__DIR__ . '/Views/edit_page.php', ['form' => $form]);
}
Fetch-Based Approach
public function recipeEdit() {
$response = ['page' => $this->page, 'title' => $this->title];
$response = array_merge($response, FormBuilder::create($this->model, $this->page)
->activeFetch()
->asOffcanvas()
->setTitle('New Recipe', 'Edit Recipe')
->dataListId('idTableRecipes')
->getResponse());
Response::json($response);
}
Installation and Testing
Create the database table
php milkadmin/cli.php recipes:update
Access the module
Navigate to: ?page=recipes
Expected behavior
- Click "Add New" → Form opens in offcanvas panel
- Fill and save → Offcanvas closes, table reloads with new record
- Click on a recipe name → Edit form opens in offcanvas
- Edit and save → Offcanvas closes, table updates
- No page reloads occur at any point
Related Documentation
- JSON Actions (MilkActions) - Complete reference for JSON responses
- JavaScript Fetch Links - How fetch links work
- TableBuilder - Table management
- FormBuilder - Form management