T3 DataTable 

Extension key

t3_datatable

Package name

hrr/t3-datatable

Language

en

Author

Himanshu Ramavat, Rohan Parmar

License

GPL-2.0-or-later

Repository

https://github.com/himanshuramavat/t3_datatable

Issues

https://github.com/himanshuramavat/t3_datatable/issues


T3 DataTable is a server-side grid engine for custom TYPO3 backend modules. Register a grid for any database table and you get a secured AJAX endpoint plus a searchable, sortable, and paginated table, without writing SQL boilerplate by hand. It complements (and does not replace) the core Web > List module (DatabaseRecordList) used for TCA records on a page.

This documentation is for two groups of people:

  • Extension developers who want to add DataTable grids to their backend modules.
  • Integrators who just need to install and switch on the package.

Introduction 

What the extension does and how it fits into TYPO3.

Installation 

Install and activate the extension.

Developer guide 

Register grids, wire DI, and use the JavaScript module.

Usage 

Demo backend module and AJAX endpoint.


Table of contents

Introduction 

What it does 

T3 DataTable gives your TYPO3 backend modules a data grid that does the search, sort, and paging work on the server. It speaks the same JSON protocol as the DataTables library, so the browser only ever loads one page of rows at a time.

Here is what you get out of the box:

  • Global search across every searchable column you declared (the shipped UI exposes one search box; per-column search values are supported by the AJAX protocol if your own JavaScript sends them).
  • Sort by any column you allow. Each column name is checked against an allowlist before it reaches the query.
  • Page through large tables on the server, so the browser never has to hold the whole result set.
  • Every request runs behind a backend-user check, binds all values as query parameters, and only touches the columns you declared.

Any extension can register a GridInterface implementation, tag it in Symfony DI, and reuse the shared AJAX route in Configuration/Backend/AjaxRoutes.php (/t3datatable/data). You do not need to write your own endpoint.

Architecture 

┌─────────────────────────────────────┐
│  Consumer extension (GridInterface)  │
└───────────────────┬───────────────────┘
                     │ tagged: t3datatable.grid
                     ▼
┌─────────────────────────────────────┐
│            GridRegistry              │
└───────────────────┬───────────────────┘
                     │ resolves grid config
                     ▼
┌─────────────────────────────────────┐
│     DataTableController (AJAX)       │
└───────────────────┬───────────────────┘
                     │ builds request
                     ▼
┌─────────────────────────────────────┐
│          DataTableRequest            │
└───────────────────┬───────────────────┘
                     │ passed to
                     ▼
┌─────────────────────────────────────┐
│             QueryEngine              │
└───────────────────┬───────────────────┘
                     │ executes & maps results
                     ▼
┌─────────────────────────────────────┐
│             JsonResponse             │
└───────────────────────────────────────┘
Copied!

The bundled ES module @hrr/t3-datatable/datatable-backend.js talks to that endpoint using TYPO3's own @typo3/core/ajax/ajax-request, so it works in the backend only.

Requirements 

  • TYPO3 13 LTS or 14
  • PHP 8.2 or newer
  • A backend module context. It is not meant for public frontend pages.

What it is not 

A few things this extension does not try to be, so you know when to reach for something else:

  • It is not a full port of Laravel DataTables. It is built for TYPO3 with Doctrine DBAL, PSR-7, and backend routes.
  • It is not a replacement for the core Web > List module (TYPO3\CMS\Backend\RecordList\DatabaseRecordList). That module already lists TCA records on a page with search, pagination, and editing. T3 DataTable targets custom backend modules that need a DataTables-style server-side grid API over a table, without building your own AJAX endpoint. It also does not replace workspace-aware record lists.
  • It is not a frontend AJAX table library. Frontend use is out of scope for now.

Installation 

Requirements 

  • TYPO3 13 LTS or 14
  • PHP 8.2 or newer

Install with Composer 

Require the package
composer require hrr/t3-datatable
Copied!

