null

JSF: composite components strikes back

В моей предыдущей статье было расмотено два способа создания переиспользуемых компонентов в JSF: с использованием ui:include или с помощью композитных компонентов. Второй способ был рассмотрен не в полной мере, так что продолжим.

Первый не рассмотренный момент — это возможность передавать в качестве значения атрибута MethodExpression. Переданный таким образом метод вы не сможете "вызвать", но сможете передать его в качетве атрибута другим тегам — например, в качестве атрибута action у h:comandLink. Для объявления такого атрибута можно воспользоваться двумя методами:

  1. Указать сигнатуру ожидаемого метода через атрибут method-signature в формате ReturnType methodName(ArgumentType1, ArgumentType2, ...)
  2. Использовать предопределенные имена “action”, “actionListener”, “validator”, и “valueChangeListener”. 

Второй способ можно использовать только при указании атрибута targets - списка идентификаторов элементов, к которым будет "привязан" метод — он будет привязан к атрибуту, соответствующему имени. Первый способ тоже допускает использование атрибута targets, но в таком случае необхоимо указание targetAttributeName. Рассмотрим на примере.

Компонент:

<f:view xmlns="http://www.w3c.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
        xmlns:h="http://java.sun.com/jsf/html" xmlns:p="http://primefaces.org/ui"
        xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:composite="http://java.sun.com/jsf/composite"
        xmlns:c="http://java.sun.com/jsp/jstl/core">
    <composite:interface>
        <!--Данный метод будет использоваться в качестве action у кнопки btn-->
        <composite:attribute name="action" targets="btn"/>
        <!--Доступ к данному методу осуществляется напрямую через EL cc.attrs.someAction-->
        <composite:attribute name="someAction" required="true" method-signature="java.lang.String action()"/>
        <!--Данный метод будет использован как action (targetAttributeName) у кнопок btn-3 и btn-4 (targets)-->
        <composite:attribute name="multipleAction" targets="btn-3 btn-4" method-signature="java.lang.String action()"
                             targetAttributeName="action"/>
    </composite:interface>
    <composite:implementation>
        <h:panelGroup id="panel">
            <h:panelGroup id="nestedPanel">
                <h:commandButton value="Action button" id="btn"/>

                <h:commandButton value="Manually specified action" id="btn-2" action="#{cc.attrs.someAction}"/>

                <h:commandButton value="Target 1" id="btn-3"/>
                <h:commandButton value="Target 2" id="btn-4"/>
            </h:panelGroup>
        </h:panelGroup>
    </composite:implementation>
</f:view>

Managed Bean:

package com.example.jsf.jsf.beans;

import javax.annotation.ManagedBean;

@ManagedBean
public class TestBean {
    public String action() {
        System.out.println("Action called");
        return "/view.html?faces-redirect=true&arg=" + 1;
    }

    public String someAction() {
        System.out.println("Some action called");
        return "/view.html?faces-redirect=true&arg=" + 2;
    }

    public String multipleAction() {
        System.out.println("Multiple action called");
        return "/view.html?faces-redirect=true&arg=" + 3;
    }
    
}

Использование компонента:

<h:form>
    <custom:test action="#{testBean.action}" someAction="#{testBean.someAction}"
                 multipleAction="#{testBean.multipleAction}"/>
</h:form>

Во-вторых, композитный компонент может быть источником событий, что позволит добавлять к нему обработчики событий (например, с помощью f:actionListener). Конечно, вы не сможете определять собственные события, но сможете использовать события от других источников событий в реализации вашего компонента (h:commandButton, h:commandLink и другие компоненты, к которым можно добавлять обработчики событий). Для этого используется тег composite:actionSource c двумя основнми атрибутами:

  1. targets — идентификаторы компонентов, события от которых будут служить источником события для компонента.
  2. name — имя события в вашем компоненте, оно будет использоваться для добавления обработчика. Если не указан атрибут targets, то будут использоваться события от компонента с таким же идентификатором.

Пример компонента:

<f:view xmlns="http://www.w3c.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
        xmlns:h="http://java.sun.com/jsf/html" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
        xmlns:composite="http://java.sun.com/jsf/composite" xmlns:c="http://java.sun.com/jsp/jstl/core"
        xmlns:p="http://java.sun.com/jsf/html">
    <composite:interface>
        <!--В качестве источника событий будет служить компонет с id firstBtn-->
        <composite:actionSource name="firstBtn"/>
        <!--В качестве источника событий будут служить компонеты с id btn-2 и link-1-->
        <composite:actionSource name="event" targets="firstBtn btn-2 link-1"/>
        <!--В качестве источника событий будет служить компонет с id btn-2-->
        <composite:actionSource name="anotherEvent" targets="btn-2"/>
    </composite:interface>
    <composite:implementation>
        <p:commandButton id="firstBtn" value="Action source for firstBtn and event"/>
        <p:commandLink id="link-1" value="Action source for event"/>
        <p:commandButton id="btn-2" value="Action source for event and anotherEvent"/>
    </composite:implementation>
</f:view>

Пример применения:

