Milk Admin

Table Filter System

After seeing how to create tables and modify them, let's now see how to manage search filters. Let's start with a simple complete example:

Search Input with Automatic Update.

To make a field a search filter you need to add the class js-milk-filter-onchange and the data attributes data-filter-id where you insert the table id and data-filter-type to specify the filter type. The search filter is preset so it will search across the entire table.

<div class="d-inline-flex" style="width: auto;">
    <?php \App\Form::input('text', 'search', 'Search', '', ['floating' => false, 'class' => 'js-milk-filter-onchange', 'data-filter-id' => $table_id, 'data-filter-type' => 'search', 'label-attrs-class' => 'p-0 pt-2 me-2']); ?>
</div>

Select with Automatic Update

<div class="card">
    <div class="card-header">
        <!-- Select that updates automatically -->
        <?php Form::select('filter_status', 'Status', [
            '' => 'All', 'active' => 'Active', 'suspended' => 'Suspended', 'trash' => 'Trash'
        ], '', [
            'data-filter-id' => 'table_id', 
            'data-filter-type' => 'status',  
            'class' => 'js-milk-filter-onchange'
        ]); ?>
    </div>

    // your table here
 
</div>

Form with Search Button

<div class="card">
    <div class="card-header">
        <div class="row g-3">
            <div class="col-md-4">
                <!-- Input that collects the value but doesn't execute automatically -->
                <input type="text" 
                       class="form-control js-milk-filter" 
                       data-filter-id="table_file_logs" 
                       data-filter-type="search" 
                       placeholder="Search in logs...">
            </div>
            <div class="col-md-3">
                <!-- Select for action -->
                <?php Form::select('filter_action', 'Action', $actions, '', [
                    'data-filter-id' => 'table_file_logs', 
                    'data-filter-type' => 'action',  
                    'class' => 'js-milk-filter'
                ]); ?>
            </div>
            <div class="col-md-2">
                <!-- Button that executes all filters -->
                <div class="btn btn-primary js-milk-filter-onclick" 
                     data-filter-id="table_file_logs">Search</div>
            </div>
        </div>
    </div>
    
    // your table here
</div>

Action List (Button Filters)

<div class="card">
    <div class="card-header">
        <!-- Action list with automatic update -->
        <?php Form::actionList('filter_action', 'Filter by action', $actions, '', [], [
            'data-filter-id' => 'table_file_logs', 
            'data-filter-type' => 'action',  
            'class' => 'js-milk-filter-onchange'
        ]); ?>
    </div>
    
    // your table here
</div>

Available CSS Classes

CSS Class Behavior When to Use
js-milk-filter-onchange Makes the field a filter that updates automatically on change Search inputs, selects, checkboxes, radios
js-milk-filter Makes the field a filter. This is not executed automatically, so you need a button to execute it with the js-milk-filter-onclick class When you want to manually control execution
js-milk-filter-onclick Filter executed on click Search buttons, filter application buttons
js-milk-filter-clear Clear all filters and reload table Clear buttons that reset all filter fields

Required Data Attributes

Attribute Required Description Example
data-filter-id ✅ Always ID of the table to filter data-filter-id="userList"
data-filter-type ✅ For onchange/onclick Filter type data-filter-type="search"

Frontend HTML Examples

Search Input with Automatic Update

<div class="card">
    <div class="card-header">
        <!-- Search input that updates automatically -->
        <input type="text" 
               class="form-control js-milk-filter-onchange" 
               data-filter-id="userList" 
               data-filter-type="search" 
               placeholder="Search users...">
    </div>
    
    <div class="card-body">
        <div id="userList" class="js-table-container">
            <!-- Table loaded here -->
        </div>
    </div>
</div>

Backend PHP

Method 1: Controller Class Extends AbstractController

You can use the get_modellist_data method to get table data and pass it to the template. This method accepts a callback as the second argument that allows you to configure filters.

