Action Column

UX DataTables builds row actions from an Actions collection. The recommended entry point is AbstractDataTable::configureActions().

use App\Entity\User;
use Pentiminax\UX\DataTables\Attribute\AsDataTable;
use Pentiminax\UX\DataTables\Column\NumberColumn;
use Pentiminax\UX\DataTables\Column\TextColumn;
use Pentiminax\UX\DataTables\Model\AbstractDataTable;
use Pentiminax\UX\DataTables\Model\Action;
use Pentiminax\UX\DataTables\Model\Actions;

#[AsDataTable(User::class)]
final class UsersDataTable extends AbstractDataTable
{
    public function configureColumns(): iterable
    {
        yield NumberColumn::new('id', 'ID');
        yield TextColumn::new('email', 'Email');
        yield TextColumn::new('status', 'Status');
    }

    public function configureActions(Actions $actions): Actions
    {
        return $actions
            ->setColumnLabel('Actions')
            ->add(
                Action::edit('Edit')
                    ->setClassName('btn btn-sm btn-warning')
                    ->icon('bi bi-pencil')
            )
            ->add(
                Action::delete('Delete')
                    ->setClassName('btn btn-sm btn-danger')
                    ->icon('bi bi-trash')
                    ->askConfirmation('Delete this user?')
                    ->displayIf('status', 'draft')
            );
    }

    protected function mapRow(mixed $item): array
    {
        /** @var User $item */
        return [
            'id'     => $item->getId(),
            'email'  => $item->getEmail(),
            'status' => $item->getStatus(),
        ];
    }
}

When configureActions() returns at least one action, AbstractDataTable automatically appends an ActionColumn named actions.

Action API

Factory Methods

Method Description
``Action::delete($label = 'Delete', $className = 'btn btn-danger')``Create the built-in delete action
``Action::detail($label = 'Detail', $className = 'btn btn-primary')``Create a link action to a detail page
``Action::edit($label = 'Edit', $className = 'btn btn-warning')``Create an inline edit action (opens the configured edit modal)

Fluent Configuration

