Во второй части мы научились динамически переключать контент, который выводится в главной части страницы компонентом ui:include
по клику на пункте меню. Теперь попробуем заполнить динамические инклуды полезной информацией. В моем случае полезной информацией является таблица, которая будет включать в себя данные, получаемые из списка, причем по каждому из полей таблицы мы реализуем фильтрацию по текстовому содержанию поля.
Пожалуй, трудно найти в библиотеке PrimeFaces компонент, у которого было бы больше разнообразных вариантов реализации, чем у компонента Data Table! На момент, когда пишутся эти строки, я насчитал 29 вариантов с разнообразными полезными плюшками и красивостями, причем каждый вариант часто представлен в 2–3 подвариантах, не исключено, что их со временем будет еще больше. Самый базовый вариант, где выводятся только строки таблицы без каких‑либо дополнений:
DataTable Basic. DataTable displays data in tabular format
Мы возьмем компонент Data Table Filter с простым дефолтным фильтром, который представлен дополнительными полями для фильтрации, расположенными над каждым полем/колонкой таблицы:
DataTable Filter. Filtering updates the data based on the constraints
Как обычно, реализация представлена отдельной xhtml-страницей с размещенным на ней компонентом и файлом управляемого бина компонента. Посмотрим на страницу:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:p="http://primefaces.org/ui">
<f:view contentType="text/html;charset=UTF-8" encoding="UTF-8">
<h:head>
<h:outputStylesheet library="webjars" name="primeflex/3.2.0/primeflex.min.css" />
<h:outputStylesheet library="css" name="styles.css"/>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<title>Заголовок страницы</title>
</h:head>
<h:body>
<div class="card">
<h:form>
<div class="col-12 md:col-2">
<p:button href="/employee/edit/0" value="Создать"/>
</div>
<p:dataTable var="employee" value="#{employeeFilterView.employees}" widgetVar="employeesTable"
emptyMessage="No employees found with given criteria"
filteredValue="#{employeeFilterView.filteredEmployees}"
filterBy="#{employeeFilterView.filterBy}"
>
<f:facet name="header">
<span>Список сотрудников</span>
</f:facet>
<p:column headerText="ФИО" sortBy="#{employee.name}"
filterMatchMode="contains"
filterBy="#{employee.name}">
<h:outputText value="#{employee.name}" />
</p:column>
<p:column headerText="Специализация" sortBy="#{employee.specialties}"
filterMatchMode="contains"
filterBy="#{employee.specialties}">
<h:outputText value="#{employee.specialties}" />
</p:column>
<p:column headerText="Отдел" sortBy="#{employee.employeeDepartment}"
filterMatchMode="contains"
filterBy="#{employee.employeeDepartment}">
<h:outputText value="#{employee.employeeDepartment}" />
</p:column>
<p:column headerText="Компетенции" sortBy="#{employee.skills}"
filterMatchMode="contains"
filterBy="#{employee.skills}">
<h:outputText value="#{employee.skills}" />
</p:column>
<p:column headerText="Уволен" sortBy="#{employee.archived}"
filterMatchMode="contains"
filterBy="#{employee.archived}">
<h:outputText value="#{employee.archived}" />
</p:column>
<p:column headerText="Карточка ресурса">
<h:outputLink value="#{employee.linkToLK}">
<h:outputText value="Открыть карточку" />
</h:outputLink>
</p:column>
</p:dataTable>
</h:form>
</div>
</h:body>
</f:view>
</html>
Как мы видим, это обычная xhtml‑страница со всеми ее обычными блоками. Если вы подумали, что страница инклудится в главную страницу, и потому заголовочные теги, может быть, не нужны, то вы подумали так же точно, как и я вначале. Но это ошибка, так работать не будет, проверял. Позже, в продолжениях статьи, мы столкнемся с ситуациями, которые покажут нам, что вложенная страница вида в PrimeFaces имеет еще и другие ограничения, например, далеко не все виды компонентов будут на ней работать корректно. Но в таком упрощенном виде компонент таблицы с данными здесь работать будет корректно.
Главное, что нужно знать о настройке компонента p:dataTable
на странице следующее:
value="#{employeeFilterView.employees}"
— ссылка на поле бина, которое содержит список, из которого выбираются строки таблицы;emptyMessage="No employees found with given criteria"
— сообщение, которое будет выводиться в случае, если список компонентов не заполнен;filteredValue="#{employeeFilterView.filteredEmployees}"
— ссылка на поле бина, которое содержит уже отфильтрованный список, когда фильтр заполнен и применен.
Далее мы указываем заголовок списка/таблицы и описываем колонки/поля таблицы. filterBy
в каждом поле и выше в самом компоненте передает значения в фильтр, которые мы будем писать в специальных полях для фильтрации. Фильтр умный, поля фильтруются совместно, то есть можно фильтровать по нескольким колонкам одновременно согласно той логике, которую вы пропишете в бине компонента. Впрочем, об этом я напишу чуть подробнее ниже. Кроме фильтрации, таблица поддерживает сортировку по каждому полю, для чего служит sortBy
. Очень важна настройка поля filterMatchMode="contains"
— с ней фильтрация работает, как полнотекстовая, с добавлением/удалением каждого нового символа, который вы печатаете в поле фильтра, результат фильтрации моментально меняется, выполняясь как по всему слову, так и по его части или отдельным символам. Это НЕ дефолтовое значение, поэтому его нужно прописать явно. Другие возможные значения этого параметра или других полезных параметров можно найти в документации
DataTable. DataTable displays data in tabular format
Встроенные в таблицу компоненты h:outputText
просто выводят значения полей в каждой строке таблицы. Если нужно обернуть это значение в ссылку, оборачиваем его еще и в компонент h:outputLink
Теперь перейдем к бину компонента:
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import lombok.Getter;
import lombok.Setter;
import org.primefaces.model.FilterMeta;
import org.primefaces.util.LangUtils;
import org.satel.ressatel.service.EmployeeService;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@Component("employeeFilterView")
@ViewScoped
@Getter
@Setter
public class EmployeeFilterView implements Serializable {
private List<Employee> employees;
private List<Employee> filteredEmployees;
private List<FilterMeta> filterBy;
private boolean globalFilterOnly;
private final EmployeeService employeeService;
@Inject
public EmployeeFilterView(EmployeeService employeeService) {
this.employeeService = employeeService;
this.init();
}
public void init() {
employees = employeeService.getShortList();
globalFilterOnly = false;
filterBy = new ArrayList<>();
}
public boolean globalFilterFunction(Object value, Object filter, Locale locale) {
String filterText = (filter == null) ? null : filter.toString().trim().toLowerCase();
if (LangUtils.isBlank(filterText)) {
return true;
}
Employee employee = (Employee) value;
return employee.getName().toLowerCase().contains(filterText)
|| employee.getSpecialties().toLowerCase().contains(filterText)
|| employee.getEmployeeDepartment().toLowerCase().contains(filterText)
|| employee.getSkills().toLowerCase().contains(filterText)
|| employee.getArchived().toLowerCase().contains(filterText);
}
public void toggleGlobalFilter() {
setGlobalFilterOnly(!isGlobalFilterOnly());
}
}
Обратите внимание, что я употребил аннотацию бина @ViewScoped
из jakarta. Spring контекст прекрасно понял этот scope и применил аннотацию к бину. Тут нужно отметить, что абсолютно точной аналогии этого scope в Spring не существует, область действия такого бина будет ограничиваться одной открытой страницей xhtml с компонентом на ней, то есть одним видом JSF + PrimeFaces. Некоторые пишут кастомные аналоги для Spring сами, если для их задач это важно, как например, вот здесь:
Если это допустимо вашим приложением, то можно попробовать использовать scope @Request, @Session или @GlobalSession, каждый решает сам. Но поскольку область видимости @ViewScoped
из jakarta не привела у меня ни к каким неприятным эффектам, я оставил ее как есть.
Также я придерживаюсь инжектирования сервисов в управляемых бинах компонентов с помощью аннотации @Inject из jakarta, а в сервисах ставлю scope @ApplicationScoped
тоже из jakarta, совместно с аннотацией @Service
из Spring, и также никаких неудобств это пока что не вызвало.
В бине мы видим обещанную мной выше реализацию логики фильтрации в методе globalFilterFunction
, не удивляйтесь, что явной связи поля filterBy с этой функцией вы не видите - все связи выполняются PrimeFaces "под капотом". filterBy просто приходит в параметр filter, а затем обрабатывается через filterText путем сравнения с данными, приходящими из БД для конкретной строки. Реализацию сервиса и репозитория приводить не буду, они у вас будут собственные, под ваши нужды.
В следующей части статьи мы начнем знакомиться с формами для отображения и редактирования данных, с типами компонентов для инпутов форм и с обработкой данных, приходящих из инпутов.
Напоминаю, что данный цикл статей подготовлен в преддверии старта курса "Java Developer. Professional". Бесплатный урок курса по теме: "Реактивное подключение к Postgresql в приложениях на Java" доступен по этой ссылке.