Skip to content

DataTables

For dynamic table views, this project uses the jQuery plugin DataTables on the client side and the corresponding PHP package yajra/laravel-datatables on the server side. In addition to the already supported Datatables extensions buttons, select are also supported.

Official Documentation

In addition to the following documentation, the official documentation for the packages used provides plenty of material.

These also give plenty of examples.

You can also find an example in the overview of the Blade components under http://localhost/widget.

Basics

The jQuery plugin DataTables is preconfigured in such a way that it attaches to all tables with the class datatable. There are Blade components (<x-base::datatable/> and <x-coreui::datatable/>), that create matching tables. The server requests from DataTables are handled by implementations of \Support\DataTables\DataTable, extending \Yajra\DataTables\Contracts\DataTable with additional functionality. The name of the corresponding route usually ends in .tabulate and their URL in /tabulate. The matching client-side JavaScript part is also available and configured in DataTables.

Client- and server side processing

DataTables supports both client- and server side processing of the data. By default, server-side processing is active. The data source must be specified with the option ajax.url.

Warning

Do not specify the AJAX URL using the plain ajax options, as this breaks merging with the default AJAX configuration. Always use the ajax.url option.

The columns can also be configured via the table header. There you can, for example set their sortability and searchability, as well as the evaluation of the data source.

In order to switch to client-side processing set alax to null and serverSide to false.

For example, server-side processing can be configured as follows.

<?php
class Controller extends \Illuminate\Routing\Controller
{
    // Used for demonstration purposes only, to render template strings.
    use \Illuminate\Foundation\Testing\Concerns\InteractsWithViews;
    public function index()
    {
        return (string) $this->blade(
            <<< 'blade'
                <x-base::datatable :dataset="['ajax' => ['url' => '/tabulate']]">
                    <thead>
                        <tr>
                            <x-base::th :dataset="['data' => 'id']">ID</x-base::th>
                            <x-base::th :dataset="['data' => 'name']">Name</x-base::th>
                        </tr>
                    </thead>
                </x-base::datatable>
            blade,
        );
    }
    public function tabulate()
    {
        return \Support\DataTables\Factory::make(collect([
            ['id' => 1, 'name' => 'one'],
            ['id' => 2, 'name' => 'two'],
            ['id' => 3, 'name' => 'three'],
        ]));
    }
}
return (new Controller())->index();

The above PHP source code results in the following HTML output.

<table class="datatable" data-ajax="{&quot;url&quot;:&quot;\/tabulate&quot;}">
    <thead>
        <tr>
            <th data-data="id">ID</th>
            <th data-data="name">Name</th>
        </tr>
    </thead>
</table>

Options

DataTables provide a whole bunch of options. All options can be set using data-* attributes of the HTML elements. These can be easily set via the dataset property of the Blade component. The values are automatically encoded. Beside the table their columns can also be configured via the header. Some common options will be explained in the next sections.

Note

Due to the limitations of the parser of the blade components, it is not possible to use the quotation marks that also enclose the value within a value of an attribute with a PHP value. for example, it poses a problem to use double quotation marks within :dataset="...". This Problem can be solved by using the PHP function chr() with the ASCII value of the quotation mark (34): :dataset="chr(34)"

Default sort order

The followin example will sort the table by the Name column in ascending order by default.

<?php
class Controller extends \Illuminate\Routing\Controller
{
    // Used for demonstration purposes only, to render template strings.
    use \Illuminate\Foundation\Testing\Concerns\InteractsWithViews;
    public function index()
    {
        return (string) $this->blade(
            <<< 'blade'
                <x-base::datatable :dataset="['order' => [1, 'asc']]">
                    <thead>
                        <tr>
                            <x-base::th :dataset="['data' => 'id']">ID</x-base::th>
                            <x-base::th :dataset="['data' => 'name']">Name</x-base::th>
                        </tr>
                    </thead>
                </x-base::datatable>
            blade,
        );
    }
}
return (new Controller())->index();

The above PHP source code results in the following HTML output.