<?php
// Usage with get_modellist_data
$modellist_data = $this->getModellistData($table_id, function($model_list) {
    // Filter for action
    $model_list->addFilter('action', function($query, $action) {
        if (!empty($action)) {
            $query->where('action = ?', [$action]);
        }
    });
    
    // Filter for search
    $model_list->addFilter('search', function($query, $search) {
        if (!empty($search)) {
            $query->where('(title LIKE ? OR description LIKE ?)', ["%{$search}%", "%{$search}%"]);
        }
    });
});
?>

get_modellist_data can be used to modify the query

<?php
// Usage with get_modellist_data
$modellist_data = $this->getModellistData($table_id, null, function($query) {
    $query->select(['id', 'title']);
});
?>

With this method the columns of list_structure extracted from the model object will be filtered with the columns selected in the query.

Method 2: Outside of AbstractController

Outside of classes that extend AbstractController, you can use the standard ModelList to configure filters.

<?php
// Complete model configuration
$model = new \App\Modellist\ModelList('#__users', 'userList');

// Filter for search
$model->addFilter('search', function($query, $search) use ($model) {
    if (!empty($search)) {
        $query->where('`username` LIKE ? OR `email` LIKE ?', ['%'.$search.'%', '%'.$search.'%']);
    }
});

// Filter for status
$model->addFilter('status', function($query, $status) use ($model) {
    $model->page_info['filter_status'] = $status;
    switch ($status) {
        case 'active':
            $query->where('`status` = 1');
            break;
        case 'suspended':
            $query->where('`status` = 0');
            break;
        case 'trash':
            $query->where('`status` = -1');
            break;
    }
});

// Table generation
$query = $model->queryFromRequest();
$rows = Get::db()->getResults(...$query->get());
$total = Get::db()->getVar(...$query->getTotal());

$page_info = $model->getPageInfo($total);
$page_info->setId('userList')->setAjax(true);

echo Get::themePlugin('table', [
    'info' => $model->getListStructure(),
    'rows' => $rows,
    'page_info' => $page_info
]);
?>

6. JavaScript API Methods (Advanced)

JavaScript Method Description Example
filter_add(filter) Adds filter manually table.filter_add('status:active')
filter_remove(filter) Removes specific filter table.filter_remove('status:active')
filter_remove_start(prefix) Removes filters by prefix table.filter_remove_start('status:')
filter_clear() Clears all filters table.filter_clear()
reload() Reloads table table.reload()
PHP Method Description Example
addFilter($type, $callback) Registers a filter $model->addFilter('status', function($q, $v) {...})
queryFromRequest() Creates query with filters $query = $model->queryFromRequest()

SearchBuilder Class (Recommended Method)

The new SearchBuilder class provides a fluent interface for creating search forms with automatic filter integration:

Basic Usage

<?php
// Create SearchBuilder from TableBuilder
$search_builder = $table_builder->createSearchBuilder()
    ->search('search', 'Search Posts')
    ->select('status', 'Status', [
        '' => 'All Status',
        'published' => 'Published',
        'draft' => 'Draft',
        'trash' => 'Trash'
    ])
    ->actionList('category', 'Category', [
        '' => 'All',
        'news' => 'News', 
        'blog' => 'Blog',
        'tutorial' => 'Tutorial'
    ]);
    
$search_html = $search_builder->render();
?>

Layout Configuration

<?php
$search_builder = $table_builder->createSearchBuilder()
    ->search('search', 'Search Posts')
    ->select('status', 'Status', $options)
    
    // Label layout
    ->setLabelLayout('side')        // 'side' (labels beside fields) or 'top' (labels above fields)
    
    // Fields layout options:
    ->setFieldsLayout('columns', 3)    // Equal columns: 'columns' with number of columns
    ->setFieldsLayout('columns', null) // Auto columns: uses 'col' class for automatic sizing
    ->setFieldsLayout('columns', ['col-md-6', 'col-md-3', 'col-md-3']) // Custom column sizes
    ->setFieldsLayout('stacked')        // Vertical layout (stacked)
    
    // Search mode
    ->setSearchMode('onchange')     // 'onchange' (automatic) or 'submit' (manual with buttons)
    // ->setSearchMode('submit', true) // Submit mode with automatic Search/Clear buttons
    
    ->render([], true);
