jQuery DataTables sind ein wirklich mächtiges Werkzeug. Besonders die Möglichkeit des Server-side Processing ist ein äußerst nettes Feature im Zusammenspiel mit TYPO3 bzw. einer Extbase Extension.

Im nachfolgenden Beispiel werden die Daten für den DataTable von einem JsonProductController über eine JsonProductView (JsonView anstelle TemplateView) bereit gestellt.

In der Konfiguration der JsonView wird unter anderem definiert, welche Properties sowie Child-Objekte des Models in der Rückgabe enthalten sein sollen (die Syntax ist gewöhnungsbedürftig und hat es besonders bei verschachtelten Objekten in sich).

Hier im Table genutzte Features (alles läuft Server-side ab und wird entsprechend vom ProductRepository bereit gestellt!): Globale Suche, spaltenbasierte Suche, spaltenbasierte Sortierung, Pagebrowser, Anzahl Datensätze.

JsonProductView

class JsonProductView extends \TYPO3\CMS\Extbase\Mvc\View\JsonView {

    /**
     * @var array
     */
    protected $configuration = [
        'response' => [
            'data' => [
                '_descendAll' => [                
                    '_descend' => [
                        'maturity' => [],
                        'currency' => [
                            '_only' => ['title'],
                        ],
                        'updated' => [],
                        'customer' => [
                            '_only' => ['title', 'title_short', 'titleShort'],
                        ],
                    ],
                ],                 
            ],
        ],        
    ];    
    
    /**
     * Transform ObjectStorages to Arrays
     *
     * @param mixed $value
     * @param array $configuration
     * @return array
     */
    protected function transformValue($value, array $configuration) {
        if ($value instanceof \TYPO3\CMS\Extbase\Persistence\ObjectStorage) {
            $value = $value->toArray();
        }
        return parent::transformValue($value, $configuration);
    }

}

JsonProductController

class JsonProductController extends BaseJsonController {

    /**
     * productRepository
     *
     * @var \Vendor\Extension\Domain\Repository\ProductRepository
     * @inject
     */
    protected $productRepository = null;

    /**
     * initialize action list
     * 
     * @return void
     */
    public function initializeListAction(){
        $this->defaultViewObjectName = \Vendor\Extension\View\JsonProductView::class;
    }       
    
    /**
     * action list
     *
     * @return void
     */
    public function listAction() {

        $draw = (int) ($this->request->hasArgument('draw') && $this->request->getArgument('draw') > 0 ? $this->request->getArgument('draw') : 0);
        $start = (int) ($this->request->hasArgument('start') && $this->request->getArgument('start') > 0 ? $this->request->getArgument('start') : 0);
        $length = (int) ($this->request->hasArgument('length') && $this->request->getArgument('length') > 0 ? $this->request->getArgument('length') : 10);

        $order = ($this->request->hasArgument('order') && count($this->request->getArgument('order')) > 0 ? $this->request->getArgument('order') : []);
        $columns = ($this->request->hasArgument('columns') && count($this->request->getArgument('columns')) > 0 ? $this->request->getArgument('columns') : []);

        $search = ($this->request->hasArgument('search') && count($this->request->getArgument('search')) > 0 ? $this->request->getArgument('search') : []);

        // Ordering
        foreach ($order as $value) {
            if ($columns[$value['column']]['orderable'] == true) {
                if ($value['dir'] == 'asc') {
                    $orderings[$columns[$value['column']]['data']] = \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING;
                } else {
                    $orderings[$columns[$value['column']]['data']] = \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_DESCENDING;
                }
            }
        }

        // Search - global
        if ($search && $search['value'] != '') {
            foreach ($columns as $column) {
                if ($column['searchable'] == true) {
                    $searchingsGlobal[$column['data']] = $search['value'];
                }
            }
        }

        // Search - columned
        foreach ($columns as $column) {
            if ($column['searchable'] == true && $column['search']['value'] != '') {
                $searchingsColumned[$column['data']] = $column['search']['value'];
            }
        }

        $allProducts = $this->productRepository->findAllByFilter(null, null, $orderings, $searchingsGlobal, $searchingsColumned);
        $filteredProducts = $this->productRepository->findAllByFilter($start, $length, $orderings, $searchingsGlobal, $searchingsColumned);

        $response = [
            'draw' => $draw,
            'start' => $start,
            'length' => $length,
            'recordsTotal' => $allProducts->count(),
            'recordsFiltered' => $allProducts->count(),
            'order' => $order,
            'search' => $search,
            'columns' => $columns,
            'data' => $filteredProducts
        ];
        $this->view->setVariablesToRender(['response']);
        $this->view->assign('response', $response);
    }

}

