PrimeFaces: Ajax, Forms and Validation

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), PrimeFaces 16.0.0 (the previous line is 15.0.x), Eclipse Mojarra 4.1.17 and Apache MyFaces Core 4.1.4, with Java 17 or later. The PrimeFaces 16 migration guide says that its baseline is Jakarta Faces 4.0 and newer (all javax.* support was removed), so the text also holds for Faces 4.0 as far as the PrimeFaces documentation goes; nothing was run on 4.0. The examples were compiled with --release 21 and deployed in a scratch Bookshelf WAR on Apache Tomcat 11.0.26 with Weld 6.0.4.Final and Hibernate Validator 9.1.4.Final, once with Mojarra and once with MyFaces. The browser side was driven with a real browser (headless Google Chrome controlled through the DevTools protocol) and the server side with raw HTTP posts, so the request parameters, the partial responses, the JavaScript results and the validation outcomes below come from those runs (checked on 2026-10-08). The places where the two implementations disagree are collected in Mojarra and MyFaces.

Standard Ajax (f:ajax, the faces.ajax API and the partial lifecycle) is on the Ajax page; this page does not repeat it. It covers what PrimeFaces adds around it for forms: its own Ajax behaviour and commands, the process and update model, the client callbacks, the input components of the Bookshelf review form, the message components, and the PrimeFaces client-side validation framework. The data tables, panels and dialogs, files and charts, the PrimeFaces Extensions and OmniFaces have their own pages in this section (data components, panels, menus and dialogs, files, media, charts and drag and drop, PrimeFaces Extensions and OmniFaces). The PrimeFaces set-up (dependency, themes, icons, the context parameters in general) is on the page about getting started with PrimeFaces. Testing pages that use these components in a real browser is the subject of the testing page; the application-wide server settings (view state in a cluster) are on the deployment page.

The PrimeFaces Ajax Model

PrimeFaces does not replace the Faces Ajax protocol; it sends it with its own client. A p:ajax behaviour, a p:commandButton and a p:remoteCommand all end in the same call, PrimeFaces.ajax.Request.handle(cfg), which the page emits as the shorthand PrimeFaces.ab({...}). On the server the request is an ordinary Faces postback with the parameters of the standard protocol. This is a request that the browser sent for the preview example of this page (typing one character into a p:inputText that carries a p:ajax), decoded and with the view state shortened:

POST /bookshelf/preview.xhtml
Faces-Request: partial/ajax
X-Requested-With: XMLHttpRequest

jakarta.faces.partial.ajax=true
jakarta.faces.source=draft:title
jakarta.faces.partial.execute=draft:title
jakarta.faces.partial.render=draft:preview+draft:keystrokes
jakarta.faces.behavior.event=keyup
jakarta.faces.partial.event=keyup
draft=draft
draft:title=D
jakarta.faces.ViewState=...

What is different from f:ajax is the client and a few server conventions:

  • Own client, based on jQuery. PrimeFaces ships jquery.js, core.js and components.js and does not use faces.js. On the scratch pages that contain no f:ajax, the global faces object was undefined and the requests still worked. If a page mixes both, each request goes through the client of the tag that created it; the Ajax page covers the standard one.

  • Identifiers are resolved while the page is rendered. The attributes process and update accept search expressions (Search Expressions in PrimeFaces). PrimeFaces resolves them to client ids when it renders the button or the behaviour, and the browser sends the result: u:"draft:preview,draft:keystrokes" in the generated onkeyup handler became jakarta.faces.partial.render=draft:preview+draft:keystrokes in the request. An expression that matches nothing is therefore an error when the page that contains it is rendered, not when the button is clicked (An Unknown Id Fails the Page).

  • Extra parameters. PrimeFaces adds its own parameters when an option is set, for example primefaces.ignoreautoupdate=true (p:autoUpdate and Event Observers), and answers with a standard partial response that may carry extension elements with the callback arguments (Client Callbacks and Callback Arguments) and with scripts (PrimeFaces.current()).

  • Components that are not Ajax commands send their own requests. p:autoComplete posts review:book_query=Du with itself as source, execute and render target (p:autoComplete).

The documentation of PrimeFaces says that its Ajax framework is built on the standard server-side APIs with no custom view root, state manager, view handler, servlet filter or phase listener. The META-INF/faces-config.xml inside primefaces-16.0.0.jar does register a number of standard extension points, and they matter when you debug:

Registered by the jar Effect

PrimePartialViewContextFactory and PrimeFacesContextFactory

They wrap the partial view context and the Faces context of every request. The callback arguments and the scripts of PrimeFaces.current() come back in the partial response as extension elements.

Phase listeners AutoUpdatePhaseListener, CspPhaseListener and DialogKeepFlashPhaseListener

p:autoUpdate (p:autoUpdate and Event Observers), the Content Security Policy support (the primefaces.CSP parameter; see the security page) and the dialog framework, which is covered on the page about panels, menus and dialogs.

PrimeResourceHandler and a system event listener MetadataTransformerExecutor (on PreRenderComponentEvent)

The resource handling of the PrimeFaces resources, and the Bean Validation metadata transformation of Bean Validation Metadata on the Components.

A client-behaviour renderer for org.primefaces.component.AjaxBehaviorRenderer

The renderer behind p:ajax.

Renderers for jakarta.faces.Head and jakarta.faces.Body

Resource ordering and the optional moving of scripts to the end of the body (primefaces.MOVE_SCRIPTS_TO_BOTTOM).

Converters for the standard ids and types (jakarta.faces.Integer, Long, Double, Float, Short, Byte, Character, Boolean, DateTime, Number, BigInteger, BigDecimal) and validators jakarta.faces.Length, LongRange, DoubleRange, RegularExpression

PrimeFaces replaces the standard ones with subclasses that also implement the client-side validation interfaces (Client-Side Validation (CSV)).

The exception handler and the matching EL resolver of PrimeFaces are not registered by the jar; the application opts in (Ajax Errors: p:ajaxExceptionHandler and the PrimeFaces ExceptionHandler).

Where It Sits in the Lifecycle

A PrimeFaces Ajax request runs the usual Faces lifecycle, limited to the components named in process (the partial processing of the lifecycle page) and followed by a partial rendering of the components named in update. The PrimeFaces documentation describes it the same way: process means executing Apply Request Values, Process Validations, Update Model Values and Invoke Application only on the named components. The scratch listener reported its phase through FacesContext.getCurrentPhaseId(): INVOKE_APPLICATION 5 for a normal p:ajax and APPLY_REQUEST_VALUES 2 with immediate="true" (Mojarra writes the phase with a space and MyFaces with parentheses).

The PrimeFaces documentation states that Ajax needs a form: the component looks for the form given by form, then for the closest parent form, then for the first form in the document, and without a form every Ajax request throws an HTTP 405 (the fallback that it suggests is an empty form at the start of the body).

A First Example: A Live Preview with p:ajax

The review form of Bookshelf starts with a title field. While the visitor types, a preview below it follows the text, 200 milliseconds after the last key-up; the Save button stores the draft and shows a message. The p:ajax is a child of the input, like f:ajax. The imports in the bean are the standard ones: PrimeFaces does not need its own listener type. The pages of this section are shown without the site template of Bookshelf (the layout of the templating page), so that each stands alone.

src/main/webapp/preview.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="http://primefaces.org/ui">
<h:head>
  <title>Review preview</title>
</h:head>
<h:body>
  <h:form id="draft">
    <p:messages id="msgs"/>

    <p:outputLabel for="title" value="Review title"/>
    <p:inputText id="title" value="#{previewBean.title}" required="true">
      <p:ajax event="keyup" delay="200" listener="#{previewBean.titleTyped}" update="preview keystrokes"/>
    </p:inputText>

    <h:outputText id="preview" value="Preview: #{previewBean.title}"/>
    <h:outputText id="keystrokes" value="#{previewBean.keystrokes} requests, last phase #{previewBean.phase}"/>

    <p:commandButton id="save" value="Save" action="#{previewBean.save}" update="msgs"/>
  </h:form>
</h:body>
</html>
src/main/java/org/example/bookshelf/web/PreviewBean.java
package org.example.bookshelf.web;

import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.event.AjaxBehaviorEvent;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;

import java.io.Serializable;

@Named
@ViewScoped
public class PreviewBean implements Serializable {

    private String title;
    private int keystrokes;
    private String phase = "none";

    public void titleTyped(AjaxBehaviorEvent event) {
        keystrokes++;
        phase = FacesContext.getCurrentInstance().getCurrentPhaseId().getName();
    }

    public void save() {
        FacesContext.getCurrentInstance().addMessage(null,
                new FacesMessage(FacesMessage.SEVERITY_INFO, "Saved", "Draft \"" + title + "\" stored"));
    }

    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public int getKeystrokes() { return keystrokes; }
    public String getPhase() { return phase; }
}

Typing Dune sent one request (the delay merged the four key-ups), the preview showed Preview: Dune and the counter said 1 requests, last phase INVOKE_APPLICATION. On both Mojarra and MyFaces the markup of the input carries the generated handler, in which s is the source, e the event, f the form, p the process list, u the update list and d the delay:

<input id="draft:title" name="draft:title" type="text" ... onkeyup="PrimeFaces.ab({s:&quot;draft:title&quot;,e:&quot;keyup&quot;,f:&quot;draft&quot;,p:&quot;draft:title&quot;,u:&quot;draft:preview,draft:keystrokes&quot;,d:200});" />

The p:ajax Behaviour

p:ajax is a client behaviour for any component that implements ClientBehaviorHolder, standard or PrimeFaces (see the client behaviours page). The PrimeFaces documentation calls it an extension of f:ajax. The attributes of the tag in PrimeFaces 16.0.0, from its tag library, are:

Attribute Meaning

event

The DOM or component event that triggers the request. Without it, the default of the component: valueChange for inputs (the browser event change) and click or action for commands. Checkboxes sent valueChange, h:commandButton sent action.

listener

A method expression. It may take an AjaxBehaviorEvent or no parameter; both worked on Mojarra and on MyFaces in the scratch application.

process

What to execute. Default for p:ajax: the component itself (@this). Observed: p:"draft:title" in the handler.

update

What to render. Default: nothing.

immediate

The listener runs in Apply Request Values instead of Invoke Application (measured: Listener and Phase), before the conversion and validation of the processed components.

delay

Milliseconds to wait; if another request is started within that time only the most recent is sent.

async

When true, the request is not queued behind the running ones.

timeout

Milliseconds after which the request is abandoned; 0 (the default) is no timeout.

global

When false, p:ajaxStatus is not triggered (Delay, Queue, Timeout and Global Status).

partialSubmit, partialSubmitFilter

Send only the parameters of the processed components (Partial Submit).

resetValues

Reset the local values of the input components that are updated (resetValues and Cancel Buttons).

skipChildren

For container components such as data tables, panels and tab views: when the request is caused by the container itself (sorting and paging of a table), do not process its children.

ignoreAutoUpdate

Do not update the components marked with p:autoUpdate for this request (p:autoUpdate and Event Observers).

ignoreComponentNotFound

Ignore references in process and update that cannot be resolved (An Unknown Id Fails the Page).

form

The id of the form to serialise; default: the enclosing form.

onstart, onsuccess, onerror, oncomplete

The client callbacks of Client Callbacks and Callback Arguments.

disabled

Disables the behaviour.

The same Ajax attributes (process, update, delay, async, timeout, global, partialSubmit, partialSubmitFilter, resetValues, ignoreAutoUpdate, ignoreComponentNotFound, form and the four callbacks) are available on p:commandButton, p:commandLink and p:remoteCommand, plus validateClient for the client-side validation (Client-Side Validation (CSV)).

Listener and Phase

The listener is invoked in Invoke Application, so it does not run when a validation or conversion error stopped the lifecycle after Process Validations. This is the situation of the immediate attribute; the same warning applies as on the Ajax page: it does not skip validation, it moves the listener before it. The scratch application printed the phase of three listeners:

Listener Phase (Mojarra / MyFaces)

p:ajax with a listener taking an AjaxBehaviorEvent

INVOKE_APPLICATION 5 / INVOKE_APPLICATION(5)

p:ajax with no-argument listener

INVOKE_APPLICATION 5 / INVOKE_APPLICATION(5)

p:ajax immediate="true"

APPLY_REQUEST_VALUES 2 / APPLY_REQUEST_VALUES(2)

onstart="return false;" on the behaviour cancelled the request: the browser sent nothing.

Commands: p:commandButton and p:commandLink

