Complex tables made easy

Introduction

DataTables is a very powerful javascript library for creating tables with complex user interactions. It has dozens of configuration options and can be quite complicated to set up correctly. The Yada Framework implements a Java Fluent API that produces the required javascript configuration and provides the needed backend functionality for server-side data loading.

The current implementation covers all the core DataTables options and the Responsive extension.

User Table
Figure 1. User Table
A full example is implemented in the YadaExamples project.

Prerequisites

The DataTables library can either be downloaded locally or from a CDN. In both cases the official download page offers different options to package the required elements into a downloadable zip or a specific CDN url for javascript and CSS. The Yada Framework implementation has been tested with:

Styling framework

Bootstrap 5

Packages

DataTables

Extensions

Responsive

This example is for a downloaded version:

<head>
	<link rel="stylesheet" type="text/css" th:href="@{/static/datatables-2.1.8/datatables.min.css}"/>
</head>
...
<script th:src="@{/static/datatables-2.1.8/datatables.min.js}"></script>

The yada.datatables.js file also needs to be loaded. This is by default automatically packaged in the war distribution for production but should be added in development:

<script th:if="${@config.developmentEnvironment}" th:src="@{/yadares/js/yada.datatables.js}"></script>

HTML Code

The basic DataTable functionality is implemented just by adding the yada:datatable tag:

<yada:datatable (1)
	yada:configuration="${userTableAttribute}"> (2)
</yada:datatable>
1 The tag inserts the table at the specified position in the HTML
2 The configuration must have been set in the @Controller as a Model attribute, called "userTableAttribute" in this example

For advanced scenarios where you need to alter the configuration before creating the table or work with the created table in javascript, two handlers can be provided via yada:preprocessor and yada:postprocessor:

<yada:datatable
	yada:configuration="${userTableAttribute}"
	yada:preprocessor="userTablePreprocessor" (1)
	yada:postprocessor="userTablePostprocessor"> (2)
</yada:datatable>
1 The preprocessor can alter the configuration before the table is created
2 The postprocessor can operate on the table once it has been created

More details below.

Java Fluent Interface

The configuration for a DataTable is implemented in a YadaDataTable instance. This instance should be added to the Model with an attribute that must have the same name used in yada:configuration, e.g. userTableAttribute in the example above.

YadaDataTable myDataTable = ...
model.addAttribute("userTableAttribute", myDataTable);

The instance is a singleton identified by a unique id and is produced by yadaDataTableFactory. This ensures that the table configuration code is run only once for all HTTP requests and that the same configuration can be used in the ajax handler that loads the table data.

To create or get an instance the syntax is as follows:

YadaDataTable myDataTable = yadaDataTableFactory.getSingleton("myTableId", locale, ...);

The locale is needed to load the i18n file and can be omitted or set to null to use the default locale that is either set in the application XML configuration or taken from the platform. In a multilanguage application it should be taken from a parameter in the @Controller @RequestMapping.

The table configuration is added as a lambda after the locale parameter and uses the fluent iterface:

YadaDataTable yadaDataTable = yadaDataTableFactory.getSingleton("userTable", locale, table -> {
	table
		.dtAjaxUrl("someUrl")
		.dt...
});
Using the lambda ensures that the configuration code is run just once.

All methods of the fluent interface have the "dt" prefix. This makes IDE suggestions more focused on the useful methods during autocompletion:

Method Autocompletion
Figure 2. Method Autocompletion

When a configuration option has many parameters, the corresponding method name has the "Obj" suffix because a new object is returned to provide the new configuration methods. To "exit" from the current object, the back() method must be called.

Basic Configuration

The most basic table configuration, that can be used to show data taken from an @Entity, only requires the entity class and fields:

@RequestMapping("/user")
public String users(Model model) {
	YadaDataTable basicTable = yadaDataTableFactory.getSingleton("basicTable", table -> {
		table
			.dtEntityClass(UserProfile.class) (1)
			.dtStructureObj()
				.dtColumnObj("Email", "userCredentials.username").back() (2)
				.dtColumnObj("Last Login", "userCredentials.lastSuccessfulLogin").back() (2)
			.back();
	});
	model.addAttribute("basicTable", basicTable);
	return "/dashboard/users";
}
1 Provide the class of the entity that holds data
2 Provide the column names and the path of the properties that hold the value to show

On the page the table is shown with:

<yada:datatable
	yada:configuration="${basicTable}">
</yada:datatable>
Basic Usage
Figure 3. Basic Usage
the endpoint is implemented in YadaController.yadaDataTableData()

Data retrieval will be automatic and will have the same security restrictions of the page where the table is shown, which is "/dashboard/user" in the example.

security is implemented in SecurityConfig if the application is secured

Advanced Configuration