<table class="datatable" data-order="[1,&quot;asc&quot;]">
    <thead>
        <tr>
            <th data-data="id">ID</th>
            <th data-data="name">Name</th>
        </tr>
    </thead>
</table>

Disable sorting and sarching for certain columns

The following example disables sorting and searching for the tenants and roles columns.

<x-coreui::datatable>
    <thead>
        <tr>
            <x-base::datatable-checkbox/>
            <x-base::th :dataset="['data' => 'email']">@lang('bc-core-domain::user.attributes.email')</x-base::th>
            <x-base::th :dataset="['data' => 'tenants', 'orderable' => 'false', 'searchable' => 'false']">@choice('bc-core-domain::tenant.name', 2)</x-base::th>
            <x-base::th :dataset="['data' => 'roles', 'orderable' => 'false', 'searchable' => 'false']">@choice('bc-core-domain::role.name', 2)</x-base::th>
            <x-base::datatable-action/>
        </tr>
    </thead>
</x-coreui::datatable>

Row actions

Various actions that relate to a row can be added by creating an additional column for this in the table and filling it with its own content.

For example, a row with row actions can be added as follows.

<?php
class Controller extends \Illuminate\Routing\Controller
{
    // Used for demonstration purposes only, to render template strings.
    use \Illuminate\Foundation\Testing\Concerns\InteractsWithViews;
    public function index()
    {
        return (string) $this->blade(
            <<< 'blade'
                <x-base::datatable :dataset="['ajax' => ['url' => '/tabulate']]">
                    <thead>
                        <tr>
                            <x-base::th :dataset="['data' => 'id']">ID</x-base::th>
                            <x-base::th :dataset="['data' => 'name']">Name</x-base::th>
                            <x-base::datatable-action/>
                        </tr>
                    </thead>
                </x-base::datatable>
            blade,
        );
    }
    public function tabulate()
    {
        return \Support\DataTables\Factory::make(collect([
            ['id' => 1, 'name' => 'one'],
            ['id' => 2, 'name' => 'two'],
            ['id' => 3, 'name' => 'three'],
        ]))->addColumn(
            'action',
             fn ($row) => sprintf('<a href="/%s">show</a>', e($row['id'])),
        );
    }
}
return (new Controller())->index();

Warning

Unless otherwise configured in config/datatables.php HTML escaping is always used for the content of all columns in order to avoid various security problems. The action column is preconfigured in a way that its content is always passed raw in order to enable any links to be inserted. Therefore, you always have to make sure that dynamic HTML parts of the action-column are always cleaned up manually using the Laravel function e().

The above PHP source code results in the following HTML output.

<table class="datatable" data-ajax="{&quot;url&quot;:&quot;\/tabulate&quot;}">
    <thead>
        <tr>
            <th data-data="id">ID</th>
            <th data-data="name">Name</th>
            <th
                data-data="action"
                data-orderable="false"
                data-searchable="false"
                data-width="0px"
            ></th>
        </tr>
    </thead>
</table>

Table actions

The buttons of the table can be configured via the DataTables extension buttons. A custom Button named openUrl has been created and can be extended to use its functions. In addition to the usual options openUrl has the following options:

  • confirm: The text of an optional confirmation dialog (default: undefined).
  • method: The HTTP method used to open the URL (default: "GET").
  • target: The target where the url should be opened (default: "_self").
  • url: The URL, which should be opened.

Warning

The text of the buttons is inserted raw and without HTML cleanup into the table. Dynamic data must be cleaned up using the Laravel function e().

For example, a table action to open URLs can be added as follows.

<?php
class Controller extends \Illuminate\Routing\Controller
{
    // Used for demonstration purposes only, to render template strings.
    use \Illuminate\Foundation\Testing\Concerns\InteractsWithViews;
    public function index()
    {
        return (string) $this->blade(
            <<< 'blade'
                <x-base::datatable :dataset="[
                    'buttons' => [
                        [
                            'extends' => 'openUrl',
                            'text' => 'Example',
                            'url' => 'http://localhost/',
                        ],
                    ],
                ]"/>
            blade,
        );
    }
}
return (new Controller())->index();

The above PHP source code results in the following HTML output.

