Type to search columns, filters, options, and extensions.

to navigate · Enter to open · Esc to close

Documentation

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 typePurpose
ButtonType::COPYCopy table data to clipboard
ButtonType::CSVExport CSV file (see Server-Side Export for every filtered row)
ButtonType::EXCELExport Excel file (see Server-Side Export for every filtered row)
ButtonType::PDFExport PDF file
ButtonType::PRINTOpen print view
ButtonType::COLUMN_VISIBILITYToggle column visibility
ButtonType::COLUMN_CONTROL_SEARCH_CLEARClear every active search — see below
ButtonType::COLLECTIONGroup other buttons in a dropdown — see below
ButtonType::CUSTOMApp-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'],
            ],
        ],
    ],
]);

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 (UserDataTableuser-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 ButtonsExtension without adding Feature::BUTTONS to 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 enabling ColumnControlExtension on the table.
  • Using Button::custom() without registering a matching buttonActions callback, or registering it after the table has already connected.
  • Using Button::csv() or Button::excel() on a serverSide() table and expecting every row — use serverSide: true instead.
  • 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.