p:commandButton and p:commandLink submit with Ajax by default. Set ajax="false" for a full page submit. The standard action, actionListener and immediate apply as for h:commandButton. A standard h:commandButton and a h:selectBooleanCheckbox also accept p:ajax as a child; the generated handler is PrimeFaces.ab({s:this,e:"action",...}) for the button and e:"valueChange" for the checkbox.

Two defaults differ from p:ajax and are the source of most surprises. The first comes from the requests recorded in the scratch application:

Component jakarta.faces.partial.execute when process is not set

p:ajax in an input

the input itself (draft:title)

p:commandButton, p:commandLink, p:remoteCommand (Ajax)

@all: every component of the view

h:commandButton with a nested p:ajax

the button itself

A command that does not name process therefore processes (decodes, converts, validates and updates) every input of the view, not only the ones in its form, which is why one required field elsewhere stops it. Name what the command needs: process="@this" for a command that only needs its own action, process="@form" for the form (the request carried the form id, for example execute=f). The second default is disableOnAjax="true": the button is disabled while its own request is running, which protects against double clicks of that button but not against clicks on others (Delay, Queue, Timeout and Global Status).

Rendered markup, for reference: a p:commandButton renders a button element with the handler PrimeFaces.ab({s:"f:save",f:"f",u:"f:msgs,f:typed"});return false;; a p:commandLink renders an a element with href="#" and the same kind of handler.

process and update

Both attributes take a list of search expressions separated by commas or spaces. They have the same meaning as execute and render of f:ajax (the Ajax page explains them), written as the PrimeFaces documentation does:

  • an identifier relative to the naming container of the component: update="display" when both are in the same form;

  • an absolute client id starting with the separator character: update=":form2:display", for a target in another form; the prependId="false" setting of a form has no effect on how this works;

  • a keyword (Search Expressions in PrimeFaces) or a PrimeFaces selector.

The naming containers are the standard ones (h:form, composite components, h:dataTable) plus, in PrimeFaces, the tab view, accordion panel, data table, data grid, data list, carousel, galleria, ring, sheet and sub table. A target inside a container that the expression does not reach is the "cannot find component" error (An Unknown Id Fails the Page).

Partial Processing: A Dependent Menu

The classic use of process is a menu that changes another one while the rest of the form is still incomplete. The review form of Bookshelf has a genre menu, a shelf menu that depends on the genre and a required reviewer field. The listener runs only if the menu is processed alone.

src/main/webapp/shelf.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="http://primefaces.org/ui">
<h:head>
  <title>Shelf picker</title>
</h:head>
<h:body>
  <h:form id="shelf">
    <p:messages id="msgs"/>

    <p:selectOneMenu id="genre" value="#{shelfBean.genre}">
      <f:selectItem itemLabel="Choose..." itemValue="#{null}"/>
      <f:selectItems value="#{shelfBean.genres}" var="g" itemLabel="#{g.label}" itemValue="#{g}"/>
      <p:ajax listener="#{shelfBean.genreChanged}" process="@this" update="place msgs"/>
    </p:selectOneMenu>

    <p:selectOneMenu id="place" value="#{shelfBean.shelf}">
      <f:selectItem itemLabel="Choose..." itemValue="#{null}"/>
      <f:selectItems value="#{shelfBean.shelves}"/>
    </p:selectOneMenu>

    <p:outputLabel for="reviewer" value="Reviewer"/>
    <p:inputText id="reviewer" value="#{shelfBean.reviewer}" required="true"/>
    <p:commandButton id="go" value="Place book" process="@form" update="@form"/>
  </h:form>
</h:body>
</html>
src/main/java/org/example/bookshelf/web/ShelfBean.java
package org.example.bookshelf.web;

import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;

import org.example.bookshelf.model.Genre;

import java.io.Serializable;
import java.util.List;

@Named
@ViewScoped
public class ShelfBean implements Serializable {

    private Genre genre;
    private String shelf;
    private String reviewer;
    private List<String> shelves = List.of();

    public void genreChanged() {
        shelves = genre == null ? List.of() : List.of(genre.name() + "-A", genre.name() + "-B");
        shelf = null;
    }

    public Genre getGenre() { return genre; }
    public void setGenre(Genre genre) { this.genre = genre; }
    public String getShelf() { return shelf; }
    public void setShelf(String shelf) { this.shelf = shelf; }
    public String getReviewer() { return reviewer; }
    public void setReviewer(String reviewer) { this.reviewer = reviewer; }
    public List<String> getShelves() { return shelves; }
    public Genre[] getGenres() { return Genre.values(); }
}

With process="@this" choosing Fiction filled the shelf menu with FICTION-A and FICTION-B. The same page with process="@all" (a copy of the file with that one change) ran into the required reviewer field: the listener did not run, the shelf menu stayed empty, and the message Reviewer: Validation Error: Value is required. was queued. In this version the message did show, because msgs is in update; leave it out and the user sees nothing at all, because the message is added to the FacesContext during that request and is not displayed by the next one. This is the PrimeFaces documentation’s own example for partial validation; note that its text uses @all as the broken version and @this as the fix.

Two more outcomes seen in the scratch application:

  • process="@form" behaves like @all for this page (the whole form is processed and the required field blocks).

  • A process list that does not contain the command itself (process="a b") behaved differently: Mojarra did not invoke the action of the button (a counter stayed where it was) and MyFaces did. Write process="a b @this" so that both agree.

  • process="@none" on a command does not run its action. A button whose action increments a counter, with process="@none", posted the request and rendered, but the counter stayed at 0, because the source component was not part of the execute list. Use @this to run only the button.

Search Expressions in PrimeFaces

Faces 2.3 added search expressions to the specification; the search expressions page covers the standard keywords, the algorithm and the handler. PrimeFaces 14 and later delegates the resolution of the standard ones to the Faces implementation (the 14.0.0 migration guide notes that this can change results compared to the old shims) and adds its own keywords and the PrimeFaces Selectors:

Expression Meaning

@this, @form, @namingcontainer, @parent, @composite, @child(n), @previous, @next, @root, @all, @none, @id(id)

The standard keywords (the PrimeFaces documentation lists them). @form is the closest ancestor form, @parent the parent of the current component.

@row(n)

The nth row of an iterating component.

@widgetVar(name)

The component whose widgetVar is name.

@obs(event)

The components that observe the event with p:autoUpdate on="event" (p:autoUpdate and Event Observers).

@(selector)

A PrimeFaces Selector: a jQuery selector evaluated in the browser. Components are matched by the DOM, so they need an explicit id.

@form:@parent, @this:@parent:@parent, @composite:mybuttonid

Composite expressions: keywords combined with colons, each step resolved from the previous one.

Checked in the scratch application, each as the update of a button inside a form (f):

Expression Rendered ids in the request

@widgetVar(wvInput)

f:wv

@(.pfs) (two inputs with that style class)

f:wv+f:wv2

@form:@child(0)

f:msgs (the first child of the form)

process="@parent" of a button in the form, update="@previous"

execute f (the form), render the previous sibling

@(.nomatch)

nothing: the request carried no render parameter and succeeded

@obs(tick)

@obs(tick), resolved on the server to the components that observe tick

PrimeFaces Selectors have two properties worth knowing. They avoid the naming-container limits, because the browser matches the DOM and ignores the component tree; and they cost the server nothing, because the tree is not traversed. In exchange, the matched elements must have ids, because the request carries the ids of the matches. The PrimeFaces documentation shows update="@(form:first)", update="@(:input)" and process="@(.ui-panel :input)".

An Unknown Id Fails the Page

Because the identifiers are resolved when the component is rendered, a typo in update breaks the page that contains it rather than the request. A button with update="doesNotExist" made Mojarra answer the whole page with an HTTP 500 and ComponentNotFoundException: Cannot find component for expressions "doesNotExist" referenced from "f:sx6". The same button with ignoreComponentNotFound="true" rendered and the click sent a request without that target. The attribute is also the way to reference an optional component that is rendered="false" on some views, but it hides typos: prefer a wrapper that is always rendered (an h:panelGroup) and update the wrapper.

Partial Submit

By default every Ajax request serialises the whole form, so a field with a p:ajax that processes only itself still uploads the other inputs. partialSubmit="true" sends only the parameters of the processed components. The scratch page had three inputs (a, b, c) and four buttons; these are the form parameters of each request (view state omitted; f is the form id):

Request Mojarra, primefaces.SUBMIT = full Mojarra, primefaces.SUBMIT = partial

p:ajax event="keyup" process="@this" on a

f=f&f:a=x&f:b=&f:c=

f:a=x

button, process="@this", partialSubmit="false"

f:full=f:full&f=f&f:a=x&f:b=&f:c=

the same (explicit setting wins)

button, process="@this", partialSubmit="true"

f:part=f:part

the same

button, process="a b", partialSubmit="true"

f:parts=f:parts&f:a=x&f:b=

the same

button, process="@this", no setting

f:deflt=f:deflt&f=f&f:a=x&f:b=&f:c=

f:deflt=f:deflt

The global switch is a context parameter, and the per-component attribute overrides it in both directions:

<context-param>
    <param-name>primefaces.SUBMIT</param-name>
    <param-value>partial</param-value>
</context-param>

partialSubmitFilter is a selector that picks which descendant inputs of a processed container are sent; the default is :input. With partial submit the hidden form-marker field (f=f on Mojarra, f_SUBMIT=1 on MyFaces) is not sent, and the actions still ran with the default settings in the scratch application. Use it on large forms with small Ajax interactions; it is not a security feature (Security Notes).

resetValues and Cancel Buttons

An input component keeps its local value and its submitted value in the component tree when validation fails. A "cancel" that re-renders the form then shows the stale values. resetValues="true" resets the local values of the input components that are updated by the request, so they read the model again; a context parameter turns it on for every Ajax request, and PrimeFaces.current().resetInputs(...) and the tag p:resetInput do the same from Java and from a command.

The scratch form had a required a with a minimum length of 3 and a free b; the visitor typed x and typed-b, pressed Check (a failed), then pressed a cancel button that calls clear() on the bean, is immediate and updates both inputs.

Cancel button Mojarra shows MyFaces shows

without resetValues

"" and typed-b

initial-a and typed-b

with resetValues="true"

initial-a and initial-b

initial-a and initial-b

So without it the valid sibling keeps what the user typed, and the two implementations disagree about the invalid one. The global switch is primefaces.RESET_VALUES (default false: "When enabled, AJAX updated inputs are always reset"). The core Faces counterpart is described on the Ajax page.

Client Callbacks and Callback Arguments

All four Ajax tags (p:ajax, p:commandButton, p:commandLink, p:remoteCommand) take the same four JavaScript callbacks. The PrimeFaces documentation gives their signatures:

Attribute

Parameters available in the code

When

onstart

none

Before the request is sent. Returning false cancels the request (checked: no request left the browser).

onsuccess

data, status, xhr

The response arrived with a success code; before the DOM is updated.

onerror

xhr, status, error

The request failed (see below for what counts as a failure).

oncomplete

xhr, status, args, data

The lifecycle is over, after the DOM updates, whether it succeeded or not.

args is the object that the server filled with PrimeFaces.current().ajax().addCallbackParam(name, value). It travels in the partial response as a JSON string in an extension element, which is how a listener can answer "was it valid?" without rendering anything:

<extension type="args" ln="primefaces">{&#34;serverCalls&#34;:2}</extension>

PrimeFaces adds one argument on its own: validationFailed is true when the request ended in a validation or conversion error. In the scratch application, oncomplete="window.lastArgs = args" on a Save button with an empty required field gave {"validationFailed":true}. The pattern for dialogs is oncomplete="if (!args.validationFailed) PF('dlg').hide()".

The example below checks an ISBN, reports the result in the callbacks, and shows the other tools of PrimeFaces.current().

src/main/webapp/callbacks.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="http://primefaces.org/ui">
<h:head>
  <title>Callbacks</title>
  <h:outputScript library="bookshelf" name="js/callbacks.js" target="head"/>
</h:head>
<h:body>
  <h:form id="f">
    <p:messages id="msgs" showDetail="true" skipDetailIfEqualsSummary="true"/>
    <p:inputText id="isbn" value="#{callbackBean.isbn}"/>

    <p:commandButton id="check" value="Check" action="#{callbackBean.check}" update="msgs"
                     onstart="bookshelfLog('start')" onsuccess="bookshelfLog('success')"
                     onerror="bookshelfLog('error')" oncomplete="bookshelfDone(xhr, status, args)"/>
    <p:commandButton id="tools" value="Tools" action="#{callbackBean.tools}" process="@this"
                     oncomplete="bookshelfLog('tools done')"/>
    <h:outputText id="log" value="#{callbackBean.log}"/>
  </h:form>
  <pre id="out"></pre>
</h:body>
</html>
src/main/webapp/resources/bookshelf/js/callbacks.js
function bookshelfLog(text) {
    var out = document.getElementById('out');
    out.textContent += text + '\n';
}
function bookshelfDone(xhr, status, args) {
    bookshelfLog('complete status=' + status + ' args=' + JSON.stringify(args));
}
src/main/java/org/example/bookshelf/web/CallbackBean.java
package org.example.bookshelf.web;

import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;

import org.primefaces.PrimeFaces;

import java.io.Serializable;
import java.time.LocalTime;

@Named
@ViewScoped
public class CallbackBean implements Serializable {

    private String isbn;
    private String log = "";

    public void check() {
        boolean valid = isbn != null && isbn.matches("\\d{13}");
        PrimeFaces pf = PrimeFaces.current();
        pf.ajax().addCallbackParam("valid", valid);
        pf.ajax().addCallbackParam("checkedAt", LocalTime.now().withNano(0).toString());
        pf.ajax().addCallbackParam("info", new IsbnInfo(String.valueOf(isbn), isbn == null ? 0 : isbn.length()));
        if (!valid) {
            pf.executeScript("console.warn('isbn rejected')");
        }
    }

    public void tools() {
        PrimeFaces pf = PrimeFaces.current();
        pf.resetInputs("f:isbn");
        pf.ajax().update("f:isbn", "f:log");
        pf.focus("f:isbn");
        pf.scrollTo("f:log");
        log = "ajax=" + pf.isAjaxRequest() + " faces=" + pf.isFacesRequest();
    }

    public static class IsbnInfo implements Serializable {
        private final String isbn;
        private final int length;

        public IsbnInfo(String isbn, int length) {
            this.isbn = isbn;
            this.length = length;
        }

        public String getIsbn() { return isbn; }
        public int getLength() { return length; }
    }

    public String getIsbn() { return isbn; }
    public void setIsbn(String isbn) { this.isbn = isbn; }
    public String getLog() { return log; }
}

Checking 9780441172719 wrote start, success and complete status=success args={"valid":true,"checkedAt":"...","info":{"isbn":"9780441172719","length":13}} in that order, on both Mojarra and MyFaces. Three details of the argument serialisation, from other runs in the scratch application:

  • A JavaBean with getters (here IsbnInfo) was serialised as an object with its properties.

  • Map.of("isbn", ..., "length", ...) arrived as {}, an empty object.

  • A Java record with the components isbn and length arrived as {"length":13}: the component isbn was lost. The reason was not investigated; the JavaBean worked, so use JavaBeans for callback arguments.

What onerror reacts to was also measured. With timeout="300" and a 1.5 second action, onerror got status="timeout"; with an action that called responseSendError(500, ...) it got status="error"; and the error facet of p:ajaxStatus appeared in both cases. A Faces exception during the lifecycle, however, is answered with HTTP 200 and a partial-response with an error element, and onerror was not called for it (Ajax Errors: p:ajaxExceptionHandler and the PrimeFaces ExceptionHandler). The documentation of Error Handling says that onerror callbacks are triggered for server-reported errors too; on Mojarra 4.1.17 and MyFaces 4.1.4 in this setup they were not.

PrimeFaces.current()

PrimeFaces.current() is the utility that replaced RequestContext. PrimeFaces 6.2 deprecated many methods of RequestContext and 7.0 replaced it; the class survives as PrimeRequestContext, which the migration guide calls internal API to avoid. The instance is thread-local and available in Ajax and non-Ajax requests. The methods of PrimeFaces 16.0.0 (read from the class with javap, described by the documentation) are:

Method What it does

isFacesRequest(), isAjaxRequest()

Whether the request is a Faces request, and whether it is a PrimeFaces Ajax request. Both returned true inside the Ajax action of the example.

executeScript(String)

Runs a script in the browser after the Ajax request completes or on page load. In the response it is an extension element of type="script".

focus(String), focus(String, UIComponent)

Focuses the input targeted by the search expression. Response: PrimeFaces.focus('f:isbn');.

scrollTo(String)

Scrolls to the component with that client id. Response: PrimeFaces.scrollTo('f:log');.

resetInputs(String...), resetInputs(Collection)

Resets the input components targeted by the expressions, so they read the model again (resetValues and Cancel Buttons).

resolveWidget(String, Consumer)

Finds a widget component by its widgetVar and calls the consumer, for example PrimeFaces.current().<TreeTable>resolveWidget("books", w -> w.reset()).

ajax()

Returns the Ajax helper: addCallbackParam(String, Object), update(String...), update(Collection), update(UIComponent...) and ignoreAutoUpdate().

dialog(), multiViewState()

The dialog framework and the multi-view state; they have their own sections (the page about panels, menus and dialogs and the PrimeFaces documentation).

ajax().update(...) renders components that the declarative update did not name, which is the answer to "the part of the page to refresh depends on the result". In the example, tools() updated f:isbn and f:log although the button has no update attribute. The PrimeFaces documentation shows a save() method that decides between a panel and a table that way. ignoreAutoUpdate() is the programmatic form of the attribute with that name (p:autoUpdate and Event Observers).

Two traps. The class has addCallbackParam; the table in the documentation spells it addCallBackParam with a capital B (its own examples use the right spelling). And PrimeFaces.current() in a class of the org.primefaces packages that the application does not own is internal API: use only the methods above.

Delay, Queue, Timeout and Global Status

  • delay. Typing Dune with delay="200" sent one request; without it the key-ups of a fast typist send several.

  • async. By default Ajax requests are queued, one at a time, in the order in which they were started. async="true" skips the queue for that request. The Ajax page covers the standard queue; the PrimeFaces queue can be emptied from JavaScript with PrimeFaces.ajax.Queue.abortAll() (documentation).

  • timeout. A request that has not answered within the given milliseconds is abandoned with onerror(xhr, "timeout", "timeout") (measured). The browser abandons the request; nothing tells the server to stop the action.

  • disableOnAjax on a command disables that button while its request runs, so one user cannot start the same action twice from the same button. It does not queue anything else.

  • global. p:ajaxStatus is the global indicator of Ajax requests. It shows the facets default, start, success, error and complete, or runs the callbacks onstart, onsuccess, onerror and oncomplete; it also reacts to f:ajax requests. global="false" on a command or behaviour makes it a silent request. If a complete facet is defined, the success and error facets are never seen (the documentation says so). Do not update the status component itself.

src/main/webapp/errors.xhtml (the status component)
  <p:ajaxStatus id="status" onstart="document.title='busy'" oncomplete="document.title='idle'">
    <f:facet name="error"><h:outputText value="Request failed"/></f:facet>
  </p:ajaxStatus>

With this status the page title changed to busy and back to idle around each request, and the error facet text appeared after the timeout and after the HTTP 500 of the previous section. Events for scripts are published by jQuery on the document while global is true: pfAjaxStart, pfAjaxSend, pfAjaxError, pfAjaxSuccess, pfAjaxUpdated and pfAjaxComplete. The context parameter primefaces.EARLY_POST_PARAM_EVALUATION (default false) is described by the configuration table as making p:ajax behave like f:ajax for queued Ajax requests; it was not exercised.

p:remoteCommand

p:remoteCommand turns a bean method into a JavaScript function, so that a script (a timer, the callback of a widget, a map click) can call the server. It must be inside a form. The name becomes a global function; parameters are passed as an array of name/value pairs and arrive as ordinary request parameters; autoRun="true" calls the command on page load.

src/main/webapp/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:p="http://primefaces.org/ui">
<h:head>
  <title>remoteCommand</title>
</h:head>
<h:body>
  <h:form id="f">
    <p:growl id="growl" showDetail="true"/>
    <p:remoteCommand name="selectBook" action="#{remoteDemoBean.select}" update="status growl"
                     oncomplete="window.lastArgs = args"/>
    <p:remoteCommand name="autoRun" autoRun="true" action="#{remoteDemoBean.select}" update="status" process="@this"/>
    <h:outputText id="status" value="calls=#{remoteDemoBean.calls} last=#{remoteDemoBean.lastBook}"/>
  </h:form>
  <button id="go" type="button" onclick="selectBook([{name: 'bookId', value: '42'}])">Select 42</button>
</h:body>
</html>
src/main/java/org/example/bookshelf/web/RemoteDemoBean.java
package org.example.bookshelf.web;

import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;

import org.primefaces.PrimeFaces;

import java.io.Serializable;
import java.util.Map;

@Named
@ViewScoped
public class RemoteDemoBean implements Serializable {

    private int calls;
    private String lastBook = "none";

    public void select() {
        Map<String, String> params = FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap();
        String id = params.get("bookId");
        calls++;
        lastBook = id == null ? "none" : id.replaceAll("[^0-9]", "");
        PrimeFaces.current().ajax().addCallbackParam("serverCalls", calls);
        FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Selected book " + lastBook));
    }

    public int getCalls() { return calls; }
    public String getLastBook() { return lastBook; }
}

What happened, on both implementations:

  • On load, the autoRun command posted execute=f:j_idt6 (itself; it has process="@this") and status read calls=1 last=none.

  • Clicking the plain button called selectBook([{name: 'bookId', value: '42'}]) and posted bookId=42 next to the form fields, with execute=@all (the default of the command, Commands: p:commandButton and p:commandLink); the growl showed Selected book 42 and oncomplete received {"serverCalls":2} in args.

  • The function returns a promise. selectBook([...]).then(r => r.jqXHR.pfArgs) resolved with an object that has document, textStatus and jqXHR, and r.jqXHR.pfArgs.serverCalls was 3. That lets calling code register its own callbacks per call. The PrimeFaces documentation shows async functions and .then(...).catch(...).

A remote command is an endpoint: anything that can run script in the page, or a forged request, can invoke it with any parameter. The bean above removes everything that is not a digit; real code parses the id and checks that the visitor may see that book (Security Notes).

p:autoUpdate and Event Observers

Some parts of a page change on nearly every request: a message list, a cart counter, a status line. p:autoUpdate is a tag handler nested in a component that marks it to be rendered by every Ajax request, so that the commands need not list it. With on="name" it becomes an observer: it is rendered only by requests whose update contains @obs(name). Before PrimeFaces 6.2 there was an autoUpdate attribute on messages, growl, outputPanel and fragment; 6.2 deprecated it and 7.0 removed it in favour of this tag.

src/main/webapp/autoupdate.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="http://primefaces.org/ui">
<h:head>
  <title>Auto update</title>
</h:head>
<h:body>
  <h:form id="f">
    <p:messages id="msgs"><p:autoUpdate/></p:messages>

    <p:outputPanel id="touches"><p:autoUpdate/>Touches: #{feedBean.touches}</p:outputPanel>
    <p:outputPanel id="news"><p:autoUpdate on="published"/>Published events: #{feedBean.events}</p:outputPanel>
    <p:outputPanel id="other"><p:autoUpdate on="other"/>Other: #{feedBean.events}</p:outputPanel>

    <p:commandButton id="publish" value="Publish" action="#{feedBean.publish}" process="@this" update="@obs(published)"/>
    <p:commandButton id="touch" value="Touch" action="#{feedBean.touch}" process="@this"/>
    <p:commandButton id="quiet" value="Touch quietly" action="#{feedBean.touch}" process="@this" ignoreAutoUpdate="true"/>
  </h:form>
</h:body>
</html>
src/main/java/org/example/bookshelf/web/FeedBean.java
package org.example.bookshelf.web;

import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;

import java.io.Serializable;

@Named
@ViewScoped
public class FeedBean implements Serializable {

    private int events;
    private int touches;

    public void publish() {
        events++;
    }

    public void touch() {
        touches++;
    }

    public int getEvents() { return events; }
    public int getTouches() { return touches; }
}

What each button rendered (the update list, then the components in the response; measured):

Button Request Components in the response

Publish

jakarta.faces.partial.render=@obs(published)

f:msgs, f:touches and f:news; not f:other

Touch (no update)

no render parameter

f:msgs and f:touches: the unconditional ones

Touch quietly (ignoreAutoUpdate="true")

primefaces.ignoreautoupdate=true

nothing; the counter on the server was 2 and the page kept showing 1

The cost is on the wire: the auto-updated components are in every response, whatever the request. Keep them small, and use observers for the rest. PrimeFaces.current().ajax().ignoreAutoUpdate() skips them from Java.

The Review Form: Input Components

The review form of Bookshelf collects what the visitor thinks of a book. It is a good test for the input components of PrimeFaces because each field needs a different component: a book picked from the catalogue (autoComplete), the date on which it was finished (datePicker), a rating, a library card number with a fixed shape (inputMask), free tags (chips), the genres (selectCheckboxMenu) and a comment. The model is a plain class with Bean Validation constraints (see the Bean Validation page); the form is validated on the server, and, with Client-Side Validation (CSV), in the browser before the request is sent.

src/main/java/org/example/bookshelf/model/ReviewSubmission.java
package org.example.bookshelf.model;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PastOrPresent;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;

import java.io.Serializable;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

public class ReviewSubmission implements Serializable {

    @NotBlank
    @Size(max = 40)
    private String reviewer;

    @NotBlank
    @Email
    private String email;

    @NotNull
    private Book book;

    @NotNull
    @PastOrPresent
    private LocalDate finishedOn;

    @NotNull
    @Min(1)
    @Max(5)
    private Integer rating;

    @NotNull
    @Pattern(regexp = "[A-Z]{2}-\\d{4}-\\d{4}", message = "{card.invalid}")
    private String card;

    @Size(max = 5)
    private List<String> tags = new ArrayList<>();

    @NotEmpty
    private List<Genre> genres = new ArrayList<>();

    @Size(max = 500)
    private String comment;

    public String getReviewer() { return reviewer; }
    public void setReviewer(String reviewer) { this.reviewer = reviewer; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public Book getBook() { return book; }
    public void setBook(Book book) { this.book = book; }
    public LocalDate getFinishedOn() { return finishedOn; }
    public void setFinishedOn(LocalDate finishedOn) { this.finishedOn = finishedOn; }
    public Integer getRating() { return rating; }
    public void setRating(Integer rating) { this.rating = rating; }
    public String getCard() { return card; }
    public void setCard(String card) { this.card = card; }
    public List<String> getTags() { return tags; }
    public void setTags(List<String> tags) { this.tags = tags; }
    public List<Genre> getGenres() { return genres; }
    public void setGenres(List<Genre> genres) { this.genres = genres; }
    public String getComment() { return comment; }
    public void setComment(String comment) { this.comment = comment; }
}
src/main/java/org/example/bookshelf/web/PrimeReviewBean.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 org.example.bookshelf.model.Book;
import org.example.bookshelf.model.Genre;
import org.example.bookshelf.model.ReviewSubmission;
import org.example.bookshelf.service.BookService;
import org.example.bookshelf.service.ReviewStore;

import java.io.Serializable;
import java.time.LocalDate;
import java.util.List;

@Named
@ViewScoped
public class PrimeReviewBean implements Serializable {

    @Inject
    private BookService books;

    @Inject
    private ReviewStore store;

    private ReviewSubmission review;

    @PostConstruct
    void init() {
        review = new ReviewSubmission();
    }

    public List<Book> complete(String query) {
        return books.search(query);
    }

    public void submit() {
        store.add(review);
        FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO,
                "Review saved", "Thanks " + review.getReviewer()));
        review = new ReviewSubmission();
    }

    public ReviewSubmission getReview() { return review; }
    public Genre[] getGenres() { return Genre.values(); }
    public LocalDate getToday() { return LocalDate.now(); }
    public int getSaved() { return store.count(); }
}