<html>
    <f:view xmlns="http://www.w3c.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
            xmlns:h="http://java.sun.com/jsf/html" xmlns:p="http://primefaces.org/ui"
            xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:custom="http://java.sun.com/jsf/composite/custom">
        <h:head>
            <title>Title here</title>
        </h:head>

        <h:body style="height: 100vh">
            <h:form>
                <custom:actionSource>
                    <f:actionListener for="firstBtn" binding="#{testBean.listener1()}"/>
                    <f:actionListener for="event" binding="#{testBean.listener2()}"/>
                    <f:actionListener for="anotherEvent" binding="#{testBean.listener3()}"/>
                </custom:actionSource>
            </h:form>
        </h:body>
    </f:view>
</html>

При нажатии на первую кнопку будут вызваны методы listener1 и listener2, при нажатии на ссылку — метод listener1, и при нажатии на вторую кнопку — listener2 и listener3.

В-третьих, компоненты из реализации, содержащие значение, могут выставить наружу интерфейс для добавления валидаторов, конвертеров, обработчиков изменения значения (f:validator, f:converter, f:valueChangeListener) к различным компонентам (h:inputText и другие реализации EditableValueHolder и ValueHolder).  Для этого используются теги valueHolder и editableValueHolder с атрибутами name и targets, аналогичными таковым для actionSource.

Пример компонента:

<f:view xmlns="http://www.w3c.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
        xmlns:h="http://java.sun.com/jsf/html" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
        xmlns:composite="http://java.sun.com/jsf/composite" xmlns:c="http://java.sun.com/jsp/jstl/core"
        xmlns:p="http://java.sun.com/jsf/html">
    <composite:interface>
        <!--Прикреплен к компоненту с id output-->
        <composite:valueHolder name="output"/>
        <!--Прикрелен к компоненту с id input-1-->
        <composite:editableValueHolder name="firstInput" targets="input-1"/>
        <!--Прикреплен к компонентам с id input-1 и input-2-->
        <composite:editableValueHolder name="allInputs" targets="input-1 input-2"/>
    </composite:interface>
    <composite:implementation>
        <h:outputText id="output" value="#{testBean.currentDate}"/>
        <br/>
        <h:inputText id="input-1" value="#{testBean.value1}"/>
        <br/>
        <h:inputText id="input-2" value="#{testBean.value2}"/>
    </composite:implementation>
</f:view>

Используемый бин:

@ManagedBean
public class TestBean {
    private String value1;
    private String value2;

    public Date getCurrentDate() {
        return new Date();
    }

    public ValueChangeListener getValueChangeListener() {
        return event -> System.out.println("Value changed! " + event.getNewValue());
    }
}

Использование компонента:

<html>
    <f:view xmlns="http://www.w3c.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
            xmlns:h="http://java.sun.com/jsf/html" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
            xmlns:custom="http://java.sun.com/jsf/composite/custom">
        <h:head>
            <title>Title here</title>
        </h:head>

        <h:body style="height: 100vh">
            <h:form>
                <custom:valueHolder>
                    <!--Добавление конвертера к h:outputText-->
                    <f:convertDateTime for="output" pattern="dd-MM-yyyy HH:mm:ss"/>
                    <!--Добавление валидатора к h:inputText с id input-1-->
                    <f:validateLength for="firstInput" minimum="2"/>
                    <!--Добавление обработчика события изменения значения в h:inputText с id input-1 и input-2-->
                    <f:valueChangeListener for="allInputs" binding="#{testBean.valueChangeListener}"/>
                </custom:valueHolder>
                <h:commandButton value="Submit"/>
                <h:messages/>
            </h:form>
        </h:body>
    </f:view>
</html>

Последняя возможность, о которой хотелось бы упомянуть, заключается в возможности отображения вложенных в композитный компонент компонентов. Это возможно с помощью тега composite:insertChildren. Продемонстрирую на примере.

Компонент:

<f:view xmlns="http://www.w3c.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
        xmlns:h="http://java.sun.com/jsf/html" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
        xmlns:composite="http://java.sun.com/jsf/composite" xmlns:c="http://java.sun.com/jsp/jstl/core">
    <composite:interface>
    </composite:interface>
    <composite:implementation>
        <div class="outer">
            Outer component
            <composite:insertChildren/>
        </div>
    </composite:implementation>
</f:view>

Использование:

<html>
    <f:view xmlns="http://www.w3c.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
            xmlns:h="http://java.sun.com/jsf/html" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
            xmlns:custom="http://java.sun.com/jsf/composite/custom">
        <h:head>
            <title>Title here</title>
        </h:head>
        <style>
            .outer {
                background-color: red;
                padding: 40px;
                height: 100%;
                font-size: 80px;
            }
            .inner {
                background-color: green;
                height: 50%;
                font-size: 50px;
            }
        </style>
        <h:body style="height: 100vh">
            <h:form>
                <custom:nested>
                    <div class="inner">
                        Inner component
                    </div>
                </custom:nested>
            </h:form>
        </h:body>
    </f:view>
</html>

Полученная страница:

Засим откланиваюсь, прощайте.