Method Description
`label(string $label)`Override the button label
`setClassName(string $className)`Customize button classes
`icon(string $icon)`Render an icon before the label
`htmlAttributes(array $htmlAttributes)`Set extra HTML attributes on the rendered action button/link
`askConfirmation(string $message)`Display a browser confirmation prompt before the action
`displayIf(string $field, mixed $value)`Show the action only when `row[field] === value`
`setIdField(string $idField)`Use another row key instead of `id`
`setEntityClass(string $entityClass)`Set the Doctrine entity class explicitly
`linkToUrl(string|callable $url)`Set the URL for detail actions (static string or per-row callable)
`collapsible(string $template, array $parameters = [])`Render a `detail` action as an expand/collapse control that lazily loads a Twig child row (the template receives the row as `entity`, plus any extra `$parameters`). Only meaningful for `Action::detail()`
`permission(string $attribute, ?callable $subjectResolver = null)`Restrict the action with a Symfony security attribute (role/voter/expression). Without a resolver it is evaluated once before serialization; with a resolver it is evaluated per row (the resolver receives the raw source row). Denied actions are removed and never serialized to the client
`position(?ActionsPosition $position)`Override the column placement for this action only (`null` inherits the `Actions` collection position). See [Column Position & Alignment](#column-position--alignment)

When linkToUrl() receives a callable, the argument depends on how the row was produced:

  • AbstractDataTable + provider or setData($objects): the callable receives the original source object
  • manual DataTable::data([...]) inline rows: the callable receives the row array

Actions API

Method Description
`add(Action $action)`Register an action for the column
`remove(ActionType $type)`Remove a previously registered action
`setColumnLabel(string $label)`Customize the action column header
`position(ActionsPosition $position)`Place the actions column before or after the data columns (default: `AfterColumns`)
`alignment(ActionsAlignment $alignment)`Horizontally align the action cell. See [Column Position & Alignment](#column-position--alignment)

Actions stores one action per ActionType. The public enum values are:

use Pentiminax\UX\DataTables\Enum\ActionType;

ActionType::Delete; // value: DELETE
ActionType::Detail; // value: DETAIL
ActionType::Edit;   // value: EDIT

Column Position & Alignment

By default, the action column is appended after the data columns. You can move it, align its content, and even split actions across two dedicated columns.

Collection-level position

Use Actions::position() to place the whole actions column before or after the data columns:

use Pentiminax\UX\DataTables\Enum\ActionsPosition;
use Pentiminax\UX\DataTables\Model\Actions;

public function configureActions(Actions $actions): Actions
{
    return $actions
        ->position(ActionsPosition::BeforeColumns)
        ->add(Action::edit('Edit'))
        ->add(Action::delete('Delete'));
}

The ActionsPosition enum exposes:

use Pentiminax\UX\DataTables\Enum\ActionsPosition;

ActionsPosition::BeforeColumns; // value: before
ActionsPosition::AfterColumns;  // value: after  (default)

Alignment

Use Actions::alignment() to horizontally align the content of the action cell:

use Pentiminax\UX\DataTables\Enum\ActionsAlignment;

$actions->alignment(ActionsAlignment::Center);

The alignment is applied as a dt-{value} CSS class on the column (for example, ActionsAlignment::Center adds dt-center).

use Pentiminax\UX\DataTables\Enum\ActionsAlignment;

ActionsAlignment::Left;   // value: left
ActionsAlignment::Center; // value: center
ActionsAlignment::Right;  // value: right

Per-action position

Action::position() overrides the placement for a single action, independently of the collection-level position. Pass null (the default) to inherit the Actions collection position, or an explicit ActionsPosition to pin that action.

When all actions resolve to the same position, a single ActionColumn named actions is produced (the default behavior). When actions are split across both positions, two action columns are produced:

  • the BeforeColumns group is prepended as a column named actions_before;
  • the AfterColumns group is appended as a column named actions.

A typical use case is pinning a collapsible detail toggle before the data while keeping edit/delete after it:

use Pentiminax\UX\DataTables\Enum\ActionsPosition;
use Pentiminax\UX\DataTables\Model\Action;
use Pentiminax\UX\DataTables\Model\Actions;

public function configureActions(Actions $actions): Actions
{
    return $actions
        ->add(
            Action::detail('Details')
                ->collapsible('data_tables/details.html.twig')
                ->position(ActionsPosition::BeforeColumns)
        )
        ->add(Action::edit('Edit'))
        ->add(Action::delete('Delete'));
}

This renders the detail toggle in an actions_before column placed before the data, and the edit/delete buttons in the trailing actions column.

Manual Usage with DataTableBuilderInterface

If you build tables manually, create an Actions collection and wrap it with ActionColumn::fromActions():

use App\Entity\User;
use Pentiminax\UX\DataTables\Column\ActionColumn;
use Pentiminax\UX\DataTables\Column\TextColumn;
use Pentiminax\UX\DataTables\Model\Action;
use Pentiminax\UX\DataTables\Model\Actions;

$actions = (new Actions())
    ->setColumnLabel('Operations')
    ->add(
        Action::delete()
            ->setEntityClass(User::class)
            ->setIdField('uuid')
            ->askConfirmation('Delete this user?')
    );

$table = $builder
    ->createDataTable('users')
    ->columns([
        TextColumn::new('email', 'Email'),
        ActionColumn::fromActions('actions', 'Operations', $actions),
    ])
    ->data([
        ['uuid' => '8f9f0c31', 'email' => 'john@example.com'],
    ]);

Manual tables must provide the entity class themselves because there is no #[AsDataTable] attribute to infer it from.

Inline Edit (Modal)

Action::edit() opens a configured edit modal with an auto-generated Symfony Form. See the dedicated Edit Modal page for full documentation: template overrides, column-to-form mapping, primary key handling, and examples.

Ajax Endpoints

All action endpoints are handled by built-in Stimulus controllers. Import the bundle routes:

// config/routes/ux_datatables.php
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;

return static function (RoutingConfigurator $routes): void {
    $routes->import('@DataTablesBundle/config/routes.php');
};

Delete Endpoint

DELETE /datatables/ajax/delete — payload: entity, id, topics (optional Mercure topics).

Edit Form Endpoints

  • GET /datatables/ajax/edit-form — returns the rendered modal HTML
  • POST /datatables/ajax/edit-form — validates and persists the form data

These endpoints are only available when symfony/form is installed.