Blog-Archiv

Sonntag, 8. September 2019

JSF View Reuse Mechanisms

JSF features several code reuse mechanisms for XHTML views. Here are those without need of additional Java code, ordered by complexity, simplest first.

  1. Including
    Done via element ui:include with src="includes/file.xhtml" attribute, and optional nested ui:param that supports #{} bean-properties, but not methods. CAUTION: When including relatively from a sub-directory, and the included file also includes relatively, that second include will fail!

  2. Templating
    The includer can override elements, marked by ui:insert, through ui:define, and can pass ui:param parameters, but also no support for methods. It is possible to inject complete HTML fragments into the included template. Useful for unified look and layout. Namespace prefix "ui:", insert, define, composition, decorate, ....

  3. Composite Component
    Advanced JSF 2 component concept. Combines a number of existing components with markup to form new components. Namespace prefix "cc:" or "composite:", interface, attribute, implementation, .... Result is an XML-element with an arbitrary name and namespace below http://xmlns.jcp.org/jsf/composite/. Makes a XHTML view reusable for different Java controllers, because the parameter attributes can be evaluated with all kinds of #{} expressions, even methods.

The differences are not that big. Inclusion does not provide method parameter passing, but it can be parameterized with Java properties. Templating doesn't provide methods either, but enables inserting whole XHTML fragments into the included file. Composite component finally allows method parameter passing, and its final appearance is a completely new XML element type, driven by attributes.

Setup

Please refer to my recent Blog for a JSF 2.3 setup with Eclipse, there you find the Maven pom.xml, Servlet web.xml, JSF faces-config.xml, CDI beans.xml and ApplicationFacesConfig.java.

I found out that you can omit the Eclipse configuration when you do following:

  • In a file explorer, create a simple Maven project directory structure (not a "dynamic web project" archetype!)
  • Add pom.xml with JSF dependencies in its root directory
  • Run Maven build from commandline: mvn package
  • Import the project into Eclipse as "Maven project" (not as "Java project"!)

Example Index

  1. Including
  2. Templating
  3. Composite Component

1. Including

This example renders dates in a special way. Here is a screenshot of the user interface:

Project source files and directories:

includer.xhtml (master view)

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
        xmlns:h="http://xmlns.jcp.org/jsf/html"
        xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
        xmlns:f="http://xmlns.jcp.org/jsf/core"
>
    <h:head>
        <title>JSF Include Example</title>
    </h:head>
    
    <h:body>
        <h2>JSF &lt;ui:include&gt; Example with &lt;ui:param&gt;</h2>
        
        <span>
            Once the 
            <ui:include src="includes/date-box.xhtml">
                <ui:param name="label" value="Creation Date"/>
                <ui:param name="value" value="#{dateHolder.creationDate}"/>
                <ui:param name="pattern" value="d.M.yyyy, HH:mm"/>
            </ui:include>
        </span>
        
        <span>
            mattered, but now it's just the
            <ui:include src="includes/date-box.xhtml">
                <ui:param name="label" value="Latest Update"/>
                <ui:param name="value" value="#{dateHolder.lastUpdate}"/>
                <ui:param name="borderColor" value="red"/>
            </ui:include>
            ....
        </span>
    </h:body>
    
</html>

date-box.xhtml (the included XHTML)

<!DOCTYPE html>
<!-- 
    Formatted date with titled border.
 -->
<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:ui="http://xmlns.jcp.org/jsf/facelets"
>
    <h:body>
        <ui:param name="borderColor" value="#{empty borderColor ? 'black' : borderColor}"/>
        <ui:param name="pattern" value="#{empty pattern ? 'yyyy-MM-dd HH:mm' : pattern}"/>
        
        <fieldset style="
                display: inline-block;
                border-radius: 0.6em;
                border-color: #{borderColor};
        ">
            <legend style="color: #{borderColor};">#{label}</legend>
        
            <h:outputText value="#{value}">
                <f:convertDateTime pattern="#{pattern}"/>
            </h:outputText>
        </fieldset>
    </h:body>

</html>

DateHolder.java (Java bean that provides dates, used by the master view)

package fri.jsf;

import java.io.Serializable;
import java.time.Instant;
import java.util.Date;
import javax.faces.view.ViewScoped;
import javax.inject.Named;

@Named
@ViewScoped
public class DateHolder implements Serializable
{
    public Date getCreationDate() {
        return Date.from(Instant.EPOCH);
    }
    
    public Date getLastUpdate() {
        return new Date();  // now
    }
}

You don't need setters here, these date properties are read-only.

Explanations