In more advanced scenarios the ajax endpoint returning data can be customized with dtAjaxUrl(). This is an alternative approach to dtEntityClass() and using both will result in error. The argument of dtAjaxUrl() is either the url for the ajax call that retrieves data from the backend or a method reference to it. The string parameter can contain any Thymeleaf expression and will be included in a standard URL expression like @{/myUrl} when not already provided.

The dtStructureObj() top method starts configuration of the "structure" of the table using a custom API that can be explored with autocompletion. This API allows the definition of columns and buttons.

The other top method is .dtOptionsObj() that allows access to the official DataTables options. For example, the PageLength option can be set with .dtOptionsObj().dtPageLength(25). All the DataTables core options and the Responsive extension options are available unless they are deprecated or not applicable in the context of the Yada Framework, like retrieve.

anything that can’t be done in Java can be done in javascript using pre- and post- processors.
YadaDataTable yadaDataTable = yadaDataTableFactory.getSingleton("userTable", locale, table -> {
	table
		.dtEntityClass(UserProfile.class)
		.dtAjaxUrl(this::userProfileTablePage)
		.dtLanguageObj("/static/datatables-2.1.8/i18n/") (1)
			.dsAddLanguage("pt", "pt-PT.json") (2)
			.back()
		.dtStructureObj()
			.dtCssClasses("yadaNoLoader") (3)
			.dtColumnObj("ID", "id")
				.dtResponsivePriority(80) (4)
				.back()
			.dtColumnObj("column.enabled", "userCredentials.enabled") (5)
				.dtResponsivePriority(40)
				.back()
			.dtColumnObj("Title", "title."+locale.getLanguage()) (6)
				.back()
			.dtColumnObj("Email", "userCredentials.username")
				.dtName("userCredentials.username") (7)
				.dtOrderAsc(0) (8)
				.back()
			.dtColumnObj("Last Login", "userCredentials.lastSuccessfulLogin")
				.dtOrderDesc(1) (9)
				.dtCssClasses("nowrap") (10)
				.back()
			.dtColumnCheckbox("select.allnone") (11)
			.dtColumnCommands("column.commands", 10) (12)
			.dtButtonObj("Disabled") (13)
				.dtUrl("@{/dashboard/user/dummy}") (14)
				.dtIcon("<i class='bi bi-0-circle'></i>") (15)
				.dtShowCommandIcon("disableCommandIcon") (16)
				.back()
			.dtButtonObj("button.add")
				.dtUrl("@{/dashboard/userwrite/ajaxEditUserProfileForm}")
				.dtGlobal() (17)
				.dtIcon("<i class='bi bi-plus-square'></i>")
				.dtToolbarCssClass("btn-success") (18)
				.dtRole("ADMIN") (19)
				.back()
			.dtButtonObj("button.impersonate")
				.dtUrlProvider("impersonate") (20)
				.dtNoAjax() (21)
				.dtIcon("<i class='bi bi-mortarboard'></i>")
				.dtRole("ADMIN").dtRole("supervisor")
				.back()
			.dtButtonObj("button.edit")
				.dtUrl("@{/dashboard/userwrite/ajaxEditUserProfileForm}")
				.dtElementLoader("#userTable") (22)
				.dtIcon("<i class='bi bi-pencil'></i>")
				.dtIdName("userProfileId") (23)
				.dtRole("ADMIN")
				.back()
			.dtButtonObj("button.delete")
				.dtUrl("@{/dashboard/userwrite/ajaxDeleteUserProfile}")
				.dtIcon("<i class='bi bi-trash'></i>")
				.dtRole("ADMIN")
				.dtMultiRow() (24)
				.dtToolbarCssClass("btn-danger")
				.dtConfirmDialogObj() (25)
					.dtTitle("Delete User")
					.dtMessageSingular("usertable.delete.confirm.singular")
					.dtMessagePlural("usertable.delete.confirm.plural")
					.dtConfirmButton("button.confirm").dtAbortButton("modal.confirm.cancel")
					.dtPlaceholderColumnName("userCredentials.username")
					.back()
				.back()
			.dtFooter() (26)
			.back()
		.dtOptionsObj() (27)
			.dtResponsiveObj() (28)
				.dtDetailsObj()
					.dtDisplay("DataTable.Responsive.display.childRowImmediate")
					.back()
				.back()
			.dtPageLength(10) (29)
			.dtColumnDefsObj() (30)
				.dtTargetsName("userCredentials.username")
				.dtAriaTitle("This is the user email")
				.back()
			.dtColumnDefsObj()
				.dtTargetsName("userCredentials.lastSuccessfulLogin")
				.dtAriaTitle("usertable.aria.lastlogin")
				.back()
			.back()
		;
});
1 Set the base url where language files are located. It can be the official DataTables URL or a local endpoint. By default the "it", "de", "es" and "fr" languages are loaded.
2 Add a language definition that is not loaded by default.
3 CSS classes to set on the <table> tag
4 Control the order in which columns are hidden when the page is resized
5 Add a new column to the table. Order is preserved.
6 The column value can be a localized text
7 Set a name on this column for database operations and cross references, e.g. with column definitions
8 Set the column as the first default sort column, ascending
9 Set the column as the second default sort column, descending
10 CSS classes to set on the cell
11 Adds the leftmost column with checkboxes to select rows
12 Adds a rightmost column with buttons for each row
13 Adds a button called "Disabled" to the toolbar
14 URL to be called when the button is clicked
15 Icon to be displayed on the button
16 Javascript function to be called to decide wether the icon on the row must be shown
17 The button is "global", i.e. is shown in the toolbar, not in the row, and is always enabled
18 CSS class to be added to the button in the toolbar
19 The button is shown only to users with the "ADMIN" role. Can be called may times with different role values
20 The URL is provided by a javascript function
21 The click does not perform an ajax call, it just follows the link
22 Specify a CSS selector for an element that should be covered with a "loader" icon while the ajax call is in progress
23 Name of the ID request parameter sent in the call, default is "id". Useless for global buttons
24 Enable the toolbar button when one or many rows are selected. The default is for the toolbar button to be enabled only when one row is selected
25 Enable javascript-side confirmation dialog for button action
26 Adds a footer to the table showing the same content as the header
27 Start configuration of the "options" of the DataTable. See the official DataTables documentation for details
28 Enable the Responsive extension
29 Number of rows to show per page
30 Configure column definitions: a way to set column options after a column has been defined