Packagist: https://packagist.org/packages/hrr/t3-datatable

For local path repositories (monorepo):

composer require hrr/t3-datatable:@dev
Copied!

Install from TER 

You can also install the extension from the TYPO3 Extension Repository using the Extension Manager, or download it and place it in your project.

Activate extension 

  1. Open the TYPO3 backend.
  2. Go to Admin Tools > Extensions.
  3. Activate EXT:t3_datatable.

Demo 

After activation, open:

Web > T3 DataTable

to see a working server-side grid using the pages table.

Verify installation 

After activation:

  • AJAX route t3datatable_data is registered.
  • JavaScript import map entry @hrr/t3-datatable/datatable-backend.js is available.
  • Demo module appears under Web > T3 DataTable (Content > T3 DataTable on TYPO3 14).

Run tests (optional) 

From the package directory:

composer install
composer ci
Copied!

Configuration 

Overview 

There is no global extension configuration here. T3 DataTable ships no ext_conf_template.txt, and there are no options to set in the install tool. You configure everything per grid inside GridInterface::build().

Service registration 

The extension ships these configuration files:

  • Configuration/Services.yaml sets up autowiring and the t3datatable.grid tag.
  • Configuration/Backend/AjaxRoutes.php registers the shared data endpoint.
  • Configuration/JavaScriptModules.php adds the ES module to the import map.
  • Configuration/Backend/Modules.php registers the demo backend module only.

Consumer extensions must add the _instanceof rule in their own Configuration/Services.yaml:

_instanceof:
  HRR\T3Datatable\Contract\GridInterface:
    tags: ['t3datatable.grid']
Copied!

Grid definition options 

Use GridDefinition inside GridInterface::build():

$definition
    ->addColumn('uid', 'UID', searchable: false, orderable: true)
    ->addColumn('title', 'Title', searchable: true, orderable: true)
    ->setDefaultOrder('crdate', 'DESC')
    ->setPageLength(25)
    ->withDeletedRestriction()
    ->withHiddenRestriction();
Copied!
Method Purpose
addColumn() Declare a column (name, label, searchable, orderable)
setDefaultOrder() Fallback sort when the client sends no order
setPageLength() Default page size hint (client may override via length)
withDeletedRestriction() Adds deleted = 0 (table must have deleted column)
withHiddenRestriction() Adds hidden = 0 (table must have hidden column)

Security defaults 

  • Column names from the client are validated against the grid allowlist.
  • All query values use Doctrine named parameters.
  • The AJAX controller requires an authenticated backend user.

Developer guide 

Quick start 

  1. composer require hrr/t3-datatable
  2. Tag GridInterface implementations in your Services.yaml
  3. Implement a grid class (table + columns)
  4. Load the JS module in your backend module template
  5. Call initDataTable() with your grid identifier

Core contract 

namespace HRR\T3Datatable\Contract;

interface GridInterface
{
    public function getIdentifier(): string;
    public function getTableName(): string;
    public function build(GridDefinition $definition): void;
}
Copied!

AJAX endpoint 

Every grid shares one route, so you never have to register your own AJAX endpoint:

Route key t3datatable_data
Path /t3datatable/data
Query param grid={your_grid_identifier}
Controller HRRT3DatatableControllerDataTableController::dataAction

In JavaScript, use TYPO3.settings.ajaxUrls.t3datatable_data.

Response shape 

{
  "draw": 1,
  "recordsTotal": 150,
  "recordsFiltered": 42,
  "data": [
    {"uid": 1, "title": "Example"}
  ]
}
Copied!

PHP classes 

Class Role
GridRegistry Resolves grids by identifier via DI tag
DataTableRequest Parses DataTables AJAX parameters
QueryEngine Builds and executes Doctrine queries
ColumnAllowlist Validates searchable/orderable columns
DataTableResponse Builds JSON payload

Registering a grid 

Step 1: Implement GridInterface 

