Во второй части мы научились динамически переключать контент, который выводится в главной части страницы компонентом 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 сами, если для их задач это важно, как например, вот здесь:

JSF View scope in 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" доступен по этой ссылке.

Комментарии (0)