Java Ajax Endpoint

The Yada Framework implementation of DataTables assumes that data is fetched via ajax from the server, therefore the ajax option is forced to be active.

The ajax endpoint is set via .dtAjaxUrl() on the YadaDataTable object as explained before. The @Controller should query the database, performing searching and sorting, and return a JSON file with the resulting data for the current page.

When the table shows data from @Entity objects, most of that code is already provided.

@RequestMapping(value ="/user/userProfileTablePage", produces = MediaType.APPLICATION_JSON_VALUE) (1)
@ResponseBody public Map<String, Object> userProfileTablePage(YadaDatatablesRequest yadaDatatablesRequest, Locale locale) { (2)
	Map<String, Object> result = yadaDataTableDao.getConvertedJsonPage(yadaDatatablesRequest, UserProfile.class, locale);	(3)
	return result;	(4)
}
1 Any type of mapping can be used, not just @RequestMapping
2 YadaDatatablesRequest is initialized with the metadata sent by DataTables, like the current page number, the value paths and the search/sort options
3 yadaDataTableDao.getConvertedJsonPage() receives the request data and the @Entity class to perform all needed operations
4 The result is a "map tree" (i.e. nested maps) that is automatically converted to json

Using a custom endpoint for table data allows to manipulate both the query and the result, for example by adding conditions or creating values not directly found on the entity:

Advanced Usage

DataTables API Access

A reference to the DataTables API, i.e. the object returned by $table.DataTable(dataTableOptions), is added to the table DOM object under the yadaDataTableApi key. It can be retrieved with

const dataTableApi = $('#yourTableId').data('yadaDataTableApi');

All standard DataTable API methods are available, for example use dataTableApi.ajax.reload() to reload the table data.

Table Filter

You can add form elements to filter the data shown in your DataTable.

Table Filter
Figure 4. Table Filter

On the HTML page create a form with the CSS class yada_dataTables_{tableId} where {tableId} is the ID of your table: any input fields, checkboxes, or select elements in this form will automatically be sent to the server when the table loads or refreshes.

On the backend, the form data is available in the YadaDatatablesRequest object passed to the ajax endpoint. You can access the form data using the getExtraParam() method, which returns a Map<String, String> containing the form field names and values.

The sql used by yadaDataTableDao.getConvertedJsonPage() to read table data can be customized by adding conditions to the YadaSql object returned by yadaDatatablesRequest.getYadaSql().

For example, if you have a table showing users, you can add checkboxes to filter by user role, or text inputs to search by name. When users interact with these form elements, call dataTableApi.ajax.reload() to refresh the table with the new filter values.

See YadaExamples/src/main/java/net/yadaframework/example/web/dashboard/UserProfileController.java for a full example.

Returning extra data

The call to yadaDataTableDao.getConvertedJsonPage() returns all data that was requested, but sometimes some other attributes may be required to compute a row value. In order to retrieve this data, use yadaDatatablesRequest.addExtraJsonAttribute("attributePath") before calling getConvertedJsonPage(). A use case could be to fetch the "enabled" attribute of the user credentials to show a different icon based on that value.

row class from the backend

reference

pre- and post- processors examples

i18n is automatic by using keys instead of words