?>

Available SearchBuilder Methods

Method Description Parameters
search($type, $label) Adds a text search input $type: filter type, $label: field label
select($type, $label, $options, $selected) Adds a select dropdown $type: filter type, $label: field label, $options: array of options
actionList($type, $label, $options, $selected) Adds an action list (button filters) $type: filter type, $label: field label, $options: array of options
addInput($input_type, $type, $label, $value) Adds a generic input field $input_type: HTML input type, $type: filter type, $label: field label
searchButton($label) Adds a search button (for submit mode) $label: button label
addClearButton($label) Adds a clear button $label: button label
setLabelLayout($layout) Sets label positioning 'side' or 'top'
setFieldsLayout($layout, $columns_config) Sets field arrangement 'columns' or 'stacked', number/array/null for columns
setSearchMode($mode, $auto_buttons) Sets search behavior 'onchange' or 'submit', auto-add buttons

SearchBuilder Examples

Example 1: Automatic Search (Default)

<?php
$search_builder = $table_builder->createSearchBuilder()
    ->search('search', 'Search')
    ->select('status', 'Status', ['active' => 'Active', 'inactive' => 'Inactive'])
    ->setLabelLayout('side')          // Labels beside fields
    ->setFieldsLayout('columns', 2)   // 2 columns layout
    ->setSearchMode('onchange');       // Automatic search on change

echo $search_builder->render([], true);
?>

Example 2: Manual Search with Buttons

<?php
$search_builder = $table_builder->createSearchBuilder()
    ->search('search', 'Search')
    ->select('category', 'Category', $categories)
    ->input('date', 'created_from', 'From Date')
    ->setLabelLayout('top')            // Labels above fields
    ->setFieldsLayout('columns', 4)    // 4 columns layout  
    ->setSearchMode('submit', true);   // Manual search with auto Search/Clear buttons

echo $search_builder->render([], true);
?>

Example 3: Custom Column Sizes

<?php
$search_builder = $table_builder->createSearchBuilder()
    ->search('search', 'Search Posts')
    ->select('status', 'Status', $statuses)
    ->actionList('category', 'Category', $categories)
    ->setLabelLayout('side')           // Labels beside fields (text-aligned right)
    ->setFieldsLayout('columns', [     // Custom column sizes
        'col-md-4',     // Search field (33%)
        'col-md-3',     // Status field (25%)  
        'col-md-3',     // Category field (25%)
        'col-md-1',     // Search button (8.3%)
        'col-md-1'      // Clear button (8.3%)
    ])
    ->setSearchMode('submit', true);   // Submit mode with auto buttons

echo $search_builder->render([], true);
?>

Example 4: Multi-Row Layout

<?php
$search_builder = $table_builder->createSearchBuilder()
    ->search('search', 'Search Posts')
    ->select('status', 'Status', $statuses)
    ->actionList('category', 'Category', $categories)
    ->setLabelLayout('side')           // Labels beside fields
    ->setFieldsLayout('columns', [     // Multi-row layout: array of arrays
        ['col-md-6', 'col-md-6'],        // Row 1: Search (50%) + Status (50%)
        ['col-md-4', 'col-md-4', 'col-md-4']  // Row 2: Category (33%) + Search btn (33%) + Clear btn (33%)
    ])
    ->setSearchMode('submit', true);   // Submit mode with auto buttons

echo $search_builder->render([], true);
?>

Layout Options Summary