The picker for the book needs a converter from the string that the browser posts to a Book. It is a managed converter with @Dependent, as the converters page explains (without a scope, a bean-discovery-mode="annotated" application does not find it).

src/main/java/org/example/bookshelf/web/CatalogueBookConverter.java
package org.example.bookshelf.web;

import jakarta.enterprise.context.Dependent;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
import jakarta.faces.convert.Converter;
import jakarta.faces.convert.ConverterException;
import jakarta.faces.convert.FacesConverter;
import jakarta.inject.Inject;

import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;

@Dependent
@FacesConverter(value = "bookshelf.catalogueBook", managed = true)
public class CatalogueBookConverter implements Converter<Book> {

    @Inject
    private BookService books;

    @Override
    public Book getAsObject(FacesContext context, UIComponent component, String value) {
        if (value == null || value.isBlank()) {
            return null;
        }
        try {
            return books.find(Long.parseLong(value.trim())).orElseThrow(NumberFormatException::new);
        } catch (NumberFormatException e) {
            throw new ConverterException(new jakarta.faces.application.FacesMessage(
                    jakarta.faces.application.FacesMessage.SEVERITY_ERROR, "Unknown book", null), e);
        }
    }

    @Override
    public String getAsString(FacesContext context, UIComponent component, Book value) {
        return value == null ? "" : Long.toString(value.id());
    }
}

The form itself, with every component of this section:

src/main/webapp/review.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="http://primefaces.org/ui">
<h:head>
  <title>Review</title>
</h:head>
<h:body>
  <h:form id="review">
    <p:growl id="growl" showDetail="true"/>
    <p:messages id="messages" closable="true">
      <p:autoUpdate/>
    </p:messages>
    <p:focus context="review"/>

    <p:panelGrid columns="3" id="grid">
      <p:outputLabel for="reviewer" value="Reviewer"/>
      <p:inputText id="reviewer" value="#{primeReviewBean.review.reviewer}" label="Reviewer"/>
      <p:message for="reviewer"/>

      <p:outputLabel for="email" value="Email"/>
      <p:inputText id="email" value="#{primeReviewBean.review.email}" label="Email"/>
      <p:message for="email"/>

      <p:outputLabel for="book" value="Book"/>
      <p:autoComplete id="book" value="#{primeReviewBean.review.book}" completeMethod="#{primeReviewBean.complete}"
                      var="b" itemLabel="#{b.title}" itemValue="#{b}" converter="bookshelf.catalogueBook"
                      minQueryLength="1" queryDelay="100" maxResults="5" label="Book" forceSelection="true"/>
      <p:message for="book"/>

      <p:outputLabel for="finished" value="Finished on"/>
      <p:datePicker id="finished" value="#{primeReviewBean.review.finishedOn}" pattern="dd/MM/yyyy"
                    maxdate="#{primeReviewBean.today}" showIcon="true" label="Finished on"/>
      <p:message for="finished"/>

      <p:outputLabel for="rating" value="Rating"/>
      <p:spinner id="rating" value="#{primeReviewBean.review.rating}" min="1" max="5" label="Rating"/>
      <p:message for="rating"/>

      <p:outputLabel for="card" value="Library card"/>
      <p:inputMask id="card" value="#{primeReviewBean.review.card}" mask="AA-9999-9999" label="Library card"/>
      <p:message for="card"/>

      <p:outputLabel for="tags" value="Tags"/>
      <p:chips id="tags" value="#{primeReviewBean.review.tags}" max="5" unique="true" label="Tags"/>
      <p:message for="tags"/>

      <p:outputLabel for="genres" value="Genres"/>
      <p:selectCheckboxMenu id="genres" value="#{primeReviewBean.review.genres}" label="Genres"
                            multiple="true" updateLabel="true" emptyLabel="Choose genres">
        <f:selectItems value="#{primeReviewBean.genres}" var="g" itemLabel="#{g.label}" itemValue="#{g}"/>
      </p:selectCheckboxMenu>
      <p:message for="genres"/>

      <p:outputLabel for="comment" value="Comment"/>
      <p:inputTextarea id="comment" value="#{primeReviewBean.review.comment}" counter="left" maxlength="500"
                       counterTemplate="{0} characters left" label="Comment"/>
      <p:message for="comment"/>
    </p:panelGrid>
    <h:outputText id="left"/>

    <p:commandButton id="save" value="Save" action="#{primeReviewBean.submit}" update="review"
                     validateClient="true" icon="pi pi-check"/>
    <p:defaultCommand target="save"/>
    <h:outputText id="saved" value="saved=#{primeReviewBean.saved}"/>
  </h:form>
</h:body>
</html>

The context parameters of the application for this page (web.xml), besides the Faces ones of the project set-up page:

<context-param>
    <param-name>jakarta.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL</param-name>
    <param-value>true</param-value>
</context-param>
<context-param>
    <param-name>primefaces.CLIENT_SIDE_VALIDATION</param-name>
    <param-value>true</param-value>
</context-param>

p:outputLabel Sets the Label

p:outputLabel takes the text of its value as the label of the input it is for, and the validation messages use it. In the scratch application this worked with p:inputText on both implementations, and it overrode a label attribute that the input also had: with p:outputLabel value="Outer label" and label="Attribute label" the message said Outer label: Validation Error: Value is required.. Two details. A value with a colon is cut at the first colon in the message (value="Colon: label" gave Colon: Validation Error...), so do not put a colon in the label text. And a standard h:outputLabel does not do it: the label="Attribute c" of its h:inputText was used, and an input with neither shows its client id (f:d: Validation Error...).

The required asterisk of p:outputLabel follows indicateRequired. With auto (the default in the documentation) it appears when the input is required or when the property is annotated @NotBlank or @NotEmpty; it also checks @NotNull, but only if jakarta.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL is true (the parameter above). In the scratch form all seven labels of properties with such constraints showed the asterisk without a required attribute anywhere.

p:datePicker

p:datePicker replaced p:calendar. The 10.0 migration guide deprecated the calendar in favour of it, and the tag calendar is still in the tag library of PrimeFaces 16.0.0. The value is a java.time.LocalDate in the default single mode; a List of them for selectionMode="multiple" or "range"; a LocalDateTime (the time picker is then on by default, showTime is detected) or a LocalTime (timeOnly is detected). Before 8.0 the default without a value binding was java.util.Date.

Attribute Use

pattern

Display and parse pattern (dd/MM/yyyy); by default derived from the locale.

mindate, maxdate

Limits as a LocalDate, LocalDateTime, LocalTime, a string or (deprecated) a java.util.Date.

disabledDates, disabledDays, enabledDates

Lists of dates; days are numbers with Sunday as 0.

model

A DateMetadataModel that attaches a disabled flag and a style class to each date, with a lazy variant that loads the metadata of the visible range.

showIcon, showButtonBar, inline, numberOfMonths, monthNavigator, yearNavigator

Presentation. yearNavigator is a string since 15.0.0; read the documentation of the version in use for its values.

hourFormat, showSeconds, timeOnly, stepHour, stepMinute

The time part.

locale

A locale key or a Locale. The labels come from the client-side locale script (see below).

mask

An input mask for the text field; not allowed with multiple or range selection.

What was measured in the scratch form (Mojarra and MyFaces gave the same results apart from the label prefix of Mojarra and MyFaces):

  • The browser posted the text of the field, finished_input=01/01/2020, and the server parsed it with the pattern.

  • A text that is not a date (not a date), and an ISO date (2020-01-01) for a dd/MM/yyyy pattern, gave Finished on: 'not a date' could not be understood as a date. and the same for the ISO string. Always post in the pattern of the field.

  • With maxdate set to today, widget.setDate(new Date(2999,0,1)) in the browser ended up at today’s date (the picker clamps). A forged post of 31/12/2999 was stopped by the Bean Validation message of @PastOrPresent: the only message was must be a date in the past or in the present. maxdate is a convenience for the visitor, not a guard on the server.

  • locale="es" alone changed nothing in the labels: the open panel still showed Jan Feb Mar ... and Today Clear. The labels come from PrimeFaces.locales, and only the English file was loaded. Load the locale script with <h:outputScript name="locales/locale-es.js" library="primefaces"/> (it exists: HTTP 200, 13,839 bytes, with accept:"Sí", addRule:"Agregar regla" and so on, and the Spanish validation messages for CSV) or rely on the context parameter primefaces.CLIENT_SIDE_LOCALISATION, whose default is true (see the context parameters): it adds locales/locale-<language>.js for the locale of the view automatically, so the manual script is needed only for a language the view locale does not select, or when the parameter is false.

p:autoComplete

p:autoComplete sends the typed text to a completeMethod that returns the suggestions. For strings that is all. For objects it needs var, itemLabel (what the list shows), itemValue (what is submitted) and a converter, as in the form above: the browser keeps the converter’s string in a hidden field (book_hinput=1) and shows the label in the visible one (book_input=Dune).

The request it sends is its own, not a command’s: source, execute and render are all the component itself, and the text travels as review:book_query=Du. Measured behaviour of the attributes used in the scratch application:

Attribute Observed

minQueryLength

minQueryLength="2": typing one character sent no request; the second character sent one.

queryDelay

Default 300 ms; the form uses 100.

maxResults, moreText

maxResults="3" showed three suggestions and a last line with the text of moreText; there is no default action for that line.

dropdown

