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

to navigate · Enter to open · Esc to close

Documentation

Column Control Extension

Add per-column sorting and searching controls

When To Use

Use Column Control when users need direct per-column order and filter controls in the table UI.

Minimal Example

$dataTable->columnControl();

Behavior

The bundled ColumnControlExtension serializes default controls for:

  • ordering actions (asc, desc, add/remove order)
  • per-column search input

Search logics that rely on SQL LIKEcontains, notContains, starts, and ends — are ignored on columns mapped to a native identifier type (guid, uuid, ulid, uuid_binary, uuid_binary_ordered_time), because PostgreSQL and SQL Server reject LIKE on those columns. equal and notEqual still work and match a complete identifier. empty and notEmpty only test IS NULL / IS NOT NULL on those columns — they must not compare the identifier to an empty string. See Searching UUID and ULID columns.

Choosing Where The Controls Go

Each control group targets one row: a header row index (0, 1, …) or a tfoot string ('tfoot', 'tfoot:1'). Pass a target to replace the defaults with that single group.

// Per-column search inputs in the table footer instead of the second header row.
$dataTable->columnControl(target: 'tfoot');

// A specific content type in a header row.
$dataTable->columnControl(target: 1, content: ['searchText']);

ColumnControl creates the targeted row when the table does not render one, so a footer target needs no <tfoot> markup — render_datatable() emits an empty <table> and every row is built at initialization.

Naming a target drops the default order controls. Keep both by building the extension directly:

use Pentiminax\UX\DataTables\Model\Extensions\ColumnControlExtension;

$dataTable->addExtension(
    (new ColumnControlExtension([]))
        ->add(0, ['order'])
        ->add('tfoot', ['search'])
);

new ColumnControlExtension() keeps the defaults, new ColumnControlExtension([]) starts from nothing, and add() appends one group per call in call order.

content needs a target to be placed in: columnControl(content: ['searchText']) throws rather than silently keeping the defaults.

Per-column Overrides

Override the control content for a single column with setColumnControl(), using the same content descriptors as the DataTables columns.columnControl option:

use Pentiminax\UX\DataTables\Column\TextColumn;

$dataTable->columnControl();

TextColumn::new('name', 'Name')
    ->setColumnControl(['colvisDropdown']);

setColumnControl() takes precedence over disableColumnControl() regardless of call order. Both require ColumnControlExtension to be added to the table — the frontend only loads the ColumnControl plugin bundle when the table-level extension is present, so a column-level override or a disabled column stays inert without it.

Overriding The Actions Column

The auto-generated actions column is built internally and has ColumnControl disabled by default, so setColumnControl() on AbstractColumn doesn’t reach it. Use Actions::setColumnControl() in configureActions() instead — it flows through to the generated column the same way setColumnClassName() already does:

use Pentiminax\UX\DataTables\Model\Actions;

public function configureActions(Actions $actions): Actions
{
    return $actions
        ->setColumnControl(['colvisDropdown'])
        ->add(Action::edit());
}

Custom Content Types

ColumnControl resolves the content descriptors above against DataTable.ColumnControl.content, a plugin registry (see the DataTables.net docs) — registering a custom type means adding to that object before the table is constructed. Neither existing controller event fits: datatables:pre-connect fires before ColumnControl’s plugin bundle has loaded, so DataTable.ColumnControl doesn’t exist yet; datatables:connect fires after the table is already constructed, which is too late — an unregistered content type makes construction throw. Use datatables:pre-init, which fires once extension plugins have loaded but before the table is built:

// assets/controllers/mytable_controller.js
import { Controller } from '@hotwired/stimulus'

export default class extends Controller {
  connect() {
    this.element.addEventListener('datatables:pre-init', this._onPreInit)
  }

  disconnect() {
    this.element.removeEventListener('datatables:pre-init', this._onPreInit)
  }

  _onPreInit(event) {
    const { DataTable } = event.detail

    DataTable.ColumnControl.content.myRating = {
      defaults: { min: 1, max: 5 },
      init(config) {
        const el = document.createElement('input')
        el.type = 'number'
        el.min = String(config.min)
        el.max = String(config.max)

        el.addEventListener('change', () => {
          this.dt().column(this.idx()).search(el.value).draw()
        })

        return el
      },
    }
  }
}

Then reference the type by name from PHP, same as any built-in content descriptor:

TextColumn::new('rating', 'Rating')
    ->setColumnControl(['myRating']);

Registering onto DataTable.ColumnControl.content only needs to happen once per page, not once per table — a listener attached to one table’s controller instance still runs before that table’s own construction, which is all a plugin registration needs.

Frequent Pitfalls

  • Calling setColumnControl() or disableColumnControl() without also enabling ColumnControlExtension on the table ($table->columnControl()); the column-level setting is silently inert because the frontend never loads the plugin bundle.
  • Enabling it without ensuring the target columns are searchable/orderable.
  • Passing a target and expecting the default order controls to stay — they are replaced. Add them back with add(0, ['order']).
  • Expecting partial search (contains, starts) to work on a UUID or ULID column.
  • Registering a custom content type on datatables:pre-connect instead of datatables:pre-initpre-connect fires before the DataTable library and its extensions have even loaded, so its event detail has no DataTable reference to register against. The table then throws Unknown ColumnControl content type once it’s constructed.