Layout Type Configuration Result
Equal Columns ->setFieldsLayout('columns', 3) 3 equal columns (col-md-4 each)
Auto Columns ->setFieldsLayout('columns', null) Automatic distribution using 'col' class
Custom Single Row ->setFieldsLayout('columns', ['col-md-6', 'col-md-3', 'col-md-3']) Custom sizes in one row
Multi-Row ->setFieldsLayout('columns', [['col-md-6', 'col-md-6'], ['col-md-4', 'col-md-4', 'col-md-4']]) Multiple rows with custom sizes per row
Stacked ->setFieldsLayout('stacked') Vertical layout (one field per line)

Manual Implementation (Legacy Method)

For backward compatibility or advanced use cases, you can still implement filters manually:

Complete Example

<?php
// Complete module with new system
function userManagement() {
    // Preparing actions for select
    $actions = [
        '' => 'All actions',
        'login' => 'Login',
        'logout' => 'Logout', 
        'create' => 'Creation',
        'update' => 'Update'
    ];
    
    // Model setup
    $model = new \App\Modellist\ModelList('#__users', 'userList');
    
    // Filters
    $model->addFilter('search', function($query, $search) {
        if (!empty($search)) {
            $query->where('(username LIKE ? OR email LIKE ? OR name LIKE ?)', 
                ["%{$search}%", "%{$search}%", "%{$search}%"]);
        }
    });
    
    $model->addFilter('status', function($query, $status) {
        switch ($status) {
            case 'active':
                $query->where('status = 1');
                break;
            case 'suspended':
                $query->where('status = 0');
                break;
            case 'trash':
                $query->where('status = -1');
                break;
        }
    });
    
    $model->addFilter('action', function($query, $action) {
        if (!empty($action)) {
            $query->where('last_action = ?', [$action]);
        }
    });
    
    // Output generation
    if (($_REQUEST['page-output'] ?? '') == 'json') {
        // AJAX output
        $query = $model->queryFromRequest();
        $rows = Get::db()->getResults(...$query->get());
        $total = Get::db()->getVar(...$query->getTotal());
        
        $page_info = $model->getPageInfo($total);
        $page_info->setId('userList')->setAjax(true);
        
        $table_html = Get::themePlugin('table', [
            'info' => $model->getListStructure(),
            'rows' => $rows,
            'page_info' => $page_info
        ]);
        
        echo json_encode(['success' => true, 'html' => $table_html]);
        return;
    }
    
    // Initial HTML output
    echo '<div class="card">
        <div class="card-header">
            <div class="row g-3">
                <div class="col-md-4">
                    <input type="text" 
                           class="form-control js-milk-filter-onchange" 
                           data-filter-id="userList" 
                           data-filter-type="search" 
                           placeholder="Search users...">
                </div>
                <div class="col-md-3">';
    
    Form::select('filter_status', 'Status', [
        '' => 'All',
        'active' => 'Active',
        'suspended' => 'Suspended', 
        'trash' => 'Trash'
    ], '', [
        'floating' => false,
        'data-filter-id' => 'userList',
        'data-filter-type' => 'status',
        'class' => 'js-milk-filter-onchange'
    ]);
    
    echo '      </div>
                <div class="col-md-3">';
    
    Form::select('filter_action', 'Action', $actions, '', [
        'floating' => false,
        'data-filter-id' => 'userList', 
        'data-filter-type' => 'action',
        'class' => 'js-milk-filter-onchange'
    ]);
    
    echo '      </div>
            </div>
        </div>
        <div class="card-body">
            <div id="userList" class="js-table-container">';
    
    // Initial table loading
    $query = $model->queryFromRequest();
    $rows = Get::db()->getResults(...$query->get());
    $total = Get::db()->getVar(...$query->getTotal());
    
    $page_info = $model->getPageInfo($total);
    $page_info->setId('userList')->setAjax(true);
    
    echo Get::themePlugin('table', [
        'info' => $model->getListStructure(),
        'rows' => $rows,
        'page_info' => $page_info
    ]);
    
    echo '      </div>
        </div>
    </div>';
}
?>

JavaScript

The system automatically handles JavaScript, however if you want to create a completely custom filter, I provide below the old tutorial on using filters: Old Tutorial

Loading...