Classes/DataTable/MyRecordsGrid.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExt\DataTable;

use HRR\T3Datatable\Contract\GridInterface;
use HRR\T3Datatable\DataTable\GridDefinition;

final class MyRecordsGrid implements GridInterface
{
    public function getIdentifier(): string
    {
        return 'myext_records';
    }

    public function getTableName(): string
    {
        return 'tx_myext_records';
    }

    public function build(GridDefinition $definition): void
    {
        $definition
            ->addColumn('uid', 'UID', searchable: false, orderable: true)
            ->addColumn('title', 'Title', searchable: true, orderable: true)
            ->addColumn('crdate', 'Created', searchable: false, orderable: true)
            ->setDefaultOrder('crdate', 'DESC')
            ->setPageLength(25)
            ->withDeletedRestriction()
            ->withHiddenRestriction();
    }
}
Copied!

Step 2: Tag in Services.yaml 

Configuration/Services.yaml
services:
  _instanceof:
    HRR\T3Datatable\Contract\GridInterface:
      tags: ['t3datatable.grid']

  MyVendor\MyExt\:
    resource: '../Classes/*'
Copied!

The grid class is auto-registered when it implements GridInterface.

Step 3: Use the grid identifier in JavaScript 

Pass the same identifier to initDataTable():

initDataTable('#my-table', {
  gridIdentifier: 'myext_records',
  columns: [
    { data: 'uid', title: 'UID' },
    { data: 'title', title: 'Title' },
    { data: 'crdate', title: 'Created' },
  ],
});
Copied!

Column rules 

  • Only columns declared in build() can be searched or sorted.
  • Request column names must match the allowlist regex ^[a-zA-Z0-9_.]+$.
  • Use withDeletedRestriction() / withHiddenRestriction() only when the target table has those fields.

Demo reference 

See HRRT3DatatableDemoPagesGrid in this extension for a working pages example.

Backend JavaScript module 

Scope 

The ES module @hrr/t3-datatable/datatable-backend.js runs only in the TYPO3 backend. It depends on:

  • TYPO3 import maps (Configuration/JavaScriptModules.php)
  • @typo3/core/ajax/ajax-request (CSRF + backend session)
  • TYPO3.settings.ajaxUrls.t3datatable_data

Public frontend pages do not have this environment.

Advanced: programmatic initialization 

If you load your own CSP-compliant module (e.g. a dedicated module file shipped in your extension, not an inline script), you can call initDataTable() directly:

import { initDataTable } from '@hrr/t3-datatable/datatable-backend.js';

initDataTable('#my-records-table', {
  gridIdentifier: 'myext_records',
  columns: [
    { data: 'uid', title: 'UID', orderable: true, searchable: false },
    { data: 'title', title: 'Title', orderable: true, searchable: true },
  ],
  pageLength: 25,
});
Copied!

Options 

Option Description
gridIdentifier Required. Matches GridInterface::getIdentifier()
columns Required. Array of column objects (see below)
pageLength Rows per page (default 25)
searchPlaceholder Placeholder for the search input
onRowsRendered Optional. (tbody, rows) => void called after each successful draw (useful for selection UI, tooltips, or event wiring on AJAX rows)

Column object 

Each entry in columns may include:

Property Description
data Required. Field name from the AJAX row (or a client-only key when virtual is true)
title Header label
searchable Included in global search when true (default true). Ignored for virtual columns
orderable Clickable sort header when true (default true)
virtual Client-only column. Omitted from the AJAX payload and from SQL. Use for checkboxes, action buttons, or other UI that is not a DB field. Order indices are remapped automatically
html When true, a string returned from render is assigned with innerHTML. Default is text (textContent)
render Optional. (value, row, td) => string | Node | null. Formats the cell. Return a Node to append DOM, a string for text/HTML, or null if you mutate td yourself (e.g. async icon loading)

Example with a virtual actions column:

