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.jsandcomponents.jsand does not usefaces.js. On the scratch pages that contain nof:ajax, the globalfacesobject wasundefinedand 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
processandupdateaccept 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 generatedonkeyuphandler becamejakarta.faces.partial.render=draft:preview+draft:keystrokesin 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 carryextensionelements 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:autoCompletepostsreview:book_query=Duwith 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 |
|---|---|
|
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 |
Phase listeners |
|
|
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 |
The renderer behind |
Renderers for |
Resource ordering and the optional moving of scripts to the end of the body ( |
Converters for the standard ids and types ( |
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.
<?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>
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:"draft:title",e:"keyup",f:"draft",p:"draft:title",u:"draft:preview,draft:keystrokes",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 |
|---|---|
|
The DOM or component event that triggers the request. Without it, the default of the component: |
|
A method expression. It may take an |
|
What to execute. Default for |
|
What to render. Default: nothing. |
|
The listener runs in Apply Request Values instead of Invoke Application (measured: Listener and Phase), before the conversion and validation of the processed components. |
|
Milliseconds to wait; if another request is started within that time only the most recent is sent. |
|
When |
|
Milliseconds after which the request is abandoned; |
|
When |
|
Send only the parameters of the processed components (Partial Submit). |
|
Reset the local values of the input components that are updated (resetValues and Cancel Buttons). |
|
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. |
|
Do not update the components marked with |
|
Ignore references in |
|
The id of the form to serialise; default: the enclosing form. |
|
The client callbacks of Client Callbacks and Callback Arguments. |
|
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) |
|---|---|
|
|
|
|
|
|
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 |
|---|---|
|
the input itself ( |
|
|
|
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; theprependId="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.
<?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>
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@allfor this page (the whole form is processed and the required field blocks). -
A
processlist 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. Writeprocess="a b @this"so that both agree. -
process="@none"on a command does not run its action. A button whose action increments a counter, withprocess="@none", posted the request and rendered, but the counter stayed at 0, because the source component was not part of the execute list. Use@thisto 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 |
|---|---|
|
The standard keywords (the PrimeFaces documentation lists them). |
|
The nth row of an iterating component. |
|
The component whose |
|
The components that observe the event with |
|
A PrimeFaces Selector: a jQuery selector evaluated in the browser. Components are matched by the DOM, so they need an explicit |
|
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 |
|---|---|
|
|
|
|
|
|
|
execute |
|
nothing: the request carried no render parameter and succeeded |
|
|
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 |
|---|---|---|
|
|
|
button, |
|
the same (explicit setting wins) |
button, |
|
the same |
button, |
|
the same |
button, |
|
|
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 |
|
|
with |
|
|
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 |
|
none |
Before the request is sent. Returning |
|
|
The response arrived with a success code; before the DOM is updated. |
|
|
The request failed (see below for what counts as a failure). |
|
|
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">{"serverCalls":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().
<?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>
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));
}
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
isbnandlengtharrived as{"length":13}: the componentisbnwas 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 |
|---|---|
|
Whether the request is a Faces request, and whether it is a PrimeFaces Ajax request. Both returned |
|
Runs a script in the browser after the Ajax request completes or on page load. In the response it is an |
|
Focuses the input targeted by the search expression. Response: |
|
Scrolls to the component with that client id. Response: |
|
Resets the input components targeted by the expressions, so they read the model again (resetValues and Cancel Buttons). |
|
Finds a widget component by its |
|
Returns the Ajax helper: |
|
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. TypingDunewithdelay="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 withPrimeFaces.ajax.Queue.abortAll()(documentation). -
timeout. A request that has not answered within the given milliseconds is abandoned withonerror(xhr, "timeout", "timeout")(measured). The browser abandons the request; nothing tells the server to stop the action. -
disableOnAjaxon 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:ajaxStatusis the global indicator of Ajax requests. It shows the facetsdefault,start,success,errorandcomplete, or runs the callbacksonstart,onsuccess,onerrorandoncomplete; it also reacts tof:ajaxrequests.global="false"on a command or behaviour makes it a silent request. If acompletefacet is defined, thesuccessanderrorfacets are never seen (the documentation says so). Do not update the status component itself.
<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.
<?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>
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
autoRuncommand postedexecute=f:j_idt6(itself; it hasprocess="@this") andstatusreadcalls=1 last=none. -
Clicking the plain button called
selectBook([{name: 'bookId', value: '42'}])and postedbookId=42next to the form fields, withexecute=@all(the default of the command, Commands: p:commandButton and p:commandLink); the growl showed Selected book 42 andoncompletereceived{"serverCalls":2}inargs. -
The function returns a promise.
selectBook([...]).then(r => r.jqXHR.pfArgs)resolved with an object that hasdocument,textStatusandjqXHR, andr.jqXHR.pfArgs.serverCallswas3. That lets calling code register its own callbacks per call. The PrimeFaces documentation showsasyncfunctions 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.
<?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>
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 |
|
|
Touch (no |
no render parameter |
|
Touch quietly ( |
|
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.
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; }
}
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).
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:
<?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>
Source: PrimeFaces 16 documentation: Client Side Validation and Jakarta Faces 4.1 VDL: f:validateBean
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 |
|---|---|
|
Display and parse pattern ( |
|
Limits as a |
|
Lists of dates; days are numbers with Sunday as |
|
A |
|
Presentation. |
|
The time part. |
|
A locale key or a |
|
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 add/MM/yyyypattern, gaveFinished 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
maxdateset to today,widget.setDate(new Date(2999,0,1))in the browser ended up at today’s date (the picker clamps). A forged post of31/12/2999was stopped by the Bean Validation message of@PastOrPresent: the only message wasmust be a date in the past or in the present.maxdateis a convenience for the visitor, not a guard on the server. -
locale="es"alone changed nothing in the labels: the open panel still showedJan Feb Mar ...andToday Clear. The labels come fromPrimeFaces.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, withaccept:"Sí",addRule:"Agregar regla"and so on, and the Spanish validation messages for CSV) or rely on the context parameterprimefaces.CLIENT_SIDE_LOCALISATION, whose default istrue(see the context parameters): it addslocales/locale-<language>.jsfor 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 isfalse.
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 |
|---|---|
|
|
|
Default 300 ms; the form uses 100. |
|
|
|
Adds a button that starts a search, with an empty query or with the current text, as |
|
Typing a text that matches no suggestion and leaving the field cleared it. |
|
The value is a |
|
Suggestions of an identical query are served from the browser (documented; not measured). |
|
Consumes a REST endpoint instead of calling a bean method, for a method that does not need the Faces lifecycle (documented; not exercised). |
|
Default |
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.
<?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>
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 Anawith the detailphone=(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. -
matchis 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 onPassword: Validation Error: Value is required.. That is the intended behaviour, not a bug: do not turnredisplayon 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:spinnerwithmin="1" max="5"for the rating: the postedreview:rating_inputis the visible text field. Values outside the range were dropped on the server (Values That PrimeFaces Drops Without a Message). -
p:inputTextareawithcounter="left"andcounterTemplate="{0} characters left"updates the output with the idleftwhile typing (495 characters leftafterhello) and setsmaxlengthon thetextarea. 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: avalue, the standardrequired,converterandvalidatorattributes, 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 |
|---|---|---|
|
|
|
|
|
none; the property was not even validated, so |
|
|
none: a parameter that is absent means "not submitted" and skips conversion, validation and the model update |
|
|
|
six values for |
empty list |
none |
|
empty list |
only the |
600 characters for |
the first 500 characters |
none |
|
validation fails |
|
|
rejected |
|
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 |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
not available |
|
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.
<?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>
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:
-
summarylisted the five messages (info, warn, error, fatal, and the one forf: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. -
titleMessageshowedField detail(the defaultshowDetail="true"andshowSummary="false"ofp:message), andtitleIcon(display="icon") rendered an icon whosetitlewasField 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 threestickyalerts 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:"f:ajax",f:"f",u:"f"});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 theupdatetargets. 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.
<?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>
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.andCode: 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"(thef:validateLengthof the page, handled by the PrimeFaces subclass of the validator). Thecodeinput carrieddata-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,DoubleRangeandRegularExpressionvalidators (standard validators) and the standard converters (converters; the table of The PrimeFaces Ajax Model) render this metadata;data-p-connames 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,PositiveOrZeroandSize(the classes*ClientValidationConstraintin 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 answeredPlain: 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 themessagesentries keyed by Faces message ids such asjakarta.faces.component.UIInput.REQUIRED. The label prefix follows the MyFaces convention on both implementations (Code: must not be blank.), while the server on Mojarra printsmust not be blankwithout it. The documentation shows how to remove the prefix: setjakarta.faces.validator.BeanValidator.MESSAGEto{0}in a server bundle and, for the client, in the locale (PrimeFaces.locales['en_US'].messages[...] = '{0}'); the context parameterprimefaces.INTERPOLATE_CLIENT_SIDE_VALIDATION_MESSAGES(defaultfalse) loads the messages through the Bean Validation message interpolator so that the text ofValidationMessages.propertiesis shown on the client too. -
Duplicates. A
p:messageswithshowDetail="true"printed each client-side message twice (summary and detail are the same text) even withskipDetailIfEqualsSummary="true", which the client renderer did not apply. LeaveshowDetailat 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:
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";
}
}
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).
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);
}
}
}
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";
}
}
<?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:
-
123in both fields and a click: no request, messagesInvalid ISBN/'123' is not a valid ISBN-13.andnot a valid ISBN-13, the last from thedata-p-isbn13-msgmetadata that the resolver created from the constraint message. The input markup wasdata-p-val="bookshelf.isbn13"for the validator anddata-p-val="Isbn13"plusdata-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 withInvalid ISBN. -
The script must load after
core.jsof PrimeFaces. Withtarget="head"it ran before it and the console saidReferenceError: PrimeFaces is not defined; the validators were never registered and an invalid ISBN passed the browser check. The page above usestarget="body". -
The documentation example of a custom constraint calls
PrimeFaces.util.ValidationContext. In PrimeFaces 16.0.0 that object isundefined; the object that exists isPrimeFaces.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(), optionalgetMetadata()) and a function inPrimeFaces.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).
<?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:selectOneMenuand so on are posted unchecked (the Plain field above). -
Components without metadata.
p:selectCheckboxMenurendered nodata-p-val, so its@NotEmptywas enforced on the server only. Look at the markup of a component (thedata-p-*attributes) when in doubt. -
Hidden fields. The script of
PrimeFaces.validation.validatefilters out invisible inputs unlessvalidateInvisibleElementsistrue, so a field in a hidden panel is not validated by a button click. -
Messages need a target. No
p:message,p:messagesorp:growlin the form (for a full submit) or inupdate(Ajax) means that the click stops and nothing explains why; the validation function has alogUnrenderedMessagesoption 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>
<?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>
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:ajaxExceptionHandlermatchedjava.lang.IllegalStateException:onexceptionran and a second request refreshedbox, butpfExceptionHandler.typewas empty, so the box stayed empty.onerrorof the button was not called (Client Callbacks and Callback Arguments). -
MyFaces: the
error-namewasorg.apache.myfaces.view.facelets.el.ContextAwareELExceptionand theerror-messagecarried the whole exception text, JDBC URL included.p:ajaxExceptionHandler type="java.lang.IllegalStateException"did not match,onexceptionnever 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,maxlengthandpartialSubmitare 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 throughp:remoteCommandparameters,completeMethodqueries and callback names as untrusted input. -
Escaping. Keep
escape="true"(the default) onp:messages,p:message,p:growlandp:autoComplete; the scratch run showed script execution withescape="false"(p:messages, p:message and p:growl). Never writeformattedStackTraceunescaped (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-changingp:buttonor link withoutcomeis 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: ap:commandButtonworked on both implementations with it, ah:commandButtonwithf:ajaxfailed on Mojarra only. -
Passwords. Do not log them, do not return them in callback arguments, and hash them with a password hash.
redisplaystaysfalse. -
Dependencies. The Ajax and CSV scripts are served from the application (the
jakarta.faces.resourceURLs), 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:ajaxlistener. It worked on MyFaces, unlike the no-argumentf:ajaxlistener that the Ajax page reports as failing there. -
Form marker. Mojarra posts the form id as a parameter (
f=f), MyFaces postsformId_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:selectBooleanCheckboxpostedonon Mojarra andtrueon MyFaces. -
Phase id text.
INVOKE_APPLICATION 5againstINVOKE_APPLICATION(5). -
Generated ids.
j_idt6againstj_id_a, and the view state element ids, as on the Ajax page. -
A
processthat leaves out the command. Mojarra did not run the action of a button withprocess="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 reportsContextAwareELExceptionwith the complete text, andp:ajaxExceptionHandlerwithout the PrimeFacesExceptionHandlerdoes 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). -
maxlengthof the standardh:inputText. A post of ten characters formaxlength="3"was stored whole on Mojarra and ignored (the old value stayed, no message) on MyFaces;p:inputTextcut it to three on both.
What Changed
| Version | Change |
|---|---|
PrimeFaces 3.x to 5.x |
The versions of the books. |
PrimeFaces 6.2 |
|
PrimeFaces 7.0 |
|
PrimeFaces 8.0 |
The server-side check of the mask ( |
PrimeFaces 10.0 |
|
PrimeFaces 14.0 |
Search expressions delegate to the Faces implementation; |
PrimeFaces 15.0 |
|
PrimeFaces 16.0 |
All |
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 |
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 useRequestContext.getCurrentInstance()to update components, add callback parameters and run scripts. Today it isPrimeFaces.current()withajax().update(...),ajax().addCallbackParam(...)andexecuteScript(...)(PrimeFaces.current()). -
p:calendar. It is deprecated (since 10.0);p:datePickertakesjava.timevalues and works withLocalDateby default. The attribute names and thepatternsyntax 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, theautoUpdateattributes,p:watermark(useplaceholder) 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.facesbecomesjakarta.facesand thehttp://primefaces.org/uinamespace is still accepted (the scratch pages use it; a page withxmlns:p="primefaces"rendered the same Ajax handler).
Common Mistakes
-
The command processes the whole view. A
p:commandButtonwithoutprocessexecutes@all; one required field elsewhere blocks it. Setprocess="@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). -
updatewith a typo fails the page. Identifiers are resolved at render time; prefer wrappers that are always rendered; useignoreComponentNotFoundonly for optional parts. -
A cancel button shows stale values. Add
resetValues="true"or usep:resetInput. -
onerroris not called for an exception. It is for transport failures; usep:ajaxExceptionHandlerand register the PrimeFacesExceptionHandler. -
Callback arguments are empty. A
Mapor a record did not serialise; use a JavaBean. -
The docs say
addCallBackParam. The method isaddCallbackParam. -
A server rule that only the browser enforces.
maxdate,min,maxandmaxlengthdrop values silently on the server; validate in the service. A missing parameter skips@NotNull. -
must not be nullfor a card number. The mask check turned a mismatching value intonull; 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 matchgetValidatorId(), or the component renders no CSV metadata. -
h:inputTextis not validated in the browser. CSV is for PrimeFaces components. -
Doubled messages with CSV. Use the default
showDetailon messages that CSV fills. -
locale="es"shows English. Load the locale script by hand, or check thatprimefaces.CLIENT_SIDE_LOCALISATIONwas not set tofalseand 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, orformattedStackTraceunescaped. Script injection and information disclosure. -
Enter submits the wrong button. Without
p:defaultCommandthe browser uses the first submit button of the form; add it and name the target. -
A
p:focusthat does not move the focus after Ajax. It was not inupdate.
References
-
PrimeFaces 16 documentation: AJAX Processing Requirements and Partial Processing
-
PrimeFaces 16 documentation: Java API (PrimeFaces.current())
-
PrimeFaces 16 documentation: Client Side Validation: Bean Validation
-
PrimeFaces 16 documentation: Client Side Validation: Messages
-
PrimeFaces 16 documentation: Client Side Validation: JavaScript API
-
Jakarta EE Tutorial: Using Ajax with Jakarta Faces Technology
-
Jakarta Faces 4.1 specification, 3.5.6 Bean Validation Integration