Adds a button that starts a search, with an empty query or with the current text, as dropdownMode says.

forceSelection

Typing a text that matches no suggestion and leaving the field cleared it.

multiple, unique, selectLimit

The value is a List; each selected item becomes a token.

cache, cacheTimeout

Suggestions of an identical query are served from the browser (documented; not measured).

completeEndpoint

Consumes a REST endpoint instead of calling a bean method, for a method that does not need the Faces lifecycle (documented; not exercised).

escape

Default true: HTML in suggestions is escaped.

The completeMethod gets raw text from the browser. Use it as a parameter of a query, never as a part of a string that is run as JPQL or SQL, and limit the result (maxResults limits what is shown, not what is fetched). The method runs a full Faces request in the lifecycle of the component; keep it fast.

p:chips

p:chips collects free-form values as tokens. The value is a List (of strings, or of objects with a converter). Pressing Enter adds the typed text as a chip; max limits their number, unique rejects duplicates, separator splits text on paste with addOnPaste, and addOnBlur adds the pending text when the field loses focus. The client widget has addItem(value), removeItem(value) and toggleEditor(). In the scratch run the component rendered a hidden multiple select, tags_hinput, and a chip appeared in it as a selected option, so the form posts review:tags_hinput=a&review:tags_hinput=b.

Two things to know. Backspace in an empty chips input removes the last chip. And a forged post with six values for max="5" left tags as an empty list on both implementations: PrimeFaces dropped the entire submitted list instead of cutting it to five, and @Size(max = 5) saw an empty list (Values That PrimeFaces Drops Without a Message).

p:inputMask

p:inputMask forces the text into a template. The mask attribute is required (since 7.0). The characters are 9 for a digit, a for a letter, A for an upper-case letter, * for a letter or digit and [ ] around an optional part; everything else is a literal. Other attributes: slotChar (the placeholder, _), autoClear (clear the field if the mask is not complete), showMaskOnFocus, showMaskOnHover and validateMask.

In the form, mask="AA-9999-9999" made typed ab12345678 appear as AB-1234-5678: the mask upper-cases, and the browser posts the value with its literals. Since PrimeFaces 8.0 the mask is also checked on the server (validateMask, default true), and the check does not produce a message. A forged ab-1234-5678, AB-12 or 12-ABCD-EFGH arrived in the bean as null. With @NotNull on the property the user got must not be null (Library card: must not be null on MyFaces), which is correct and unhelpful; without @NotNull the value is dropped without a word (Values That PrimeFaces Drops Without a Message). Set validateMask="false" only for masks that PrimeFaces cannot express, and then validate the pattern yourself. The widget methods are getValue() (with the mask), getValueUnmasked(), setValue(v), enable() and disable().

p:selectCheckboxMenu

p:selectCheckboxMenu is a multi-select in an overlay. It is used like the standard selectManyCheckbox: a value that is a List and f:selectItems for the options. Selected items are not shown as the label unless updateLabel="true"; multiple="true" shows them as tokens like chips; filter with filterMatchMode adds a search box; dynamic renders the overlay on demand; showSelectAll shows the "all" checkbox. Since 14.0.0, with a nested f:selectItem (singular) the itemLabel is required.

Each ticked option is posted as review:genres=FICTION next to a review:genres_focus field. The measured server behaviour is the same on both implementations: a value that is not one of the options (POETRY) does not produce Validation Error: Value is not valid; the list simply ends up empty, and @NotEmpty is what reported it (must not be empty, Genres: must not be empty on MyFaces). This component renders no CSV metadata, so the required rule is enforced on the server only (What CSV Does Not Do).

p:password

p:password is an inputSecret with themes and an optional strength meter. feedback="true" shows it, with the labels promptLabel, weakLabel, goodLabel and strongLabel for translations; inline puts the meter in the flow and toggleMask adds a show/hide button. match names the other password field, and a mismatch fails validation. The redisplay attribute is false by default: a password is never written back into the page.

src/main/webapp/signup.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="http://primefaces.org/ui">
<h:head>
  <title>Sign up</title>
</h:head>
<h:body>
  <h:form id="f">
    <p:focus/>
    <p:growl id="growl" showDetail="true" life="3000"/>
    <p:messages id="msgs" showDetail="true" skipDetailIfEqualsSummary="true"/>

    <p:outputLabel for="name" value="Name"/>
    <p:inputText id="name" value="#{signupBean.name}" required="true" label="Name"/>
    <p:message for="name"/>

    <p:outputLabel for="phone" value="Phone"/>
    <p:inputMask id="phone" value="#{signupBean.phone}" mask="(999) 999-9999" label="Phone"/>
    <p:message for="phone"/>

    <p:outputLabel for="pwd1" value="Password"/>
    <p:password id="pwd1" value="#{signupBean.password}" feedback="true" match="pwd2" required="true" label="Password"
                promptLabel="Choose a password" weakLabel="Weak" goodLabel="Good" strongLabel="Strong"/>
    <p:message for="pwd1"/>

    <p:outputLabel for="pwd2" value="Confirm"/>
    <p:password id="pwd2" value="#{signupBean.confirm}" required="true" label="Confirm"/>
    <p:message for="pwd2"/>

    <p:commandButton id="b1" value="Register" action="#{signupBean.register}" update="@form" partialSubmit="true" process="@form"/>
    <p:defaultCommand target="b1"/>
    <h:outputText id="submits" value="submits=#{signupBean.submits}"/>
  </h:form>
</h:body>
</html>
src/main/java/org/example/bookshelf/web/SignupBean.java
package org.example.bookshelf.web;

import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;

import java.io.Serializable;

@Named
@ViewScoped
public class SignupBean implements Serializable {

    private String name;
    private String phone;
    private String password;
    private String confirm;
    private int submits;

    public void register() {
        submits++;
        // A real application would hash the password here (see the security page) and never keep the clear text.
        password = null;
        confirm = null;
        FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO,
                "Registered " + name, "phone=" + phone));
    }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getPhone() { return phone; }
    public void setPhone(String phone) { this.phone = phone; }
    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
    public String getConfirm() { return confirm; }
    public void setConfirm(String confirm) { this.confirm = confirm; }
    public int getSubmits() { return submits; }
}

The scratch run, with Abc123!x in the first field and different in the second: pressing Enter in the last field submitted the form (the p:defaultCommand, p:defaultCommand) with execute=f and partialSubmit, and the messages area showed Password: Validation Error. with the detail 'Password' should match with 'Confirm'. (the texts of primefaces.password.INVALID_MATCH and ..._detail, which are in the server message bundle). The meter said Strong. Two consequences:

  • With matching passwords, Enter registered the visitor: the message said Registered Ana with the detail phone=(555) 123-4567, which shows that the phone number reaches the bean with the mask literals; strip them in the bean if the model stores digits only. Both password fields were empty again after the update.

  • match is declared on the first field and names the second, as in the documentation’s example; the message mentions both labels.

  • After the failed validation the password fields were rendered empty (redisplay="false"): retyping only the second field and pressing Enter failed on Password: Validation Error: Value is required.. That is the intended behaviour, not a bug: do not turn redisplay on to fix it.

The component compares the two values and hands the first to the bean; the bean of the example clears both fields in register() so that the clear text does not stay in the view-scoped bean (which is part of the saved view). What the application does with it belongs to the security page: never keep the clear text longer than the request, and hash it with a password hash, for example the Pbkdf2PasswordHash that the identity store section uses. Do not put passwords in log statements or into the Ajax oncomplete arguments.

Other Input Components in the Form

  • p:spinner with min="1" max="5" for the rating: the posted review:rating_input is the visible text field. Values outside the range were dropped on the server (Values That PrimeFaces Drops Without a Message).

  • p:inputTextarea with counter="left" and counterTemplate="{0} characters left" updates the output with the id left while typing (495 characters left after hello) and sets maxlength on the textarea. A forged post of 600 characters was truncated to 500 on the server.

  • The other PrimeFaces inputs (p:inputNumber, p:slider, p:rating, p:toggleSwitch, p:keyboard, and so on) follow the same pattern: a value, the standard required, converter and validator attributes, and, where they render CSV metadata, the client validation of Client-Side Validation (CSV).

Values That PrimeFaces Drops Without a Message

The most useful finding of the scratch run for a security review: several components check their own attributes on the server and discard a value that breaks them; no message is queued, the component is treated as submitted with nothing. Raw POST requests to a copy of the form (parameters taken from a real request and edited; Mojarra and MyFaces gave the same results):

Edited parameter Result in the bean Message

review:card=ab-1234-5678 (also AB-12, 12-ABCD-EFGH)

null (mask check)

must not be null from @NotNull; nothing if the property is optional

review:rating_input=9, 6, 0 or abc (spinner with min="1" max="5")

null (5 was accepted)

none; the property was not even validated, so @NotNull did not fire

review:rating_input removed from the request

null

none: a parameter that is absent means "not submitted" and skips conversion, validation and the model update

review:rating_input= (empty)

null

must not be null: an empty value is validated

six values for review:tags_hinput (max="5")

empty list

none

review:genres=POETRY (not an option)

empty list

only the @NotEmpty message

600 characters for review:comment (maxlength="500")

the first 500 characters

none

review:book_hinput=999 (the converter rejects it)

validation fails

Unknown book (the converter’s message)

review:finished_input=31/12/2999 (maxdate today)

rejected

must be a date in the past or in the present from @PastOrPresent

Three rules follow. The model must not rely on a Bean Validation annotation to reject an omitted or tampered field: @NotNull did not fire when the parameter was missing, or when the spinner discarded the value. Validate what the action needs again in the service layer, or put required="true" on the component and check that case. Second, these server checks are not messages for the user; the user-facing feedback is the client side and the converter or validator messages of the field. Third, do not assume that a length shown in the browser is a length enforced on the server. A post of ten characters to a field with maxlength="3" gave abc for p:inputText on both implementations, the whole ten characters for the standard h:inputText on Mojarra, and no change at all (the old value stayed, no message) on MyFaces. Use f:validateLength or @Size for a limit that counts.

p:messages, p:message and p:growl

The message components of PrimeFaces show FacesMessage objects with the theme. They are drop-in replacements for h:messages and h:message (the messages page explains the standard ones) and, importantly, they are the only components that the client-side validation of Client-Side Validation (CSV) can write into. The defaults differ between them and are easy to get wrong:

Attribute p:messages p:message p:growl

showSummary

true

false

true

showDetail

false

true

false

escape

true

true

true

redisplay

true

true

true

globalOnly

false

not available

false

The other attributes of p:messages are closable (a close icon), showIcon and severity (a comma separated list of info, warn, error, fatal); p:message has display (text, icon, both, the default, and tooltip in the documentation); p:growl has life (milliseconds, default 6000), sticky and keepAlive. All three have for, forType and forIgnores, and skipDetailIfEqualsSummary.

src/main/webapp/messages-demo.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="http://primefaces.org/ui">
<h:head>
  <title>Messages</title>
</h:head>
<h:body>
  <h:form id="f">
    <p:growl id="toasts" severity="info,warn" showDetail="true" life="4000"/>
    <p:growl id="alerts" severity="error,fatal" showDetail="true" sticky="true"/>
    <p:messages id="summary" showDetail="true" skipDetailIfEqualsSummary="true" closable="true"/>
    <p:messages id="globals" globalOnly="true" showSummary="false" showDetail="true"/>

    <p:outputLabel for="title" value="Title"/>
    <p:inputText id="title" value="#{messagesBean.title}"/>
    <p:message id="titleMessage" for="title" display="both"/>
    <p:message id="titleIcon" for="title" display="icon"/>

    <p:commandButton id="all" value="Add messages" action="#{messagesBean.all}"
                     update="toasts alerts summary globals titleMessage titleIcon"/>
  </h:form>
</h:body>
</html>
src/main/java/org/example/bookshelf/web/MessagesBean.java
package org.example.bookshelf.web;

import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;

import java.io.Serializable;

@Named
@ViewScoped
public class MessagesBean implements Serializable {

    private String title;

    public void all() {
        FacesContext ctx = FacesContext.getCurrentInstance();
        ctx.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Info summary", "Info detail"));
        ctx.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_WARN, "Warn summary", "Warn detail"));
        ctx.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "Error summary", "Error detail"));
        ctx.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_FATAL, "Fatal summary", "Fatal detail"));
        ctx.addMessage("f:title", new FacesMessage(FacesMessage.SEVERITY_ERROR, "Field summary", "Field detail"));
    }

    public void html() {
        FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO,
                "<b>bold</b> <img src=x onerror=alert(1)>", "detail <i>italic</i>"));
        FacesContext.getCurrentInstance().addMessage("f:rawtarget", new FacesMessage(FacesMessage.SEVERITY_INFO,
                "<b>bold</b> <img src=x onerror=alert(1)>", "detail"));
    }

    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
}

