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