Buttons Extension
Add export and utility buttons to your tables
When To Use
Use Buttons when users need export actions (CSV, Excel, PDF, Print) or column visibility controls.
Minimal Example
use Pentiminax\UX\DataTables\Enum\ButtonType;
$dataTable->buttons([
ButtonType::COPY,
ButtonType::CSV,
ButtonType::EXCEL,
]);
Buttons is a layout-aware extension, so it needs two things: the button list,
and a marker telling DataTables where to render the .dt-buttons container.
buttons() writes both. It accepts ButtonType enums, Button objects, and
raw DataTables button names.
Choosing The Position
The second argument is a DataTables position name and defaults to topStart:
$dataTable->buttons([ButtonType::CSV], 'topEnd');
The target position keeps whatever it already holds, so ordering layout()
before buttons() combines features instead of replacing them:
use Pentiminax\UX\DataTables\Enum\Feature;
$dataTable
->layout([
'topStart' => Feature::PAGE_LENGTH,
'topEnd' => Feature::SEARCH,
])
->buttons([ButtonType::CSV]);
// topStart renders the page length selector, then the buttons container.
Buttons are keyed by extension name like every other extension, so calling
buttons() twice replaces the button list rather than appending to it. Build
the full list in one call.
Configuring The Extension Directly
buttons() is a shortcut over addExtension() plus Feature::BUTTONS. Use
the explicit form when you need to hold the extension in a variable, or when
layout() should stay the single source of truth for positioning:
use Pentiminax\UX\DataTables\Enum\Feature;
use Pentiminax\UX\DataTables\Model\Extensions\ButtonsExtension;
$dataTable
->addExtension(new ButtonsExtension([
ButtonType::COPY,
ButtonType::CSV,
]))
->layout([
'topStart' => Feature::BUTTONS,
'topEnd' => Feature::SEARCH,
]);
Without Feature::BUTTONS in layout(), this form generates no buttons
container at all.
Available Button Types
| Button type | Purpose |
|---|---|
ButtonType::COPY | Copy table data to clipboard |
ButtonType::CSV | Export CSV file (see Server-Side Export for every filtered row) |
ButtonType::EXCEL | Export Excel file (see Server-Side Export for every filtered row) |
ButtonType::PDF | Export PDF file |
ButtonType::PRINT | Open print view |
ButtonType::COLUMN_VISIBILITY | Toggle column visibility |
ButtonType::COLUMN_CONTROL_SEARCH_CLEAR | Clear every active search — see below |
ButtonType::COLLECTION | Group other buttons in a dropdown — see below |
ButtonType::CUSTOM | App-defined click behavior — see Custom Buttons below |
Advanced Example
ButtonsExtension exposes a fluent with*Button() API for building a list
across several statements:
$buttons = (new ButtonsExtension([ButtonType::CSV]))
->withExcelButton()
->withPrintButton();
$dataTable
->addExtension($buttons)
->layout([
'topStart' => Feature::BUTTONS,
]);
Customizing Buttons
Use Button objects when you need DataTables button options such as text,
className, exportOptions, or export-specific options like filename.
use Pentiminax\UX\DataTables\Model\Extensions\Button;
$dataTable->buttons([
Button::csv()
->text('Export CSV')
->className('btn btn-sm btn-outline-primary')
->exportOptions(['columns' => '.dt-exportable:visible']),
Button::excel()
->text('Excel')
->option('filename', 'users-export'),
Button::colVis()->text('Columns'),
]);
Button options must be JSON-serializable. JavaScript callbacks such as
DataTables customize functions cannot be serialized from PHP. For a
button whose click behavior lives in JavaScript, see Custom Buttons below.
For options not covered by the typed API, you can still pass a raw DataTables layout object:
$dataTable->layout([
'topStart' => [
'buttons' => [
[
'extend' => 'csv',
'text' => 'Export CSV',
'className' => 'btn btn-primary',
'exportOptions' => ['columns' => ':visible'],
],
],
],
]);
Clearing Every Active Search
Button::ccSearchClear() uses the ColumnControl extension’s own native Buttons entry to clear the
global search box and every ColumnControl per-column search in one click:
use Pentiminax\UX\DataTables\Enum\Feature;
use Pentiminax\UX\DataTables\Model\Extensions\Button;
use Pentiminax\UX\DataTables\Model\Extensions\ButtonsExtension;
$dataTable
->columnControl()
->addExtension(new ButtonsExtension([Button::ccSearchClear()]))
->layout([
'topStart' => Feature::BUTTONS,
]);
Requires ColumnControlExtension on the table ($table->columnControl()) — the button reads and
clears ColumnControl’s own search state. It enables and disables itself automatically based on
whether any search (global or per-column) is currently active, and defaults to the text “Clear
search” (localized). ButtonsExtension::withCcSearchClearButton() is the fluent-collection
shortcut.
Grouping Buttons In A Dropdown
Button::collection(array $buttons) builds a dropdown that groups other buttons together, using
DataTables’ generic collection button type — the same mechanism Button::colVis() builds on
internally, made directly available for a plain grouping menu:
use Pentiminax\UX\DataTables\Enum\Feature;
use Pentiminax\UX\DataTables\Model\Extensions\Button;
use Pentiminax\UX\DataTables\Model\Extensions\ButtonsExtension;
$dataTable
->addExtension(new ButtonsExtension([
Button::collection([
Button::csv(),
Button::excel(),
'colvis',
])->text('Export'),
]))
->layout([
'topStart' => Feature::BUTTONS,
]);
buttons accepts Button objects, raw arrays, or bare extend-name strings, mixed freely — they
serialize the same way whether nested here or at the top level, since Button implements
JsonSerializable and PHP’s json_encode() resolves nested JsonSerializable values
automatically. text defaults to DataTables’ own “Collection” label if omitted.
ButtonsExtension::withCollectionButton(array $buttons) is the fluent-collection shortcut.
Custom Buttons
Button::custom(string $action) adds a button whose click behavior is defined in JavaScript, for
cases the typed export/visibility buttons don’t cover — for example resetting ColReorder’s column
order. (For clearing ColumnControl searches specifically, prefer Button::ccSearchClear() above
— it’s a native plugin button, not app-defined behavior.)
use Pentiminax\UX\DataTables\Enum\Feature;
use Pentiminax\UX\DataTables\Model\Extensions\Button;
use Pentiminax\UX\DataTables\Model\Extensions\ButtonsExtension;
$dataTable
->addExtension(new ButtonsExtension([
Button::custom('restoreOrder')->text('Restore order')->className('btn btn-sm'),
]))
->layout([
'topStart' => Feature::BUTTONS,
]);
$action isn’t a callback — PHP can’t serialize JavaScript functions — it’s a name the frontend
resolves against a registry. Register the real callback once, in your own JavaScript entrypoint,
before any table connects:
import { buttonActions } from '@pentiminax/ux-datatables'
buttonActions.register('restoreOrder', (e, dt) => {
dt.colReorder.reset()
})
The callback receives the same (e, dt, node, config) arguments as a native DataTables Buttons
action function. If $action has no matching registration when the table connects, the button
renders but does nothing, and the frontend logs
No button action registered for "<name>" to the console — check the registered name matches the
PHP-side $action string exactly, and that the registration code runs before the table connects.
ButtonsExtension::withCustomButton(string $action) is the fluent-collection shortcut, equivalent
to Button::custom($action) with no further options.
Server-Side Export (CSV & XLSX)
DataTables’ built-in CSV and Excel buttons only see the rows currently loaded in the browser. For a
serverSide() table, pass serverSide: true so the bundle streams every filtered row from PHP:
use Pentiminax\UX\DataTables\Enum\Feature;
use Pentiminax\UX\DataTables\Model\Extensions\Button;
use Pentiminax\UX\DataTables\Model\Extensions\ButtonsExtension;
$dataTable
->addExtension(new ButtonsExtension([
Button::csv(serverSide: true)
->text('Export CSV')
->filename('users'),
Button::excel(serverSide: true)
->text('Export XLSX')
->filename('users'),
]))
->layout([
'topStart' => Feature::BUTTONS,
]);
ButtonsExtension::withCsvButton(serverSide: true) and
ButtonsExtension::withExcelButton(serverSide: true) are the fluent shortcuts. Without the flag,
Button::csv() and Button::excel() stay the DataTables client-side exports.
Both formats require openspout/openspout:
composer require openspout/openspout
The export reuses the request’s current search, order, and filters, and ignores pagination.
Columns use the same setExportable(false) flag as client-side export (and
#[Column(exportable: false)]). Action columns, template columns, and hidden columns
(setVisible(false)) are already excluded. Order in the file is configureColumns() order, and
headers are column titles.
A TemplateColumn renders markup for the browser, so it stays out of an export unless
setExportable(true) opts it back in — its rendered HTML is then stripped to text and its
whitespace collapsed onto one line.
Because the rows an export streams are mapped from the exportable columns alone, the work the
displayed table needs — rendering a template column’s Twig, resolving an action’s voters, URL, and
CSRF token — does not run for exported rows. A data provider you build yourself in
createDataProvider() keeps the single mapper you gave it and pays that cost on every exported row.
Filename defaults to a slug of the table class (UserDataTable → user-data-table.csv).
filename('users') becomes users.csv or users.xlsx depending on the button’s format.
Page Projection
projectPage() batch-enriches a page to avoid an N+1. An export has no page — it streams every
filtered row — so the projector is called once per batch (250 rows by default) rather than once over
the whole result set. Per-item mapping and batch loading are unaffected. A projector computing
something relative to the items it received (a rank, a running total, a share of the batch maximum)
returns different values in an export than on screen; those values already shift with the page
length on screen. Build the provider yourself in createDataProvider() with a larger
exportChunkSize when a bigger batch is genuinely required.
Export Keys
Each server-side button is addressed on the export endpoint by an export key, defaulting to its
format (csv or xlsx). Two server-side buttons on the same table cannot share a key — the
extension throws an InvalidArgumentException at configuration time. Give one of them its own key
when you need two buttons of the same format:
Button::csv(serverSide: true)->text('Export all')->filename('all');
Button::csv(serverSide: true)->exportKey('subset')->text('Export subset')->filename('subset');
Formula Injection
Spreadsheet software evaluates a cell whose value starts with =, @, a tab, or a carriage
return, which turns exported data into executable formulas. Those values are prefixed with an
apostrophe so they render as text. + and - are only prefixed when the value is not a number, so
a decimal such as -42.50 stays a number.
The writer behind each format is an ExporterInterface. Replacing one — a different CSV delimiter,
another XLSX layout — is documented under
Custom Exporters.
XLSX Specifics
The XLSX writer adds a bold header row, a frozen header, an auto-filter, and a fixed column width.
Unlike CSV, it is not streamed row by row: OpenSpout builds the workbook in a temporary folder and
zips it into the response on close, so the download starts only once the last row is written and
the server needs temporary disk space for the workbook. ext-zip ships as an
openspout/openspout requirement.
Nesting Inside A Collection Button
A collection-type button (Button::colVis(), or anything using extend: 'collection') can nest
further buttons under buttons, postfixButtons, or prefixButtons. Button objects placed in
those arrays serialize correctly at any depth — Button implements JsonSerializable, and PHP’s
json_encode() resolves nested JsonSerializable values automatically:
Button::colVis()
->text('Columns')
->option('postfixButtons', [
['extend' => 'colvisRestore'],
Button::custom('restoreOrder')->text('Restore order'),
]);
The frontend resolver walks buttons/postfixButtons/prefixButtons recursively, so a custom
action nested this way is registered exactly like a top-level one.
Frequent Pitfalls
- Adding
ButtonsExtensionwithout addingFeature::BUTTONSto the layout —buttons()avoids this entirely. - Calling
buttons()twice and expecting the lists to merge; the second call replaces the first. - Mixing button values as invalid strings.
- Forgetting that non-column-visibility buttons apply export filtering by default.
- Using
Button::ccSearchClear()without also enablingColumnControlExtensionon the table. - Using
Button::custom()without registering a matchingbuttonActionscallback, or registering it after the table has already connected. - Using
Button::csv()orButton::excel()on aserverSide()table and expecting every row — useserverSide: trueinstead. - Declaring two server-side export buttons of the same format without giving one its own
exportKey(). - Expecting
projectPage()to receive the whole export in one call — it is batched; see Page Projection.