The messages that the components themselves produce, such as the password mismatch, come from the server bundle org/primefaces/Messages.properties; the documentation says to add a file for your language (for example Messages_de.properties) in the same location of your WAR. The client texts (the date picker labels, the CSV messages) come from the locale scripts of p:datePicker.

After pressing the button, on both Mojarra and MyFaces:

  • summary listed the five messages (info, warn, error, fatal, and the one for f:title) with summary and detail. Their order was not the order of insertion (Warn, Error, Field, Fatal, Info); do not rely on it.

  • globals (globalOnly="true", showSummary="false") listed only the four messages without a client id, details only.

  • titleMessage showed Field detail (the default showDetail="true" and showSummary="false" of p:message), and titleIcon (display="icon") rendered an icon whose title was Field detail.

  • The growls: the toast growl showed the info and warn messages and the alert growl the error and fatal ones. After four seconds (life="4000") the toasts were gone and the three sticky alerts stayed.

for takes a message key or a component. for="title" found the messages of the input with that id and for="f:rawtarget" found messages added with that literal client id for which there is no component; for="rawtarget" matched nothing, because no component has that id. forType="expression" evaluates for as a search expression (a byExpr messages component with for="title" forType="expression" showed the field’s message).

Escaping is on by default for all three, and it must stay on for anything that contains user input. The PrimeFaces documentation shows escape="false" for HTML messages. In the scratch application a message whose summary was <b>bold</b> <img src=x onerror=alert(1)> was shown as text by default, and the same message in a component with escape="false" created an img element and ran the script (the headless browser recorded the alert). Use escape="false" only on text that the application itself wrote, never on exception messages, form values or anything from a database.

Client-Side Validation (CSV)

PrimeFaces Client Side Validation validates a form in the browser, with the same rules and the same messages as the server, before the request is sent. The documentation states its goals: compatibility with the server side (conversion and then validation, like the lifecycle), partial processing and update for Ajax, i18n, client-side renderers for the message components, custom client validators and converters, global or per-component switches, and Bean Validation integration. It works for PrimeFaces components only.

Switching It On

CSV is off by default. The global switch is a context parameter; the per-use switch is validateClient="true" on a p:commandButton, p:commandLink or p:remoteCommand. p:ajax has no such attribute (Validating While Typing: p:clientValidator is the way to validate on an event).

<context-param>
    <param-name>primefaces.CLIENT_SIDE_VALIDATION</param-name>
    <param-value>true</param-value>
</context-param>

The page rendered by PrimeFaces shows how it is wired. For validateClient="true" on an Ajax button and on a button with ajax="false", the generated onclick were (shortened):

<button id="f:ajax" ... onclick="if(PrimeFaces.vb({s:this,a:true,u:'f'})){PrimeFaces.ab({s:&quot;f:ajax&quot;,f:&quot;f&quot;,u:&quot;f&quot;});return false;}else{return false;}" type="submit">
<button id="f:nonajax" ... onclick="if(PrimeFaces.vb({s:this})){}else{return false;}" type="submit">

PrimeFaces.vb runs the validation of the processed inputs; if it fails, the handler returns false and nothing is sent. The global object PrimeFaces.settings in the page head carries the Faces settings that CSV mirrors (validateEmptyFields:true,considerEmptyStringNull:true in the scratch application, from the empty-value parameters).

Which inputs are validated depends on the request type:

  • Ajax (vb({a:true,u:'f'})): the inputs that the request processes (process), and the messages are rendered into the message components inside the update targets. Validation and update happen in the browser.

  • Non-Ajax: all visible, editable inputs of the form, and the message components have to be inside the form.

What Is Validated: Required, Validators, Converters and Bean Validation

The page below has a required input with a standard length validator and a client validator, one that is validated while typing, a property with Bean Validation constraints on the bean, and a standard h:inputText as the control. Three buttons use CSV for an Ajax request, CSV for a full submit and no CSV.

src/main/webapp/csv-basic.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="http://primefaces.org/ui"
      xmlns:pt="jakarta.faces.passthrough">
<h:head>
  <title>Client-side validation</title>
</h:head>
<h:body>
  <h:form id="f">
    <p:messages id="msgs"><p:autoUpdate/></p:messages>

    <p:outputLabel for="title" value="Title"/>
    <p:inputText id="title" value="#{csvBean.title}" required="true">
      <f:validateLength minimum="2" maximum="5"/>
      <p:clientValidator/>
    </p:inputText>
    <p:message for="title" display="icon"/>

    <p:outputLabel for="keyup" value="Reviewer"/>
    <p:inputText id="keyup" value="#{csvBean.keyup}" required="true">
      <p:clientValidator event="keyup"/>
    </p:inputText>
    <p:message for="keyup" display="text"/>

    <p:outputLabel for="code" value="Code"/>
    <p:inputText id="code" value="#{csvBean.code}"/>
    <p:message for="code"/>

    <p:outputLabel for="plain" value="Plain"/>
    <h:inputText id="plain" value="#{csvBean.plain}" required="true"/>
    <p:message for="plain"/>

    <p:commandButton id="ajax" value="Save (Ajax)" action="#{csvBean.save}" validateClient="true" update="@form"/>
    <p:commandButton id="nonajax" value="Save (full)" action="#{csvBean.save}" ajax="false" validateClient="true"/>
    <p:commandButton id="off" value="Save (server only)" action="#{csvBean.save}" update="@form"/>
  </h:form>
</h:body>
</html>
src/main/java/org/example/bookshelf/web/CsvBean.java
package org.example.bookshelf.web;

import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

import org.example.bookshelf.csv.Isbn13;

import java.io.Serializable;

@Named
@ViewScoped
public class CsvBean implements Serializable {

    private String title;
    private String keyup;
    private String isbn;
    private String beanIsbn;
    private String plain;
    private boolean nameRequired;
    private String name;
    private int saves;

    @NotBlank
    @Size(min = 2, max = 5)
    private String code;

    @Isbn13
    public String getBeanIsbn() { return beanIsbn; }
    public void setBeanIsbn(String beanIsbn) { this.beanIsbn = beanIsbn; }

    public void save() {
        saves++;
        FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Saved", "save #" + saves));
    }

    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public String getKeyup() { return keyup; }
    public void setKeyup(String keyup) { this.keyup = keyup; }
    public String getIsbn() { return isbn; }
    public void setIsbn(String isbn) { this.isbn = isbn; }
    public String getPlain() { return plain; }
    public void setPlain(String plain) { this.plain = plain; }
    public boolean isNameRequired() { return nameRequired; }
    public void setNameRequired(boolean nameRequired) { this.nameRequired = nameRequired; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getCode() { return code; }
    public void setCode(String code) { this.code = code; }
    public int getSaves() { return saves; }
}

Results (the same on Mojarra and MyFaces, in headless Chrome):

  • All fields empty, Save (Ajax): the browser sent no request, and the messages area showed Title: Validation Error: Value is required., Reviewer: Validation Error: Value is required. and Code: must not be blank.. The same click for Save (full) also made no request.

  • The metadata for each input is in its markup. The title input carried data-p-label="Title" data-p-required="true" data-p-minlength="2" data-p-maxlength="5" data-p-val="jakarta.faces.Length" (the f:validateLength of the page, handled by the PrimeFaces subclass of the validator). The code input carried data-p-minlength="2" data-p-maxlength="5" data-p-val="Size,NotBlank", from the two annotations on the bean.

  • Standard validators and converters. The PrimeFaces-replaced Length, LongRange, DoubleRange and RegularExpression validators (standard validators) and the standard converters (converters; the table of The PrimeFaces Ajax Model) render this metadata; data-p-con names the converter.

  • Bean Validation. The constraints with a client implementation in PrimeFaces 16.0.0 are AssertFalse, AssertTrue, DecimalMax, DecimalMin, Digits, Email, Future, FutureOrPresent, Max, Min, Negative, NegativeOrZero, NotBlank, NotEmpty, NotNull, Null, Past, PastOrPresent, Pattern, Positive, PositiveOrZero and Size (the classes *ClientValidationConstraint in the jar). A constraint without a client implementation, for example one of your own that has no @ClientConstraint, is checked on the server only.

  • The standard component is not covered. The valid form with an empty h:inputText required="true" was sent to the server, which answered Plain: Validation Error: Value is required.. This is the rule of the documentation: CSV supports PrimeFaces components only.

  • Messages on the client come from the locale script (locale-en.js, or the one for the locale, p:datePicker), as the messages entries keyed by Faces message ids such as jakarta.faces.component.UIInput.REQUIRED. The label prefix follows the MyFaces convention on both implementations (Code: must not be blank.), while the server on Mojarra prints must not be blank without it. The documentation shows how to remove the prefix: set jakarta.faces.validator.BeanValidator.MESSAGE to {0} in a server bundle and, for the client, in the locale (PrimeFaces.locales['en_US'].messages[...] = '{0}'); the context parameter primefaces.INTERPOLATE_CLIENT_SIDE_VALIDATION_MESSAGES (default false) loads the messages through the Bean Validation message interpolator so that the text of ValidationMessages.properties is shown on the client too.

  • Duplicates. A p:messages with showDetail="true" printed each client-side message twice (summary and detail are the same text) even with skipDetailIfEqualsSummary="true", which the client renderer did not apply. Leave showDetail at its default for CSV.

Validating While Typing: p:clientValidator

p:clientValidator is a client behaviour for instant validation: nested in an input, it validates that input on an event, change by default. The title input of the page above has it with the default event, which the markup shows as onchange="return PrimeFaces.vi(this, true, true)"; the reviewer input uses event="keyup". In the run, typing abcdefg into the title and leaving the field set the class ui-state-csv-invalid on it, and typing one letter into the reviewer and deleting it set the same class and the required message. The attribute disabled switches it off. PrimeFaces styles the states with ui-state-error and, since the Immediate CSV section of the documentation, ui-state-csv-valid and ui-state-csv-invalid, which you can style as the validity classes of other frameworks.

PrimeFaces.vi(el, highlight, renderMessages) is PrimeFaces.validation.validateInstant, and PrimeFaces.vb is the form validation behind PrimeFaces.validation.validate(source, process, update, highlight, focus, renderMessages, validateInvisibleElements, logUnrenderedMessages), which returns an object with valid. Call them from your own script when you need to validate something that no command triggers.

Custom Validators, Converters and Constraints

A custom Faces validator. Implement org.primefaces.validate.ClientValidator next to jakarta.faces.validator.Validator: it gives the id of the client validator and an optional metadata map (rendered as data-* attributes and read with element.data(key) in JavaScript). The validator is plugged in as usual with f:validator, and the matching JavaScript function is registered in PrimeFaces.validator. For the ISBN-13 of Bookshelf (digits and a weighted checksum), the server class, the JavaScript and the page are:

src/main/java/org/example/bookshelf/csv/Isbn13Validator.java
package org.example.bookshelf.csv;

import jakarta.faces.application.FacesMessage;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
import jakarta.faces.validator.FacesValidator;
import jakarta.faces.validator.Validator;
import jakarta.faces.validator.ValidatorException;

import org.primefaces.validate.ClientValidator;

import java.util.Map;

@FacesValidator("bookshelf.isbn13")
public class Isbn13Validator implements Validator<String>, ClientValidator {

    public static boolean isValid(String isbn) {
        if (isbn == null || !isbn.matches("\\d{13}")) {
            return false;
        }
        int sum = 0;
        for (int i = 0; i < 13; i++) {
            sum += (isbn.charAt(i) - '0') * (i % 2 == 0 ? 1 : 3);
        }
        return sum % 10 == 0;
    }

    @Override
    public void validate(FacesContext context, UIComponent component, String value) {
        if (value == null || value.isBlank()) {
            return;
        }
        if (!isValid(value)) {
            throw new ValidatorException(new FacesMessage(FacesMessage.SEVERITY_ERROR,
                    "Invalid ISBN", "'" + value + "' is not a valid ISBN-13."));
        }
    }

    @Override
    public Map<String, Object> getMetadata() {
        return Map.of();
    }