ProductRepository

class ProductRepository extends \TYPO3\CMS\Extbase\Persistence\Repository {

    protected $defaultOrderings = array(
        'maturity' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING,
        'title' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING
    );
    
    /**
     * findAllByFilter
     * 
     * @param integer $start
     * @param integer $length
     * @param mixed $order
     * @param mixed $searchGlobal
     * @param mixed $searchColumned
     * 
     * @return QueryResultInterface|array
     */
    public function findAllByFilter($start=0, $length=0, $order=[], $searchGlobal=[], $searchColumned=[]) {

        $query=$this->createQuery();
        $constraints = [];

        // Start
        if($start > 0) {
            $query->setOffset($start);
        }

        // Length
        if($length > 0) {
            $query->setLimit($length);
        }

        // Orderings
        if(count($order) > 0) {
             $query->setOrderings($order);
        }

        // Search - global
        if(count($searchGlobal) > 0) {
            foreach($searchGlobal as $key => $value) {
                $constraintsSearchGlobal[] = $query->like($key, '%' . $value . '%');
            }
            $constraints[] = $query->logicalOr($constraintsSearchGlobal);
        }
        
        // Search - columned
        if(count($searchColumned) > 0) {
            foreach($searchColumned as $key => $value) {
                $constraintsSearchColumned[] = $query->like($key, '%' . $value . '%');
            }
            $constraints[] = $query->logicalAnd($constraintsSearchColumned);
        }

        // Combine constraints
        if(count($constraints) > 0) {
            $query->matching($query->logicalAnd($constraints));
        }
        
        return $query->execute();
    }

}

TypoScript (Bootstrap Json Plugin)

page_json = PAGE
page_json {
    typeNum = 9876
    10 = USER
    10 {
        userFunc = TYPO3\CMS\Extbase\Core\Bootstrap->run
        extensionName = Extension
        pluginName = JsonData
        vendorName = Vendor
    }
    config {
        disableAllHeaderCode = 1
        additionalHeaders = Content-type:application/json
        xhtml_cleaning = 0
        admPanel = 0
        debug = 0
        no_cache = 1
        contentObjectExceptionHandler = 0
    }
}

Template (DataTable)

<table 
    id="datatable-products-scroller" 
    rel="{f:uri.action(pluginName: 'JsonData', controller: 'JsonProduct', action: 'list', pageType: 9876, noCacheHash: 1, absolute: 1)}"
>
    <thead>
        <tr>
            <th>Currency</th>
            <th>Maturity</th>
            <th>Ticker</th>
        </tr>
    </thead>
    <tbody>

    </tbody>
    <tfoot>
        <tr>
            <th class="search_column">
                <input type="text" class="form-control input-sm input-search" placeholder="Search Currency" />
            </th>
            <th class="search_column">
                <input type="text" class="form-control input-sm input-search" placeholder="Search Maturity" />
            </th>
            <th class="search_column">
                <input type="text" class="form-control input-sm input-search" placeholder="Search Name" />
            </th>
        </tr>
    </tfoot>
</table>

JavaScript (DataTable Initialisierung)

// DataTable
tableProducts = $('#datatable-products-scroller').DataTable({
    deferRender: true,
    scrollY: 280,
    scrollCollapse: false,
    scroller: false,
    paging: true,
    order: [[1, 'asc'], [2, 'asc']],
    lengthChange: true,
    info: false,
    searching: true,
    iDisplayLength: 100,
    select: true,
    rowId: 'uid',
    processing: true,
    serverSide: true,
    ajax: {
        url: $('#datatable-products-scroller').attr('rel'),
        data: function (data) {
            return {tx_extension_jsondata: data};
        },
        error: function (hxr, error, thrown) {
            console.log('Uups...');
        }
    },
    columns: [
        {data: 'currency.title'},
        {
            data: 'maturity',
            render: function (data) {
                var date = new Date(data);
                var month = date.getMonth() + 1;
                return (date.getDate() < 10 ? '0' : '') + date.getDate() + '.' + (date.getMonth() < 10 ? '0' : '') + date.getMonth() + '.' + date.getFullYear();
            }
        },
        {data: 'title'}
    ]
});

// Search - columned
tableProducts.columns().every(function () {
    var that = this;
    $('input', this.footer()).on('keyup change', function () {
        if (that.search() !== this.value) {
            that
                .search(this.value)
                .draw();
        }
    });
});

// Clicked row - attach anything
$('#datatable-products-scroller tbody').on('click', 'tr', function () {
    var data = tableProducts.row(this).data();
    productId = data['uid'];
    doSomething(productId);
});