<table
    class="datatable"
    data-buttons="[{&quot;extends&quot;:&quot;openUrl&quot;,&quot;text&quot;:&quot;Example&quot;,&quot;url&quot;:&quot;http:\/\/localhost\/&quot;}]"
></table>

Process records server-side

You can not only create buttons that simply open a URL, but also buttons that trigger server-side actions that also have access to the table data. A separate button has also been created for this. Additional to the usual options the button openAction has the following options:

  • confirm: The text of an optional confirmation dialog (default: undefined).
  • method: The HTTP method used to open the action (default: "GET").
  • target: The target where, where the action should be opened (default: "_self").

Warning

The text of the buttons is inserted raw and without HTML cleanup into the table. Dynamic data must be cleaned up using the Laravel function e().

The name of the button is transferred to the server in the action request parameter. This is used to decide, among other things, which of the program logic configured with \Support\DataTables\DataTable::addAction() is to be executed.

Note

In contrast to \Yajra\DataTables\Services\DataTable the action methods of \Support\DataTables\DataTable receive the table as an argument and have direct access to its data.

For example, a table action that processes data from the table can be implemented as follows.

<?php
class Controller extends \Illuminate\Routing\Controller
{
    // Used for demonstration purposes only, to render template strings.
    use \Illuminate\Foundation\Testing\Concerns\InteractsWithViews;
    public function index()
    {
        return (string) $this->blade(
            <<< 'blade'
                <x-base::datatable :dataset="[
                    'ajax' => ['url' => '/tabulate'],
                    'buttons' => [
                        [
                            'extends' => 'openAction',
                            'name' => 'example',
                            'text' => 'Example',
                        ],
                    ],
                ]">
                    <thead>
                        <tr>
                            <x-base::th :dataset="['data' => 'id']">ID</x-base::th>
                            <x-base::th :dataset="['data' => 'name']">Name</x-base::th>
                        </tr>
                    </thead>
                </x-blade::datatable>
            blade,
        );
    }
    public function tabulate()
    {
        return \Support\DataTables\Factory::make(collect([
            ['id' => 1, 'name' => 'one'],
            ['id' => 2, 'name' => 'two'],
            ['id' => 3, 'name' => 'three'],
        ]))->addActionFromObject('example', $this);
    }
    public function example($table)
    {
        dd($table);
    }
}
return (new Controller())->index();

The above PHP source code results in the following HTML output.

<table
    class="datatable"
    data-ajax="{&quot;url&quot;:&quot;\/tabulate&quot;}"
    data-buttons="[{&quot;extends&quot;:&quot;openAction&quot;,&quot;name&quot;:&quot;example&quot;,&quot;text&quot;:&quot;Example&quot;}]"
>
    <thead>
        <tr>
            <th data-data="id">ID</th>
            <th data-data="name">Name</th>
        </tr>
    </thead>
</table>

Process selected data records server side

You can use the DataTables extension select to make a selection of the data in the table and then process it further

The IDs of the selected rows are automatically transferred to the server by buttons that extend openAction. You only have to configure the ID of the rows and the methods to be called there. The table is then pre-filtered so that it only contains the selected data records. The corresponding method can then access the appropriate data in the table.

A checkbox column can be easily inserted using the Blade component <x-base::datatable-checkbox/>.

For example, a table action that only relates to selected data can be implemented as follows.

<?php
class Controller extends \Illuminate\Routing\Controller
{
    // Used for demonstration purposes only, to render template strings.
    use \Illuminate\Foundation\Testing\Concerns\InteractsWithViews;
    public function index()
    {
        return (string) $this->blade(
            <<< 'blade'
                <x-base::datatable :dataset="[
                    'ajax' => ['url' => '/tabulate'],
                    'buttons' => [
                        [
                            'extends' => 'openAction',
                            'name' => 'example',
                            'text' => 'Example',
                        ],
                    ],
                ]">
                    <thead>
                        <tr>
                            <x-base::datatable-checkbox/>
                            <x-base::th :dataset="['data' => 'id']">ID</x-base::th>
                            <x-base::th :dataset="['data' => 'name']">Name</x-base::th>
                        </tr>
                    </thead>
                </x-blade::datatable>
            blade,
        );
    }
    public function tabulate()
    {
        return \Support\DataTables\Factory::make(collect([
            ['id' => 1, 'name' => 'one'],
            ['id' => 2, 'name' => 'two'],
            ['id' => 3, 'name' => 'three'],
        ]))->addActionFromObject('example', $this)->setRowId('id');
    }
    public function example($table)
    {
        dd($table);
    }
}
return (new Controller())->index();

