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:

  1. The value attribute is evaluated and wrapped in a jakarta.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.

  2. The component moves the cursor from first to the last row (or through rows rows) by calling setRowIndex(i). At each position it takes the row object and exposes it under the name given in var as a request attribute, so #{book.title} in a column reads the title of the current row.

  3. 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:title is the title component in the third row of the books table in the form form.

  4. After the last row the index is set to -1, the var attribute 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.

Table 1. Where in the request processing lifecycle the iteration happens
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

UIData.processDecodes evaluates value, then visits every displayed row and lets the row’s input components decode their submitted values. Events queued by a command in a row (for example a click on a "Delete" button) are wrapped so that they remember their row.

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 #{row.title}.

Invoke Application

The wrapped event is broadcast. UIData.broadcast first moves the cursor back to the recorded row, so the action method is called with var and rowData pointing at the row that was clicked.

Render Response

encodeBegin discards the saved per-row state, except what is needed to show errors again, and the renderer walks the rows to produce markup.

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):

sequenceDiagram autonumber participant P as Lifecycle phase participant T as UIData (h:dataTable) participant M as DataModel participant C as Row children P->>T: processDecodes, processValidators, processUpdates or encode T->>T: evaluate value and wrap it in a DataModel loop each row from first, up to rows rows T->>M: setRowIndex(i) M-->>T: row object for index i T->>T: expose the row as the request attribute named by var T->>C: restore the row's state, do the phase's work, save the state end T->>M: setRowIndex(-1) T->>T: remove var from the request

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.

model/Book.java
package org.example.bookshelf.model;

import java.io.Serializable;

public record Book(long id, String title, String author, int year) implements Serializable {
}
service/BookService.java
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:

catalogue.xhtml
<!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>

Mojarra answered with this markup (blank lines and indentation trimmed, three rows because of rows="3"):

Rendered output (Mojarra 4.1.17)
<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 caption facet becomes a caption element directly under the table; captionClass and captionStyle go on it.

  • A header facet on the table becomes one th that spans all the columns (colspan, scope="colgroup"). If any column has a header facet, a second header row is written with one th per column (scope="col"); a column without a header facet gets an empty th. All the header cells are inside a thead.

  • The footer facets follow the same rules in a tfoot made of td cells, without a scope. The tfoot is written before the tbody, the order HTML 4 required.

  • A column marked rowHeader="true" renders its cells as th scope="row" instead of td, which lets a screen reader announce "Title: Dracula" for the other cells of that row.

  • A colgroups facet (one child, normally a h:panelGroup holding colgroup and col markup) is written before the header, which helps accessibility and column-wide styling.

The attributes of h:dataTable that control behaviour and style:

Attribute Meaning

value

The data: a DataModel or anything Faces can wrap in one (see the section on DataModel). A null value gives an empty table.

var

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 var.

first

Zero-based index of the first row displayed. Bind it to a bean property that your paging actions change.

rows

Number of rows displayed from first. 0 means all the remaining rows.

rowStatePreserved

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.

bodyrows

Comma-separated row indices before which a new tbody starts, for grouped rows.

rowClasses, columnClasses

Comma-separated lists of CSS classes applied cyclically to rows and to columns.

styleClass, style, headerClass, footerClass, captionClass, captionStyle

CSS hooks for the table and its parts. A column’s own headerClass and footerClass override the table’s.

summary, border, cellpadding, cellspacing, frame, rules, width, bgcolor

Legacy presentational HTML 4 attributes that Faces still renders. Use CSS for new work.

dir, lang, role, title, and the DOM event attributes such as onclick

Passed through to the table element. Any other attribute can be passed through with the jakarta.faces.passthrough namespace.

id, rendered, binding

Common component attributes. rendered="false" removes the whole table and, like any component, its children from the lifecycle.

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 is odd. The lists cycle: with two classes, row 2 is odd again. Spaces after the commas are copied: columnClasses="num, title" produced class=" title" with a leading space. Write the list without spaces. Separate several classes for one cell with spaces, for example columnClasses="num,title wide".

  • When there are fewer entries in columnClasses than columns, the remaining columns get no class; 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.

repeat-doc.xhtml, four loops over the catalogue
<!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

