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="{"url":"\/tabulate"}">
<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,"asc"]">
<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="{"url":"\/tabulate"}">
<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="[{"extends":"openUrl","text":"Example","url":"http:\/\/localhost\/"}]"
></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="{"url":"\/tabulate"}"
data-buttons="[{"extends":"openAction","name":"example","text":"Example"}]"
>
<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="{"url":"\/tabulate"}"
data-buttons="[{"extends":"openAction","name":"example","text":"Example"}]"
>
<thead>
<tr>
<th
data-default-content="<input type="checkbox"/>"
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="{"url":"\/tabulate"}"
data-buttons="[{"extend":"collection","text":"Filter","autoClose":true,"buttons":[{"attr":{"data-target":"#myFilterName","data-toggle":"modal"},"text":"myFilterText"}]}]"
>
<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>