initDataTable('#my-records-table', {
  gridIdentifier: 'myext_records',
  columns: [
    { data: 'uid', title: 'UID', searchable: false },
    { data: 'title', title: 'Title' },
    {
      data: '_actions',
      title: '',
      virtual: true,
      orderable: false,
      searchable: false,
      render: (_value, row) => {
        const button = document.createElement('button');
        button.type = 'button';
        button.className = 'btn btn-default btn-sm';
        button.dataset.uid = String(row.uid);
        button.textContent = 'Delete';
        return button;
      },
    },
  ],
  onRowsRendered: (tbody) => {
    // Wire listeners on newly drawn rows if needed
  },
});
Copied!

Return value 

initDataTable() returns { reload: () => Promise } for manual refresh.

Protocol 

The module POSTs DataTables-compatible parameters (draw, start, length, search, columns, order) to the shared AJAX endpoint. The server responds with recordsTotal, recordsFiltered, and data.

Usage 

Demo backend module 

After installation, open:

Web > T3 DataTable

to see a working server-side grid using the pages table.

This module lists pages via the demo_pages grid identifier. It demonstrates:

  • Module registration in Configuration/Backend/Modules.php
  • Grid class HRRT3DatatableDemoPagesGrid
  • Fluid template Resources/Private/Templates/Backend/Demo/Index.html
  • CSP-safe JavaScript auto-initialisation via data-* attributes

AJAX data endpoint 

Any registered grid can be queried directly (for debugging):

POST /typo3/ajax/t3datatable/data?grid=demo_pages
Copied!

Parameters follow the DataTables server-side protocol.

Typical workflow for integrators 

  1. Install and activate the extension.
  2. Open the demo module to confirm AJAX + rendering works.
  3. Implement your own GridInterface in your extension.
  4. Add the grid to your backend module template.

Troubleshooting 

Empty table / zero records 

  • Confirm the grid identifier in JavaScript matches getIdentifier().
  • Check the table name in getTableName() exists and has data.
  • If using withDeletedRestriction() or withHiddenRestriction(), ensure those columns exist on the table.

Invalid column name error 

The client sent a column not declared on the grid, or a name failing the allowlist. Only use alphanumeric column names with dots/underscores.

Grid not registered / 400 response 

  • Implement GridInterface in your extension.
  • Add the t3datatable.grid tag via _instanceof in your Services.yaml (the tag in T3 DataTable alone does not apply to foreign packages).
  • Flush caches after adding a new grid class.

Unauthorized / 401 

The AJAX endpoint requires a logged-in backend user. Do not call it from the public frontend without a separate route and security model.

JavaScript: ajaxUrls.t3datatable_data undefined 

  • Activate EXT:t3_datatable.
  • Flush all caches.
  • Ensure you are in a backend module context (not frontend).

Executing inline script violates ... Content Security Policy 

The backend CSP blocks inline <script> tags. This happens if you place an inline <script type="module">import { initDataTable } ...</script> directly in a Fluid template.

Fix: remove the inline script. Load the module from the controller with $pageRenderer->loadJavaScriptModule('@hrr/t3-datatable/datatable-backend.js') and configure the table via data-* attributes (data-t3-datatable, data-columns, data-page-length). The module auto-initializes on DocumentService.ready(). See Backend JavaScript module.

Adding a Configuration/ContentSecurityPolicies.php mutation only helps for external resources (e.g. a CDN host). It does not unblock inline scripts without an 'unsafe-inline' relaxation, which is discouraged.

Search returns no results on SQLite / case sensitivity 

Global search uses LOWER(column) LIKE for case-insensitive matching. Verify searchable columns are declared with searchable: true.

Render documentation locally 

You can preview this documentation on your own machine. You need Docker installed, the same as with other TYPO3 extensions. From the package root, run:

composer doc-watch
Copied!

This starts the official TYPO3 documentation renderer and watches for changes, so the pages rebuild as you edit them.

Sitemap