Inclusion can happen everywhere, in this case among inline elements. The parametrization is easy to understand, although restricted to Java bean properties and plain text. As you see we can pass a date into date-box.xhml, and it gets formatted there, so parametrization is not restricted to strings.

Have a eye on the parameters in includer.xhml and their re-definitions inside date-box.xhml. That's the way to establish default values. You can use the same parameter-names on both sides!

We make the first date-box use another date-format pattern than the second.
We insert the borderColor parameter into CSS code to set a red color.


2. Templating

This example lays out a scrolling web page with a fixed header, the header having one or two lines. The first header line is divided into left, center and right part. Here is a screenshot with a one-line header:

File structure:

templater.xhtml (master view)

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
      xmlns:f="http://xmlns.jcp.org/jsf/core"
>
    <h:head>
        <title>JSF Template Example</title>
    </h:head>
    
    <h:body>
        <!-- Everything outside <ui:composition> will be ignored! -->
        
        <ui:composition template="templates/sticky-header-page.xhtml">
            <ui:param name="headerBackColor" value="orange"/>

            <ui:define name="headerTextLeft"> <!-- Explicitly erase left side default text -->
            </ui:define>
            
            <ui:define name="headerTextCenter">
                Pirate Adventures
            </ui:define>
            
            <!-- ui:define name="headerSecondLine">
                Second Line
            </ui:define -->
            
            <ui:define name="contentText">
                <ui:include src="includes/long-text.html"/>
            </ui:define>
            
        </ui:composition>
        
        <!-- To not ignore everything outside <ui:composition>, use <ui:decorate> -->
    </h:body>
    
</html>

sticky-header-page.xhtml (the template)

<!DOCTYPE html>
<!-- 
    A fixed header and a page scrolling underneath.
    The header has a left/center/right part, and an optional second line.
 -->
<html xmlns="http://www.w3.org/1999/xhtml"
        xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
        xmlns:f="http://xmlns.jcp.org/jsf/core"
        xmlns:h="http://xmlns.jcp.org/jsf/html">
    
    <h:body>
        <!-- The fixed page header -->
        <div style="
                position: fixed;
                top: 0;
                left: 0;
                right: 0;
                text-align: center;
                padding: 0.5em;
                background-color: #{empty headerBackColor ? 'lightGray' : headerBackColor};
        ">
            <div>
                <span style="float: left;">
                    <ui:insert name="headerTextLeft">
                        [Author]
                    </ui:insert>
                </span>
                <span><b>
                    <ui:insert name="headerTextCenter">
                        [Title]
                    </ui:insert>
                </b></span>
                <span style="float: right;">
                    <ui:insert name="headerTextRight">
                        [Date]
                    </ui:insert>
                </span>
            </div>
            
            <div>
                <ui:insert name="headerSecondLine">
                    <ui:param name="noSecondLine" value="true" />
                </ui:insert>
            </div>
        </div>
        <div style="clear: both;"></div>
        
        <!-- The scrolling page content -->
        <div style="padding-top: #{noSecondLine ? '2em' : '3em'};">
            <ui:insert name="contentText"/>
        </div>
        
    </h:body>

</html>

long-text.html (long example text for scrollbars, goes to contentText)

<!DOCTYPE html>
<html>
<body>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
......
</body>
</html>

Copy the complete nonsense text from lipsum.com to make the page scroll.

Explanations

Parameterization is the same as with inclusions. Here we have headerBackColor as plain <ui:param>, which sets an orange background color to the header. The difference between parameters and insertions is that the latter can receive entire HTML fragments.

Essential is that any name attribute in a <define> of the master page matches a name attribute in an <insert> of the template. Such insertion points are headerTextLeft, headerTextCenter, headerTextRight, headerSecondLine, and contentText.

Mind that not all insertion points are used, and that the insertion points sometimes define defaults and sometimes not. In case they define defaults, you can erase those explicitly by passing empty, as done with headerTextLeft (see screnshot, left text is not present). In case no define was done, the insert's default will be visible when present, like in headerTextRight. On the other hand the headerSecondLine insertion point does not have a default and will not be present when no define was done for it.

This opens a dependency - the template's layout depends on the presence of headerSecondLine, because the padding-top of the contentText must be set correctly to not obscure the scrolling text underneath. The nested <ui:param> noSecondLine will be true when no define for headerSecondLine was done. In that case the padding-top goes to 3em, else it will be 2em. Setting such a <ui:param> is a trick to find out whether a define was done or not, as the whole headerSecondLine insertion point would be removed when being replaced by a define. Try commenting in the out-commented headerSecondLine in templater.xhtml master file, and check that the padding-top automatically grows!