value, var

Required. The data and the name of the current item. Both are scoped to the body of the tag.

varStatus

Name of a request-scoped variable holding the status of the iteration, with the read-only properties listed below.

begin, end

Zero-based index of the first item and of the last item, inclusive.

offset, size

The VDL describes offset as the index where the iteration starts and size as the size of the collection to iterate; the two implementations read size differently, see the differences section.

step

Process every step-th item, starting with the first one.

rowStatePreserved

Same meaning as on h:dataTable; new on ui:repeat in 4.1.

rendered

true, false or an expression that evaluates to one of them.

id, binding

As on any component. An id makes the client ids of the children predictable (fm:r:0:t).

varStatus property Value

index

The zero-based index of the current item in the collection. With begin="1" the first item has index 1.

begin, end, step

The attributes of the same name, or empty when they were not set (Mojarra) or 0, the item count and 1 (MyFaces).

first

true for the first item rendered.

last

true for the last item rendered. With an explicit end the two implementations do not agree on what that means.

even, odd

Parity of the iteration number counted from the first item rendered, not of index.

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 table with thead, tfoot, th/td

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 (form:books:2:del)

yes (fm:r:1:btn)

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

component.namingContainer.rowIndex

varStatus

varStatus

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

begin="2" end="4" (the portable form)

items 2, 3, 4

items 2, 3, 4

begin="1" end="7" step="3"

items 1, 4, 7

items 1, 4, 7

end="3" only

items 0 to 3

all twelve items: end ignored

begin="8" only

items 8 to 11

nothing rendered

offset="8" only

items 8 to 11

items 8 to 11

size="3" only

items 0 to 2

items 0 to 2

offset="2" size="3"

item 2 only: size acts as an exclusive end index

items 2, 3, 4: size is a count

size="0"

one item

nothing

end or size beyond the item count

FacesException, "Iteration end index is greater than the number of available rows."

exception, "end cannot be greater than collection size" or "iteration size cannot be greater than collection size"

varStatus.last with end="4" and begin="2"

true on items 3 and 4: one iteration early

false on every item (it is true only for the last item of the whole collection)

varStatus.begin, end, step when unset

empty

0, item count, 1

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

setRowIndex(int), getRowIndex()

Move the cursor to a zero-based row, or to -1 for "no row". Setting an index that has no row is allowed; ask isRowAvailable() before reading.

isRowAvailable()

Whether there is a row at the current index.

getRowData()

The row object at the current index.

getRowCount()

Number of rows, or -1 when that is unknown or there is no wrapped data.

getWrappedData(), setWrappedData(Object)

The underlying collection. Concrete models must have a public no-argument constructor that wraps nothing and a convenience constructor that takes the wrapped object.

addDataModelListener, removeDataModelListener, getDataModelListeners

Register DataModelListener`s, which receive `rowSelected(DataModelEvent) when the row index changes.

iterator()

A read-only iterator over the row data.

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

ArrayDataModel

an array of objects

ListDataModel

a java.util.List

ResultSetDataModel

a java.sql.ResultSet, hence a javax.sql.RowSet as well

CollectionDataModel

a java.util.Collection

IterableDataModel

an Iterable, which includes Set; a Map is wrapped through its entrySet(), so the row is a Map.Entry

ScalarDataModel

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:

model/BookPage.java
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) {
}
web/BookPageDataModel.java
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.

actions.xhtml, three ways to name the row and a selection column
<!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>
web/RowActionsBean.java
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();
    }
}
web/BookRow.java
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; }
}
Technique How it works and what the run showed

Method with a parameter: action="#{rowActions.remove(book)}"

Expression Language 2.2 method parameters. book is evaluated when the action is invoked, with the cursor moved back to the clicked row. The third row’s button removed the third book. This is the shortest form and the one to prefer. See the expression language page for the rules.

The model’s cursor: action="#{rowActions.removeCurrent}" with model.getRowData()

The bean keeps the DataModel that the table iterates and asks it for the current row. The first row’s link removed the first book. It works because UIData moves the model’s cursor to the row of the event before it broadcasts.

f:setPropertyActionListener

Copies the row into a bean property before the action runs. The second row’s link set current to the second book. It keeps the action method free of EL, and is useful when several actions need the row.