    @Override
    public String getValidatorId() {
        return "bookshelf.isbn13";
    }
}
src/main/webapp/resources/bookshelf/js/isbn13.js
PrimeFaces.validator['bookshelf.isbn13'] = {
    validate: function (element, value) {
        if (!value) {
            return;
        }
        if (!/^\d{13}$/.test(value) || !PrimeFaces.bookshelf.isbn13Checksum(value)) {
            throw {
                summary: 'Invalid ISBN',
                detail: "'" + value + "' is not a valid ISBN-13."
            };
        }
    }
};

PrimeFaces.bookshelf = PrimeFaces.bookshelf || {};
PrimeFaces.bookshelf.isbn13Checksum = function (isbn) {
    var sum = 0;
    for (var i = 0; i < 13; i++) {
        sum += parseInt(isbn.charAt(i), 10) * (i % 2 === 0 ? 1 : 3);
    }
    return sum % 10 === 0;
};

PrimeFaces.validator['Isbn13'] = {
    validate: function (element, value) {
        if (!value) {
            return;
        }
        if (!/^\d{13}$/.test(value) || !PrimeFaces.bookshelf.isbn13Checksum(value)) {
            var custom = element.data('p-isbn13-msg');
            throw {
                summary: custom || 'Invalid ISBN',
                detail: custom || ("'" + value + "' is not a valid ISBN-13.")
            };
        }
    }
};

PrimeFaces.validator['bookshelf.nameWhenRequired'] = {
    validate: function () {
        var required = PF('nameRequired');
        var name = PF('name');
        var vc = PrimeFaces.validation.ValidationContext;
        if (required.isChecked() && name.getValue().length === 0) {
            vc.highlight(name);
            vc.addMessage(name, { summary: 'Validation Error', detail: 'A name is required!' });
        } else {
            vc.unhighlight(name);
        }
    }
};

A Bean Validation constraint with a client side. Annotate the constraint with @ClientConstraint(resolvedBy = ...); the resolver implements ClientValidationConstraint (getMetadata(ConstraintDescriptor) and getValidatorId()), and the id selects the entry of PrimeFaces.validator (Isbn13 in the file above).

src/main/java/org/example/bookshelf/csv/Isbn13.java
package org.example.bookshelf.csv;

import jakarta.validation.Constraint;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import jakarta.validation.Payload;

import org.primefaces.validate.bean.ClientConstraint;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = Isbn13.Check.class)
@ClientConstraint(resolvedBy = Isbn13ClientConstraint.class)
public @interface Isbn13 {

    String message() default "not a valid ISBN-13";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    class Check implements ConstraintValidator<Isbn13, String> {
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            return value == null || value.isBlank() || Isbn13Validator.isValid(value);
        }
    }
}
src/main/java/org/example/bookshelf/csv/Isbn13ClientConstraint.java
package org.example.bookshelf.csv;

import jakarta.validation.metadata.ConstraintDescriptor;

import org.primefaces.validate.bean.ClientValidationConstraint;

import java.util.HashMap;
import java.util.Map;

public class Isbn13ClientConstraint implements ClientValidationConstraint {

    @Override
    public Map<String, Object> getMetadata(ConstraintDescriptor<?> descriptor) {
        Map<String, Object> metadata = new HashMap<>();
        Object message = descriptor.getAttributes().get("message");
        if (message != null) {
            metadata.put("data-p-isbn13-msg", message);
        }
        return metadata;
    }

    @Override
    public String getValidatorId() {
        return "Isbn13";
    }
}
src/main/webapp/csv-custom.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="http://primefaces.org/ui"
      xmlns:pt="jakarta.faces.passthrough">
<h:head>
  <title>Custom client validators</title>
</h:head>
<h:body>
  <h:form id="f">
    <p:messages id="msgs" showDetail="true"><p:autoUpdate/></p:messages>

    <p:outputLabel for="isbn" value="ISBN-13 (JSF validator)"/>
    <p:inputText id="isbn" value="#{csvBean.isbn}">
      <f:validator validatorId="bookshelf.isbn13"/>
    </p:inputText>
    <p:message for="isbn"/>

    <p:outputLabel for="beanIsbn" value="ISBN-13 (Bean Validation)"/>
    <p:inputText id="beanIsbn" value="#{csvBean.beanIsbn}"/>
    <p:message for="beanIsbn"/>

    <p:commandButton id="ajax" value="Save" action="#{csvBean.save}" validateClient="true" update="@form"/>
  </h:form>
  <h:outputScript library="bookshelf" name="js/isbn13.js" target="body"/>
</h:body>
</html>

Results:

  • 123 in both fields and a click: no request, messages Invalid ISBN / '123' is not a valid ISBN-13. and not a valid ISBN-13, the last from the data-p-isbn13-msg metadata that the resolver created from the constraint message. The input markup was data-p-val="bookshelf.isbn13" for the validator and data-p-val="Isbn13" plus data-p-isbn13-msg="not a valid ISBN-13" for the bean property.

  • 9780441172719 (a valid ISBN-13) in both: one request, saved. 9780441172710, with a wrong check digit: stopped by the browser with Invalid ISBN.

  • The script must load after core.js of PrimeFaces. With target="head" it ran before it and the console said ReferenceError: PrimeFaces is not defined; the validators were never registered and an invalid ISBN passed the browser check. The page above uses target="body".

  • The documentation example of a custom constraint calls PrimeFaces.util.ValidationContext. In PrimeFaces 16.0.0 that object is undefined; the object that exists is PrimeFaces.validation.ValidationContext (used in the next section).

  • Third-party annotations (for example from Hibernate Validator) are mapped with BeanValidationMetadataMapper.registerConstraintMapping(constraint, clientValidationConstraint) (documented; not exercised).

  • Converters. A converter that implements org.primefaces.convert.ClientConverter (getConverterId(), optional getMetadata()) and a function in PrimeFaces.converter['id'] that returns the converted value or throws a message give client-side conversion, as the built-in ones do (documented; not exercised).

Validating a Group of Fields, and Enabling a Button

Complex validation. A container with data-p-val="validatorId" is validated by that function when it is part of the processed components; the function gets no single value and uses PrimeFaces.validation.ValidationContext to mark fields and add messages. The rule here: the name is required when the checkbox is ticked. The container carries the attribute with a passthrough attribute (pt:data-p-val).

src/main/webapp/csv-complex.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="http://primefaces.org/ui"
      xmlns:pt="jakarta.faces.passthrough">
<h:head>
  <title>Complex client validation</title>
</h:head>
<h:body>
  <h:form id="g">
    <p:panel id="complex" header="Name" pt:data-p-val="bookshelf.nameWhenRequired">
      <p:messages id="msgs" showDetail="true"><p:autoUpdate/></p:messages>
      <p:selectBooleanCheckbox id="nameRequired" widgetVar="nameRequired" value="#{csvBean.nameRequired}"/>
      <p:inputText id="name" widgetVar="name" value="#{csvBean.name}" label="Name"/>
      <p:commandButton id="save" value="Save" action="#{csvBean.save}" validateClient="true" update="@form"/>
    </p:panel>
  </h:form>

  <h:form id="e">
    <p:inputText id="a" value="#{csvBean.title}" required="true" label="A">
      <p:clientValidator event="keyup"/>
    </p:inputText>
    <p:inputText id="b" value="#{csvBean.keyup}" required="true" label="B">
      <p:clientValidator event="keyup"/>
    </p:inputText>
    <p:commandButton id="gated" value="Save" action="#{csvBean.save}" validateClient="true"
                     enabledByValidateClient="true" update="@form"/>
  </h:form>
  <h:outputScript library="bookshelf" name="js/isbn13.js" target="body"/>
</h:body>
</html>

With the box ticked and the name empty, Save made no request and showed the summary Validation Error and the detail A name is required!, with the name input in the classes ui-state-error ui-state-csv-invalid. With a name, a request left. The function in isbn13.js is:

PrimeFaces.validator['bookshelf.nameWhenRequired'] = {
    validate: function () {
        var required = PF('nameRequired');
        var name = PF('name');
        var vc = PrimeFaces.validation.ValidationContext;
        ...
    }
};

A button that waits for valid input. enabledByValidateClient="true" on a command with validateClient="true" keeps it disabled until every input in its process passes CSV, using the instant validators of the inputs. In the second form of the page, the Save button was disabled when the page loaded (disabled and the class ui-state-csv-invalid), became enabled after two inputs with required="true" and p:clientValidator event="keyup" were filled, and was disabled again after one was emptied. The documentation lists the triggers: the first full page load, Ajax updates, and a p:clientValidator event.

What CSV Does Not Do

  • It is not a security control. The validation runs in the browser, only when a command with validateClient="true" is used. The same form posted by a script, or with JavaScript off or edited, reaches the server, which validates again. The scratch application used this on purpose: the Save (server only) button posted invalid data and received the server’s messages; the raw posts of Values That PrimeFaces Drops Without a Message did the same. Keep every rule on the server, as the Bean Validation page does.

  • Only PrimeFaces components. A h:inputText, h:selectOneMenu and so on are posted unchecked (the Plain field above).

  • Components without metadata. p:selectCheckboxMenu rendered no data-p-val, so its @NotEmpty was enforced on the server only. Look at the markup of a component (the data-p-* attributes) when in doubt.

  • Hidden fields. The script of PrimeFaces.validation.validate filters out invisible inputs unless validateInvisibleElements is true, so a field in a hidden panel is not validated by a button click.

  • Messages need a target. No p:message, p:messages or p:growl in the form (for a full submit) or in update (Ajax) means that the click stops and nothing explains why; the validation function has a logUnrenderedMessages option for that case.

  • Locale. The client messages are in English unless the locale script of the language is on the page (p:datePicker).

p:defaultCommand

A form with several buttons has no standard "Enter submits this one". p:defaultCommand fixes it: nested in the form with a target that references a command (a p:commandButton or p:commandLink), it clicks that command when the user presses Enter in an input. An input must have the focus. scope names an ancestor of the inputs so that one form can have several default commands, each for its region.

The sign-up form above has <p:defaultCommand target="b1"/>. Pressing Enter in the confirmation field (no button focused) posted jakarta.faces.source=f:b1, f:b1=f:b1 and the form fields: the same request as clicking Register. The documentation example shows a target that does not match the id of the button in its own form; check that the id exists.

p:focus

p:focus manages the focus: without attributes it focuses the first enabled, visible input of the page when the page loads, and after a failed validation it focuses the first invalid component. for focuses a specific component (useful in a dialog), context limits the search to a part of the page and minSeverity sets the lowest message severity that counts as invalid (default error). PrimeFaces.current().focus(expression) does the same from Java (PrimeFaces.current()).

Measured: the sign-up form (<p:focus/>) put the focus in the name field on load; after the failed password validation the focus was in the first password field. The review form with <p:focus context="review"/> focused the reviewer, the first invalid field, after the empty submit. An Ajax request moves the focus only if the p:focus is updated in the response (the documentation says so; the forms above update the whole form, which includes it).

Bean Validation Metadata on the Components

With primefaces.TRANSFORM_METADATA set to true PrimeFaces copies Bean Validation metadata to the components: the maximum length of a size constraint becomes the maxlength of the input, and a required constraint can switch on required (so that the label shows the indicator, though the p:outputLabel already checks the annotations, see p:outputLabel Sets the Label). The documentation adds that @NotNull only works fully if jakarta.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL is true.

<context-param>
    <param-name>primefaces.TRANSFORM_METADATA</param-name>
    <param-value>true</param-value>
</context-param>

In the review form, without the parameter the reviewer input had no maxlength; with it, the input rendered maxlength="40" from @Size(max = 40) on the property. (The comment input has the attribute set by hand and rendered maxlength="500" either way.) As in Values That PrimeFaces Drops Without a Message, a maxlength in the markup is an aid for the visitor and not a server rule.

Ajax Errors: p:ajaxExceptionHandler and the PrimeFaces ExceptionHandler

The general treatment of exceptions, error pages and the standard Ajax error element is on the exception handling page, which describes the PrimeFaces chain from the documentation (its section); this section reports what happened when it was run. The PrimeFaces documentation describes two levels. On the client, an error in an Ajax response (a server-reported error element, or a failed or timed-out connection) is handed to the onerror callbacks and global listeners, then to a matching p:ajaxExceptionHandler, then to a redirect to the matching error-page, and last to the console. On the server, PrimeFaces offers its own ExceptionHandler that redirects to the error page for Ajax requests as well, and exposes the exception through the EL name pfExceptionHandler (type, message, stackTrace, formattedStackTrace, timestamp, formattedTimestamp).

The PrimeFaces handler and its EL resolver are opt-in, in faces-config.xml:

<application>
    <el-resolver>org.primefaces.application.exceptionhandler.PrimeExceptionHandlerELResolver</el-resolver>
