Data Tables and Iteration
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Jakarta Faces 4.1 (API jakarta.faces-api 4.1.2, part of Jakarta EE 11), Eclipse Mojarra
4.1.17 and Apache MyFaces Core 4.1.4, with Java 17 or later. Faces 4.0 behaves the same for everything below except where
the section on version changes says otherwise. The examples were deployed in a scratch Bookshelf WAR served by Apache
Tomcat 11.0.26 with Weld 6.0.4.Final, once with Mojarra and once with MyFaces, and driven with curl: the HTML shown, the
numbers of rows, the identifiers and every "what happened" statement below come from those runs (checked on 2026-10-08).
Where the two implementations disagree, the section on Mojarra and MyFaces differences collects the cases.
Almost every page of a catalogue application contains a repeated structure: a table of books, a list of reviews, a row of
page links. Faces has two tools for it. h:dataTable is a component that renders an HTML table and gives every row
its own copy of the child components' state. ui:repeat is a Facelets component that renders its children once per
item and adds no markup of its own. Both are driven by a var that names the current item, and both can hold buttons,
links and input fields in their rows, which is what separates them from the JSTL c:forEach tag. This page explains how
they iterate, every attribute and facet that matters, the DataModel abstraction under both, and the usual patterns:
row actions, selection, editable rows, sorting and paging.
How Faces Iterates
h:dataTable is the tag of the component class jakarta.faces.component.html.HtmlDataTable, a subclass of
jakarta.faces.component.UIData (component family jakarta.faces.Data, renderer type jakarta.faces.Table).
ui:repeat is the tag of a Facelets-specific component that is not in the public API; its implementation classes are in
Mojarra and MyFaces. The ideas are the same:
-
The
valueattribute is evaluated and wrapped in ajakarta.faces.model.DataModel. The model is a cursor over rows: it can be moved to a row index and asked for the row object at that index. -
The component moves the cursor from
firstto the last row (or throughrowsrows) by callingsetRowIndex(i). At each position it takes the row object and exposes it under the name given invaras a request attribute, so#{book.title}in a column reads the title of the current row. -
The children (the columns, or the content of the
ui:repeat) exist once in the component tree. Each pass over a row restores the state that belongs to that row (submitted values, validity) into the single set of children, lets them do their work and saves the state again. This is why the client ids of components in rows carry the row index:form:books:2:titleis thetitlecomponent in the third row of thebookstable in theformform. -
After the last row the index is set to
-1, thevarattribute is removed and the children go back to having no row.
The component never holds the row objects between requests. It holds only the per-row state of its input children while it iterates. The data itself must therefore be available again on the postback, and in the same order, which is the subject of the pitfalls below.
| Phase | What the data table does |
|---|---|
Restore View |
The tree, including the table and its columns, is restored. No rows are involved yet. |
Apply Request Values |
|
Process Validations |
Every displayed row is visited again; converters and validators of the row’s inputs run, and each row keeps its own valid flag and submitted value. |
Update Model Values |
Every displayed row is visited once more and the inputs of each row write into that row’s object, for example
|
Invoke Application |
The wrapped event is broadcast. |
Render Response |
|
Source: Jakarta Faces 4.1 API: UIData and Jakarta EE Tutorial: The Lifecycle of a Jakarta Faces Application
The same walk as a sequence. The loop runs once for each phase that processes the table, which is four times on a postback (three of them before the action runs, as the run showed):
The measured consequence is worth stating plainly. In the Mojarra run a table whose value expression called a method
that built a new model each time evaluated that expression three times before the action ran and once more to render the
response, and once for the initial request; with a model cached for the request, Mojarra and MyFaces both walked the rows
four times on the postback. Never put a database query or any other work with side effects in a getter used as value; see the
section on lazy models for the version that does it correctly. The architecture page of this section explains how
client ids are built from naming containers, and the
lifecycle page describes the phases.
The Data Used on This Page
The examples use the Book record of the project setup page, now Serializable because view-scoped beans keep books
between requests, and an application-scoped BookService with twelve books in memory. Its find method sorts and returns
one page, count returns the total, and remove, save and add change the list. A real application would put a
repository behind the same methods; the persistence page of this section covers that.
package org.example.bookshelf.model;
import java.io.Serializable;
public record Book(long id, String title, String author, int year) implements Serializable {
}
package org.example.bookshelf.service;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.example.bookshelf.model.Book;
@ApplicationScoped
public class BookService {
private final List<Book> books = new CopyOnWriteArrayList<>(List.of(
new Book(1, "Pride and Prejudice", "Jane Austen", 1813),
new Book(2, "Emma", "Jane Austen", 1815),
new Book(3, "Frankenstein", "Mary Shelley", 1818),
new Book(4, "Jane Eyre", "Charlotte Bronte", 1847),
new Book(5, "Wuthering Heights", "Emily Bronte", 1847),
new Book(6, "Moby-Dick", "Herman Melville", 1851),
new Book(7, "Great Expectations", "Charles Dickens", 1861),
new Book(8, "Alice in Wonderland", "Lewis Carroll", 1865),
new Book(9, "Dracula", "Bram Stoker", 1897),
new Book(10, "The Time Machine", "H. G. Wells", 1895),
new Book(11, "Heart of Darkness", "Joseph Conrad", 1899),
new Book(12, "The Call of the Wild", "Jack London", 1903)));
public List<Book> findAll() {
return new ArrayList<>(books);
}
public int count() {
return books.size();
}
/** Returns one page, sorted; field is title, author or year. */
public List<Book> find(int first, int pageSize, String sortField, boolean ascending) {
Comparator<Book> order = switch (sortField == null ? "title" : sortField) {
case "author" -> Comparator.comparing(Book::author);
case "year" -> Comparator.comparingInt(Book::year);
default -> Comparator.comparing(Book::title);
};
if (!ascending) {
order = order.reversed();
}
return books.stream().sorted(order).skip(first).limit(pageSize).toList();
}
public void remove(long id) {
books.removeIf(b -> b.id() == id);
}
public void save(Book book) {
for (int i = 0; i < books.size(); i++) {
if (books.get(i).id() == book.id()) {
books.set(i, book);
}
}
}
public void add(Book book) {
books.add(book);
}
}
h:dataTable
The Bookshelf catalogue is a table of books. A table is one h:dataTable whose value is a collection and whose var
names the current row; each h:column child is one column:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core">
<h:head>
<title>Catalogue</title>
</h:head>
<h:body>
<h:dataTable id="books" value="#{catalogue.books}" var="book" rows="3"
styleClass="catalogue" captionClass="caption" headerClass="head" footerClass="foot"
rowClasses="odd,even" columnClasses="num,title,author,num">
<f:facet name="caption">Catalogue</f:facet>
<h:column>
<f:facet name="header">Id</f:facet>
#{book.id}
</h:column>
<h:column rowHeader="true">
<f:facet name="header">Title</f:facet>
#{book.title}
</h:column>
<h:column>
<f:facet name="header">Author</f:facet>
#{book.author}
</h:column>
<h:column footerClass="total">
<f:facet name="header">Year</f:facet>
#{book.year}
<f:facet name="footer">#{catalogue.rowCount} books</f:facet>
</h:column>
</h:dataTable>
</h:body>
</html>
Source: Jakarta EE Tutorial: Using Data-Bound Table Components and Jakarta Faces 4.1 VDL documentation: h:dataTable
Mojarra answered with this markup (blank lines and indentation trimmed, three rows because of rows="3"):
<table id="books" class="catalogue">
<caption class="caption">Catalogue</caption>
<thead>
<tr>
<th class="head" scope="col">Id</th><th class="head" scope="col">Title</th>
<th class="head" scope="col">Author</th><th class="head" scope="col">Year</th>
</tr>
</thead>
<tfoot>
<tr>
<td class="foot"></td><td class="foot"></td><td class="foot"></td>
<td class="total">11 books</td>
</tr>
</tfoot>
<tbody>
<tr class="odd">
<td class="num">8</td>
<th scope="row" class="title">Alice in Wonderland</th>
<td class="author">Lewis Carroll</td>
<td class="num">1865</td>
</tr>
<tr class="even"> ... </tr>
<tr class="odd"> ... </tr>
</tbody>
</table>
The mapping from tags to markup is fixed by the specification of the standard HTML render kit:
-
The
captionfacet becomes acaptionelement directly under thetable;captionClassandcaptionStylego on it. -
A
headerfacet on the table becomes oneththat spans all the columns (colspan,scope="colgroup"). If any column has aheaderfacet, a second header row is written with onethper column (scope="col"); a column without a header facet gets an emptyth. All the header cells are inside athead. -
The
footerfacets follow the same rules in atfootmade oftdcells, without ascope. Thetfootis written before thetbody, the order HTML 4 required. -
A column marked
rowHeader="true"renders its cells asth scope="row"instead oftd, which lets a screen reader announce "Title: Dracula" for the other cells of that row. -
A
colgroupsfacet (one child, normally ah:panelGroupholdingcolgroupandcolmarkup) is written before the header, which helps accessibility and column-wide styling.
The attributes of h:dataTable that control behaviour and style:
| Attribute | Meaning |
|---|---|
|
The data: a |
|
Name of the request attribute under which the current row is exposed. It must be a literal name: the API rejects a
value expression for |
|
Zero-based index of the first row displayed. Bind it to a bean property that your paging actions change. |
|
Number of rows displayed from |
|
Keep the per-row state of input children across requests on the same view (see the editable rows section). The VDL documents the attribute since 4.1; the property exists since 2.1. |
|
Comma-separated row indices before which a new |
|
Comma-separated lists of CSS classes applied cyclically to rows and to columns. |
|
CSS hooks for the table and its parts. A column’s own |
|
Legacy presentational HTML 4 attributes that Faces still renders. Use CSS for new work. |
|
Passed through to the |
|
Common component attributes. |
Columns, Headers and Footers
h:column accepts only id, rendered, binding, headerClass, footerClass and rowHeader. A column has no
colspan, no style and no sortBy: whatever a header needs, for example a sort link, is put in its header facet,
and a facet holds only one child, so wrap several components in a h:panelGroup. Facets belong to the column, not to the
row, so their components keep a client id without a row index: a link in a header facet of the books table in a form
form is form:books:sortTitle, whereas a button in the cells is form:books:2:del.
A table-wide header facet gives a title row above the column headers; there is no standard way to build a header with
several levels or merged cells. For that, either write the HTML table yourself with ui:repeat or use a component
library, the subject of the PrimeFaces data components page of this section.
Two details of the rendered class lists, both visible in the run:
-
rowClasses="odd,even"restarts at the first class for every table, so row 0 isodd. The lists cycle: with two classes, row 2 isoddagain. Spaces after the commas are copied:columnClasses="num, title"producedclass=" title"with a leading space. Write the list without spaces. Separate several classes for one cell with spaces, for examplecolumnClasses="num,title wide". -
When there are fewer entries in
columnClassesthan columns, the remaining columns get noclass; extra entries are ignored.
Grouped body rows: bodyrows="0,2,4" starts a new tbody before the rows at those indices. With five rows it produced
three tbody elements holding two, two and one row. With bodyrows="2,4" the first two rows are written without any
tbody, because the attribute lists only the rows that start a body.
Empty Tables
A table whose collection is empty (or whose value is null) is still rendered, with its caption, header and footer, and
with one empty body row in the form <tr><td></td></tr>, both in Mojarra and in MyFaces. A styled blank row looks like a
data row, so show nothing or a message instead:
<h:dataTable id="books" value="#{catalogue.books}" var="book" rendered="#{not empty catalogue.books}">
<h:column>#{book.title}</h:column>
</h:dataTable>
<h:outputText value="No books yet." rendered="#{empty catalogue.books}"/>
ui:repeat
<ui:repeat> iterates without producing a table. It has no markup, so the children decide the output: list items,
definition lists, cards, table rows of a hand-written table. It takes the same kinds of value as h:dataTable
(a List, an array, a Map, any Iterable, a DataModel, a ResultSet or a single object, which is one row) and
does nothing when the value is null.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:ui="jakarta.faces.facelets">
<h:head><title>ui:repeat</title></h:head>
<h:body>
<ul id="list">
<ui:repeat value="#{catalogue.books}" var="book" varStatus="status" begin="0" end="2">
<li class="#{status.even ? 'even' : 'odd'}">#{status.index + 1}. #{book.title}</li>
</ui:repeat>
</ul>
<dl id="authors">
<ui:repeat value="#{catalogue.countByAuthor}" var="entry" begin="0" end="2">
<dt>#{entry.key}</dt>
<dd>#{entry.value}</dd>
</ui:repeat>
</dl>
<ul id="nested">
<ui:repeat value="#{misc.groups}" var="group" begin="0" end="1">
<li>#{group.author}
<ul>
<ui:repeat value="#{group.books}" var="book">
<li>#{book.title}</li>
</ui:repeat>
</ul>
</li>
</ui:repeat>
</ul>
<p id="range">
<ui:repeat value="#{catalogue.books}" var="book" varStatus="s" begin="1" end="7" step="3">[#{s.index}:#{s.first}:#{s.even}] </ui:repeat>
</p>
</h:body>
</html>
The output (Mojarra and MyFaces gave the same markup; the books in the two implementations differed only because each had its own copy of the data):
<ul id="list">
<li class="even">1. Alice in Wonderland</li>
<li class="odd">2. Dracula</li>
<li class="even">3. Emma</li>
</ul>
<dl id="authors">
<dt>Bram Stoker</dt><dd>1</dd>
<dt>Charles Dickens</dt><dd>1</dd>
<dt>Charlotte Bronte</dt><dd>1</dd>
</dl>
<p id="range">[1:true:true] [4:false:false] [7:false:true] </p>
Three things to read from this output. Over a Map the variable is the entry, so entry.key and entry.value work. The
status of the first item rendered is even, whatever its index (even and odd count iterations from zero, not item
indices). And begin="1" end="7" step="3" visited the items 1, 4 and 7: end is inclusive.
Attributes and varStatus
| Attribute | Meaning |
|---|---|
|
Required. The data and the name of the current item. Both are scoped to the body of the tag. |
|
Name of a request-scoped variable holding the status of the iteration, with the read-only properties listed below. |
|
Zero-based index of the first item and of the last item, inclusive. |
|
The VDL describes |
|
Process every |
|
Same meaning as on |
|
|
|
As on any component. An |
varStatus property |
Value |
|---|---|
|
The zero-based index of the current item in the collection. With |
|
The attributes of the same name, or empty when they were not set (Mojarra) or |
|
|
|
|
|
Parity of the iteration number counted from the first item rendered, not of |
varStatus has no count property in the VDL. Asking for it gave 0 for every item in Mojarra and a
PropertyNotFoundException in MyFaces. Use #{status.index + 1} for a one-based number. h:dataTable has no
varStatus at all: the attribute is ignored, and #{status.index} renders as an empty string. To number the rows of a
table, ask the table for its row index through the component’s naming container:
<h:dataTable id="t" value="#{catalogue.books}" var="b" first="2" rows="3">
<h:column><h:outputText value="#{component.namingContainer.rowIndex + 1}"/></h:column>
<h:column>#{b.title}</h:column>
</h:dataTable>
This printed 3, 4 and 5 in front of the third, fourth and fifth books on both implementations: rowIndex is the absolute
index in the collection, not the position in the page, so it is the right number when first is not zero.
ui:repeat, h:dataTable and c:forEach
h:dataTable |
ui:repeat |
c:forEach |
|
|---|---|---|---|
Output |
an HTML |
none: the children |
none: the children |
When it acts |
render time, for every phase that touches the rows |
render time, for every phase that touches the rows |
view build time, once |
Components per row |
one set, state saved per row |
one set, state saved per row |
one copy of each component per item |
Row index in client ids |
yes ( |
yes ( |
no: the copies need distinct ids written by hand |
Inputs and buttons inside |
yes |
yes |
yes, but only for a list that does not change between requests |
Row status |
|
|
|
Choose h:dataTable for tabular data and ui:repeat for everything else that repeats. Use c:forEach only for the
structural cases explained on the JSTL page. A plain
HTML attribute inside a ui:repeat is not rewritten: with <span id="x"> in the body, twelve rows produced twelve
elements with id="x", which is invalid HTML. Build the id from the status, for example id="row-#{status.index}", or
use a Faces component, whose client id already contains the index.
Inputs and buttons work inside a ui:repeat as they do in a table. In the test run a <ui:repeat id="r" ...> inside the form
fm rendered fm:r:0:t and fm:r:1:btn, and a click on the second button called #{rowActions.remove(b)} with the
second book. Templating-related uses of ui:repeat with ui:include are covered on the
templating page.
Mojarra and MyFaces Differences in ui:repeat
The range attributes were run against a list of twelve books on both implementations. The two disagree where the VDL is loose, so use only the combinations in the first line of the table if the application may change implementation.
| Attributes | Mojarra 4.1.17 | MyFaces 4.1.4 |
|---|---|---|
|
items 2, 3, 4 |
items 2, 3, 4 |
|
items 1, 4, 7 |
items 1, 4, 7 |
|
items 0 to 3 |
all twelve items: |
|
items 8 to 11 |
nothing rendered |
|
items 8 to 11 |
items 8 to 11 |
|
items 0 to 2 |
items 0 to 2 |
|
item 2 only: |
items 2, 3, 4: |
|
one item |
nothing |
|
|
exception, "end cannot be greater than collection size" or "iteration size cannot be greater than collection size" |
|
|
|
|
empty |
|
So: set both begin and end, keep end below the item count, and do not rely on last when end is set. To show
"the first five" of a list that might be shorter, iterate over a sublist built in the bean
(books.subList(0, Math.min(5, books.size()))) instead of passing an end that may be too large.
Source: Jakarta Faces 4.1 VDL documentation: ui:repeat and the measurements described at the top of this page
rowStatePreserved
Both components keep a single set of children, and they save and restore the state of the input children (value, local
value flag, valid flag, submitted value) as the cursor moves. Properties that you change on a child component in code, a
styleClass set from a listener for one row only, are not part of that saved set and are lost when the cursor leaves the
row. Setting rowStatePreserved="true" makes the component save and restore the whole state of the children of a row
(StateHolder.saveState and TransientStateHolder.saveTransientState) when it enters and leaves the row, and keep it
across requests on the same view. The Javadoc is explicit about the price and the limit: it adds overhead, it should be
used judiciously, and it works reliably only when the data model does not change across requests on the same view, for
example by sorting or by adding or removing rows. For the alternative it suggests c:forEach.
The attribute has existed on UIData since Faces 2.1; Faces 4.1 added it to UIRepeat and finally documented it in the
VDL of h:dataTable. One thing it does not do, in the run on both implementations: after a row’s input failed validation,
a later Ajax request on another row that re-rendered the table showed the model value in that input, with
rowStatePreserved set to false or to true. The attribute is not a way to keep invalid input on screen.
DataModel and ListDataModel
jakarta.faces.model.DataModel<E> is the abstraction between a data source and the components that iterate. It is an
abstract class, and an Iterable<E>, with a small contract:
| Member | Contract |
|---|---|
|
Move the cursor to a zero-based row, or to |
|
Whether there is a row at the current index. |
|
The row object at the current index. |
|
Number of rows, or |
|
The underlying collection. Concrete models must have a public no-argument constructor that wraps nothing and a convenience constructor that takes the wrapped object. |
|
Register |
|
A read-only iterator over the row data. |
Source: Jakarta Faces 4.1 API: DataModel
What Faces Wraps Automatically
UIData adapts the value to a DataModel when it is not one already. These are the concrete classes in the
jakarta.faces.model package and the type each one wraps:
| Class | Wraps |
|---|---|
|
an array of objects |
|
a |
|
a |
|
a |
|
an |
|
any other single object, shown as a one-row table |
In the run, Set and Map values, an array (#{misc.page.items.toArray()}), a single Book and a List all rendered
as tables without any explicit wrapper. The @FacesDataModel Javadoc gives the order of adaptation: the wrappers for specific
types such as Set are tried first, then the classes registered with @FacesDataModel, and ScalarDataModel is the last
resort. The Jakarta EE Tutorial still
lists the JSTL Result type among the possible values. The 4.1 jakarta.faces.model package has no class for it, so
treat that sentence as historical.
Most pages never create a DataModel. Create one yourself when you need its cursor in the bean: the row that was clicked,
the row count, or a model that loads rows lazily.
A Custom DataModel with @FacesDataModel
A class annotated with @FacesDataModel(forClass = ...) registers a DataModel for a type that Faces cannot wrap on
its own. Take a repository result that is not a collection:
package org.example.bookshelf.model;
import java.util.List;
/** A page of books as returned by a repository; not a Collection, so Faces cannot wrap it by itself. */
public record BookPage(List<Book> items, int pageNumber, long totalItems) {
}
package org.example.bookshelf.web;
import jakarta.enterprise.context.Dependent;
import jakarta.faces.model.DataModel;
import jakarta.faces.model.FacesDataModel;
import java.util.List;
import org.example.bookshelf.model.BookPage;
@Dependent
@FacesDataModel(forClass = BookPage.class)
public class BookPageDataModel<E> extends DataModel<E> {
private BookPage page;
private int rowIndex = -1;
@Override
public boolean isRowAvailable() {
return page != null && rowIndex >= 0 && rowIndex < page.items().size();
}
@Override
public int getRowCount() {
return page == null ? -1 : page.items().size();
}
@Override
@SuppressWarnings("unchecked")
public E getRowData() {
if (!isRowAvailable()) {
throw new IllegalArgumentException("no row at " + rowIndex);
}
return (E) page.items().get(rowIndex);
}
@Override
public int getRowIndex() {
return rowIndex;
}
@Override
public void setRowIndex(int rowIndex) {
this.rowIndex = rowIndex;
}
@Override
public Object getWrappedData() {
return page;
}
@Override
public void setWrappedData(Object data) {
this.page = (BookPage) data;
this.rowIndex = data == null ? -1 : 0;
}
}
With this class, <h:dataTable value="#{misc.page}" var="b"> iterates the books of the page. Without it, Faces wrapped
the BookPage in a ScalarDataModel and the page failed with a missing property title on BookPage, because the row
was the page itself. The precise way to get it discovered is a quirk confirmed in the run on both implementations: with
bean-discovery-mode="annotated" the class had to carry a bean-defining annotation, here @Dependent. Without it the
@FacesDataModel class was not found and the table silently fell back to the single-row wrapper. Using
bean-discovery-mode="all" would avoid the extra annotation, at the price of making every class in the archive a bean.
Row Actions
A click on a button in a row must act on that row. Faces offers four ways to know which one. The method-parameter form was run on both implementations and the others on Mojarra. The first three need the same collection to be there on the postback (see the pitfalls); the fourth carries the identity of the row from the client.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core"
xmlns:ui="jakarta.faces.facelets">
<h:head><title>Actions</title></h:head>
<h:body>
<h:form id="form">
<h:messages id="msgs"/>
<h:dataTable id="books" value="#{rowActions.model}" var="book">
<h:column><h:selectBooleanCheckbox id="pick" value="#{rowActions.selected[book.id]}"/></h:column>
<h:column>#{book.title}</h:column>
<h:column>
<h:commandButton id="del" value="Delete (argument)" action="#{rowActions.remove(book)}"/>
<h:commandLink id="del2" value="Delete (rowData)" action="#{rowActions.removeCurrent}"/>
<h:commandLink id="del3" value="Delete (listener)" action="#{rowActions.removeSelectedProperty}">
<f:setPropertyActionListener target="#{rowActions.current}" value="#{book}"/>
</h:commandLink>
</h:column>
</h:dataTable>
<h:commandButton id="bulk" value="Delete selected" action="#{rowActions.deleteSelected}"/>
<h:outputText id="last" value="#{rowActions.lastAction}"/>
</h:form>
</h:body>
</html>
package org.example.bookshelf.web;
import jakarta.annotation.PostConstruct;
import jakarta.faces.model.DataModel;
import jakarta.faces.model.ListDataModel;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;
@Named("rowActions")
@ViewScoped
public class RowActionsBean implements Serializable {
@Inject
private BookService service;
private List<BookRow> rows;
private DataModel<Book> model;
private final Map<Long, Boolean> selected = new HashMap<>();
private Book current;
private String lastAction = "";
@PostConstruct
void init() {
reload();
}
private void reload() {
rows = new ArrayList<>();
for (Book b : service.findAll()) {
rows.add(new BookRow(b));
}
model = new ListDataModel<>(service.findAll());
}
public List<BookRow> getRows() { return rows; }
public DataModel<Book> getModel() { return model; }
public Map<Long, Boolean> getSelected() { return selected; }
public Book getCurrent() { return current; }
public void setCurrent(Book current) { this.current = current; }
public String getLastAction() { return lastAction; }
/** Row passed as an EL method argument. */
public void remove(Book book) {
service.remove(book.id());
lastAction = "removed " + book.title();
reload();
}
/** Row taken from the DataModel the table iterates over. */
public void removeCurrent() {
Book book = model.getRowData();
service.remove(book.id());
lastAction = "removed (rowData) " + book.title();
reload();
}
/** Row set by f:setPropertyActionListener. */
public void removeSelectedProperty() {
service.remove(current.id());
lastAction = "removed (listener) " + current.title();
reload();
}
public void deleteSelected() {
int n = 0;
for (Map.Entry<Long, Boolean> e : selected.entrySet()) {
if (Boolean.TRUE.equals(e.getValue())) {
service.remove(e.getKey());
n++;
}
}
selected.clear();
lastAction = "deleted " + n;
reload();
}
public void edit(BookRow row) { row.setEditing(true); }
public void cancel(BookRow row) { row.reset(); }
public void save(BookRow row) {
Book b = row.getBook();
service.save(new Book(b.id(), row.getTitle(), row.getAuthor(), b.year()));
lastAction = "saved " + row.getTitle();
reload();
}
}
package org.example.bookshelf.web;
import java.io.Serializable;
import org.example.bookshelf.model.Book;
/** Edit state for one row: the immutable Book stays untouched until Save. */
public class BookRow implements Serializable {
private final Book book;
private boolean editing;
private String title;
private String author;
public BookRow(Book book) {
this.book = book;
reset();
}
public void reset() {
this.title = book.title();
this.author = book.author();
this.editing = false;
}
public Book getBook() { return book; }
public boolean isEditing() { return editing; }
public void setEditing(boolean editing) { this.editing = editing; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getAuthor() { return author; }
public void setAuthor(String author) { this.author = author; }
}
Source: Jakarta EE Tutorial: Registering Listeners on Components and Jakarta Faces 4.1 VDL documentation: f:setPropertyActionListener
| Technique | How it works and what the run showed |
|---|---|
Method with a parameter: |
Expression Language 2.2 method parameters. |
The model’s cursor: |
The bean keeps the |
|
Copies the row into a bean property before the action runs. The second row’s link set |
|
The browser sends the value that was rendered, so the identity is the client’s, not the position in a list. See the next section. |
Selecting rows uses a Map in the bean and one check box per row: value="#{rowActions.selected[book.id]}" writes
true for each ticked row into selected under the book’s id. Ticking rows 1 and 3 and pressing "Delete selected"
removed exactly those two books. The Map approach needs no selected flag on the model and survives sorting, because
the key is the id, not the position. Clear the map after use, or the ids of deleted books stay in it.
Act on an Identifier, Not a Position
Everything above resolves the row by its index when the postback arrives. If the list is different on the postback from
what the browser showed, the index points at another book. The run reproduced it with a request-scoped bean whose
getBooks() re-queries the service, sorted by title:
Browser A clicked "Delete" next to Alice in Wonderland, and the server deleted Aardvark Tales, a book A had never
seen. It happened on both Mojarra and MyFaces. Two fixes. Keep the list in a view-scoped bean (the examples on this page
do; see the scopes page), so the postback sees the list the browser was shown. And, when the rows can change under the user, send the
identity from the client with f:param:
<h:commandLink id="delId" value="Delete by id" action="#{live.removeById}">
<f:param name="id" value="#{book.id}"/>
</h:commandLink>
package org.example.bookshelf.web;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.faces.annotation.ManagedProperty;
import jakarta.inject.Named;
import java.util.List;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;
/** Request-scoped on purpose: re-queries the list on every request. */
@Named("live")
@RequestScoped
public class LiveBean {
@Inject
private BookService service;
@Inject
@ManagedProperty("#{param.id}")
private Long id;
private List<Book> books;
public List<Book> getBooks() {
if (books == null) {
books = service.find(0, 100, "title", true);
}
return books;
}
public void remove(Book book) {
service.remove(book.id());
}
public void removeById() {
service.remove(id);
}
public void addFirst() {
service.add(new Book(99, "Aardvark Tales", "A. Writer", 2000));
}
}
In the same sequence on Mojarra, the "by id" link on Alice in Wonderland removed Alice (id 8) and left Aardvark Tales
alone. The parameter is a request parameter, injected with @Inject @ManagedProperty("#{param.id}"); Faces evaluates the
expression when the bean instance is created, so the bean is request-scoped. Treat the id as untrusted input: load the
entity and check on the server that the user may change it, which the security page of this section covers. The f:param
form was run on h:commandLink; this page does not claim it for h:commandButton.
Editable Rows
Inputs inside a column are read and written per row. The pattern that works with immutable records such as Book is a
small view model per row, BookRow, holding the edit flag and the draft values; the table shows the draft inputs for the
row being edited and read-only text for the others. Saving copies the draft into a new Book.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core"
xmlns:ui="jakarta.faces.facelets">
<h:head><title>Editable</title></h:head>
<h:body>
<h:form id="form">
<h:dataTable id="books" value="#{rowActions.rows}" var="row">
<h:column>
<h:outputText value="#{row.book.title}" rendered="#{not row.editing}"/>
<h:inputText id="title" value="#{row.title}" required="true" rendered="#{row.editing}"/>
<h:message id="titleMsg" for="title" style="color:red"/>
</h:column>
<h:column>
<h:commandButton id="edit" value="Edit" action="#{rowActions.edit(row)}" rendered="#{not row.editing}"/>
<h:commandButton id="save" value="Save" action="#{rowActions.save(row)}" rendered="#{row.editing}"/>
<h:commandButton id="cancel" value="Cancel" action="#{rowActions.cancel(row)}" immediate="true" rendered="#{row.editing}"/>
</h:column>
</h:dataTable>
<h:outputText id="last" value="#{rowActions.lastAction}"/>
</h:form>
</h:body>
</html>
The RowActionsBean and BookRow shown above provide rows, edit, save and cancel. Behaviour measured on both
implementations:
-
Pressing "Edit" on the second row rendered a text field with the title only in that row (
form:books:1:title). -
"Save" with an empty title failed validation of that row only:
form:books:1:titleMsgcontained "Validation Error: Value is required." and the other rows had empty message spans. The field kept the empty submitted value and the page stayed in edit mode. -
"Cancel" has
immediate="true", so it runs in Apply Request Values and skips validation of the empty field;reset()restored the draft and the row returned to read-only. Withoutimmediate, the empty required field would fail validation before the cancel action could run. Useimmediateonly on buttons that must ignore the form, and never for an action that saves. -
A
h:message for="title"inside a column shows the message of the row being rendered, because theforis resolved inside the row’s naming container.
For a table where every row is always editable, bind the inputs straight to the row objects (value="#{book.title}") and
add one "Save all" button; Update Model Values writes every row, and a mutable row type is required.
Pagination and Sorting by Hand
Faces has no built-in pager or sorter: the standard table renders what it is given. The two tools are the first and
rows attributes for paging and a bean that reorders the data for sorting.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core"
xmlns:ui="jakarta.faces.facelets">
<h:head><title>Paging</title></h:head>
<h:body>
<h:form id="form">
<h:dataTable id="books" value="#{catalogue.books}" var="book" first="#{catalogue.first}" rows="#{catalogue.pageSize}">
<h:column>
<f:facet name="header">
<h:commandLink id="sortTitle" value="Title" action="#{catalogue.sortBy('title')}"/>
</f:facet>
#{book.title}
</h:column>
<h:column>
<f:facet name="header">
<h:commandLink id="sortAuthor" value="Author" action="#{catalogue.sortBy('author')}"/>
</f:facet>
#{book.author}
</h:column>
<h:column>
<f:facet name="header">
<h:commandLink id="sortYear" value="Year" action="#{catalogue.sortBy('year')}"/>
</f:facet>
#{book.year}
</h:column>
</h:dataTable>
<h:commandButton id="prev" value="Previous" action="#{catalogue.previous}" disabled="#{not catalogue.hasPrevious}"/>
<h:outputText id="range" value="#{catalogue.first + 1}-#{catalogue.first + catalogue.pageSize} of #{catalogue.rowCount}"/>
<h:commandButton id="next" value="Next" action="#{catalogue.next}" disabled="#{not catalogue.hasNext}"/>
</h:form>
</h:body>
</html>
package org.example.bookshelf.web;
import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;
@Named("catalogue")
@ViewScoped
public class CatalogueBean implements Serializable {
@Inject
private BookService service;
private List<Book> books;
private int first;
private int pageSize = 5;
private String sortField = "title";
private boolean ascending = true;
@PostConstruct
void init() {
books = service.findAll();
books.sort(comparator());
}
/** The whole list, for tables that page with first/rows. */
public List<Book> getBooks() {
return books;
}
/** One page, for the "paging in the service" variant. */
public List<Book> getPage() {
return service.find(first, pageSize, sortField, ascending);
}
/** Books per author, to show ui:repeat over a Map. */
public Map<String, Integer> getCountByAuthor() {
Map<String, Integer> counts = new TreeMap<>();
for (Book b : books) {
counts.merge(b.author(), 1, Integer::sum);
}
return counts;
}
public int getFirst() { return first; }
public void setFirst(int first) { this.first = first; }
public int getPageSize() { return pageSize; }
public int getRowCount() { return books.size(); }
public String getSortField() { return sortField; }
public boolean isAscending() { return ascending; }
public boolean isHasPrevious() { return first > 0; }
public boolean isHasNext() { return first + pageSize < books.size(); }
public void next() {
if (isHasNext()) {
first += pageSize;
}
}
public void previous() {
first = Math.max(0, first - pageSize);
}
public void sortBy(String field) {
if (field.equals(sortField)) {
ascending = !ascending;
} else {
sortField = field;
ascending = true;
}
first = 0;
books.sort(comparator());
}
private Comparator<Book> comparator() {
Comparator<Book> order = switch (sortField) {
case "author" -> Comparator.comparing(Book::author);
case "year" -> Comparator.comparingInt(Book::year);
default -> Comparator.comparing(Book::title);
};
return ascending ? order : order.reversed();
}
}
How it behaved:
-
rows="#{catalogue.pageSize}"showed five of twelve books; "Next" movedfirstto 5 and the range text read "6-10 of 12" with the second page of the title-sorted list. -
A click on the "Year" header link ran
sortBy('year'), which resetfirstto 0 and sorted the whole list, and the first page showed 1813, 1815, 1818, 1847, 1847. The links are in theheaderfacet, so the control ids have no row index (form:books:sortYear). -
The view-scoped bean keeps
sortField,ascendingandfirstacross the postbacks. A request-scoped bean would forget them on every click. -
The buttons are disabled at the ends with
disabled="#{not catalogue.hasPrevious}". A disabled button is not submitted, so nobody can overrun the list, but still check the bounds innext().
To page without a full-page reload, add f:ajax to the buttons and re-render the table, the range and both buttons; the
ids resolve from the button, which sits in the same form:
<h:commandButton id="next" value="Next" action="#{catalogue.next}" disabled="#{not catalogue.hasNext}">
<f:ajax render="books range prev next"/>
</h:commandButton>
The ajax response contained updates for form:books, form:prev, form:range and form:next. A cell in one row can be re-rendered alone: a button with <f:ajax render="cell"/>
next to <h:outputText id="cell" .../> in the same row produced onclick="mojarra.ab(this,event,'action',0,'form:books:1:cell')"
and a partial response with an update for form:books:1:cell only, "Emma (touched 1)". The Ajax page of this section covers f:ajax itself.
Paging in the Service
The example above loads the whole list and shows a window of it, which is fine for hundreds of rows. For thousands, ask
the service for one page. CatalogueBean.getPage() in the listing calls service.find(first, pageSize, sortField,
ascending) and the table binds to it without first and rows, since the list already is the page. Because a getter
runs several times per request, cache the page in the bean (or compute it in an action) rather than querying inside the
getter, as the pitfalls list. The queries behind find are the place where the choice between offset and keyset paging
matters; the pagination strategies page explains why a large OFFSET gets slow
and how a keyset (seek) query avoids that. A keyset pager has no "page 7" link, only "next" and "previous", which fits the
simplest Faces pager.
A Lazy DataModel
When the total is large and the user pages through, a DataModel can load one page at a time and report the full
getRowCount() for the pager. The table walks the rows first to first + rows - 1 using absolute indices, so the
model translates an absolute index to the page it holds:
package org.example.bookshelf.web;
import jakarta.faces.model.DataModel;
import java.util.List;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;
/**
* Loads only the rows of the page being rendered. The table asks for absolute
* row indices (first .. first + rows - 1); the model translates them to the
* window it currently holds.
*/
public class LazyBookModel extends DataModel<Book> {
private final BookService service;
private final int pageSize;
private final String sortField;
private final boolean ascending;
private int rowIndex = -1;
private int windowStart = -1;
private List<Book> window = List.of();
private int count = -1;
public LazyBookModel(BookService service, int pageSize, String sortField, boolean ascending) {
this.service = service;
this.pageSize = pageSize;
this.sortField = sortField;
this.ascending = ascending;
}
@Override
public int getRowCount() {
if (count < 0) {
count = service.count();
}
return count;
}
@Override
public boolean isRowAvailable() {
return rowIndex >= 0 && rowIndex < getRowCount();
}
@Override
public Book getRowData() {
if (!isRowAvailable()) {
throw new IllegalArgumentException("no row at " + rowIndex);
}
int start = rowIndex / pageSize * pageSize;
if (start != windowStart) {
window = service.find(start, pageSize, sortField, ascending);
windowStart = start;
}
return window.get(rowIndex - windowStart);
}
@Override
public int getRowIndex() {
return rowIndex;
}
@Override
public void setRowIndex(int rowIndex) {
this.rowIndex = rowIndex;
}
@Override
public Object getWrappedData() {
return window;
}
@Override
public void setWrappedData(Object data) {
throw new UnsupportedOperationException("read-only model");
}
}
package org.example.bookshelf.web;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.model.DataModel;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;
@Named("lazy")
@ViewScoped
public class LazyCatalogueBean implements Serializable {
private static final String MODEL_KEY = "bookshelf.lazyBookModel";
@Inject
private BookService service;
@Inject
private ExternalContext externalContext;
private int first;
private final int pageSize = 4;
private String sortField = "title";
private boolean ascending = true;
/** One model per request: the table asks for it in every phase it takes part in. */
@SuppressWarnings("unchecked")
public DataModel<Book> getModel() {
return (DataModel<Book>) externalContext.getRequestMap().computeIfAbsent(MODEL_KEY,
key -> new LazyBookModel(service, pageSize, sortField, ascending));
}
public int getFirst() { return first; }
public void setFirst(int first) { this.first = first; }
public int getPageSize() { return pageSize; }
public void next() {
if (first + pageSize < service.count()) {
first += pageSize;
}
}
public void previous() {
first = Math.max(0, first - pageSize);
}
public void sortBy(String field) {
ascending = field.equals(sortField) ? !ascending : true;
sortField = field;
first = 0;
externalContext.getRequestMap().remove(MODEL_KEY);
}
}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core">
<h:head><title>Lazy</title></h:head>
<h:body>
<h:form id="form">
<h:dataTable id="books" value="#{lazy.model}" var="book" first="#{lazy.first}" rows="#{lazy.pageSize}">
<h:column><f:facet name="header"><h:commandLink id="sortTitle" value="Title" action="#{lazy.sortBy('title')}"/></f:facet>#{book.title}</h:column>
<h:column><f:facet name="header"><h:commandLink id="sortYear" value="Year" action="#{lazy.sortBy('year')}"/></f:facet>#{book.year}</h:column>
</h:dataTable>
<h:commandButton id="prev" value="Previous" action="#{lazy.previous}"/>
<h:commandButton id="next" value="Next" action="#{lazy.next}"/>
</h:form>
</h:body>
</html>
Source: Jakarta Faces 4.1 API: DataModel
Two points were measured, and they explain the shape of the bean. The first version of the bean built a new
LazyBookModel in getModel(). Because the table asks for its value in each phase, a click on "Next" in the Mojarra run
loaded the first window three times (before the action) and the new window once more to render. Storing the model in the request map
for the request’s duration, as getModel() above does, cut that to one load of the old window plus one of the new
one on both implementations. And changing the sort order has to discard the cached model (sortBy removes the request
attribute), or the render phase would reuse the window loaded in the order before the click. On a plain initial request
the model loaded exactly one window on both implementations.
Do not keep the model in the view-scoped bean itself unless you reset its cache on every request: a model that remembers its window would show stale data after a change made elsewhere, because a view-scoped bean lives across requests.
Nested Tables, Groups and Dynamic Columns
A table can sit in a column of another table, which is how a master-detail layout is built. The inner table’s client id
includes the outer row (groups:0:inner, groups:1:inner):
<h:dataTable id="groups" value="#{misc.groups}" var="g" rows="2">
<h:column>#{g.author}
<h:dataTable id="inner" value="#{g.books}" var="b">
<h:column>#{b.title}</h:column>
</h:dataTable>
</h:column>
</h:dataTable>
A table whose columns come from data can be written with c:forEach around the h:column tags, because a column is
structure that is decided at view build time. In the run, #{b[col.property]} read the property named by each column
descriptor:
<h:dataTable id="dyn" value="#{catalogue.books}" var="b" rows="2">
<c:forEach items="#{misc.columns}" var="col">
<h:column>
<f:facet name="header">#{col.label}</f:facet>
#{b[col.property]}
</h:column>
</c:forEach>
</h:dataTable>
The list of columns must be stable between the request that builds the view and the postbacks, for example a constant or
a view-scoped list; the JSTL page explains why a list that changes breaks c:forEach. The c: prefix is
jakarta.tags.core.
Common Mistakes
-
Logic or queries in the getter bound to
value. The value is evaluated once for each phase that processes the table. Load in@PostConstructof a view-scoped bean or in an action, or cache for the request as the lazy model does. -
A request-scoped bean behind a table with actions. The postback rebuilds the list, and an index can point at a different row. Use a view-scoped bean, or act on identifiers.
-
Binding the table to a long-lived bean. Binding the
UIDatawithbindingto a bean that outlives the request is the mistake described in the binding section of the architecture page; use a method parameter or aDataModelinstead. -
Different data on the postback than on render. Sorting a shared list in a getter, or a refresh between render and click, shifts the rows under the user’s click. The page reproduced it.
-
Duplicate ids in a
ui:repeat. Plain HTML ids are not rewritten per item. -
An empty row for an empty table. Use
renderedon the table and a message. -
Relying on
varStatus.lastwithend, or onsize/offsetas a window. Both implementations differ. -
@FacesDataModelnot found. Add a bean-defining annotation. -
Using
varStatusonh:dataTable. It does not exist there. -
Large
rowswithfirstbound to user input. Clampfirstbetween 0 and the count; a non-numeric or negative value must not reach the table (setFirstrejects a negative number with anIllegalArgumentException). -
Presentational attributes (
border,cellpadding,width). They are legacy; style with CSS.
Mojarra and MyFaces Compared
Beyond the ui:repeat table above, the runs showed these differences for h:dataTable, none of which changes what a
well-written page does, but all of which are visible in the markup:
| Observation | Mojarra 4.1.17 | MyFaces 4.1.4 |
|---|---|---|
Output formatting |
several lines per row, with the whitespace of the source |
compact, almost no whitespace |
|
|
|
|
written |
not written: the rows follow the table directly |
Spaces after the commas of |
copied into |
trimmed |
Form submission marker |
hidden |
hidden |
Empty value |
one empty |
the same |
Both implementations behaved the same for row actions, selection, validation of a row, the lazy model’s loads, and the
@FacesDataModel discovery rule.
What Changed per Version
| Version | Change |
|---|---|
1.x |
|
2.0 |
Facelets, with |
2.1 |
|
2.2 |
|
2.3 |
|
4.0 |
JSP views are gone, so every table is a Facelets view; namespaces become URNs ( |
4.1 |
|
The 5.0 milestones (5.0.0-M9, 2026-09-18) are not treated here: no change to these components was checked, and
anything about Faces 5.0 is upcoming, not current behaviour (the planned list is on the versions page). The Faces 4.0 row above and the statement that 4.0 behaves the same were not run on 4.0. The 1.x and 2.0 rows are background knowledge about the original design, not checked against the old specifications.
Where the Books Are Outdated
The concepts of the older Faces books on data tables are still true: a table is a component bound to a DataModel, with
columns, facets and a per-row cursor. The details differ.
-
JSP and the
javax.facesnamespaces. The JSF 1.x books (2004 to 2006) are JSP-based. JSP views were removed in 4.0, the package isjakarta.facessince 3.0, and the tags live in the URNsjakarta.faces.htmlandjakarta.faces.core. Facelets is the only view technology. -
@ManagedBeanbacking beans. Tables are fed from CDI@Namedbeans with@ViewScopedfromjakarta.faces.view, not fromjavax.faces.bean.ManagedBean, which Faces 4.0 removed. A table with actions needs the list to survive the postback, which is what the view scope is for. -
DataModeltypes. The books listArrayDataModel,ListDataModel,ResultSetDataModelandScalarDataModel.CollectionDataModel,IterableDataModeland@FacesDataModelcame with 2.3, so aSet, aMapor a custom type works without a hand-written wrapper. -
Sorting and paging. The standard table never had a built-in pager or sorter. The JSF 2.0 Cookbook targets RichFaces 3.3, Tomahawk and Trinidad for richer tables; those libraries are not part of Jakarta Faces, so check each project before using it with Faces 4.1. The PrimeFaces data components, covered on the data components page of the PrimeFaces part of this section, supply sorting, filtering, selection, lazy loading and in-cell editing. The PrimeFaces Cookbook targets PrimeFaces 5.1 and 5.2, so verify every
p:dataTableattribute and class name against the PrimeFaces 16 documentation before copying it. -
ui:repeatandjsfc. The Facelets book of 2008 usesui:repeatand thejsfcattribute to turn plain HTML elements into components.ui:repeatis as described here. For plain HTML markup that is also a component, use the pass-through elements of the HTML5 friendly markup page. -
Row context. A common older pattern binds the table to a bean with
bindingand readsgetRowData()from it. Use a method parameter, aDataModelheld by a view-scoped bean, or an id. -
Ajax. The JSF 1.x books predate
f:ajax. A single row or the whole table is re-rendered with the client ids shown on this page.
Use the books for the idea of the model, the cursor and the row-state saving, and this page for what to type.
Source: Mert Çalışkan and Oleg Varaksin, PrimeFaces Cookbook, Second Edition (Packt, 2015, ISBN 978-1-78439-342-7); Ed Burns, Chris Schalk and Neil Griffin, JavaServer Faces 2.0: The Complete Reference (McGraw-Hill, 2010, ISBN 978-0-07-162509-8); Kito D. Mann, JavaServer Faces in Action (Manning, 2005, ISBN 978-1-932394-12-2); Bruno Aranda and Zubin Wadia, Facelets Essentials (Apress, 2008, ISBN 978-1-4302-1049-8)
References
-
Jakarta EE Tutorial: Using Data-Bound Table Components (the tutorial
currentis the Jakarta EE 10 one) -
Jakarta EE Tutorial: Optional Attributes for the h:dataTable Tag
-
Jakarta EE Tutorial: The Lifecycle of a Jakarta Faces Application
-
Jakarta Faces 4.1 specification, 9.5 Standard HTML RenderKit Tag Library
-
Çalışkan, Varaksin: PrimeFaces Cookbook, Second Edition, Packt, 2015, publisher page
-
Burns, Schalk, Griffin: JavaServer Faces 2.0: The Complete Reference, McGraw-Hill, 2010, publisher page
-
Aranda, Wadia: Facelets Essentials, Apress, 2008, publisher page
-
Mann: JavaServer Faces in Action, Manning, 2005, publisher page