The above PHP source code results in the following HTML output.

<table
    class="datatable"
    data-ajax="{&quot;url&quot;:&quot;\/tabulate&quot;}"
    data-buttons="[{&quot;extends&quot;:&quot;openAction&quot;,&quot;name&quot;:&quot;example&quot;,&quot;text&quot;:&quot;Example&quot;}]"
>
    <thead>
        <tr>
            <th
                data-default-content="<input type=&quot;checkbox&quot;/>"
                data-orderable="false"
                data-searchable="false"
                data-width="0px"
            ><input class="current-page-selector" type="checkbox"/></th>
            <th data-data="id">ID</th>
            <th data-data="name">Name</th>
        </tr>
    </thead>
</table>

Batch actions

Actions that affect multiple records should be grouped under same drop-down button in the following way:

<x-coreui::datatable
        :dataset="[
            'ajax' => ['url' => route('roles.permissions.tabulate', ['role' => $role->id])],
            'buttons' => [
                [
                    'extend' => 'collection',
                    'text' => '<i class=' . chr(34) . 'cil-clear-all' . chr(34) . '></i> ' . trans('app.batch_actions'),
                    'autoClose' => true,
                    'className' => 'btn-info',
                    'buttons' => [
                        [
                            'extend' => 'openAction',
                            'className' => 'btn-danger',
                            'method' => 'POST',
                            'name' => 'revoke',
                            'confirm' => trans_choice('app.deleteConfirmation', 0, ['name'=> trans_choice('bc-core-domain::permission.name', 1)]),
                            'text' => '<i class=' . chr(34) . 'cil-trash' . chr(34) . '></i> '. trans('app.remove'),
                        ],
                        [
                            ... each action goes into its own sub array ...
                        ]
                    ]
                ],
            ],
            'order' => [1, 'asc'],
            'pageLength' => 50
        ]"
    >
    ...
</x-coreui::datatable>

Filters

For complex filtering mechanisms it is possible to add custom filters with a custom user interface and filtering logic.

Forms of filters are placed in the HTML document next to the DataTable. When using the CoreUI variant of the DataTables Blade component the filters are placed in modals. Filters also provide a method for generating a DataTables button to toggle this modal. A change of the filters will automatically reload the data. The form’s data is included with the DataTable’s AJAX request and is then passed to the custom filtering logic on the server.

All filters must be descendants of the abstract class \Support\DataTables\Filter. The form template is referenced by \Support\DataTables\EloquentFilter::$template and the filtering logic is placed in \Support\DataTables\EloquentFilter::filter(). The filter also needs a unique name by which its data is identified.

Warning

The names of all form controls belonging to filters MUST begin with filter[$nameOfTheFilter].

Note

While the class \Support\DataTables\Filter can be extended directly it is better to extend one of the filter classes for the corresponding DataTable to get more precise autocompletion. For example a filter for \Support\DataTables\EloquentDataTable should extend \Support\DataTables\EloquentFilter, which provides useful type hinting for \Support\DataTables\EloquentFilter::filter().

Note

\Support\DataTables\EloquentFilter::filter() is only called when its parameters are present. When the parameters are absent the filter is not applied at all.

Form template

The form template for providing input data for a filter can be defined using the $template property of the JohnIt\Bc\Ui\Presentation\DataTables\Filter class.

Example:

class TenantFilter extends EloquentFilter
{
    protected string $template = 'bc-core-presentation::filter.tenantFilter';
}

Authorizing filters

Sometimes you wan’t to restrict displaying filters to certain users. Let’s consider a tenant based application in example. If a user is not allowed to see other tenants then also the tenants filter must be hidden. To control whether a user can see a filter or not the JohnIt\Bc\Ui\Presentation\DataTables\Filter class provides an authorized method.