</application>
<factory>
    <exception-handler-factory>org.primefaces.application.exceptionhandler.PrimeExceptionHandlerFactory</exception-handler-factory>
</factory>
src/main/webapp/errors.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="http://primefaces.org/ui">
<h:head><title>errors</title></h:head>
<h:body>
  <p:ajaxStatus id="status" onstart="document.title='busy'" oncomplete="document.title='idle'">
    <f:facet name="error"><h:outputText value="Request failed"/></f:facet>
  </p:ajaxStatus>
  <h:form id="f">
    <p:ajaxExceptionHandler type="java.lang.IllegalStateException" update="box" onexception="window.handled = 'IllegalStateException'"/>
    <h:panelGroup id="box" layout="block">
      <h:outputText value="Type: #{pfExceptionHandler.type} Message: #{pfExceptionHandler.message}" rendered="#{not empty pfExceptionHandler.type}"/>
    </h:panelGroup>
    <p:commandButton id="fail" value="Fail" action="#{errorsBean.fail}" update="box"
                     onerror="window.onerrorCalled = [typeof xhr, status, error]"/>
    <p:commandButton id="ok" value="Ok" action="#{errorsBean.ok}"/>
  </h:form>
</h:body>
</html>
src/main/java/org/example/bookshelf/web/ErrorsBean.java
package org.example.bookshelf.web;

import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;

import java.io.Serializable;

@Named
@ViewScoped
public class ErrorsBean implements Serializable {

    public void fail() {
        throw new IllegalStateException("catalogue database at jdbc:h2:mem:fake is unavailable");
    }

    public void slow() throws InterruptedException {
        Thread.sleep(1500);
    }

    public void http500() throws java.io.IOException {
        jakarta.faces.context.FacesContext ctx = jakarta.faces.context.FacesContext.getCurrentInstance();
        ctx.getExternalContext().responseSendError(500, "boom");
        ctx.responseComplete();
    }

    public void ok() {
    }
}

The action fail() throws an IllegalStateException whose message has a (fake) JDBC URL, to see what leaks. Without the PrimeFaces ExceptionHandler (Production stage):

  • Mojarra: the response was HTTP 200 with <error><error-name>class java.lang.IllegalStateException</error-name><error-message>See your server log for more information</error-message></error>. p:ajaxExceptionHandler matched java.lang.IllegalStateException: onexception ran and a second request refreshed box, but pfExceptionHandler.type was empty, so the box stayed empty. onerror of the button was not called (Client Callbacks and Callback Arguments).

  • MyFaces: the error-name was org.apache.myfaces.view.facelets.el.ContextAwareELException and the error-message carried the whole exception text, JDBC URL included. p:ajaxExceptionHandler type="java.lang.IllegalStateException" did not match, onexception never ran.

With the two registrations above, both implementations answered the same normal partial response: an update of box with Type: java.lang.IllegalStateException Message: catalogue database at jdbc:h2:mem:fake is unavailable and an extension script calling the handler function with the type, the message and a timestamp. The type matched on MyFaces too.

Two rules from this. Register PrimeExceptionHandlerFactory if you use p:ajaxExceptionHandler and want one behaviour on both implementations. And treat pfExceptionHandler.message as sensitive: it printed the JDBC URL. The documentation prints formattedStackTrace with escape="false"; do not, in Production: it discloses internals and, being unescaped, can carry markup from the message. Show a fixed text and a correlation id, as the exception handling page does. Rendering errors need jakarta.faces.FACELETS_BUFFER_SIZE big enough for the response not to be committed, or no handler can redirect.

Security Notes

  • The server decides. CSV, maxdate, min/max, masks, maxlength and partialSubmit are conveniences for the visitor. A forged post changes or omits anything the browser sent (Values That PrimeFaces Drops Without a Message), and a missing parameter skips validation of that field. Repeat each rule that matters in the action or the service (see the security page), and treat everything that arrives through p:remoteCommand parameters, completeMethod queries and callback names as untrusted input.

  • Escaping. Keep escape="true" (the default) on p:messages, p:message, p:growl and p:autoComplete; the scratch run showed script execution with escape="false" (p:messages, p:message and p:growl). Never write formattedStackTrace unescaped (Ajax Errors: p:ajaxExceptionHandler and the PrimeFaces ExceptionHandler).

  • Callback arguments are plain JSON in the response; do not put secrets or personal data into addCallbackParam.

  • View state and CSRF. Every PrimeFaces Ajax request in the scratch application carried jakarta.faces.ViewState, like a Faces postback, and the protections of the security page (protected views, SameSite, the same-origin checks) apply to them unchanged. A state-changing p:button or link with outcome is a GET and has the problems of any GET.

  • Content Security Policy. The handlers shown above are inline (onclick="PrimeFaces.ab(...)"). PrimeFaces has its own CSP support (primefaces.CSP, primefaces.CSP_POLICY); the security page reports what was run: a p:commandButton worked on both implementations with it, a h:commandButton with f:ajax failed on Mojarra only.

  • Passwords. Do not log them, do not return them in callback arguments, and hash them with a password hash. redisplay stays false.

  • Dependencies. The Ajax and CSV scripts are served from the application (the jakarta.faces.resource URLs), not from a CDN; keep PrimeFaces updated as a security dependency.

Mojarra and MyFaces

PrimeFaces gave the same results on Mojarra 4.1.17 and MyFaces 4.1.4 for the request format, the generated handlers, process and update resolution, partialSubmit, the callbacks and their arguments, PrimeFaces.current(), p:remoteCommand, p:autoUpdate, the input components, the silent drops, the message components, the whole of CSV, p:defaultCommand and p:focus. The differences found in the scratch application:

  • No-argument p:ajax listener. It worked on MyFaces, unlike the no-argument f:ajax listener that the Ajax page reports as failing there.

  • Form marker. Mojarra posts the form id as a parameter (f=f), MyFaces posts formId_SUBMIT=1. A raw post to a MyFaces page without that parameter is treated as if nothing was submitted and no validation runs, so scripts that replay requests need it.

  • Checkbox value. h:selectBooleanCheckbox posted on on Mojarra and true on MyFaces.

  • Phase id text. INVOKE_APPLICATION 5 against INVOKE_APPLICATION(5).

  • Generated ids. j_idt6 against j_id_a, and the view state element ids, as on the Ajax page.

  • A process that leaves out the command. Mojarra did not run the action of a button with process="a b"; MyFaces did (Partial Processing: A Dependent Menu).

  • Bean Validation messages on the server. MyFaces prefixes the label (Email: must be a well-formed email address), Mojarra does not. The client-side messages of CSV have the label on both.

  • Ajax error element. Mojarra reports the exception class and "See your server log for more information" in Production; MyFaces reports ContextAwareELException with the complete text, and p:ajaxExceptionHandler without the PrimeFaces ExceptionHandler does not match there (Ajax Errors: p:ajaxExceptionHandler and the PrimeFaces ExceptionHandler).

  • Cancel buttons. Without resetValues, the invalid field shows an empty text on Mojarra and the model value on MyFaces (resetValues and Cancel Buttons).

  • maxlength of the standard h:inputText. A post of ten characters for maxlength="3" was stored whole on Mojarra and ignored (the old value stayed, no message) on MyFaces; p:inputText cut it to three on both.

What Changed

Version Change

PrimeFaces 3.x to 5.x

The versions of the books. RequestContext.getCurrentInstance(), p:calendar, javax.faces, the ThemeRoller themes and the first CSV.

PrimeFaces 6.2

p:autoUpdate replaced the autoUpdate attribute (deprecated). Many RequestContext methods deprecated in favour of the new API.

PrimeFaces 7.0

PrimeFaces.current() replaced RequestContext (which remains internal as PrimeRequestContext); the autoUpdate attributes were removed; p:push was removed (use f:websocket or OmniFaces sockets); mask of p:inputMask became required.

PrimeFaces 8.0

The server-side check of the mask (validateMask); p:watermark was reimplemented with the HTML placeholder; a calendar or date picker without a value binding converts to LocalDate (before 8.0: Date).

PrimeFaces 10.0

p:calendar deprecated in favour of p:datePicker (still in the tag library of 16.0.0).

PrimeFaces 14.0

Search expressions delegate to the Faces implementation; p:fileUpload client validation moved to CSV; the itemLabel of f:selectItem in p:selectCheckboxMenu is required; texts of p:autoComplete moved to the client locale.

PrimeFaces 15.0

emptyMessage of p:autoComplete came back; yearNavigator of p:datePicker became a string; the themes were renamed (saga-blue).

PrimeFaces 16.0

All javax.* support removed (baseline: Jakarta Faces 4.0 and newer); the client widgets became JavaScript classes (code that patches widgets may need changes).

Faces 4.1 and 5.0

Nothing in this page depends on a 4.1-specific feature. Faces 5.0 is under development (the latest milestone at the time of writing is jakarta.faces-api:5.0.0-M9); no change to PrimeFaces Ajax is described here, because none was verified.

Where the Books Are Outdated

Çalışkan and Varaksin (the PrimeFaces cookbook of 2015, PrimeFaces 5.1 and 5.2 on Faces 2.2) are the one book that is about this page; their chapters on Ajax and partial processing, on the inputs, on client-side validation and on the advanced topics map to the sections above. Burns and Schalk, and Leonard, write about f:ajax and the standard validators, on the Ajax and validators pages. Jonna’s book is about the PrimeFaces Extensions, which have their own page. What has changed:

  • RequestContext. The books use RequestContext.getCurrentInstance() to update components, add callback parameters and run scripts. Today it is PrimeFaces.current() with ajax().update(...), ajax().addCallbackParam(...) and executeScript(...) (PrimeFaces.current()).

  • p:calendar. It is deprecated (since 10.0); p:datePicker takes java.time values and works with LocalDate by default. The attribute names and the pattern syntax differ, as do the locale scripts.

  • Client-side validation. The framework of the book is the ancestor of the present one, with the same ideas (validateClient, p:clientValidator, ClientValidator, Bean Validation integration), but the context parameter, the packages (jakarta.*), the message handling and the JavaScript API changed (Client-Side Validation (CSV)); copy nothing from a PrimeFaces 5 CSV example without reading the present documentation.

  • Removed features. p:push, the autoUpdate attributes, p:watermark (use placeholder) and the mobile components are gone or replaced. Themes and icons are on the page about getting started with PrimeFaces.

  • Caching. The book’s caching chapter is about p:cache, which the performance page covers.

  • Namespaces. javax.faces becomes jakarta.faces and the http://primefaces.org/ui namespace is still accepted (the scratch pages use it; a page with xmlns:p="primefaces" rendered the same Ajax handler).

Common Mistakes

  • The command processes the whole view. A p:commandButton without process executes @all; one required field elsewhere blocks it. Set process="@this" or "@form".

  • The action does not run. process="@none", or (on Mojarra) a list that leaves out the command, skips the action. Add @this.

  • The error message never appears. The message component is not in update (or not auto-updated).

  • update with a typo fails the page. Identifiers are resolved at render time; prefer wrappers that are always rendered; use ignoreComponentNotFound only for optional parts.

  • A cancel button shows stale values. Add resetValues="true" or use p:resetInput.

  • onerror is not called for an exception. It is for transport failures; use p:ajaxExceptionHandler and register the PrimeFaces ExceptionHandler.

  • Callback arguments are empty. A Map or a record did not serialise; use a JavaBean.

  • The docs say addCallBackParam. The method is addCallbackParam.

  • A server rule that only the browser enforces. maxdate, min, max and maxlength drop values silently on the server; validate in the service. A missing parameter skips @NotNull.

  • must not be null for a card number. The mask check turned a mismatching value into null; show a message of your own, or validate the pattern with the mask check off.

  • Custom client validator never runs. The script was loaded before PrimeFaces (use target="body"), the id does not match getValidatorId(), or the component renders no CSV metadata.

  • h:inputText is not validated in the browser. CSV is for PrimeFaces components.

  • Doubled messages with CSV. Use the default showDetail on messages that CSV fills.

  • locale="es" shows English. Load the locale script by hand, or check that primefaces.CLIENT_SIDE_LOCALISATION was not set to false and that the view locale is the one you expect.

  • Password fields come back empty. That is redisplay="false"; keep it.

  • Passing a user id through p:remoteCommand. Check access to that id on the server.

  • escape="false" on messages, or formattedStackTrace unescaped. Script injection and information disclosure.

  • Enter submits the wrong button. Without p:defaultCommand the browser uses the first submit button of the form; add it and name the target.

  • A p:focus that does not move the focus after Ajax. It was not in update.

References