The contentText gets replaced by a HTML file loaded through <ui:include>. Such can be mixed in at any time.

Please mind that, when you have a file containing <ui:composition>, JSF would ignore anything outside that tag. If you don't like that, use <ui:decorate> instead.


3. Composite Component

Example is a text field with a label and a submit-button, reused to enter either a name or an e-mail (both have their own button). Whatever was entered by the user gets stored in the backing bean, and is rendered in the list at bottom. The screenshot shows a state when two names and one e-mail were entered:

Source files:

composer.xhtml (master view)

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:cc="http://xmlns.jcp.org/jsf/composite"
      xmlns:tl="http://xmlns.jcp.org/jsf/composite/textline"
>
    <h:head>
        <title>JSF Composite Example</title>
    </h:head>
    
    <h:body>
        <h2>JSF &lt;composite&gt; Example</h2>
        
        <tl:textlineSubmit
            label="Name"
            text="#{personTextlinesController.name}"
            buttonLabel="Register"
            buttonAction="#{personTextlinesController.submitName}"
        />
        
        <tl:textlineSubmit
            label="E-Mail"
            text="#{personTextlinesController.email}"
            buttonLabel="Send"
            buttonAction="#{personTextlinesController.submitEmail}"
        />
        
        <h:dataTable value="#{personTextlinesController.summary}" var="item">
            <h:column>#{item}</h:column>
        </h:dataTable>
    </h:body>
    
</html>

textlineSubmit.xhtml (the component)

<!DOCTYPE html>
<!-- 
    A label with a input text field and a command-button.
 -->
<html xmlns="http://www.w3.org/1999/xhtml"
        xmlns:composite="http://xmlns.jcp.org/jsf/composite"
        xmlns:h="http://xmlns.jcp.org/jsf/html">
    
   <composite:interface>
      <composite:attribute name="label" default="Input"/>
      <composite:attribute name="text"/>
      <composite:attribute name="buttonLabel" default="Submit"/>
      <composite:attribute name="buttonAction" method-signature="java.lang.String method()" required="true"/>
   </composite:interface>
   
   <composite:implementation>
      <h:form>
         <h:panelGrid columns="3">
             #{cc.attrs.label}: 
             
             <h:inputText value="#{cc.attrs.text}"/>
             
             <h:commandButton value="#{cc.attrs.buttonLabel}"
                 action="#{cc.attrs.buttonAction}"/>
         </h:panelGrid>
      </h:form>
   </composite:implementation>

</html>

PersonTextlinesController.java (Java controller, used by the master view)

package fri.jsf;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.faces.view.ViewScoped;
import javax.inject.Named;

@Named
@ViewScoped
public class PersonTextlinesController implements Serializable
{
    private String name;
    private String email;
    
    private List<String> summary = new ArrayList<>();
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String submitName()  {
        summary.add("name="+name);
        return "";
    }
    
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public String submitEmail()  {
        summary.add("email="+email);
        return "";
    }
    
    public List<String> getSummary() {
        return summary;
    }
}

Explanations

The composer.xhtml master page uses two instances of the component, parameterized through attributes of the component tag, which are label, text, buttonLabel and buttonAction. The labels are plain texts, text is a Javabean property, buttonAction is a Java bean method. This makes composite component special. So, here parameterization happens through attributes of the XML-element that represents the component.

We can set the buttons of the two component instances to different callbacks of the backing Java bean. The Java bean returning "" from these callbacks makes JSF stay on the same page. The list on bottom mirrors the history of inputs. Mind that this page suffers from the double-submit problem, refreshing the browser page submits a value.

The texlineSubmit.xhtml component is built like a function. It has a signature-like interface section, and body-like implementation section. Mind that the interface attributes allow definition of default values. Inside the implementation, you refer via cc.attrs.xxx to interface-attribute xxx, this prefix is hardcoded in JSF.

The component's element-tag <texlineSubmit> is the file basename of the component (texlineSubmit.xhtml), and that file must reside in

src/main/webapp/resources/textline/

when the namespace should be http://xmlns.jcp.org/jsf/composite/textline, like it is referenced in head of composer.xhtml.

The PersonTextlinesController bean is @ViewScoped to not lose the list of inputs. Every bean-property also needs an action-callback for that property. In real life we would use different controllers for each instance of a composite component.


Conclusion

It's a little bit hard to know what to use when. The composite for sure is the most reusable component, but it is also the most complex. Still there are more options, e.g. Java tag handlers. JSF is big.




Keine Kommentare: