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:

Table 1. Version pairing from the README (16.x and the three previous lines)
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.version to 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 about process, update and the lifecycle applies unchanged.

  • Some components exist in both libraries. pe:blockUI, pe:tooltip and pe:remoteCommand have 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:

pom.xml (dependencies)
<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.

Table 2. Optional dependencies of primefaces-extensions 16.0.0
Library (coordinates) Needed by Version the 16.0.0 build uses

com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer

pe:sanitizer, pe:sanitizerLenient, pe:sunEditor, pe:markdownEditor with secure="true" (the default). By its source, such an editor throws a FacesException if the sanitizer is not on the classpath

20260313.1

com.google.code.gson:gson

pe:convertJson and pe:convertGson, pe:layout options, pe:gChart, pe:markText

2.14.0

org.commonmark:commonmark

Rendering Markdown content in pe:localized (markdown="true"); it is the only class in the 16.0.0 sources that imports it

0.30.0

com.googlecode.libphonenumber:libphonenumber

pe:inputPhone

9.0.38

org.primefaces.extensions:resources-monacoeditor

pe:monacoEditor and the other Monaco components: their large JavaScript bundle is a separate JAR

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:

basics.xhtml: namespaces, a tooltip and a QR code
<?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.

Table 3. Tags of the pe: namespace in 16.0.0, by purpose
Purpose Tags

Editors

pe:codeMirror (syntax highlighting and completion), pe:monacoEditor, pe:monacoEditorFramed, pe:monacoDiffEditor, pe:monacoDiffEditorFramed, pe:sunEditor (WYSIWYG), pe:markdownEditor

Data entry

pe:timePicker, pe:clockpicker, pe:inputOtp (one-time code), pe:inputPhone, pe:inputPlace, pe:creditCard, pe:calculator, pe:fuzzySearch, pe:triStateManyCheckbox, pe:sheet with pe:sheetcolumn (spreadsheet-like editing)

Dynamic forms and layout

pe:dynaForm with pe:dynaFormControl, pe:fluidGrid with pe:fluidGridItem, pe:layout with pe:layoutPane (border layout), pe:masterDetail with pe:masterDetailLevel and pe:selectDetailLevel, pe:slideout, pe:switch with pe:case and pe:defaultCase, pe:keynote with pe:keynoteItem (presentations on Reveal.js), pe:kanban, pe:orgchart, pe:commandPalette with pe:commandPaletteItem

Charts, maps and media

pe:echart (Apache ECharts from a raw JSON model), pe:gChart (Google Charts), pe:legend, pe:meterGroup, pe:osmap (OpenStreetMap with Leaflet), pe:documentViewer (PDF.js), pe:imageAreaSelect, pe:imageRotateAndResize, pe:imageZoom, pe:qrCode, pe:codeScanner (barcode and QR from the camera)

Behaviour and utilities

pe:waypoint, pe:remoteCommand with pe:methodSignature, pe:methodParam and pe:assignableParam, pe:javascript (a behaviour), pe:blockUI, pe:tooltip, pe:session (client events before and at session expiry), pe:clipboard, pe:counter, pe:timer, pe:timeAgo, pe:speedtest, pe:markText (highlight search terms), pe:lightSwitch (dark theme from the OS setting), pe:cookiePolicy with pe:saveCookiePolicy, pe:localized, pe:head

Converters

pe:sanitizer, pe:sanitizerLenient, pe:convertJson, pe:convertGson, pe:convertLocale

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.

sanitize.xhtml: the default (strict) converter, shown escaped and 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/>
    </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>
ReviewCommentBean.java
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:

Table 4. Submitted text and what the page rendered (default converter)
Submitted Escaped output (h:outputText) Unescaped output (escape="false")

Nice <b>book</b> & <i>great</i>

Nice book &amp; great

Nice book & great

<script>alert(1)</script>before

before

before

<a href="javascript:alert(2)">x</a> <img src=x onerror=alert(3)>

x

x

Tom &amp; Jerry (an entity)

Tom &amp; Jerry

Tom & Jerry

&lt;script&gt;alert(1)&lt;/script&gt; (entity-encoded markup)

&lt;script&gt;alert(1)&lt;/script&gt;

<script>alert(1)</script>

&lt;img src=x onerror=alert(1)&gt;

&lt;img src=x onerror=alert(1)&gt;

<img src=x onerror=alert(1)>

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:

sanitize2.xhtml (the converter line) and the policy bean
<?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>
ReviewPolicies.java
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;
    }
}

The same inputs through this configuration (policy: inline formatting and links), again identical on both implementations:

Table 5. Submitted text and what the page rendered (decodeHtml false, formatting and links allowed)
Submitted Escaped output Unescaped output

Nice <b>book</b> & <i>great</i>

Nice &lt;b&gt;book&lt;/b&gt; &amp;amp; &lt;i&gt;great&lt;/i&gt;

Nice <b>book</b> &amp; <i>great</i>

<script>alert(1)</script>before

before

before

&lt;script&gt;alert(1)&lt;/script&gt;

&amp;lt;script&amp;gt;alert(1)&amp;lt;/script&amp;gt;

&lt;script&gt;alert(1)&lt;/script&gt;

&lt;img src=x onerror=alert(1)&gt;

&amp;lt;img src&amp;#61;x onerror&amp;#61;alert(1)&amp;gt;

&lt;img src&#61;x onerror&#61;alert(1)&gt;

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

editors.xhtml
<?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>
ReviewEditorBean.java
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.

faces-config.xml (excerpt)
<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

stored.xhtml
<?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.

code.xhtml
<?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>
ImportSettingsBean.java
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 &amp; 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.

grid.xhtml
<?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>
CoverGalleryBean.java
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.

master.xhtml
<?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>
BookDrilldownBean.java
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).

dyna.xhtml
<?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>
BorrowDynaFormBean.java
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.

sheet.xhtml
<?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>
StockSheetBean.java
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;
    }
}
StockRow.java
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;
    }
}

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 change Ajax behaviour at once, and the row object held 7 afterwards.

  • Editing a text cell fires change later: after the selection moves on. The event then describes the newly selected row and column: with row 2’s shelf edited to Z9 and the selection moved to row 3, the bean saw row 3 (event.getRowIndex() 3), while the value Z9 did 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 NullPointerException in Sheet.queueEvent because getSelectedRow() was null. 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 for Sheet with the PrimeFaces DataExporters registry 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.

waypoint.xhtml
<?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>
FeedScrollBean.java
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.

remote.xhtml
<?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>
ShelfRemoteBean.java
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.

time.xhtml
<?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>
PickupTimeBean.java
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:

session.xhtml: warn before the session expires
<?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:

export.xhtml
<?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.

Table 6. Earlier Extensions components and where they are in 16.0.0
Earlier component Today in Extensions 16.0.0 What to use

pe:ckEditor

Not in the tag library. Removed in 12.0.5 "due to legal reasons"; pe:sunEditor was added in 12.0.6 as a replacement (release notes and migration guide)

pe:sunEditor or pe:markdownEditor (Safe HTML: Sanitizing Converters, Editors and the Output Listener)

pe:exporter

Not in the tag library. Deprecated in 10.0.0, removed in 10.0.1 (migration guide)

p:dataExporter (Data Export: p:dataExporter, Not pe:exporter)

pe:timeline

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

p:timeline, which PrimeFaces 16 documents as an interactive graph of events in time (Timeline: Now p:timeline)

pe:spotlight

Not in the tag library

p:spotlight (PrimeFaces 16 documents it as highlighting a component on the page)

pe:ajaxErrorHandler (Ajax error handling)

Not in the tag library

p:ajaxExceptionHandler and PrimeFaces' exception handler, covered in the exception handling page

pe:gravatar, pe:letterAvatar

Not in the tag library. Deprecated in 10.0.0 and removed in 10.0.1 (migration guide)

p:avatar

pe:badge

Not in the tag library. Deprecated in 10.0.0 and removed in 10.0.1 (migration guide)

p:badge

pe:fab

Not in the tag library. Deprecated in 11.0.0 (migration guide)

p:speedDial