f:param with the id, read in the bean

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:

sequenceDiagram autonumber participant A as Browser A participant B as Browser B participant S as Server A->>S: GET live.xhtml S-->>A: rows: Alice in Wonderland (0), Dracula (1) ... B->>S: POST add "Aardvark Tales" Note over S: sorts before Alice, so it is now row 0 A->>S: POST click Delete on row 0 (remove(book)) Note over S: decode finds row 0 = Aardvark Tales S-->>A: page now shows Alice again, Aardvark Tales is gone

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>
web/LiveBean.java, the request-scoped bean of the test
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.

editable.xhtml
<!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:titleMsg contained "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. Without immediate, the empty required field would fail validation before the cancel action could run. Use immediate only 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 the for is 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.

paging.xhtml, sortable columns and Previous/Next
<!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>
web/CatalogueBean.java
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" moved first to 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 reset first to 0 and sorted the whole list, and the first page showed 1813, 1815, 1818, 1847, 1847. The links are in the header facet, so the control ids have no row index (form:books:sortYear).

  • The view-scoped bean keeps sortField, ascending and first across 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 in next().

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:

web/LazyBookModel.java
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");
    }
}
web/LazyCatalogueBean.java
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);
    }
}
lazy.xhtml
<!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>

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 @PostConstruct of 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 UIData with binding to a bean that outlives the request is the mistake described in the binding section of the architecture page; use a method parameter or a DataModel instead.

  • 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 rendered on the table and a message.

  • Relying on varStatus.last with end, or on size/offset as a window. Both implementations differ.

  • @FacesDataModel not found. Add a bean-defining annotation.

  • Using varStatus on h:dataTable. It does not exist there.

  • Large rows with first bound to user input. Clamp first between 0 and the count; a non-numeric or negative value must not reach the table (setFirst rejects a negative number with an IllegalArgumentException).

  • 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

tbody elements

<tbody> without an id

<tbody id="tableId:tbody_element">, with a counter on the extra ones (tbody_element1)

tbody with first above 0 and no bodyrows

written

not written: the rows follow the table directly

Spaces after the commas of columnClasses

copied into class=" b"

trimmed

Form submission marker

hidden form field named after the form id

hidden form_SUBMIT field; a script posting a form by hand must send form_SUBMIT=1

Empty value

one empty <tr><td></td></tr>

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

h:dataTable, h:column, DataModel and its array, list, result set and scalar implementations are the original design. The 4.1 package jakarta.faces.model has no class for the JSTL Result type that the tutorial still lists.

2.0

Facelets, with ui:repeat, becomes part of the specification.

2.1

UIData.rowStatePreserved.

2.2

UIData accepts any Collection (CollectionDataModel) rather than only a List; the role pass-through attribute; rowStatePreserved documented in the VDL.

2.3

IterableDataModel: UIData and ui:repeat accept Map and Iterable; the @FacesDataModel annotation registers custom models.

4.0

JSP views are gone, so every table is a Facelets view; namespaces become URNs (jakarta.faces.html, jakarta.faces.facelets).

4.1

rowStatePreserved on UIRepeat and ui:repeat, and the missing rowStatePreserved attribute of h:dataTable is documented in the VDL.

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.faces namespaces. The JSF 1.x books (2004 to 2006) are JSP-based. JSP views were removed in 4.0, the package is jakarta.faces since 3.0, and the tags live in the URNs jakarta.faces.html and jakarta.faces.core. Facelets is the only view technology.

  • @ManagedBean backing beans. Tables are fed from CDI @Named beans with @ViewScoped from jakarta.faces.view, not from javax.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.

  • DataModel types. The books list ArrayDataModel, ListDataModel, ResultSetDataModel and ScalarDataModel. CollectionDataModel, IterableDataModel and @FacesDataModel came with 2.3, so a Set, a Map or 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:dataTable attribute and class name against the PrimeFaces 16 documentation before copying it.

  • ui:repeat and jsfc. The Facelets book of 2008 uses ui:repeat and the jsfc attribute to turn plain HTML elements into components. ui:repeat is 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 binding and reads getRowData() from it. Use a method parameter, a DataModel held 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)