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

to navigate · Enter to open · Esc to close

Documentation

Mercure Integration

Real-time DataTables refresh with Mercure SSE

Use Mercure when table data can change outside the current browser session and you want automatic refresh.

MercureConfig can be configured manually through DataTable::mercure(...), declared on #[AsDataTable(...)], or auto-resolved from #[AsDataTable(..., mercure: true)].

What The Integration Does

When mercure is configured on a table:

  1. Backend options include a mercure object (hubUrl, topics, optional withCredentials, optional debounceMs).
  2. The Stimulus controller opens an EventSource to the hub with ?topic=....
  3. On each message, the controller emits a datatables:mercure:message event and triggers table.ajax.reload(null, false).
  4. The SSE connection is closed automatically on controller disconnect().

Minimal Setup

#[AsDataTable(Book::class, mercure: true)]
final class BookDataTable extends AbstractDataTable
{
}

When auto-resolution is enabled:

  • the bundle reads the default Symfony Mercure hub URL
  • explicit API Platform mercure.topics are reused when available
  • otherwise the bundle falls back to an item IRI template (for example /api/books/{id})
  • if no API Platform item metadata is available, it falls back to /datatables/books/{id}

Attribute Topics

Use explicit topics on the attribute when you do not want topic auto-resolution from API Platform metadata:

#[AsDataTable(
    entityClass: Book::class,
    mercure: [
        'topics' => [
            'https://example.com/books',
        ],
    ],
)]
final class BookDataTable extends AbstractDataTable
{
}

The same form accepts one topic or several topics:

#[AsDataTable(
    entityClass: Book::class,
    mercure: [
        'topics' => [
            'https://example.com/books',
            'https://example.com/authors',
        ],
        'withCredentials' => true,
        'debounceMs' => 300,
    ],
)]

Mercure supports multiple subscriptions by repeating the topic query parameter. For most tables, one topic is enough. Use several topics when a single DataTable must refresh after changes published on different resources or channels.

Advanced Setup

$dataTable->mercure(
    topics: ['admin/books', 'admin/authors'],
    withCredentials: true,
    debounceMs: 300,
);

Notes:

  • withCredentials enables cookie/auth forwarding in the SSE request.
  • debounceMs is applied client-side before reload (500ms by default).
  • topics accepts one or many topics.

Publishing Updates

If symfony/mercure is available, the bundle registers MercureUpdatePublisher and injects it into the Ajax edit controller.

When boolean inline edit is called with topics, the controller publishes:

{
  "type": "edit",
  "id": "...",
  "field": "..."
}

You can also publish manually with MercureUpdatePublisher::publish() or publishForDataTable().

Highlighting Updated Cells

A refresh that changes a value silently is easy to miss. highlightUpdates() briefly emphasizes the cells a Mercure refresh changed:

public function configureDataTable(DataTable $table): DataTable
{
    return $table
        ->mercure(topics: ['https://example.com/users'])
        ->highlightUpdates(ignoreColumns: ['lastLoginAt'])
        ->serverSide();
}
ParameterDefaultPurpose
durationMs1200How long a cell stays emphasized.
ignoreColumns[]Data keys never highlighted.
idField'id'Property read to identify a row.

ignoreColumns matters more than it looks. A column whose rendered value changes on every refresh - a relative date, a counter, an “updated at” stamp - would be highlighted on every single event and bury the real updates under constant noise. List those columns here.

How It Works

  1. Enabling the option serializes a highlight object into the frontend payload and adds a DT_RowId key to every row, which also feeds the DataTables rowId option. API Platform rows never go through the PHP row mapper, so there the key is added client-side from idField.
  2. On an incoming Mercure message the controller records the values currently displayed, then reloads the table.
  3. Once that reload has redrawn the table, rows are matched by DT_RowId and compared field by field. Changed cells receive the dt-cell-updated class, and a datatables:highlight event carries their nodes. A sort, a search or a paging draw landing while the refresh is in flight leaves the recorded values untouched.

Comparison runs on row data rather than on rendered markup: a redraw regenerates cell HTML even where nothing changed, so comparing markup would light up the whole table. Matching rows by id means a row that only moved to another position is not reported as updated.

Only refreshes triggered by Mercure are compared - sorting, searching, and paging never highlight.

Styling

The animation lives in the bundle stylesheet and is driven by two custom properties:

:root {
    --dt-highlight-color: rgb(16 185 129 / 0.3);
}

--dt-highlight-duration is written inline from durationMs. Under prefers-reduced-motion: reduce the animation is replaced by a static background.

To drive your own effect instead, skip the class and listen to the event:

document.addEventListener('datatables:highlight', (event) => {
    for (const cell of event.detail.cells) {
        // ...
    }
})

Operational Notes

  • Mercure refresh uses ajax.reload(...): tables configured only with static data will not auto-refresh from SSE.
  • No Mercure config means no SSE subscription (dynamic import is skipped).
  • If the table receives many events, tune debounceMs to protect backend load.
  • private: true from API Platform metadata is not mapped automatically to withCredentials.
  • highlightUpdates() needs rows to expose an identifier; a table whose idField cannot be read leaves those rows unhighlighted.