Bulk Actions Documentation
Revision: 2025/12/02
Overview
Actions vs Bulk Actions
- Row Actions: Actions column at the end of each table row for single record operations (edit, delete, view)
- Bulk Actions: Actions that appear when selecting multiple rows via checkboxes (bulk delete, export, update status)
- Checkboxes: Automatically appear when you define bulk actions using
setBulkActions()
Basic Setup
<?php
namespace Modules\Posts;
use App\Abstracts\AbstractController;
use Builders\TableBuilder;
class PostsController extends AbstractController {
#[RequestAction('home')]
public function postsList() {
$response = ['page' => $this->page, 'title' => $this->title];
$tableBuilder = TableBuilder::create($this->model, 'idTablePosts')
->setBulkActions([
'delete' => [
'label' => 'Delete Selected',
'action' => [$this, 'actionBulkDelete']
]
]);
$response = array_merge($response, $tableBuilder->getResponse());
Response::render(__DIR__ . '/Views/list_page.php', $response);
}
public function actionBulkDelete($record, $request) {
if ($record->delete($record->id)) {
return ['success' => true, 'msg' => 'Deleted successfully'];
}
return ['success' => false, 'msg' => 'Delete failed'];
}
}
getResponse() instead of render() when using bulk actions or row actions with callbacks. This ensures action return values are properly merged with the table response.
Configuration Methods
addBulkAction() vs setBulkActions()
- addBulkAction($key, $action_data): Adds a single bulk action without modifying existing ones
- setBulkActions($actions): Replaces all bulk actions with the provided set (uses addBulkAction() internally)
setBulkActions() - Set All Actions
Replaces all bulk actions with a new set:
->setBulkActions([
'delete' => [
'label' => 'Delete',
'action' => [$this, 'actionDelete']
],
'export' => [
'label' => 'Export',
'action' => [$this, 'actionExport'],
'mode' => 'batch',
'updateTable' => false
]
])
addBulkAction() - Add Single Action
Add one action at a time without replacing existing ones:
->addBulkAction('archive', [
'label' => 'Archive',
'action' => [$this, 'actionArchive']
])
// Chain multiple actions
->addBulkAction('publish', ['label' => 'Publish', 'action' => [$this, 'actionPublish']])
->addBulkAction('unpublish', ['label' => 'Unpublish', 'action' => [$this, 'actionUnpublish']])
getBulkActions() - Inspect Configured Actions
Retrieve the list of all configured bulk actions:
$tableBuilder = TableBuilder::create($model, 'idTablePosts')
->addBulkAction('delete', ['label' => 'Delete', 'action' => [$this, 'actionDelete']])
->addBulkAction('export', ['label' => 'Export', 'action' => [$this, 'actionExport']]);
// Get all configured bulk actions
$bulkActions = $tableBuilder->getBulkActions();
// Returns: ['delete' => ['label' => 'Delete', 'action' => ...], 'export' => [...]]
Configuration Options
| Parameter | Type | Default | Description |
|---|---|---|---|
label |
string | required | Action name shown in dropdown |
action |
callable | required | Function to execute: [$this, 'methodName'] |
mode |
string | 'single' | 'single' = called once per record'batch' = called once with all records |
updateTable |
bool | true | Whether to reload table after execution |
showIfFilter |
array | null | Conditional visibility based on active filters |
Action Modes
Single Mode (Default)
The action function is called once for each selected record individually:
'delete' => [
'label' => 'Delete',
'action' => [$this, 'actionDelete']
// mode defaults to 'single'
]
public function actionDelete($record, $request) {
// Called once per selected record
// $record is a single Model instance
if ($record->delete($record->id)) {
return ['success' => true, 'msg' => 'Deleted'];
}
return ['success' => false, 'msg' => 'Failed'];
}
Batch Mode
The action function is called once with all selected records together:
'export' => [
'label' => 'Export',
'action' => [$this, 'actionExport'],
'mode' => 'batch',
'updateTable' => false
]
public function actionExport($records, $request) {
// Called once with all records
// $records is a collection of Model instances
$records->setOutputMode('formatted');
$csv = '';
foreach ($records as $record) {
$csv .= "{$record->id},{$record->title},{$record->status}\n";
}
return [
'success' => true,
'offcanvas_end' => [
'title' => 'Export Results',
'body' => '' . $csv . '
'
]
];
}
Action Method Signature
Single Mode
public function actionMethodName($record, $request) {
// $record - Single Model instance of selected row
// $request - Full $_REQUEST array
// Perform operation on $record
// Return array with success and msg
return ['success' => true, 'msg' => 'Operation completed'];
}
Batch Mode
public function actionMethodName($records, $request) {
// $records - Collection of Model instances
// $request - Full $_REQUEST array
// Perform operation on all $records
// Return array with success and msg
return ['success' => true, 'msg' => 'Batch operation completed'];
}
Return Values
Action methods must return an associative array. Available keys:
| Key | Type | Description |
|---|---|---|
success |
bool | Whether the operation succeeded |
msg |
string | Feedback message to display (note: msg not message) |
reload |
bool | Force table reload (overrides updateTable setting) |
offcanvas_end |
array | Opens offcanvas panel with content: ['title' => '...', 'body' => '...', 'size' => 'xl'] |
modal |
array | Opens modal dialog: ['title' => '...', 'body' => '...', 'footer' => '...'] |
redirect |
string | URL to redirect to after operation |
Practical Examples
Status Update
->setBulkActions([
'publish' => [
'label' => 'Publish',
'action' => [$this, 'actionPublish']
]
])
public function actionPublish($record, $request) {
$record->status = 'published';
$record->published_at = date('Y-m-d H:i:s');
if ($record->save()) {
return ['success' => true, 'msg' => 'Published'];
}
return ['success' => false, 'msg' => 'Publish failed'];
}
Comparison (Batch Mode with Offcanvas)
->setBulkActions([
'compare' => [
'label' => 'Compare',
'action' => [$this, 'actionCompare'],
'mode' => 'batch',
'updateTable' => false
]
])
public function actionCompare($records, $request) {
$records->setOutputMode('formatted');
$fields = ['title', 'status', 'created_at'];
$html = '';
foreach ($fields as $field) {
$html .= '' . $field . ' ';
foreach ($records as $record) {
$html .= '' . ($record->$field ?? '-') . ' ';
}
$html .= ' ';
}
$html .= '
';
return [
'success' => true,
'offcanvas_end' => [
'title' => 'Comparison',
'body' => $html,
'size' => 'xl'
]
];
}
Using Built-in Delete Action
// You can use the built-in actionDeleteRow method:
->setBulkActions([
'delete' => [
'label' => 'Delete',
'action' => [$tableBuilder, 'actionDeleteRow']
]
])
// Or for default row actions with delete:
->setDefaultActions() // Automatically includes edit and delete actions
Conditional Visibility with showIfFilter
Show or hide bulk actions based on active table filters. Perfect for soft-delete patterns or status-based workflows.
Example: Soft Delete Pattern
// Create status filter in SearchBuilder
$searchBuilder = \Builders\SearchBuilder::create('idTablePosts')
->actionList('status', 'Status:', [
'active' => 'Active',
'deleted' => 'Deleted'
], 'active');
// Configure table with conditional bulk actions
$tableBuilder = TableBuilder::create($postModel, 'idTablePosts')
->filter('status', function($query, $value) {
if ($value === 'deleted') {
$query->where('deleted_at IS NOT NULL');
} else {
$query->where('deleted_at IS NULL');
}
}, 'active')
->setBulkActions([
'soft_delete' => [
'label' => 'Move to Trash',
'action' => [$this, 'actionSoftDelete'],
'showIfFilter' => ['status' => 'active'] // Only when viewing active
],
'hard_delete' => [
'label' => 'Delete Permanently',
'action' => [$this, 'actionHardDelete'],
'showIfFilter' => ['status' => 'deleted'] // Only when viewing deleted
],
'restore' => [
'label' => 'Restore',
'action' => [$this, 'actionRestore'],
'showIfFilter' => ['status' => 'deleted'] // Only when viewing deleted
]
]);
public function actionSoftDelete($record, $request) {
$record->deleted_at = date('Y-m-d H:i:s');
if ($record->save()) {
return ['success' => true, 'msg' => 'Moved to trash'];
}
return ['success' => false, 'msg' => 'Failed'];
}
public function actionHardDelete($record, $request) {
if ($record->deleted_at !== null) {
if ($record->delete($record->id)) {
return ['success' => true, 'msg' => 'Permanently deleted'];
}
}
return ['success' => false, 'msg' => 'Cannot delete active records'];
}
public function actionRestore($record, $request) {
$record->deleted_at = null;
if ($record->save()) {
return ['success' => true, 'msg' => 'Restored'];
}
return ['success' => false, 'msg' => 'Restore failed'];
}
How It Works
When filter is 'active': Users see only "Move to Trash"
When filter is 'deleted': Users see "Delete Permanently" and "Restore"
Dynamic UI: The dropdown updates automatically when filters change
JavaScript Hooks
Intercept bulk actions on the client side for confirmations, custom downloads, or preventing default behavior:
Hook Syntax
registerHook('table-action-{action_name}', function(ids, elclick, form, sendform) {
// ids: array of selected record IDs
// elclick: clicked button element
// form: table form element
// sendform: always true (legacy)
return true; // Proceed with server request
return false; // Cancel request
});
Example: Confirmation
registerHook('table-action-delete', function(ids, elclick, form, sendform) {
return confirm('Delete ' + ids.length + ' items? This cannot be undone.');
});
Example: Custom Download
registerHook('table-action-download', function(ids, elclick, form, sendform) {
// Create download form
const downloadForm = document.createElement('form');
downloadForm.method = 'POST';
downloadForm.action = milk_url;
downloadForm.style.display = 'none';
const input = document.createElement('input');
input.name = 'ids';
input.value = ids.join(',');
downloadForm.appendChild(input);
document.body.appendChild(downloadForm);
downloadForm.submit();
document.body.removeChild(downloadForm);
return false; // Don't send default request
});
Working with Links
While bulk actions typically use callbacks, you can also create link-based actions with fetch:
// Row action with link and fetch
->setActions([
'restore' => [
'label' => 'Restore',
'link' => '?page=posts&action=restore&id=%id%',
'fetch' => 'post' // Send link as POST via fetch
]
])
// Or use activeFetch() to enable fetch mode for all links
->activeFetch()
->setActions([
'restore' => [
'label' => 'Restore',
'link' => '?page=posts&action=restore&id=%id%'
// fetch: 'post' is added automatically
]
])
Best Practices
- Mode Selection: Use 'single' for operations on individual records, 'batch' for operations requiring all records at once
- Table Updates: Set
updateTable => falsefor actions that open offcanvas/modal or don't modify data - Validation: Always validate operations in action handlers to prevent misuse
- Error Handling: Return detailed error messages in the
msgfield for better user feedback - Built-in Actions: Use
[$tableBuilder, 'actionDeleteRow']for standard delete operations - Conditional Actions: Use
showIfFilterto show/hide actions based on active filters (e.g., soft-delete patterns)
Common Pitfalls
Common Mistakes
- Using 'message' instead of 'msg': The return key is
'msg'not'message' - Using render() instead of getResponse(): Action returns won't be merged with the table response
- Confusing modes: 'single' mode receives
$record(singular), 'batch' receives$records(collection) - Wrong addBulkAction() signature: Use
addBulkAction($key, $action_data), notaddBulkAction($action_data)
Related Documentation
- Row Actions - Single record actions in the actions column
- TableBuilder Overview - Main table builder documentation
- Search & Filters - Creating filters for showIfFilter
- Offcanvas Component - Offcanvas panel documentation