Milk Admin

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.

What You'll Learn:
  • 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()
Note: Without 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 successfully
  • list - Instructions to reload a table (only appears if dataListId is 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

Loading...