pe:scaffolding

Not in the tag library. Deprecated in 11.0.0 (migration guide)

p:outputPanel with deferred="true" and a loading facet

pe:social

Not in the tag library. Deprecated in 10.0.0 and removed in 10.0.1, with no replacement (migration guide)

nothing

PrinterBehavior

Removed in 11.0.0 (migration guide)

p:printer exists in the 16.0.0 core tag library

pe:blockUI, pe:tooltip, pe:remoteCommand

Still present, and PrimeFaces core has its own tags with the same names

Choose one library per element (What Is in 16.0.0)

pe:codeMirror, pe:dynaForm, pe:fluidGrid, pe:layout, pe:masterDetail, pe:qrCode, pe:sheet, pe:timePicker, pe:waypoint, pe:imageAreaSelect, pe:imageRotateAndResize, pe:imageZoom

Present in the 16.0.0 tag library

Same tags; check every attribute against the VDL (for example the pe:qrCode label attributes were renamed)

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.

timeline.xhtml
<?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>
AcquisitionTimelineBean.java
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:"&lt;b&gt;Dune&lt;/b&gt; &#34;q&#34; &lt;/script&gt;&lt;i&gt;x&lt;/i&gt;" 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).

Table 7. Where each group of components takes part
Group Phases that matter

Input components: pe:codeMirror, pe:sunEditor, pe:markdownEditor, pe:timePicker, pe:sheet, pe:dynaForm controls

Apply Request Values reads the submitted string (the editors sanitize here), Process Validations converts and validates (so the pe:sanitizer converter runs here), Update Model Values calls the setter. Validation failure skips Invoke Application

Output only: pe:fluidGrid, pe:qrCode, pe:tooltip, pe:session

Render Response only

Behaviour sources: pe:waypoint, pe:sheet events, pe:remoteCommand

An Ajax request enters at Apply Request Values with the process ids; listeners run in Invoke Application (or earlier when immediate is set); pe:remoteCommand parameters are converted by the nested converters before the method is called

Navigation: pe:masterDetail

The level is an input of the component (level binding); pe:selectDetailLevel changes it in the same request, then Render Response draws the new level

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 bad pe:timePicker value, MyFaces Pickup 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=1 that 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:... versus j_id__v_0:...) in the raw Ajax responses; Extensions does not hide that.

  • The pe:qrCode radius defect: 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; ThemeAccentColorResourceHandler moved to org.primefaces.extensions.application in 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:inputPhone decodes values as E.164 numbers; use formatOnDisplay="true" or a converter to display another form. In 14.0.1 its countrySearch attribute was removed (always on). In 14.0.4 the pe:qrCode label attributes were renamed.

  • 15.0.2: ThemeAccentColorResourceHandler renamed ThemeModificationResourceHandler.

  • 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:markText a 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 the complete event to pe:inputOtp and an outsideClickDeselects option on pe:sheet.

  • PrimeFaces core, for contrast: the PrimeFaces 16 "What is new" page lists its own changes (button size, dialog dismissableMask, GMap asynchronous loading, and others); they are not Extensions changes.

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.faces pages with xmlns:pe="http://primefaces.org/ui/extensions" become jakarta.faces.* with the URN primefaces.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 is provided).

  • 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 through pe:methodSignature and pe: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 out primefaces and 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:convertJson wants Gson, pe:inputPhone wants libphonenumber. Add the dependency, or set secure="false" only if you accept the consequences.

  • Printing sanitized text with escape="false" after decodeHtml="true". Entity-encoded markup comes back as markup (Safe HTML: Sanitizing Converters, Editors and the Output Listener).

  • Trusting secure on pe:codeMirror. The renderer in 16.0.0 does not apply it; escape and validate the value yourself.

  • pe:qrCode without radius. A ClassCastException in 16.0.0 on both implementations; set radius="0".

  • Raw JavaScript attributes. pe:waypoint offset is 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: and pe: versions of blockUI, tooltip and remoteCommand. They are different components with different attributes.

  • User text in p:timeline events. Encode it with EscapeUtils.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.CSP set to reportOnly first (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.