Example:

class TenantFilter extends EloquentFilter
{
    /**
     * This method is used to control wether a user can see/use this filter
     * @return bool
     */
    public function authorized(): bool
    {
        // using this filter is allowed if sharing is enabled or if the user can list all tenants.
        return config('bc-core.enable_sharing') || Gate::allows('viewAny', Tenant::class);
    }
}

Example

For example the following source code creates a filter which allows to toggle between showing all records and showing only records with even IDs.

Filter class:

<?php
class Filter extends \JohnIt\Bc\Ui\Presentation\DataTables\CollectionFilter
{
    // Used for demonstration purposes only, to render template strings.
    use \Illuminate\Foundation\Testing\Concerns\InteractsWithViews;
    public string $name = 'myFilterName';
    public string $text = 'myFilterText';
    public string $description = 'myFilterDescription';
    public function filter($collection, array $parameters = []): \Illuminate\Support\Collection {
        if ($parameters['even'] ?? null) {
            $collection = $collection->filter(fn ($it) => $it['id'] % 2 === 0);
        }
        return $collection;
    }
    // The inline string is used fo demonstration purposes only.
    // Usually one would just specify a template file in Filter::$template.
    public function toHtml(): string {
        return $this->blade(
            <<< 'blade'
                <label>
                    even
                    <x-base::in.checkbox name="filter.{{ $name }}.even"/>
                </label>
            blade,
            ['name' => $this->name],
        );
    }
}

Controller:

class Controller extends \Illuminate\Routing\Controller
{
    // Used for demonstration purposes only, to render template strings.
    use \Illuminate\Foundation\Testing\Concerns\InteractsWithViews;
    public function index()
    {
        return (string) $this->blade(
            <<< 'blade'
                <div>
                    <x-base::datatable
                        :dataset="[
                            'ajax' => ['url' => '/tabulate'],
                            'buttons' => [
                                [
                                    'extend' => 'collection',
                                    'text' => 'Filter',
                                    'autoClose' => true,
                                    'buttons' => array_map(fn ($filter) => $filter->button(), $filters),
                                ],
                            ],
                        ]"
                        :filters="$filters"
                    >
                        <thead>
                            <tr>
                                <x-base::th :dataset="['data' => 'id']">ID</x-base::th>
                                <x-base::th :dataset="['data' => 'name']">Name</x-base::th>
                            </tr>
                        </thead>
                    </x-blade::datatable>
                </div>
            blade,
            ['filters' => $this->tabulate()->getFilters()],
        );
    }
    public function tabulate()
    {
        return \Support\DataTables\Factory::make(collect([
            ['id' => 1, 'name' => 'one'],
            ['id' => 2, 'name' => 'two'],
            ['id' => 3, 'name' => 'three'],
        ]))->addFilter(new Filter())->setRowId('id');
    }
}
return (new Controller())->index();

Note

Even though this example doesn’t use Bootstrap/CoreUI modals the buttons which would toggle these modals are included for demonstration purposes and grouped inside a DataTables collections button.

The above PHP source code results in the following HTML output.

<div>
    <table
        class="datatable"
        data-ajax="{&quot;url&quot;:&quot;\/tabulate&quot;}"
        data-buttons="[{&quot;extend&quot;:&quot;collection&quot;,&quot;text&quot;:&quot;Filter&quot;,&quot;autoClose&quot;:true,&quot;buttons&quot;:[{&quot;attr&quot;:{&quot;data-target&quot;:&quot;#myFilterName&quot;,&quot;data-toggle&quot;:&quot;modal&quot;},&quot;text&quot;:&quot;myFilterText&quot;}]}]"
    >
        <thead>
            <tr>
                <th data-data="id">ID</th>
                <th data-data="name">Name</th>
            </tr>
        </thead>
    </table>
    <form class="datatable-filters">
        <label>
            even
            <input id="filter.myFilterName.even" name="filter[myFilterName][even]" type="checkbox"/>
        </label>
    </form>
</div>