PrimeFaces Extensions
|
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) with Eclipse Mojarra 4.1.17 and
Apache MyFaces Core 4.1.4, Java 17 or later (the examples were compiled with --release 21), PrimeFaces 16.0.0 and PrimeFaces
Extensions 16.0.0 (both released on 2026-09-04, checked against Maven Central). The code was deployed as an exploded Bookshelf WAR
on Tomcat 11.0.26 with Weld 6.0.4.Final, once with Mojarra and once with MyFaces. Server-side behaviour was driven with scripted
Ajax and form posts, and the components that run JavaScript were loaded in headless Chrome and driven through their widget APIs, on
2026-10-08. Claims that were not run are marked as such. PrimeFaces itself is covered by the other PrimeFaces pages of this
section (getting started and themes, Ajax, forms and validation, data components, panels, menus and dialogs, files, media, charts
and drag and drop); this page assumes you have a working PrimeFaces page, and adds the extra library.
What PrimeFaces Extensions Is
PrimeFaces Extensions is a community-driven open source project (MIT licensed) that adds Faces components and utilities to
PrimeFaces: components missing from PrimeFaces, and variants of existing ones. It is a separate project from PrimeFaces: it lives in
its own GitHub organisation (primefaces-extensions) and has its own issue tracker and release notes. The project site says its releases are intended to follow the PrimeFaces releases, and the repository README maps each
Extensions major line to one PrimeFaces line:
| PrimeFaces Extensions | Built for |
|---|---|
16.x (16.0.0) |
PrimeFaces 16.0.0 |
15.x |
PrimeFaces 15.0.0 |
14.x |
PrimeFaces 14.0.0 |
13.x |
PrimeFaces 13.0.0 |
Three consequences follow from the pairing, and they decide most upgrade problems:
-
Upgrade both together. An Extensions release is compiled against one PrimeFaces version (the parent POM of 16.0.0 sets
primefaces.core.versionto 16.0.0). Mixing Extensions 16 with PrimeFaces 15 is not something the project promises. -
The components are PrimeFaces components. Most of them extend PrimeFaces base classes, use PrimeFaces widgets (
PF('name')), its Ajax engine and its search expressions, so what the other PrimeFaces pages say aboutprocess,updateand the lifecycle applies unchanged. -
Some components exist in both libraries.
pe:blockUI,pe:tooltipandpe:remoteCommandhave PrimeFaces core counterparts (p:blockUI,p:tooltip,p:remoteCommand); see What Is in 16.0.0.
Setup
Maven coordinates
The Extensions JAR does not bring PrimeFaces with it. In the published POM of 16.0.0 the primefaces dependency has scope
provided, so a project that lists only the Extensions JAR gets no transitive PrimeFaces (checked with mvn dependency:tree,
which listed the Extensions JAR alone). Declare both, with the same version:
<properties>
<primefaces.version>16.0.0</primefaces.version>
</properties>
<dependencies>
<dependency>
<groupId>org.primefaces</groupId>
<artifactId>primefaces</artifactId>
<version>${primefaces.version}</version>
</dependency>
<dependency>
<groupId>org.primefaces.extensions</groupId>
<artifactId>primefaces-extensions</artifactId>
<version>${primefaces.version}</version>
</dependency>
</dependencies>
The README still shows a <classifier>jakarta</classifier> dependency under "Release (jakarta)". For 16.0.0 the Maven Central
directory holds one binary JAR (plus sources, Javadoc and SBOM files) and no jakarta classifier JAR, and its faces-config.xml
declares Faces 4.0, so use the plain coordinates above.
Optional libraries
The core JAR stays small by treating heavy third-party code as optional. Each optional dependency below is declared
optional in the Extensions POM, so Maven does not pull it in: add it when you use the component. The versions are the ones
the 16.0.0 build uses; what happens without the library is stated where it was observed.
| Library (coordinates) | Needed by | Version the 16.0.0 build uses |
|---|---|---|
|
|
20260313.1 |
|
|
2.14.0 |
|
Rendering Markdown content in |
0.30.0 |
|
|
9.0.38 |
|
|
16.0.0 (same as the main JAR) |
The "needed by" column comes from which classes import each library in the 16.0.0 sources and from the POM comments, not from a
run of every component. The scratch application had the sanitizer, Gson, commonmark and libphonenumber on its classpath; the Monaco components, pe:inputPhone and pe:localized were not run.
The namespace
Faces 4.0 and later identify tag libraries by URN. The Extensions JAR ships two tag library descriptors (checked in the JAR): the
URN primefaces.extensions and the old URI http://primefaces.org/ui/extensions. PrimeFaces core has the same pair
(primefaces and http://primefaces.org/ui). The old namespaces still work in 4.1, but support for them is planned to be removed in Faces 5.0, so use the URNs; the prefix is only an alias, and pe is the convention:
<?xml version="1.0" encoding="UTF-8"?>
<!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"
xmlns:p="primefaces"
xmlns:pe="primefaces.extensions">
<h:head>
<title>Bookshelf extensions</title>
</h:head>
<h:body>
<h:form id="f">
<p:growl id="msg"/>
<p:commandButton id="share" value="Share" icon="pi pi-share-alt" type="button"/>
<pe:tooltip for="share" value="Copy the link to this book"/>
<pe:qrCode id="qr" text="https://bookshelf.example.org/book.xhtml?isbn=9780441172719" size="160" label="Dune" radius="0"/>
</h:form>
</h:body>
</html>
Two observations from running this page. First, radius="0" on pe:qrCode is deliberate: in 16.0.0 the component’s radius
property is declared Double but its default is the Integer 0, so a pe:qrCode without an explicit radius fails
while rendering with ClassCastException: class java.lang.Integer cannot be cast to class java.lang.Double, on Mojarra (HTTP
500) and on MyFaces alike (reproduced on both; the cast is in QRCodeBaseImpl.getRadius). Setting the attribute makes the
component store a Double and the page renders. Second, the rendered page contained the Extensions script
(primefaces-extensions.js, ?ln=primefaces-extensions) and a widget call PrimeFaces.cw("ExtTooltip",...): the library
registers a resource handler and a system event listener from its own META-INF/faces-config.xml (it is ordered after
PrimeFaces), so nothing is needed in web.xml or in your faces-config.xml.
Resources and the build
Extensions' own CSS and JavaScript are served by the Faces resource handler like any library resource (see
resource handling) and are minified inside the JAR. The wiki page that explains a
org.primefaces.extensions.DELIVER_UNCOMPRESSED_RESOURCES context parameter describes releases before 3.2.0 only and says that
later releases ship no uncompressed copies (they use source maps for debugging); do not set it. For your own pages, the project
also publishes a Maven plugin, org.primefaces.extensions:resources-optimizer-maven-plugin, which compresses and merges
JavaScript and CSS at build time; Maven Central lists 2.7.9 as the latest version (checked 2026-10-08; the 16.0.0 build itself
uses 2.6.6), and it was not run for this page. The performance page
(combining resources) discusses the OmniFaces alternative; whether
CombinedResourceHandler and the Extensions resource handler cooperate was not tested.
What Is in 16.0.0
The 16.0.0 tag library (META-INF/primefaces-extensions.urn.taglib.xml) declares 67 tags. The table groups them by purpose,
using the one-line descriptions of the VDL; the sections that follow run the ones the Bookshelf uses. The library declares no EL
functions (the descriptor has no function entries), so there is nothing to import beyond tags, converters and one behaviour.
| Purpose | Tags |
|---|---|
Editors |
|
Data entry |
|
Dynamic forms and layout |
|
Charts, maps and media |
|
Behaviour and utilities |
|
Converters |
|
Where a component exists in both libraries the two are separate implementations with different attributes. pe:tooltip
(positioning corners, shared and global tooltips, showEvent/hideEvent) and p:tooltip; pe:blockUI (source,
target, content, event, timeout) and p:blockUI (block and trigger in its documentation); and
pe:remoteCommand (typed method parameters, below) and p:remoteCommand. Pick one per page rather than mixing them on the
same element, and read the attribute list of the one you use, not the other’s.
Safe HTML: Sanitizing Converters, Editors and the Output Listener
Extensions is the one library of this section that ships a dedicated defence for user-supplied HTML, and it deserves the first
example because it is easy to use wrongly. It is built on the OWASP Java HTML Sanitizer, which parses markup and rebuilds it from
an allow-list (a PolicyFactory).
pe:sanitizer: a converter
pe:sanitizer is a converter tag (class org.primefaces.extensions.converter.SanitizingConverter, registered as
primefaces.SanitizingConverter). Its attributes are policy (a PolicyFactory; the default is a strict policy that
allows no tags) and decodeHtml (default true). As a converter it runs when the submitted value is converted, in Process
Validations (converters, lifecycle phases),
so the bean setter only ever sees the sanitized text; getAsString sanitizes again on the way out.
<?xml version="1.0" encoding="UTF-8"?>
<!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"
xmlns:p="primefaces"
xmlns:pe="primefaces.extensions">
<h:head>
<title>Review comment</title>
</h:head>
<h:body>
<h:form id="f">
<p:messages id="msgs"/>
<h:inputTextarea id="comment" value="#{reviewCommentBean.comment}" rows="4" cols="60">
<pe:sanitizer/>
</h:inputTextarea>
<p:commandButton id="save" value="Save" action="#{reviewCommentBean.save}" update="@form"/>
<h:panelGroup id="out" layout="block">
<p>Escaped: <h:outputText id="escaped" value="#{reviewCommentBean.saved}"/></p>
<p>Unescaped: <h:outputText id="raw" value="#{reviewCommentBean.saved}" escape="false"/></p>
</h:panelGroup>
</h:form>
</h:body>
</html>
package org.example.bookshelf.web;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
import java.io.Serializable;
/** Review text typed into the sanitizing form; the converter runs before the setter is called. */
@Named
@ViewScoped
public class ReviewCommentBean implements Serializable {
private String comment = "";
private String saved = "";
public void save() {
saved = comment;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
public String getSaved() {
return saved;
}
}
What the stored value was for each submitted review text (Ajax post, then the rendered f:escaped and f:raw spans), identical
on Mojarra and MyFaces:
| Submitted | Escaped output (h:outputText) |
Unescaped output (escape="false") |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The last two rows are the trap. With the default decodeHtml="true" the converter sanitizes and then decodes HTML entities
(SanitizingConverter.sanitize calls the OWASP decoder after the policy). Entity-encoded markup survives the policy as harmless
text and is then turned back into live markup: the stored value is plain text that contains <script> or an <img onerror>.
That is safe only if every place that prints it escapes it. Printed with escape="false", as in the last column, the response
contains the payload as markup, which is cross-site scripting (the test inspected the response, no browser ran it). The rule,
which the security page gives for escape in general:
-
With the default converter, store plain text and print it escaped. The converter then merely removes markup the user should not be sending.
-
If you must print HTML, sanitize with
decodeHtml="false"and an explicit policy, so the stored value is HTML that is already safe to print unescaped:
<?xml version="1.0" encoding="UTF-8"?>
<!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"
xmlns:p="primefaces"
xmlns:pe="primefaces.extensions">
<h:head>
<title>Review comment</title>
</h:head>
<h:body>
<h:form id="f">
<p:messages id="msgs"/>
<h:inputTextarea id="comment" value="#{reviewCommentBean.comment}" rows="4" cols="60">
<pe:sanitizer decodeHtml="false" policy="#{reviewPolicies.review}"/>
</h:inputTextarea>
<p:commandButton id="save" value="Save" action="#{reviewCommentBean.save}" update="@form"/>
<h:panelGroup id="out" layout="block">
<p>Escaped: <h:outputText id="escaped" value="#{reviewCommentBean.saved}"/></p>
<p>Unescaped: <h:outputText id="raw" value="#{reviewCommentBean.saved}" escape="false"/></p>
</h:panelGroup>
</h:form>
</h:body>
</html>
Source: PrimeFaces Extensions VDL: sanitizer and OWASP Java HTML Sanitizer: Sanitizers and PolicyFactory
package org.example.bookshelf.web;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Named;
import org.owasp.html.PolicyFactory;
import org.owasp.html.Sanitizers;
/** The HTML a review may contain: inline formatting and links, nothing else. */
@Named
@ApplicationScoped
public class ReviewPolicies {
private final PolicyFactory review = Sanitizers.FORMATTING.and(Sanitizers.LINKS);
public PolicyFactory getReview() {
return review;
}
}
Source: PrimeFaces Extensions VDL: sanitizer and OWASP Java HTML Sanitizer: Sanitizers and PolicyFactory
The same inputs through this configuration (policy: inline formatting and links), again identical on both implementations:
| Submitted | Escaped output | Unescaped output |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Now the unescaped column is inert text in every row. The text area shows the stored HTML source when the form is redisplayed,
because the converter’s getAsString runs again; re-submitting the redisplayed source stores the same value as the original input (observed with
Tom & Jerry <b>x</b> 1 < 2 versus the same text with entities). pe:sanitizerLenient is the same converter with a built-in policy that allows
blocks, formatting, links, styles, images, tables and media; it is permissive on purpose, and is a fair choice for output of
trusted-but-legacy content, not for accepting input from strangers.
pe:sunEditor and pe:markdownEditor: editors that sanitize
pe:sunEditor (a WYSIWYG editor; it replaced the removed CKEditor, see What Moved into PrimeFaces Core or Was Removed) and pe:markdownEditor carry the secure
attribute (default true) and the allowBlocks, allowFormatting, allowLinks, allowStyles, allowImages,
allowTables and allowMedia switches. With secure="true" their renderers sanitize the submitted value in decode (and
pe:sunEditor also sanitizes the value it renders into the editor).
<?xml version="1.0" encoding="UTF-8"?>
<!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"
xmlns:p="primefaces"
xmlns:pe="primefaces.extensions">
<h:head>
<title>Rich review</title>
</h:head>
<h:body>
<h:form id="f">
<p:messages id="msgs"/>
<pe:sunEditor id="html" value="#{reviewEditorBean.html}" height="200px"/>
<pe:markdownEditor id="md" value="#{reviewEditorBean.markdown}" mode="standard"/>
<p:commandButton id="save" value="Save" action="#{reviewEditorBean.save}" update="@form"/>
<div id="stored-html"><h:outputText value="#{reviewEditorBean.html}"/></div>
<div id="stored-md"><h:outputText value="#{reviewEditorBean.markdown}"/></div>
</h:form>
</h:body>
</html>
package org.example.bookshelf.web;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
import java.io.Serializable;
/** Rich-text review: the editor components sanitize the HTML they receive. */
@Named
@ViewScoped
public class ReviewEditorBean implements Serializable {
private String html = "<p>Great <b>book</b>.</p>";
private String markdown = "# Notes\n\nA *fine* read.";
public String getHtml() {
return html;
}
public void setHtml(String html) {
this.html = html;
}
public String getMarkdown() {
return markdown;
}
public void setMarkdown(String markdown) {
this.markdown = markdown;
}
public void save() {
// the values were already sanitized by the components
}
}
Posting <p>ok <b>b</b></p><script>alert(1)</script><img src=x onerror=alert(2)><a href="javascript:alert(3)">l</a> to the
SunEditor and text <script>alert(1)</script> and [x](javascript:alert(2)) <b>b</b> to the Markdown editor stored (HTML-escaped
here for display) <p>ok <b>b</b></p><img src="x" />l and text and [x](javascript:alert(2)) <b>b</b> on both
implementations: the script element, the event handler and the javascript: link of the HTML editor were removed. Note the
second value: the sanitizer works on HTML, so the Markdown editor removed the <script> but left the javascript: link in
the Markdown source untouched. If you render stored Markdown to HTML, sanitize the rendered HTML again (and treat the Markdown
renderer’s link handling as untrusted). In headless Chrome the page rendered both editors; SunEditor logged four console warnings
about optional plugins (file upload, layout, template, math) that need options you did not set.
EscapeSanitizerComponentListener: a safety net for old data
A different tool for a different problem: pages that already print stored HTML with escape="false" and that you cannot
rewrite today. org.primefaces.extensions.application.EscapeSanitizerComponentListener is an opt-in system event listener. When
registered, on the PostAddToViewEvent of the h:body it walks the view and sanitizes the value of every h:outputText
and p:outputLabel whose escape is false.
<application>
<system-event-listener>
<system-event-listener-class>org.primefaces.extensions.application.EscapeSanitizerComponentListener</system-event-listener-class>
<system-event-class>jakarta.faces.event.PostAddToViewEvent</system-event-class>
<source-class>jakarta.faces.component.html.HtmlBody</source-class>
</system-event-listener>
</application>
Source: PrimeFaces Extensions project site and the Javadoc of EscapeSanitizerComponentListener in primefaces-extensions-16.0.0-sources.jar
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core"
xmlns:p="primefaces">
<h:head>
<title>Stored review</title>
</h:head>
<h:body>
<h:outputText id="legacy" value="#{storedReviewBean.body}" escape="false"/>
<p:outputLabel id="lbl" value="#{storedReviewBean.body}" escape="false"/>
<h:form id="f">
<h:outputText id="inform" value="#{storedReviewBean.body}" escape="false"/>
</h:form>
</h:body>
</html>
Source: PrimeFaces Extensions project site and the Javadoc of EscapeSanitizerComponentListener in primefaces-extensions-16.0.0-sources.jar
With a stored review <b>fine</b> <img src=x onerror=alert(1)> <script>alert(2)</script> in an application-scoped bean, a plain GET
of this page rendered the payload untouched without the listener, and <b>fine</b> with it, for h:outputText and
p:outputLabel on Mojarra and MyFaces (h:outputText inside a form too). Its limits, found by trying it on the form of the
previous section: it sanitizes at the time the view is built, from the value the expression has then, and it replaces the
component’s value with that sanitized string. A value that changes during the same request, such as the saved text that
an action sets, is not covered (the unescaped output of sanitize.xhtml still printed live markup with the listener registered),
and it only knows h:outputText and p:outputLabel. It is a net under old pages, not a replacement for sanitizing on input.
Code Editors: pe:codeMirror
pe:codeMirror wraps a textarea in the CodeMirror editor (syntax highlighting, line numbers, bracket matching and the
usual editor options). Its value is a plain String. The Bookshelf uses it for the JSON settings of a catalogue import.
<?xml version="1.0" encoding="UTF-8"?>
<!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"
xmlns:p="primefaces"
xmlns:pe="primefaces.extensions">
<h:head>
<title>Import settings</title>
</h:head>
<h:body>
<h:form id="f">
<p:messages id="msgs"/>
<pe:codeMirror id="editor" value="#{importSettingsBean.json}" mode="javascript" theme="eclipse"
lineNumbers="true" matchBrackets="true" lineWrapping="true" widgetVar="jsonEditor"/>
<p:commandButton id="apply" value="Apply" action="#{importSettingsBean.apply}" update="@form"/>
<pre id="applied"><h:outputText value="#{importSettingsBean.applied}"/></pre>
</h:form>
</h:body>
</html>
package org.example.bookshelf.web;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
import java.io.Serializable;
/** JSON settings of the catalogue import, edited in a CodeMirror editor. */
@Named
@ViewScoped
public class ImportSettingsBean implements Serializable {
private String json = """
{
"source": "https://example.org/catalogue.csv",
"separator": ";",
"skipHeader": true
}
""";
private String applied = "";
public void apply() {
applied = json;
}
public String getJson() {
return json;
}
public void setJson(String json) {
this.json = json;
}
public String getApplied() {
return applied;
}
}
The page loaded codemirror.js and codemirror.css from ?ln=primefaces-extensions and created one .CodeMirror element
in headless Chrome. mode names the language; the attribute list also has theme, lineNumbers, lineWrapping,
matchBrackets, tabSize, indentUnit, readonly, completeMethod (a server-side suggestion method) and extender
(a JavaScript function that receives the widget so you can set options the tag does not expose). Which mode values the
bundled JAR supports was not enumerated here; check the VDL and the showcase before relying on a language.
One security-relevant observation. The VDL lists secure (default true) and the allow* switches on pe:codeMirror
too, but in 16.0.0 its renderer does not use them: a JSON text containing <b>bold</b><script>alert(1)</script>, 1 < 2 && 3 > 2
and x & y came back from the postback byte for byte, on Mojarra and MyFaces. That is the right behaviour for a code
editor (you do not want < mangled in code), and it means secure gives no protection there: treat the value as untrusted
text, escape it when printing, and validate it as the format it claims to be (here: parse the JSON with a real parser before
using it). Monaco (pe:monacoEditor and friends, with the separate resources-monacoeditor JAR) is the heavier alternative
from the same library and was not run.
Layout and Navigation Components
pe:fluidGrid: a masonry wall
pe:fluidGrid lays items out in the best free position by vertical space (masonry). It takes a List<FluidGridItem> as
value, exposes each item’s data as var and picks the item template whose type matches FluidGridItem.getType().
Attributes include hGutter, vGutter, fitWidth, hasImages (wait for images before laying out), originLeft,
originTop, stamp (a search expression for elements to keep out of the flow), transitionDuration and resizeBound.
<?xml version="1.0" encoding="UTF-8"?>
<!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"
xmlns:p="primefaces"
xmlns:pe="primefaces.extensions">
<h:head>
<title>Cover wall</title>
<style>
.cover { width: 150px; margin: 6px; padding: 8px; border: 1px solid #ccc; }
.classic { height: 120px; }
.recent { height: 180px; }
</style>
</h:head>
<h:body>
<pe:fluidGrid value="#{coverGalleryBean.items}" var="data" hGutter="10" vGutter="10" fitWidth="true">
<pe:fluidGridItem type="classic" styleClass="cover classic">
<h:outputText value="#{data.title} (#{data.year})"/>
</pe:fluidGridItem>
<pe:fluidGridItem type="recent" styleClass="cover recent">
<strong><h:outputText value="#{data.title}"/></strong><br/>
<h:outputText value="#{data.author}"/>
</pe:fluidGridItem>
</pe:fluidGrid>
</h:body>
</html>
package org.example.bookshelf.web;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.util.ArrayList;
import java.util.List;
import org.example.bookshelf.service.BookService;
import org.primefaces.extensions.model.fluidgrid.FluidGridItem;
/** Items of the masonry cover wall; each item carries a Book and a type naming its template. */
@Named
@RequestScoped
public class CoverGalleryBean {
@Inject
private BookService books;
private List<FluidGridItem> items;
@PostConstruct
void init() {
items = new ArrayList<>();
books.all().forEach(b -> items.add(new FluidGridItem(b, b.year() < 1975 ? "classic" : "recent")));
}
public List<FluidGridItem> getItems() {
return items;
}
}
Both implementations rendered 13 elements with a fluid-grid class for the 12 books in headless Chrome (the 12 items plus the container). The items are
a request-scoped list of FluidGridItem objects; each one gets a generated eight-character key, and its data must be
Serializable (the Bookshelf Book record is). pe:fluidGrid is output-only: it does not post anything back.
pe:masterDetail: drill-down without leaving the page
pe:masterDetail shows one of several pe:masterDetailLevel containers at a time and draws a breadcrumb over them.
pe:selectDetailLevel is a child of a command component: it moves to a level, relative (step) or absolute (level),
and may hand a contextValue to the target level, which receives it under the name given in that level’s contextVar.
<?xml version="1.0" encoding="UTF-8"?>
<!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"
xmlns:p="primefaces"
xmlns:pe="primefaces.extensions">
<h:head>
<title>Browse by author</title>
</h:head>
<h:body>
<h:form id="f">
<pe:masterDetail id="md" level="#{bookDrilldownBean.currentLevel}">
<pe:masterDetailLevel level="1" levelLabel="Authors">
<ui:repeat value="#{bookDrilldownBean.authors}" var="a">
<p:commandLink value="#{a}" style="display:block">
<pe:selectDetailLevel contextValue="#{a}"/>
</p:commandLink>
</ui:repeat>
</pe:masterDetailLevel>
<pe:masterDetailLevel level="2" contextVar="author" levelLabel="Books by #{author}">
<ui:repeat value="#{bookDrilldownBean.booksOf(author)}" var="b">
<div><h:outputText value="#{b.title} (#{b.year})"/></div>
</ui:repeat>
<p:commandButton value="Back to authors">
<pe:selectDetailLevel level="1"/>
</p:commandButton>
</pe:masterDetailLevel>
</pe:masterDetail>
</h:form>
</h:body>
</html>
package org.example.bookshelf.web;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;
/** Authors, then their books: the two levels of a pe:masterDetail. */
@Named
@ViewScoped
public class BookDrilldownBean implements Serializable {
@Inject
private transient BookService books;
private int currentLevel = 1;
public Map<String, List<Book>> getByAuthor() {
return books.all().stream().collect(Collectors.groupingBy(Book::author, TreeMap::new, Collectors.toList()));
}
public List<String> getAuthors() {
return List.copyOf(getByAuthor().keySet());
}
public List<Book> booksOf(String author) {
return getByAuthor().getOrDefault(author, List.of());
}
public int getCurrentLevel() {
return currentLevel;
}
public void setCurrentLevel(int currentLevel) {
this.currentLevel = currentLevel;
}
}
Clicking an author link in headless Chrome swapped level 1 for level 2 and the page then showed Books by Frank Herbert, the
author’s books and the back button, on both implementations. The level labels may use the context variable
(levelLabel="Books by #{author}" above), which is how the breadcrumb gets a readable trail. The VDL also lists a
selectLevelListener method on pe:masterDetail that is called when a level is selected, showBreadcrumb and
showAllBreadcrumbItems; they were not run. The state is the level binding: keep the bean @ViewScoped, as here, or the
level resets on every request.
pe:dynaForm: Forms Built from a Model
pe:dynaForm renders a form from a DynaFormModel built in Java: rows, labels and controls with optional column and row
spans. For each control the page supplies a template by type (pe:dynaFormControl), and the component renders one template
per model control, binding var to the control’s data object. Use it when the fields come from configuration (per-tenant
forms, questionnaires) and are not known when the page is written. The model classes live in
org.primefaces.extensions.model.dynaform (DynaFormModel, DynaFormRow, DynaFormLabel, DynaFormControl,
DynaFormModelElement).
<?xml version="1.0" encoding="UTF-8"?>
<!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"
xmlns:p="primefaces"
xmlns:pe="primefaces.extensions">
<h:head>
<title>Borrow form</title>
</h:head>
<h:body>
<h:form id="f">
<p:messages id="msgs"/>
<pe:dynaForm id="dyna" value="#{borrowDynaFormBean.model}" var="data">
<pe:dynaFormControl type="text" for="txt">
<p:inputText id="txt" value="#{data.value}" required="#{data.required}"/>
</pe:dynaFormControl>
<pe:dynaFormControl type="select" for="sel">
<p:selectOneMenu id="sel" value="#{data.value}">
<f:selectItems value="#{borrowDynaFormBean.weeks}"/>
</p:selectOneMenu>
</pe:dynaFormControl>
<pe:dynaFormControl type="area" for="area">
<p:inputTextarea id="area" value="#{data.value}"/>
</pe:dynaFormControl>
<f:facet name="buttonBar">
<p:commandButton id="send" value="Send" action="#{borrowDynaFormBean.submit}" process="dyna" update="@form"/>
</f:facet>
</pe:dynaForm>
<ul><ui:repeat value="#{borrowDynaFormBean.submitted}" var="s"><li><h:outputText value="#{s}"/></li></ui:repeat></ul>
</h:form>
</h:body>
</html>
package org.example.bookshelf.web;
import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import org.primefaces.extensions.model.dynaform.DynaFormControl;
import org.primefaces.extensions.model.dynaform.DynaFormLabel;
import org.primefaces.extensions.model.dynaform.DynaFormModel;
import org.primefaces.extensions.model.dynaform.DynaFormRow;
/** A form whose fields come from a model instead of from the page. */
@Named
@ViewScoped
public class BorrowDynaFormBean implements Serializable {
/** The value holder of one field; the page binds the control to {@code data.value}. */
public static class Field implements Serializable {
private final String name;
private final boolean required;
private Object value;
public Field(String name, boolean required) {
this.name = name;
this.required = required;
}
public String getName() {
return name;
}
public boolean isRequired() {
return required;
}
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
private DynaFormModel model;
private final List<String> submitted = new ArrayList<>();
@PostConstruct
void init() {
model = new DynaFormModel();
row(model.createRegularRow(), "Reader name", new Field("name", true), "text");
row(model.createRegularRow(), "Email", new Field("email", true), "text");
row(model.createRegularRow(), "Loan weeks", new Field("weeks", false), "select");
row(model.createRegularRow(), "Note", new Field("note", false), "area");
}
private void row(DynaFormRow row, String label, Field field, String type) {
DynaFormLabel l = row.addLabel(label);
DynaFormControl c = row.addControl(field, type);
l.setForControl(c);
}
public void submit() {
submitted.clear();
for (DynaFormControl control : model.getControls()) {
Field f = (Field) control.getData();
submitted.add(f.getName() + "=" + f.getValue());
}
}
public DynaFormModel getModel() {
return model;
}
public List<String> getSubmitted() {
return submitted;
}
public List<Integer> getWeeks() {
return List.of(1, 2, 3, 4);
}
}
Two details matter for correctness. label.setForControl(control) links a generated label to its control; with the link in place
the validation message carries the label text: submitting an empty required name produced Reader name: Validation Error: Value is
required. on both implementations, and a valid submission delivered name=Mira, email=mira@example.org, weeks=3 and
note=hi to the bean. And the button bar’s command button uses process="dyna" (the id of the pe:dynaForm): the controls
are children of the dynamic form, so the form component, not the buttons or a hand-written list of ids, is the right execute
target. The generated client ids look like f:dyna:r1c2p1reg:txt (row, column, position), so write scripts and tests against
the form, not against these ids. The select’s value arrived as 3 through a value holder typed Object, so no conversion to
Integer took place; give the field a typed property or a converter if the bean needs an Integer.
pe:sheet: Spreadsheet-Style Editing
pe:sheet is a data grid on top of Handsontable: cell editing, copy and paste, frozen rows and columns, sorting and filtering,
and typed columns (colType: text, numeric, checkbox, date, time, dropdown, autocomplete, password). It binds to a List
with a required rowKey, and each pe:sheetcolumn binds to a property of var.
<?xml version="1.0" encoding="UTF-8"?>
<!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"
xmlns:p="primefaces"
xmlns:pe="primefaces.extensions">
<h:head>
<title>Stock sheet</title>
</h:head>
<h:body>
<h:form id="f">
<p:growl id="msg"><p:autoUpdate/></p:growl>
<pe:sheet id="sheet" widgetVar="sheetWidget" value="#{stockSheetBean.rows}" var="row"
rowKey="#{row.id}" height="260" showRowHeaders="false" stretchH="all"
filteredValue="#{stockSheetBean.filtered}">
<p:ajax event="change" listener="#{stockSheetBean.cellChanged}" update="last"/>
<pe:sheetcolumn headerText="Title" value="#{row.title}" readOnly="true" colWidth="260"/>
<pe:sheetcolumn headerText="Copies" value="#{row.copies}" colType="numeric" colWidth="80"
required="true" requiredMessage="Copies is required"/>
<pe:sheetcolumn headerText="Shelf" value="#{row.shelf}" colWidth="80"/>
<pe:sheetcolumn headerText="On loan" value="#{row.onLoan}" colType="checkbox" colWidth="80"/>
</pe:sheet>
<p:commandButton id="csv" value="CSV" ajax="false">
<p:dataExporter type="csv" target="sheet" fileName="stock"/>
</p:commandButton>
<h:outputText id="last" value="#{stockSheetBean.lastChange}"/>
</h:form>
</h:body>
</html>
Source: PrimeFaces Extensions VDL: sheet and sheetcolumn and PrimeFaces 16 documentation: DataExporter
package org.example.bookshelf.web;
import jakarta.annotation.PostConstruct;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import org.example.bookshelf.service.BookService;
import org.primefaces.extensions.event.SheetEvent;
/** Stock levels edited like a spreadsheet; the rows live as long as the view. */
@Named
@ViewScoped
public class StockSheetBean implements Serializable {
@Inject
private transient BookService books;
private List<StockRow> rows;
private List<StockRow> filtered;
private String lastChange = "none";
@PostConstruct
void init() {
rows = new ArrayList<>();
books.all().forEach(b -> rows.add(new StockRow(b.id(), b.title(), 2, "A" + b.id())));
}
/** Called for each changed cell, after the new value was stored in the row. */
public void cellChanged(SheetEvent event) {
StockRow row = (StockRow) event.getRowData();
lastChange = "row " + event.getRowIndex() + " column " + event.getColumnIndex() + " -> copies " + row.getCopies() + ", shelf " + row.getShelf();
FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Saved " + row.getTitle(), null));
}
public List<StockRow> getRows() {
return rows;
}
public List<StockRow> getFiltered() {
return filtered;
}
public void setFiltered(List<StockRow> filtered) {
this.filtered = filtered;
}
public String getLastChange() {
return lastChange;
}
}
Source: PrimeFaces Extensions VDL: sheet and sheetcolumn and PrimeFaces 16 documentation: DataExporter
package org.example.bookshelf.web;
import java.io.Serializable;
/** One editable row of the stock sheet. */
public class StockRow implements Serializable {
private final long id;
private final String title;
private int copies;
private String shelf;
private boolean onLoan;
public StockRow(long id, String title, int copies, String shelf) {
this.id = id;
this.title = title;
this.copies = copies;
this.shelf = shelf;
}
public long getId() {
return id;
}
public String getTitle() {
return title;
}
public int getCopies() {
return copies;
}
public void setCopies(int copies) {
this.copies = copies;
}
public String getShelf() {
return shelf;
}
public void setShelf(String shelf) {
this.shelf = shelf;
}
public boolean isOnLoan() {
return onLoan;
}
public void setOnLoan(boolean onLoan) {
this.onLoan = onLoan;
}
}
Source: PrimeFaces Extensions VDL: sheet and sheetcolumn and PrimeFaces 16 documentation: DataExporter
What was observed in headless Chrome by selecting a cell and calling the Handsontable API (the instance is PF('sheetWidget').ht),
identically on Mojarra and MyFaces:
-
The sheet loaded in headless Chrome and showed the 12 books.
-
Editing a numeric cell (the copies of the row at index 1, set to 7) fired the
changeAjax behaviour at once, and the row object held 7 afterwards. -
Editing a text cell fires
changelater: after the selection moves on. The event then describes the newly selected row and column: with row 2’s shelf edited toZ9and the selection moved to row 3, the bean saw row 3 (event.getRowIndex()3), while the valueZ9did land in the right row object (checked by listing the rows). So read the new data from the model, and use the event only to learn that something changed. -
Firing a change with no cell selected (a script that sets a value without selecting a cell) threw a
NullPointerExceptioninSheet.queueEventbecausegetSelectedRow()wasnull. A person always selects a cell first; this matters for UI tests that edit through the widget API: select the cell first. -
p:dataExporter type="csv" target="sheet"produced a download (text/csv,attachment;filename="stock.csv") with the header row and 12 data rows on both implementations. The Extensions JAR registers CSV and XML exporters forSheetwith the PrimeFacesDataExportersregistry when the first sheet renders, which is how a core component exports an extension component.
The other Ajax events of the sheet, from its ClientBehaviorEventKeys, are cellSelect, sort, filter,
columnSelect and rowSelect. A sheet is for editing many cells; it is not a replacement for p:dataTable when the user
only reads (see the PrimeFaces data components page for the lazy model). Because cell edits arrive as one delta document in a
hidden field, validate every value on the server as you would any posted field: the bean here would accept any number of copies,
and a real one would add a @Min(0) constraint, which Bean Validation applies to the row property as it does for any
input (Bean Validation; not run for the sheet).
pe:waypoint: React to Scrolling
pe:waypoint registers a trigger on the position of an element in the viewport (the jQuery Waypoints library). The behaviour
event is reached, and the event object carries the scroll direction. Attributes: for (the target component; the parent
if omitted), offset, continuous, triggerOnce, enabled, horizontal and forContext (a scrollable container
instead of the window). The Bookshelf feed loads four more books when the sentinel at the end of the list is reached.
<?xml version="1.0" encoding="UTF-8"?>
<!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"
xmlns:p="primefaces"
xmlns:pe="primefaces.extensions">
<h:head>
<title>Catalogue feed</title>
<style>li { height: 300px; }</style>
</h:head>
<h:body>
<h:form id="f">
<h:panelGroup id="feed" layout="block">
<ul>
<ui:repeat value="#{feedScrollBean.shown}" var="b">
<li><h:outputText value="#{b.title}"/></li>
</ui:repeat>
</ul>
</h:panelGroup>
<h:panelGroup id="sentinel" layout="block">End of the list</h:panelGroup>
<pe:waypoint for="sentinel" offset="'100%'">
<p:ajax event="reached" listener="#{feedScrollBean.onReached}" update="feed count"/>
</pe:waypoint>
<h:outputText id="count" value="#{feedScrollBean.events}"/>
</h:form>
</h:body>
</html>
package org.example.bookshelf.web;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;
import org.primefaces.extensions.event.WaypointEvent;
/** Loads the next chunk of books when the sentinel at the end of the list scrolls into view. */
@Named
@ViewScoped
public class FeedScrollBean implements Serializable {
private static final int CHUNK = 4;
@Inject
private transient BookService books;
private final List<Book> shown = new ArrayList<>();
private int events;
public List<Book> getShown() {
if (shown.isEmpty()) {
appendChunk();
}
return shown;
}
/** Fires for both directions; only scrolling down loads more. */
public void onReached(WaypointEvent event) {
events++;
if (event.getDirection() == WaypointEvent.Direction.DOWN) {
appendChunk();
}
}
private void appendChunk() {
List<Book> all = books.all();
int from = Math.min(shown.size(), all.size());
shown.addAll(all.subList(from, Math.min(from + CHUNK, all.size())));
}
public int getEvents() {
return events;
}
}
Three things learned by running it. First, offset is written into the widget configuration as raw JavaScript: offset="bottom-in-view"
(the string form some waypoint libraries use) rendered offset:bottom-in-view and the page threw SyntaxError: Unexpected token 'in',
so the widget never existed. A JavaScript string literal such as offset="'100%'" and a function expression (the form the showcase uses) worked; a bare number is
documented in the VDL but was not run.
Second, reached fires for both directions: scrolling back up fires it too (7 events in the test below), so the listener loads
only for WaypointEvent.Direction.DOWN. Third, in headless Chrome the list grew from 4 to 8 when the test scrolled to the bottom,
and to 12 only after it scrolled back to the top and down again (the sequence 8, 12, 12, 12 on both implementations); repeatedly
scrolling to the bottom without leaving it did not load a third chunk. Waypoints compare positions at scroll time and the Ajax update
moves the sentinel, so test the feel in a real browser, and keep the sentinel outside the region that you update (it is a sibling of
feed above). Scrolling was driven with window.scrollTo, not with a mouse wheel.
Calling the Server from JavaScript: pe:remoteCommand
pe:remoteCommand creates a JavaScript function, named by name, that runs a server method over Ajax. It has the usual Ajax
attributes (process, update, oncomplete, partialSubmit, resetValues, autoRun, delay, timeout) and adds what
the core component lacks: typed parameters. pe:methodSignature declares the Java types, in order; each pe:methodParam
names a JavaScript argument and may carry a converter; pe:assignableParam writes a parameter straight into a bean property
through an EL value expression.
<?xml version="1.0" encoding="UTF-8"?>
<!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"
xmlns:p="primefaces"
xmlns:pe="primefaces.extensions">
<h:head>
<title>Shelf scanner</title>
</h:head>
<h:body>
<h:form id="f">
<p:growl id="msg"/>
<pe:remoteCommand id="rc" name="shelfScanned" process="@this" update="last"
actionListener="#{shelfRemoteBean.received}">
<pe:methodSignature parameters="java.lang.String, java.util.Date, org.example.bookshelf.web.ShelfRemoteBean$Scan"/>
<pe:methodParam name="reader"/>
<pe:methodParam name="when">
<f:convertDateTime type="date" pattern="yyyy-MM-dd"/>
</pe:methodParam>
<pe:methodParam name="scan">
<pe:convertJson/>
</pe:methodParam>
</pe:remoteCommand>
<pe:remoteCommand id="rc2" name="assignNote" process="@this" update="note">
<pe:assignableParam name="note" assignTo="#{shelfRemoteBean.assigned}"/>
</pe:remoteCommand>
<h:outputText id="last" value="#{shelfRemoteBean.last}"/>
<h:outputText id="note" value="#{shelfRemoteBean.assigned}"/>
<p:commandButton id="go" type="button" value="Scan"
onclick="shelfScanned('mira', '2026-10-08', JSON.stringify({shelf: 'B2', slot: 7})); assignNote('hello')"/>
</h:form>
</h:body>
</html>
package org.example.bookshelf.web;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.Date;
/** Receives typed parameters from a JavaScript call through pe:remoteCommand. */
@Named
@ViewScoped
public class ShelfRemoteBean implements Serializable {
/** A scanned shelf label, sent from JavaScript as JSON. */
public static class Scan implements Serializable {
private String shelf;
private int slot;
public String getShelf() {
return shelf;
}
public void setShelf(String shelf) {
this.shelf = shelf;
}
public int getSlot() {
return slot;
}
public void setSlot(int slot) {
this.slot = slot;
}
}
private String last = "nothing yet";
private String assigned = "";
/** The signature declared with pe:methodSignature: String, Date, Scan. */
public void received(String reader, Date when, Scan scan) {
last = reader + " | " + (when == null ? "no date" : LocalDate.ofInstant(when.toInstant(), java.time.ZoneOffset.UTC)) + " | " + scan.getShelf() + "/" + scan.getSlot();
}
public String getLast() {
return last;
}
public String getAssigned() {
return assigned;
}
public void setAssigned(String assigned) {
this.assigned = assigned;
}
}
Clicking the button in headless Chrome called shelfScanned('mira', '2026-10-08', JSON.stringify({shelf: 'B2', slot: 7})); the
bean received a String, a Date (converted by f:convertDateTime) and a Scan object (built by pe:convertJson, which
needs Gson) and the page showed mira | 2026-10-08 | B2/7; assignNote('hello') stored hello through pe:assignableParam.
Both implementations agreed. The parameters come from the client, so treat them like any request parameter: the Scan object
is built from client JSON, so validate its fields and never trust it for authorisation. The Java class named in
pe:methodSignature is written with its binary name (ShelfRemoteBean$Scan for the nested class).
pe:javascript is a client behaviour that runs a JavaScript expression on an event instead of sending a request (its VDL
description says it intercepts the event to prevent the Ajax call); it receives the widget’s event data as ext.
See client behaviours for the mechanism it plugs into; it was not run here.
Inputs: pe:timePicker and Friends
pe:timePicker binds a java.time.LocalTime (or a Date) in one of three modes (popup, spinner (default) and
inline), with startHours, endHours, intervalMinutes, showOn="button", showPeriod for a 12-hour clock and the
timeSelect, beforeShow and close Ajax events. It is a time-only input; for dates the PrimeFaces 16 documentation presents p:datePicker as the replacement of the old p:calendar.
<?xml version="1.0" encoding="UTF-8"?>
<!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"
xmlns:p="primefaces"
xmlns:pe="primefaces.extensions">
<h:head>
<title>Pickup time</title>
</h:head>
<h:body>
<h:form id="f">
<p:messages id="msgs"/>
<pe:timePicker id="pickup" value="#{pickupTimeBean.pickup}" mode="popup" startHours="9" endHours="18"
intervalMinutes="15" showOn="button" label="Pickup time"/>
<p:commandButton id="save" value="Reserve" update="@form"/>
<h:outputText id="out" value="#{pickupTimeBean.pickup}"/>
</h:form>
</h:body>
</html>
package org.example.bookshelf.web;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
import java.io.Serializable;
import java.time.LocalTime;
/** Pickup time of a reserved book, bound to a pe:timePicker. */
@Named
@ViewScoped
public class PickupTimeBean implements Serializable {
private LocalTime pickup = LocalTime.of(10, 0);
public LocalTime getPickup() {
return pickup;
}
public void setPickup(LocalTime pickup) {
this.pickup = pickup;
}
}
Posting 14:45 stored LocalTime 14:45 on both implementations. Posting the invalid 25:99 kept the old value and produced
a conversion error; the message differs, which is a Faces difference rather than an Extensions one: Mojarra printed
{0}: Conversion error occurred. (the {0} placeholder for the label stayed unresolved) and MyFaces printed
Pickup time: Conversion error occurred.. Set converterMessage if the text matters to you, and do not rely on either default.
pe:clockpicker (a clock-face picker), pe:inputOtp (separate cells for a one-time code; the 16.0.0 notes add a complete
event when the last digit is entered), pe:inputPhone (country selector, needs libphonenumber; its 14.0.0 release started decoding
values as E.164 numbers, per the migration guide), pe:creditCard, pe:calculator and pe:fuzzySearch follow the same
input pattern. They were not run on this page.
Utility Components
The remaining "utility" tags of the catalogue are small, mostly script-side helpers. Two were run:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core"
xmlns:p="primefaces"
xmlns:pe="primefaces.extensions">
<h:head>
<title>Session timeout</title>
</h:head>
<h:body>
<h:form id="f">
<p:dialog widgetVar="expiring" header="Session expiring" modal="true" closable="false">
<h:outputText value="Your session is about to expire."/>
</p:dialog>
<pe:session reactionPeriod="30" onexpire="PF('expiring').show()"
onexpired="window.location.href = 'login.xhtml?expired=true'"/>
</h:form>
</h:body>
</html>
pe:session renders PrimeFaces.cw("Session", ...) with the servlet session’s max_inactive_interval (1800 seconds here, read
from the HttpSession) and reactionPeriod (the VDL calls it the reaction period in seconds); onexpire is the callback for a session
that is about to expire and onexpired the one for a session that has expired, and the VDL marks both as required. The page rendered on both implementations; the timers themselves were not
waited out. A warning dialog before expiry belongs together with a server-side error page for ViewExpiredException
(exception handling), because the client timer cannot know about a session that the server
invalidated for another reason.
The rest, from their VDL descriptions only (not run): pe:clipboard copies or cuts text or an element, pe:counter animates a
number, pe:timer and pe:timeAgo handle time display, pe:markText highlights search terms in containers (the 16.0.0 notes
mention a backend Ajax round trip), pe:lightSwitch switches to a dark theme from the operating-system setting, pe:cookiePolicy
shows content depending on cookie consent, and pe:localized picks content by locale from files or facets. The latter pairs with
resource bundles for text but loads whole documents.
Data Export: p:dataExporter, Not pe:exporter
The book’s exporter component is gone. The PrimeFaces Extensions migration guide lists Exporter as deprecated (use
p:dataExporter) for the step from 8.0 to 10.0.0 and as removed in 10.0.0 to 10.0.1, and the 16.0.0 tag library has no
exporter tag. Export is a PrimeFaces core feature now, whose documented types are xls, xlsx, xlsxstream, pdf, csv and xml (PDF needs OpenPDF and the Excel types need Apache POI
on the classpath) and which has a way to plug in your own exporter class. The data-component page
covers the table export; this page ran it twice. A p:dataTable over the catalogue:
<?xml version="1.0" encoding="UTF-8"?>
<!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"
xmlns:p="primefaces"
xmlns:pe="primefaces.extensions">
<h:head>
<title>Export</title>
</h:head>
<h:body>
<h:form id="f">
<p:dataTable id="books" var="book" value="#{catalogueExportBean.books}">
<p:column headerText="Title" field="title"/>
<p:column headerText="Author" field="author"/>
<p:column headerText="Year" field="year"/>
</p:dataTable>
<p:commandButton id="csv" value="CSV" ajax="false">
<p:dataExporter type="csv" target="books" fileName="catalogue"/>
</p:commandButton>
</h:form>
</h:body>
</html>
A form post of the CSV button answered 200, Content-Type: text/csv;charset=UTF-8 and Content-Disposition: attachment;filename="catalogue.csv"
with a header row "Title","Author","Year" and the books, on both implementations (the file starts with a byte-order mark). The sheet
example above exports the same way. Exported values that start with =, @, + or - can be interpreted as formulas by
spreadsheet programs: the file download page
(file upload and download) owns the CSV escaping rules, and the run on the
data components page found that the PrimeFaces 16.0.0 CSV exporter does not neutralise them, so escape user-supplied text before it reaches an export.
What Moved into PrimeFaces Core or Was Removed
Sudheer Jonna’s book on the library was written for Extensions 1.2.1 and PrimeFaces 4.0. Many of its components have since moved into PrimeFaces core, been replaced, or been dropped. The table is built from the migration guide of the Extensions wiki and from comparing the tag library of Extensions 16.0.0 (67 tags, read from the JAR) with the PrimeFaces 16.0.0 core tag library (read from its JAR). "Not in the 16.0.0 tag library" means exactly that; where the guide gives the version or reason, it is stated, and where nothing does, no version is claimed.
The table lists components that Extensions had at some point (the book’s era, and the 5.x to 11.x lines) and where they are in 16.0.0.
| Earlier component | Today in Extensions 16.0.0 | What to use |
|---|---|---|
|
Not in the tag library. Removed in 12.0.5 "due to legal reasons"; |
|
|
Not in the tag library. Deprecated in 10.0.0, removed in 10.0.1 (migration guide) |
|
|
Not in the 16.0.0 tag library. The migration guide does not list it; the release notes checked do not say when it left |
|
|
Not in the tag library |
|
|
Not in the tag library |
|
|
Not in the tag library. Deprecated in 10.0.0 and removed in 10.0.1 (migration guide) |
|
|
Not in the tag library. Deprecated in 10.0.0 and removed in 10.0.1 (migration guide) |
|
|
Not in the tag library. Deprecated in 11.0.0 (migration guide) |
|
|
Not in the tag library. Deprecated in 11.0.0 (migration guide) |
|
|
Not in the tag library. Deprecated in 10.0.0 and removed in 10.0.1, with no replacement (migration guide) |
nothing |
|
Removed in 11.0.0 (migration guide) |
|
|
Still present, and PrimeFaces core has its own tags with the same names |
Choose one library per element (What Is in 16.0.0) |
|
Present in the 16.0.0 tag library |
Same tags; check every attribute against the VDL (for example the |
Source: PrimeFaces Extensions wiki: Migration Guide, PrimeFaces Extensions release notes and PrimeFaces 16 documentation
The migration guide also records two renames that the 16.x line inherits: pe:qrCode attributes mSize, mPosX and
mPosY became labelSize, labelPosX and labelPosY in 14.0.4, and ThemeAccentColorResourceHandler became
ThemeModificationResourceHandler in 15.0.2. Note the JavaScript configuration of the rendered QR code still contains the
mSize, mPosX and mPosY keys (observed in the page output), which is internal.
Timeline: Now p:timeline
The book’s pe:timeline is not in the Extensions tag library any more; PrimeFaces core has p:timeline, documented as an
interactive graph to visualize events in time. Its value is a TimelineModel of TimelineEvent objects built with
TimelineEvent.<String>builder(). The PrimeFaces timeline page also shows a custom menu facet, loading screens and tooltips.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:p="primefaces">
<h:head>
<title>Acquisitions</title>
</h:head>
<h:body>
<p:timeline id="acquisitions" value="#{acquisitionTimelineBean.model}" height="250px"/>
</h:body>
</html>
package org.example.bookshelf.web;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Named;
import java.time.LocalDate;
import org.primefaces.model.timeline.TimelineEvent;
import org.primefaces.model.timeline.TimelineModel;
import org.primefaces.util.EscapeUtils;
/** Acquisition dates of the catalogue, drawn by the PrimeFaces core p:timeline. */
@Named
@RequestScoped
public class AcquisitionTimelineBean {
private TimelineModel<String, String> model;
@PostConstruct
void init() {
model = new TimelineModel<>();
add("Dune acquired", LocalDate.of(2024, 3, 12));
add("Hyperion acquired", LocalDate.of(2025, 9, 2));
}
/** The event text is written into the page as markup: encode whatever did not come from you. */
private void add(String text, LocalDate day) {
model.add(TimelineEvent.<String>builder().data(EscapeUtils.forHtml(text)).startDate(day).build());
}
public TimelineModel<String, String> getModel() {
return model;
}
}
The bean compiled against PrimeFaces 16.0.0 as written (TimelineModel<String, String>; the documentation example uses the raw
type) and both implementations drew the two events in headless Chrome. The browser console logged
You disabled XSS protection for vis-Timeline, a warning from the bundled library, and it is accurate. The event data goes into
the page as the content of an inline script object and is shown as HTML. With the text <b>Dune</b> "q" </script><i>x</i>
passed unencoded (Mojarra), the page contained content:"<b>Dune</b> "q" </script><i>x</i>": the double quote ended the
JavaScript string (SyntaxError: Unexpected identifier 'q') and </script> ended the script element. That is script
injection if the text comes from users. Encoding the text first with org.primefaces.util.EscapeUtils.forHtml (the helper class the
PrimeFaces documentation uses in its exporter example), as the bean above does, produced
content:"<b>Dune</b> "q" </script><i>x</i>" and the timeline showed the text literally. This is
behaviour of PrimeFaces core, not of Extensions; it is recorded here because moving from pe:timeline to p:timeline is the
migration this section is about, and the fix is on your side.
Where the Components Sit in the Lifecycle
Extensions components follow the same request processing lifecycle as every Faces component
(the six phases), and the earlier pages on Ajax and PrimeFaces apply:
process chooses what is decoded, converted, validated and updated, update what is re-rendered
(execute and render).
| Group | Phases that matter |
|---|---|
Input components: |
Apply Request Values reads the submitted string (the editors sanitize here), Process Validations converts and validates (so the |
Output only: |
Render Response only |
Behaviour sources: |
An Ajax request enters at Apply Request Values with the |
Navigation: |
The level is an input of the component ( |
Source: Jakarta Faces 4.1 specification, 2.2 Standard Request Processing Lifecycle Phases and PrimeFaces Extensions VDL
The table is derived from how the components are built (input components, behaviours, renderers), and from the observed results
above; the exact phase of every event of every component was not traced with a PhaseListener.
Mojarra and MyFaces
Every example above ran on both implementations with the same results, except for these points:
-
Conversion messages: Mojarra printed
{0}: Conversion error occurred.for a badpe:timePickervalue, MyFacesPickup time: Conversion error occurred.(the label placeholder). -
Scripted Ajax posts: MyFaces only treated a post as coming from the form when the request carried the hidden field
f_SUBMIT=1that its rendered form contains (a hand-built request without it did not apply the posted values). Browsers send the field, so the difference matters only for tests that build requests by hand, and for tools that post forms without rendering the page first. -
Rendering details: partial-response and view-state ids differ between the two (
j_id1:...versusj_id__v_0:...) in the raw Ajax responses; Extensions does not hide that. -
The
pe:qrCoderadiusdefect: both implementations fail the same way (Setup).
Everything else, including the sanitizer results, the editors, the sheet events, pe:remoteCommand conversion and the exports, was
identical in the runs. Not compared: application servers other than Tomcat with Weld, and Faces 4.0.
What Changed
-
Extensions 8.0 to 11.0: Badge, Exporter, Gravatar, LetterAvatar and Social deprecated in 10.0.0 and removed in 10.0.1; FAB and Scaffolding deprecated in 11.0.0 and PrinterBehavior removed in 11.0.0;
ThemeAccentColorResourceHandlermoved toorg.primefaces.extensions.applicationin 11.0.0 (migration guide). -
12.0.5 and 12.0.6: CKEditor removed (legal reasons); SunEditor added as the replacement.
-
13 to 14:
pe:inputPhonedecodes values as E.164 numbers; useformatOnDisplay="true"or a converter to display another form. In 14.0.1 itscountrySearchattribute was removed (always on). In 14.0.4 thepe:qrCodelabel attributes were renamed. -
15.0.2:
ThemeAccentColorResourceHandlerrenamedThemeModificationResourceHandler. -
16.0.0-RC1 to 16.0.0 (2026): the project moved to PrimeFaces' component development kit (the release notes list "CDK" migrations for many components, which is why attributes now come with generated descriptions), gave
pe:markTexta backend Ajax round trip, updated Orgchart, SunEditor 3.x and jQuery UI Layout 2.0.0 and added touch drag and drop to Kanban. 16.0.0 adds thecompleteevent tope:inputOtpand anoutsideClickDeselectsoption onpe:sheet. -
PrimeFaces core, for contrast: the PrimeFaces 16 "What is new" page lists its own changes (button
size, dialogdismissableMask, GMap asynchronous loading, and others); they are not Extensions changes.
Source: PrimeFaces Extensions wiki: Migration Guide, PrimeFaces Extensions release notes and PrimeFaces 16 documentation: What is new
Faces 5.0 is upcoming (5.0.0-M9 at the time of writing) and the Extensions project has not documented support for it; do not assume it.
Where the Books Are Outdated
Sudheer Jonna’s Learning PrimeFaces Extensions Development (Packt, 2014) targets Extensions 1.2.1 and PrimeFaces 4.0, and its sample code is on GitHub. For the Extensions-specific chapters it is a useful tour of ideas, but little of the setup and many of the components have changed:
-
Packaging and namespace. Its
javax.facespages withxmlns:pe="http://primefaces.org/ui/extensions"becomejakarta.faces.*with the URNprimefaces.extensions(Setup). The project wiki’s "Getting Started" page uses the old URI (still accepted) and says PrimeFaces is a transitive dependency, which is not true for 16.0.0 (the dependency isprovided). -
Components that are gone or renamed: CKEditor, exporter, timeline, spotlight and Ajax error handling (What Moved into PrimeFaces Core or Was Removed); their replacements are in PrimeFaces core, SunEditor and the core exception handler.
-
Components that stayed but were reworked: tooltips,
pe:remoteCommand(typed parameters throughpe:methodSignatureandpe:methodParam),pe:dynaForm,pe:fluidGrid,pe:layout,pe:masterDetail,pe:codeMirror,pe:waypoint,pe:timePicker, QR codes and the image components: check each attribute against the VDL instead of trusting the book. -
Utility functions and resource optimisation. The 16.0.0 tag library declares no EL functions, and resource handling is the optimizer plugin plus ordinary Faces resource handling (Setup, and the section’s performance page).
-
Security. Check any book sample that prints rich text or user input against Safe HTML: Sanitizing Converters, Editors and the Output Listener before copying it, in particular any
escape="false"output. The CKEditor XSS fix in 8.0.4 and its removal in 12.0.5 show that rich-text input needs care. -
Release practice. The book describes one downloadable release; Extensions now releases in step with PrimeFaces and still releases older lines (the release list shows 15.0.18 and 13.0.21), so choose the line that matches your PrimeFaces line.
Pitfalls
-
Mixed versions. Extensions 16.0.0 with PrimeFaces 15 (or the reverse) is not supported by the pairing table. Use one property for both.
-
Expecting a transitive PrimeFaces. The Extensions dependency is
provided; leave outprimefacesand the application has no PrimeFaces classes (not run: expect a class-loading failure). -
Missing optional libraries. Editors with
secure="true"want the OWASP sanitizer on the classpath,pe:convertJsonwants Gson,pe:inputPhonewants libphonenumber. Add the dependency, or setsecure="false"only if you accept the consequences. -
Printing sanitized text with
escape="false"afterdecodeHtml="true". Entity-encoded markup comes back as markup (Safe HTML: Sanitizing Converters, Editors and the Output Listener). -
Trusting
secureonpe:codeMirror. The renderer in 16.0.0 does not apply it; escape and validate the value yourself. -
pe:qrCodewithoutradius. AClassCastExceptionin 16.0.0 on both implementations; setradius="0". -
Raw JavaScript attributes.
pe:waypointoffsetis emitted as JavaScript source, so a string needs quotes inside the attribute value. -
Sheet events describe the selection, not the edit. Read changed values from the model; select a cell before scripting a change.
-
Mixing
p:andpe:versions of blockUI, tooltip and remoteCommand. They are different components with different attributes. -
User text in
p:timelineevents. Encode it withEscapeUtils.forHtml(or sanitize) before adding it to the model. -
Assuming CSP support. The security page documents PrimeFaces' content security policy mode; whether the Extensions widgets (Handsontable, CodeMirror, SunEditor and others) work under a strict policy was not tested, so test them with
primefaces.CSPset toreportOnlyfirst (CSP). -
Components not run. Monaco, Kanban, OrgChart, ECharts, GChart, OSMap, DocumentViewer, Keynote, CodeScanner, InputPhone and the rest of the catalogue were not run; learn each from the showcase before depending on it.
References
-
PrimeFaces Extensions on GitHub (README: versioning, Maven, namespaces)
-
PrimeFaces Extensions wiki: Maven plugin for web resource optimization
-
Jakarta Faces 4.1 specification: Standard Request Processing Lifecycle Phases
-
Learning PrimeFaces Extensions Development, Sudheer Jonna, Packt, 2014 (publisher page)