Ajax

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Jakarta Faces 4.1 (API jakarta.faces-api 4.1.2, part of Jakarta EE 11), Eclipse Mojarra 4.1.17 and Apache MyFaces Core 4.1.4, with Java 17 or later. The change lists of the 4.1 and 4.0 specifications contain a single Ajax entry (an improvement of f:ajax inside composite components in 4.0) and no 4.1 entry, so the text also describes Faces 4.0 (nothing was run on 4.0); the earlier releases are in What Changed. The examples were compiled and deployed in a scratch Bookshelf WAR on Apache Tomcat 11.0.26 with Weld 6.0.4.Final, once with Mojarra and once with MyFaces. The server side was driven with curl, and the JavaScript side with a real browser (headless Google Chrome controlled through the DevTools protocol), so the request parameters, the partial responses, the event and error payloads and the timings come from those runs (checked on 2026-10-08). The places where the two implementations disagree are collected in Mojarra and MyFaces.

Ajax is how a Faces page changes part of itself without a full page load. Faces does it without any hand-written JavaScript or response format on the application’s side: a behaviour attached to a component (f:ajax) makes the browser send the form with a marker header, the server runs the usual lifecycle on only the components that the page named, and it answers with an XML document, the partial response, that tells the browser which pieces of markup to replace. Everything the framework does here is specified (chapters 13 and 14 of the specification, the JavaScript documentation and the PartialViewContext API), which is why the same page works on every implementation and with every component library.

The Ajax Model

An Ajax request in Faces is not a different kind of request. It is a normal postback of the page’s form, with three differences:

  • It is sent by faces.ajax.request (the JavaScript function in the faces.js resource) as an asynchronous POST, with the request header Faces-Request: partial/ajax and the request parameter jakarta.faces.partial.ajax=true. The URL is the action of the form.

  • It names which components take part in the execute half of the lifecycle (jakarta.faces.partial.execute) and which are re-rendered (jakarta.faces.partial.render). Both are lists of client ids, or one of the keywords @all and @none.

  • The response is not a page. It is a text/xml document of update, insert, delete, attributes, eval, redirect and error operations that faces.ajax.response applies to the DOM.

There are three ways to start such a request from a view, and all three end in faces.ajax.request:

Entry point What it is Use it when

f:ajax

A tag that attaches an AjaxBehavior to a component, or to every component inside it.

A component should send a request on one of its own events. This is the normal case.

faces.ajax.request(source, event, options)

The JavaScript API, called from any script or event attribute.

The request is triggered by something that is not a Faces component event, or the options are computed.

h:commandScript

A component that renders a named JavaScript function which, when called, sends the request.

A script (a timer, a callback of a third-party widget, the load event) needs to call a server method.

The server side is jakarta.faces.context.PartialViewContext. The tutorial says it captures the state of an Ajax request (which components are executed, which are rendered), and that its processPartial(PhaseId) method does the partial tree traversal in each phase. A request is partial when PartialViewContext.isPartialRequest() is true; it is an Ajax request when isAjaxRequest() is true, which the API documentation defines as the presence of the Faces-Request header with the value partial/ajax. The same documentation says that isPartialRequest() is also true for the header value partial/process, a partial processing request that is not Ajax; this page deals with partial/ajax.

A First Example: Live Search in Bookshelf

The catalogue of Bookshelf gets a search box that filters the list while the user types, without a page load. The page below is shown without the site template. The f:ajax inside the h:inputText sends a request 300 milliseconds after the last key-up, runs the bean’s search listener, and re-renders the result list and the counter.

src/main/webapp/search.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:ui="jakarta.faces.facelets">
<h:head>
  <title>Search the catalogue</title>
</h:head>
<h:body>
  <h:form id="search">
    <h:inputText id="q" value="#{searchBean.query}">
      <f:ajax event="keyup" delay="300" listener="#{searchBean.search}" render="results count"/>
    </h:inputText>
    <h:outputText id="count" value="#{searchBean.searches} searches"/>
    <h:panelGroup id="results" layout="block">
      <ui:repeat value="#{searchBean.results}" var="b">
        <div class="book">#{b.title} by #{b.author}</div>
      </ui:repeat>
    </h:panelGroup>
  </h:form>
</h:body>
</html>
src/main/java/org/example/bookshelf/web/SearchBean.java
package org.example.bookshelf.web;

import jakarta.faces.event.AjaxBehaviorEvent;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.List;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;

@Named
@ViewScoped
public class SearchBean implements Serializable {

    @Inject
    private BookService books;

    private String query = "";
    private List<Book> results;
    private int searches;

    public void search(AjaxBehaviorEvent event) {
        searches++;
        results = books.search(query);
    }

    public List<Book> getResults() {
        return results != null ? results : books.findAll();
    }

    public int getSearches() { return searches; }
    public String getQuery() { return query; }
    public void setQuery(String query) { this.query = query; }
}

Typing Dune, Gibson and Le Guin into the field in a browser changed the counter to 1 searches, 2 searches and 3 searches and the list to the matching books, on both implementations. The counter keeps growing because the bean is @ViewScoped: the same instance serves every Ajax request of the view, which is the scope an Ajax-driven page almost always wants. A @RequestScoped bean would be created again for every request; that is fine for a listener whose result is rendered in the same request, as in the review preview below, and wrong for state that must survive until the next request.

Later XHTML listings of this page show only the h:head and h:body part: they sit inside the same html element, with the same h, f and ui namespace declarations and the DOCTYPE, as this first one.

A Throwaway Bean for the Small Examples

The short examples further down (groups, resetValues, h:commandScript, the listener traces and the programmatic behaviour) share one view-scoped bean, jsBean, so that each page stays a few lines. It is scaffolding for the experiments, not a Bookshelf class:

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

import jakarta.faces.context.FacesContext;
import jakarta.faces.event.AjaxBehaviorEvent;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.List;

@Named
@ViewScoped
public class JsBean implements Serializable {

    @Inject
    private FacesContext faces;

    private String name = "start";
    private String last = "";
    private boolean flag;
    private boolean visible;

    public void refresh() {
        last = "refreshed:" + name + ":" + faces.getExternalContext().getRequestParameterMap().get("tag");
    }

    public void noArg() { last = "noarg-ran"; }

    public void phase(AjaxBehaviorEvent event) {
        System.out.println("LISTENER " + event.getComponent().getId() + " in " + faces.getCurrentPhaseId() + " name=" + name);
    }

    public String toggle() { visible = !visible; return null; }

    public String go() { return "js?faces-redirect=true"; }

    public List<String> getIds() { return List.of("a", "b"); }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getLast() { return last; }
    public boolean isFlag() { return flag; }
    public void setFlag(boolean flag) { this.flag = flag; }
    public boolean isVisible() { return visible; }
}

One Request from Keystroke to DOM Update

The numbered steps of the diagram are the ones to keep in mind when something does not update.

sequenceDiagram autonumber actor U as User participant B as Browser with faces.js participant S as FacesServlet participant L as Lifecycle and component tree participant M as SearchBean U->>B: types in the search field B->>B: delay 300 ms, keep only the last call, queue the request B->>S: POST /search.xhtml with Faces-Request partial/ajax Note over B,S: parameters partial.execute, partial.render, source, behavior.event, ViewState and the executed fields S->>L: Restore View, PartialViewContext lists execute and render ids L->>L: Apply Request Values, Process Validations, Update Model Values for the execute ids only L->>M: Invoke Application, AjaxBehaviorEvent reaches search() M-->>L: query applied, results computed L->>L: Render Response for the render ids only L-->>B: text/xml partial-response with update elements and the new ViewState B->>B: events complete and success, faces.ajax.response replaces the DOM elements B-->>U: the list and the counter change, the rest of the page is untouched

The request that the browser actually sent for the live search (decoded, from a request filter in the scratch application; Mojarra 4.1.17):

POST /bookshelf/search.xhtml
Faces-Request: partial/ajax
Content-Type: application/x-www-form-urlencoded;charset=UTF-8

search                          = search
search:q                        = Dune
jakarta.faces.ViewState         = ...
jakarta.faces.partial.ajax      = true
jakarta.faces.partial.event     = keyup
jakarta.faces.behavior.event    = keyup
jakarta.faces.source            = search:q
jakarta.faces.partial.execute   = search:q
jakarta.faces.partial.render    = search:results search:count

and the answer (Mojarra; the markup inside each CDATA section is the normal rendering of that component):

<?xml version='1.0' encoding='UTF-8'?>
<partial-response><changes>
  <update id="search:count"><![CDATA[<span id="search:count">1 searches</span>]]></update>
  <update id="search:results"><![CDATA[<div id="search:results">
        <div class="book">Dune by Frank Herbert</div>
        <div class="book">Dune Messiah by Frank Herbert</div></div>]]></update>
  <update id="j_id1:jakarta.faces.ViewState:0"><![CDATA[8277362929392712006:9149938755704541982]]></update>
</changes></partial-response>

Three details of the capture are worth knowing. The render list named results first, but the update for count comes first: components are rendered in the order of the component tree, not in the order of the list (on both implementations). The ViewState update is always last and keeps the form’s view state current, so the next request carries a valid one. And the browser sent only the field that was executed: Mojarra 4.1.17 posts just the controls named in execute (plus the form marker), while MyFaces 4.1.4 posts every field of the form and the hidden formId_SUBMIT marker. Both are valid, because only executed components are decoded.

The f:ajax Tag

f:ajax registers an AjaxBehavior on one component, or on every ClientBehaviorHolder component inside it (see Grouping Components). The tag library is jakarta.faces.core. The attributes, as the VDL documentation lists them:

Attribute Value Meaning

execute

space-separated search expressions, or an expression that evaluates to a Collection<String>

The components that take part in Apply Request Values, Process Validations and Update Model Values. Default @this.

render

same

The components rendered in Render Response. Default @none.

event

a string or an expression

The client event that sends the request. A DOM event name without the on prefix (click, keyup, blur), or action for command components and valueChange for input components. If omitted, the component’s default is used.

listener

a method expression

Called once with an AjaxBehaviorEvent when the behaviour event is broadcast. See The listener and the AjaxBehaviorEvent.

onevent

the name of a JavaScript function

Called at the begin, complete and success stages of the request. See Events on the Client: onevent and addOnEvent.

onerror

the name of a JavaScript function

Called when the request or the response fails. See Ajax Error Handling.

delay

milliseconds, or the literal none

If less than this much time passes between two calls, only the last is sent. See delay, Queueing and disabled.

resetValues

a boolean

Resets the inputs named in render before rendering. See resetValues.

disabled

a boolean

true keeps the behaviour from being rendered. Default false.

immediate

a boolean

true broadcasts the behaviour event in Apply Request Values instead of Invoke Application. See The listener and the AjaxBehaviorEvent.

The execute and render attributes accept search expressions, so the keywords and the relative and absolute client ids are described together in execute and render.

The event Attribute and the Default Events

f:ajax with no event uses the default of the component it is nested in: action for an ActionSource such as a button or a link, valueChange for an EditableValueHolder such as an input (the tutorial). The names are logical, and each component maps them to a DOM event. What the two implementations rendered for the standard components:

Component Mojarra 4.1.17 MyFaces 4.1.4

h:inputText, h:inputTextarea, h:inputSecret, h:selectOneMenu

onchange

onchange

h:selectBooleanCheckbox

onclick

onchange

h:selectOneRadio, h:selectManyCheckbox (each item)

onclick

onchange

h:commandButton, h:commandLink

onclick, with event.preventDefault()

onclick, through faces.util.chain, returning false

Only the browser’s own events are possible beyond the logical ones, and each component accepts a fixed list. getEventNames() of ClientBehaviorHolder returns it. An unsupported name fails when the view is built, not when the user acts: with event="onkeyup" on an h:inputText, Mojarra answered with a 500 whose title read 'onkeyup' is not a supported event for HtmlInputText and listed blur, change, click, dblclick, focus, keydown, keypress, keyup, mousedown, mousemove, mouseout, mouseover, mouseup, select, valueChange, and MyFaces answered 500 with it is not a valid eventName defined for this component. The on prefix is the usual mistake.

The standard components that implement ClientBehaviorHolder (the API documentation lists them) include the input, select and command components, h:form, h:body, h:outputLabel, h:outputLink, h:link, h:button, h:graphicImage, h:panelGrid, h:panelGroup, h:dataTable, h:inputFile and UIWebsocket (the component behind f:websocket, which accepts a nested f:ajax). h:outputText is not one; to make a piece of text clickable, wrap it in an h:panelGroup with event="click":

<h:panelGroup id="rating" layout="block">
  <f:ajax event="click" render="details"/>
  <h:outputText value="#{book.title}"/>
</h:panelGroup>

execute and render

execute answers "which components does the server process?", render answers "which components does the server send back?". They are independent: a request can execute one input and render a different panel, execute a whole form and render nothing, or execute nothing and render a clock.

Keywords

The specification defines four keywords for both attributes. They are resolved in the browser: the handler that the server renders still contains the keyword, and faces.ajax.request replaces it with client ids before it sends the request.

Keyword Meaning

@this

The component that triggered the request (the source). Default of execute.

@form

The form that encloses the source.

@all

The whole view. For render the response is one update of jakarta.faces.ViewRoot that replaces the document.

@none

Nothing. Default of render; the jakarta.faces.partial.render parameter is then not sent at all.

The search expression framework of Faces 2.3 adds more (@parent, @namingcontainer, @next, @previous, @child(n), @id(...), custom keywords), and the page on search expressions covers all of them and SearchExpressionHandler. The VDL documentation of f:ajax says the values are search expressions, so any of them is valid here.

Two things happen between the tag and the wire. At render time the tag handler resolves relative and absolute ids to client ids: in the review form, render="@form :messages" was rendered as mojarra.ab(this,event,'action','@form','@form messages') and myfaces.ab(this,event,'action','@form','@form messages',{}), so :messages (absolute, from the view root) became the client id messages of a component outside the form. And when the user clicks, faces.ajax.request resolves the keywords: a button with execute="@form" in a form with the id f sent jakarta.faces.partial.execute=f:boom f on Mojarra and f f:boom on MyFaces, the source’s id and the form’s id. So the source is part of the execute list even when the page names other components (Mojarra sent f:btn f:name for execute: 'f:name', MyFaces f:name f:btn).

That last fact matters for tests written by hand. A request posted with curl whose partial.execute omits the source button was processed by Mojarra without ever invoking the button’s f:ajax listener (the phase trace showed exec=[f:title] and no listener call), while MyFaces invoked it anyway. A raw request that sends the keyword itself (jakarta.faces.partial.execute=@form) fared no better: neither implementation expanded it on the server, so Mojarra processed nothing at all (no setter ran, no message, no error) and MyFaces ran the button’s action without the inputs (the action failed on the null rating). Copy what the browser sends.

Review Preview: Execute Several Inputs, Render the Preview

The review form of Bookshelf shows a preview as the user fills in the rating. The rating input executes both inputs (they belong together), runs the listener and re-renders the preview and the two field messages. A final button submits the whole form and renders the messages that live outside it.

src/main/webapp/review.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core">
<h:head>
  <title>Review a book</title>
</h:head>
<h:body>
  <h:messages id="messages" globalOnly="true"/>
  <h:form id="review">
    <h:inputText id="reviewer" value="#{reviewBean.reviewer}" required="true"/>
    <h:message id="reviewerMsg" for="reviewer"/>

    <h:inputText id="rating" value="#{reviewBean.rating}" required="true">
      <f:validateLongRange minimum="1" maximum="5"/>
      <f:ajax event="change" execute="reviewer rating" listener="#{reviewBean.preview}"
              render="summary reviewerMsg ratingMsg"/>
    </h:inputText>
    <h:message id="ratingMsg" for="rating"/>

    <h:outputText id="summary" value="#{reviewBean.summary}"/>

    <h:commandButton id="send" value="Send" action="#{reviewBean.submit}">
      <f:ajax execute="@form" render="@form :messages"/>
    </h:commandButton>
  </h:form>
</h:body>
</html>
src/main/java/org/example/bookshelf/web/ReviewBean.java
package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.event.AjaxBehaviorEvent;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.example.bookshelf.service.ReviewService;

@Named
@RequestScoped
public class ReviewBean {

    @Inject
    private FacesContext faces;
    @Inject
    private ReviewService reviews;

    private String reviewer;
    private Integer rating;
    private String summary = "";

    public void preview(AjaxBehaviorEvent event) {
        summary = reviewer + " gives " + rating + " stars";
    }

    public void submit() {
        int number = reviews.save(reviewer, rating);
        faces.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Review " + number + " saved", null));
        summary = "";
    }

    public void failing() {
        throw new IllegalStateException("The review service is down");
    }

    public String getReviewer() { return reviewer; }
    public void setReviewer(String reviewer) { this.reviewer = reviewer; }
    public Integer getRating() { return rating; }
    public void setRating(Integer rating) { this.rating = rating; }
    public String getSummary() { return summary; }
}

A change request with reviewer=Ann and rating=4 answered with an update for review:summary containing Ann gives 4 stars and empty updates for the two messages; the phase listener trace showed the request going through all six phases with ajax=true, exec=[review:reviewer, review:rating] and render=[review:summary, review:reviewerMsg, review:ratingMsg]. With rating=9 the response carried the range message in review:ratingMsg, an empty review:summary, and the listener was not called (see The listener and the AjaxBehaviorEvent). The Send button re-renders @form and the absolute :messages, which is why the saved-message appears even though the h:messages component is not inside the form. If :messages were left out of render, the message would be added to the FacesContext of that request, never sent to the browser and lost with the request, which is the most common "my Ajax action does nothing" report.

Ids from an Expression

Both attributes accept a value expression that evaluates to a Collection<String>, for ids that are decided at run time. With a bean method getIds() returning List.of("a", "b"), the markup

<h:inputText id="a" value="#{jsBean.name}">
  <f:ajax event="blur" execute="#{jsBean.ids}" render="#{jsBean.ids}"/>
</h:inputText>
<h:outputText id="b" value="#{jsBean.name}"/>

was rendered with the handler onblur="mojarra.ab(this,event,'blur','f:a f:b','f:a f:b')" (and the MyFaces equivalent): the collection was evaluated and resolved to client ids when the page was rendered. The expression is therefore evaluated during Render Response of the page that contains the tag, not when the user acts. A literal value is split on spaces.

What Can Be Rendered

A component can be updated only if its element exists in the page. The browser replaces the contents of the element whose id matches the update. A component with rendered="false" produces no element, and a later request cannot add it:

<h:form id="f">
  <h:commandButton id="show" value="Show" action="#{jsBean.toggle}">
    <f:ajax render="ghost wrapper"/>
  </h:commandButton>
  <h:outputText id="ghost" value="ghost-text" rendered="#{jsBean.visible}"/>
  <h:panelGroup id="wrapper" layout="block">
    <h:outputText id="inner" value="inner-text" rendered="#{jsBean.visible}"/>
  </h:panelGroup>
</h:form>

Rendering ghost, whose element was absent at the first render, failed on Mojarra with the client error malformedXML and the description During update: f:ghost not found, and the updates after it in the response were not applied (the wrapper stayed empty). MyFaces skipped the unknown element silently and rendered wrapper with its inner text. The portable form is the second one: always render an element that is always in the page (wrapper) and let the conditional content live inside it. The same holds for tables and repeated rows; render the table, not a row that may not exist.

A rendered condition is evaluated on every request, so a component can switch from hidden to visible within the wrapper. Do not render h:head, h:body and the form element casually either: @all is the supported way to replace the document.

The listener and the AjaxBehaviorEvent

The listener attribute is a method expression with the signature public void processAjaxBehavior(AjaxBehaviorEvent event), the method of jakarta.faces.event.AjaxBehaviorListener; it may throw AbortProcessingException to stop lifecycle processing. The tutorial says it is called once, in Invoke Application. The event is queued during Apply Request Values (the behaviour’s renderer decodes it) and broadcast later, so the listener follows the same rules as an action method: it runs only if the executed components converted and validated without error and the model has been updated.

The immediate attribute changes only the broadcast phase. A trace of the two variants, with a listener that logs the current phase and the bean property bound to the executed input:

<h:inputText id="name" value="#{jsBean.name}" required="true"/>
<h:message id="msg" for="name"/>
<h:commandButton id="normal" value="n">
  <f:ajax execute="name" render="msg" listener="#{jsBean.phase}"/>
</h:commandButton>
<h:commandButton id="imm" value="i">
  <f:ajax execute="name" render="msg" immediate="true" listener="#{jsBean.phase}"/>
</h:commandButton>
normal, name=Zed   LISTENER normal in INVOKE_APPLICATION 5 name=Zed
immediate, name=Zed   LISTENER imm in APPLY_REQUEST_VALUES 2 name=start
normal, name=""    (required fails: the listener is not called)
immediate, name="" LISTENER imm in APPLY_REQUEST_VALUES 2 name=start

(MyFaces printed the same with INVOKE_APPLICATION(5) and APPLY_REQUEST_VALUES(2).) The ordinary listener sees the new model value and is skipped by a validation failure; the immediate one runs first, before conversion, validation and the model update of the executed inputs, so it sees the old value and runs even when the input is invalid. That is exactly the immediate of the lifecycle page, with the same consequences, and the same advice: use it for a Cancel-style action, not as a way to skip validation. For "validate only these fields", execute is the clean tool.

The Signature Differs

The VDL documentation says the method takes an AjaxBehaviorEvent. A method without parameters also worked on Mojarra (listener="#{jsBean.noArg}" ran) and failed on MyFaces with MethodNotFoundException: Method not found: …​JsBean@74b45567.noArg(jakarta.faces.event.AjaxBehaviorEvent) in the partial response. Declare the parameter, even if the method does not use it.

The event gives the source component (event.getComponent()) and the behaviour (event.getBehavior()); the request parameters and the FacesContext are available in the usual way. The listener is the place to change model state and to queue messages, and, via PartialViewContext (Ajax from Java: PartialViewContext and AjaxBehavior), to add render targets that the page did not name.

The Partial Lifecycle

The lifecycle page describes the six phases and how the partial lifecycle visits fewer components. Here is the same request with a trace, using a phase listener that prints the PartialViewContext ids (the review preview, Mojarra 4.1.17; the MyFaces trace has the same phases):

PHASE before RESTORE_VIEW ajax=true
PHASE after  RESTORE_VIEW ajax=true exec=[review:reviewer, review:rating] render=[review:summary, review:reviewerMsg, review:ratingMsg]
PHASE before APPLY_REQUEST_VALUES ...
PHASE after  APPLY_REQUEST_VALUES ...
PHASE before PROCESS_VALIDATIONS ...
PHASE after  PROCESS_VALIDATIONS ...
PHASE before UPDATE_MODEL_VALUES ...
SET reviewer=Ann
SET rating=4
PHASE after  UPDATE_MODEL_VALUES ...
PHASE before INVOKE_APPLICATION ...
PHASE after  INVOKE_APPLICATION ...
PHASE before RENDER_RESPONSE ...
PHASE after  RENDER_RESPONSE ...
  • Restore View. The view state posted in jakarta.faces.ViewState is used to restore the component tree, and the PartialViewContext reads the execute and render lists from the request. FacesContext.isPostback() is true.

  • Apply Request Values. Only the execute ids and their descendants decode. The behaviour renderer checks jakarta.faces.behavior.event and jakarta.faces.source and queues the AjaxBehaviorEvent. (immediate="true" broadcasts it here.)

  • Process Validations. The same components convert and validate. A failure calls renderResponse() and skips phases 4 and 5.

  • Update Model Values. Only the executed components write their model; SET reviewer=Ann and SET rating=4 are the setters. The fields that were not executed were not touched, which is the point of narrowing execute.

  • Invoke Application. The listener runs here, and the action of an executed command component, and so does navigation.

  • Render Response. The render ids are encoded into update elements. If the lifecycle calls responseComplete() earlier (a redirect, a file download), there is no render phase.

UIViewRoot takes the decision between full and partial processing in its processDecodes, processValidators and processUpdates and encodeChildren: they call PartialViewContext.processPartial when isPartialRequest() is true, for rendering when isAjaxRequest() is true. A custom phase listener or system event listener sees the same phases and the same ids; one trap is that getExecuteIds() and getRenderIds() cannot be read before the view has been restored: calling them in beforePhase of Restore View threw a NullPointerException (root is null) in Mojarra 4.1.17.

Dependent Menus: Execute the Source, Render the Dependent

The classic Ajax recipe is a pair of menus, where the second one depends on the first. The author menu executes only itself (default execute="@this"), so the dependent required menu is not validated while the user is still choosing, and renders the book menu:

src/main/webapp/cascade.xhtml
<h:form id="pick">
  <h:selectOneMenu id="author" value="#{cascadeBean.author}">
    <f:selectItem itemLabel="-- author --" itemValue="" noSelectionOption="true"/>
    <f:selectItems value="#{cascadeBean.authors}"/>
    <f:ajax render="book"/>
  </h:selectOneMenu>
  <h:selectOneMenu id="book" value="#{cascadeBean.bookId}" required="true">
    <f:selectItem itemLabel="-- book --" itemValue="" noSelectionOption="true"/>
    <f:selectItems value="#{cascadeBean.books}" var="b" itemValue="#{b.id}" itemLabel="#{b.title}"/>
  </h:selectOneMenu>
</h:form>
src/main/java/org/example/bookshelf/web/CascadeBean.java
package org.example.bookshelf.web;

import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.List;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;

@Named
@ViewScoped
public class CascadeBean implements Serializable {

    @Inject
    private BookService books;

    private String author;
    private Long bookId;
    private Integer year = 1965;
    private Integer copies = 1;

    public List<String> getAuthors() { return books.authors(); }

    public List<Book> getBooks() {
        return author == null || author.isEmpty() ? List.of() : books.byAuthor(author);
    }

    public String getAuthor() { return author; }
    public void setAuthor(String author) { this.author = author; }
    public Long getBookId() { return bookId; }
    public void setBookId(Long bookId) { this.bookId = bookId; }
    public Integer getYear() { return year; }
    public void setYear(Integer year) { this.year = year; }
    public Integer getCopies() { return copies; }
    public void setCopies(Integer copies) { this.copies = copies; }
}

The year and copies properties are used by the resetValues page below. Choosing William Gibson in a browser changed the book menu to -- book --, Neuromancer, Count Zero. The getBooks() getter is called while the menu is rendered, after the model update of the author, so it sees the new author. That works because the getter is a plain, cheap derivation; a getter that queries a database must be backed by a cached field.

resetValues

A component that took part in a request keeps state in the component tree: the local value of an input that converted and validated successfully stays set until the model is updated, and the model is updated only if the whole request is valid. So a second request that re-renders such an input shows the typed value, not the model’s. The resetValues attribute exists for this case. It makes the server call UIViewRoot.resetValues(context, clientIds) on the render ids, which calls EditableValueHolder.resetValue() on each input: the submitted value, the local value and the valid flag are cleared and the model value is shown again.

The test page has a valid year and an invalid copies (range 1 to 10), a Check button that executes both and re-renders them, and two Cancel buttons that execute only themselves and re-render the inputs, one with resetValues="true":

src/main/webapp/reset.xhtml
<h:form id="f">
  <h:inputText id="year" value="#{cascadeBean.year}"/>
  <h:inputText id="copies" value="#{cascadeBean.copies}">
    <f:validateLongRange minimum="1" maximum="10"/>
  </h:inputText>
  <h:message id="copiesMsg" for="copies"/>
  <h:commandButton id="check" value="Check">
    <f:ajax execute="year copies" render="year copies copiesMsg"/>
  </h:commandButton>
  <h:commandButton id="plain" value="Cancel">
    <f:ajax execute="@this" render="year copies copiesMsg"/>
  </h:commandButton>
  <h:commandButton id="clean" value="Cancel clean">
    <f:ajax execute="@this" render="year copies copiesMsg" resetValues="true"/>
  </h:commandButton>
</h:form>

With the model year=1965 and the browser typing 1970 and 99 and pressing Check, then each Cancel (a fresh page for the second):

after Check:         year=1970 copies=99 msg=f:copies: Validation Error: Specified attribute is not between 1 and 10.
after Cancel:        year=1970 copies=1  msg=
after Cancel clean:  year=1965 copies=1  msg=

The identical result came from both implementations. year was valid, so its local value 1970 stayed in the saved view state while the model was never updated (the other input failed); the plain Cancel therefore re-rendered 1970, which looks like a value the application accepted, and resetValues brought back the model’s 1965. The invalid copies shows 1 in both Cancel variants because the invalid submitted value is a transient property of the component and is not kept between requests. resetValues clears the valid flag too, so a component that failed validation no longer counts as invalid. The same switch exists as the resetValues request option of faces.ajax.request (the request parameter jakarta.faces.partial.resetValues=true) and as an attribute of h:commandScript. The JavaScript documentation warns not to use keywords such as @form or @this in the render list with resetValues, because the method does not descend into the children of the listed components. In the test, render="@form" with resetValues="true" did reset both inputs on Mojarra 4.1.17 and MyFaces 4.1.4; list the input ids and do not rely on it. The core tags page covers f:resetValues as well, which does the same for non-Ajax submits.

delay, Queueing and disabled

The Request Queue

The specification requires every Ajax request to go through a client-side queue so that requests are sent in the order they were initiated: the oldest waiting request is sent next; when it completes (success or error) it leaves the queue, and the next one is sent. The page below has three buttons whose listener sleeps 400 milliseconds on the server and records which button called it; onevent records when the client saw each stage (times are milliseconds since the first click):

src/main/webapp/queue.xhtml
<h:form id="f">
  <h:commandButton id="a" value="A">
    <f:attribute name="label" value="A"/>
    <f:ajax listener="#{slowBean.work}" render="log" onevent="ev"/>
  </h:commandButton>
  <h:commandButton id="b" value="B">
    <f:attribute name="label" value="B"/>
    <f:ajax listener="#{slowBean.work}" render="log" onevent="ev"/>
  </h:commandButton>
  <h:outputText id="log" value="#{slowBean.log}"/>
</h:form>
<pre id="out"></pre>
<script>
  const t0 = Date.now();
  function ev(d) {
    document.getElementById('out').textContent += (Date.now() - t0) + 'ms ' + d.source.id + ' ' + d.status + '\n';
  }
</script>
src/main/java/org/example/bookshelf/web/SlowBean.java
package org.example.bookshelf.web;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.faces.event.AjaxBehaviorEvent;
import jakarta.inject.Named;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

@Named
@ApplicationScoped
public class SlowBean {

    private final List<String> log = new CopyOnWriteArrayList<>();

    public void work(AjaxBehaviorEvent event) throws InterruptedException {
        String who = String.valueOf(event.getComponent().getAttributes().get("label"));
        Thread.sleep(400);
        log.add(who);
    }

    public String getLog() { return String.join(",", log); }
}

Clicking A, B and A immediately one after the other produced, on both implementations (Mojarra shown):

   1ms f:a begin
 422ms f:a complete
 423ms f:a success
 423ms f:b begin
 837ms f:b complete
 837ms f:b success
 837ms f:a begin
1257ms f:a complete
1257ms f:a success       server log: A,B,A

The begin event of a queued request fires when it leaves the queue and is sent, not when the user clicked, and the three requests ran strictly one after the other, in order, on the server as well. The consequence is useful and dangerous: a double click on a submit button sends two requests (the second after the first finished), so the action runs twice. The queue guarantees order, not idempotence: make the action idempotent, hide or disable the trigger in the begin handler (Events on the Client: onevent and addOnEvent), or use delay.

delay

delay="500" (or {delay: 500} in the JavaScript API) discards a request if another call to faces.ajax.request arrives within that many milliseconds; only the last call is sent. This debounces keystrokes. Three clicks on a button with delay="500" produced one request, whose begin came about 500 ms after the first click:

503ms f:c begin
916ms f:c complete
917ms f:c success       (Mojarra; MyFaces: 507 / 928 / 932)

The literal none, or no delay, turns it off. The live search of the first example uses it so that fast typing sends one request per pause rather than one per key.

disabled

disabled="true" (or an expression) keeps the behaviour from being rendered. It is the tag-level switch for "Ajax only in some cases": a component whose f:ajax is disabled is rendered without the handler. A disabled f:ajax inside a group had different effects on the two implementations; see Grouping Components.

Grouping Components

An f:ajax that is not inside a component but around others applies to every ClientBehaviorHolder inside it, each with its own default event. The tutorial shows a group whose event and render apply to each member, and a nested f:ajax that makes the functionality cumulative. The group below wraps four fields and a command:

src/main/webapp/group.xhtml
<h:form id="f">
  <f:ajax render="out">
    <h:inputText id="a" value="#{jsBean.name}"/>
    <h:inputText id="b" value="#{jsBean.name}">
      <f:ajax event="keyup" render="out2"/>
    </h:inputText>
    <h:commandButton id="c" value="c" action="#{jsBean.go}"/>
    <h:inputText id="d" value="#{jsBean.name}">
      <f:ajax disabled="true"/>
    </h:inputText>
    <h:inputText id="e" value="#{jsBean.name}">
      <f:ajax disabled="#{jsBean.flag}" render="out"/>
    </h:inputText>
    <h:outputText id="plain" value="no handlers"/>
  </f:ajax>
  <h:outputText id="out"/><h:outputText id="out2"/>
</h:form>

What each implementation rendered (event attributes only):

Field Mojarra 4.1.17 MyFaces 4.1.4

a (group only)

onchange for valueChange, render out

the same

b (group plus nested keyup)

onkeyup render out2 and onchange render out (and an extra onvalueChange attribute that is not a DOM event)

onchange render out and onkeyup render out2

c

onclick for action, render out

the same

d (nested disabled="true")

no handler at all

the group’s handler

e (nested enabled, same event)

one onchange (both tags have the same render, so this cannot tell replacing from merging); one request per change

two handlers chained: two identical requests per change (counted in the server log)

plain (h:outputText)

no handler

no handler

So the group is cumulative on both for different events. A nested f:ajax for the same event was rendered once on Mojarra and twice on MyFaces (a double request per change), and a nested disabled="true" left the field without any handler on Mojarra and with the group’s handler on MyFaces. Use a group for the plain case (several components, one setting), and do not nest a second f:ajax for the same event inside it.

The JavaScript API

All of the API is in faces.js, in the namespace faces. The specification (chapter 14) and the JavaScript documentation list faces.ajax.request, faces.ajax.response, faces.ajax.addOnEvent, faces.ajax.addOnError, faces.getViewState, faces.getClientWindow, faces.getProjectStage and faces.util.chain; the JavaScript documentation also lists the properties faces.contextpath, faces.separatorchar, faces.specversion and faces.implversion, and the faces.push namespace for WebSocket (page on WebSocket push).

Getting faces.js onto the Page

The script is the resource faces.js in the library jakarta.faces (ResourceHandler.FACES_SCRIPT_RESOURCE_NAME and FACES_SCRIPT_LIBRARY_NAME). It is added automatically when the page contains an f:ajax or an h:commandScript, so the usual pages do nothing. You must add it yourself in three situations, with h:outputScript:

<h:head>
  <title>Status</title>
  <h:outputScript library="jakarta.faces" name="faces.js" target="head"/>
  <h:outputScript library="js" name="bookshelf-ajax.js" target="head"/>
</h:head>
  • Your own script calls faces while it loads (the example below). Without the first line, a script loaded as a resource ran before faces.js on Mojarra 4.1.17 and on MyFaces 4.1.4: the head contained bookshelf-ajax.js first and faces.js second, so the script could not register its callbacks (in the test the failure message simply never appeared). Resources in the head keep the order of the tags, so list faces.js first. For an inline <script> the order is worse: Mojarra moved the automatically added faces.js to the end of the head, after the inline script, so an inline script in h:head that uses faces at load time fails. Put such a script in a resource, or in the body.

  • The page uses the API from onclick attributes and no f:ajax, because nothing then adds it.

  • A behaviour is attached from Java (Ajax from Java: PartialViewContext and AjaxBehavior). Mojarra 4.1.17 did not add faces.js for an AjaxBehavior added programmatically, so the generated onkeyup handler failed with mojarra is not defined; MyFaces added it. A component class can ask for it with @ResourceDependency(library = "jakarta.faces", name = "faces.js", target = "head") (the specification’s annotation approach).

The old names do not exist any more. Requesting jsf.js from jakarta.faces.resource/ and javax.faces.resource/jsf.js returned 404 on both implementations, and window.jsf is undefined; ResourceHandler.JSF_SCRIPT_LIBRARY_NAME and JSF_SCRIPT_RESOURCE_NAME are deprecated for removal in the API documentation.

faces.ajax.request

faces.ajax.request(source, event, options) sends the request:

  • source is the DOM element that triggers it, or its id. The request is sent from that element’s form (and fails if there is none or if the jakarta.faces.ViewState field is missing).

  • event is the DOM event (optional; its type is sent as jakarta.faces.partial.event).

  • options is an object with execute, render (space-delimited client ids, or the keywords), onevent, onerror (functions), params (an object of extra request parameters), delay and resetValues.

The default execute is @this, the default render is @none. The request is always a POST to the form’s action URL (or to the hidden jakarta.faces.encodedURL field’s value, if present); the header is Faces-Request: partial/ajax; the parameters sent are jakarta.faces.ViewState, jakarta.faces.partial.ajax=true, jakarta.faces.source and, when the view uses the client window, jakarta.faces.ClientWindow; plus the execute and render lists, resetValues, and the extra parameters. The JavaScript documentation adds that a form with enctype="multipart/form-data" is sent through a hidden inline frame instead of XMLHttpRequest with the same parameters, and that the begin, complete and success events are emulated for it (this was not run on this scratch application).

Calling the API directly and passing extra parameters
<h:form id="f">
  <h:inputText id="name" value="#{jsBean.name}"/>
  <h:outputText id="out" value="#{jsBean.last}"/>
  <h:commandButton id="btn" value="Go"
                   onclick="faces.ajax.request(this, event, {execute: 'f:name', render: 'f:out', params: {tag: 'T1', other: 'two'}, resetValues: true}); return false;"/>
</h:form>

From a browser, that request carried (Mojarra):

f=f   f:name=start   jakarta.faces.ViewState=...   jakarta.faces.partial.ajax=true
jakarta.faces.partial.execute=f:btn f:name   jakarta.faces.partial.render=f:out
jakarta.faces.partial.resetValues=true   jakarta.faces.source=f:btn   other=two   tag=T1

The return false keeps the button from also submitting the form normally. Two calls within delay milliseconds sent only the second, with its own options, on both implementations. Ids in the options are client ids of the rendered page (f:name, because the form is a naming container), written out in full; the keywords work too. The f:ajax tag generates calls like this one: Mojarra writes mojarra.ab(this,event,'action','@form','f:out',{...}) and MyFaces myfaces.ab(this,event,'action','@form','f:out',{}), thin wrappers of faces.ajax.request that are implementation internals and must not be called by application code.

The Other Functions

Function What it does

faces.ajax.addOnEvent(fn), faces.ajax.addOnError(fn)

Register a callback for every Ajax request of the page (Events on the Client: onevent and addOnEvent, Ajax Error Handling). They throw if the argument is not a function (Added a callback that was not a function on Mojarra).

faces.getViewState(form)

The encoded form data, including the hidden view state, as the string that faces.ajax.request sends (the string for a form with one text field was f=f&f%3Aname=start&jakarta.faces.ViewState=... on Mojarra, with f_SUBMIT=1 added by MyFaces).

faces.util.chain(source, event, fn1, fn2, ...)

Calls the functions in order and stops when one returns false; returns false in that case. MyFaces uses it in its generated handlers.

faces.getProjectStage()

The ProjectStage of the application as a string, without a network call (Development in the test).

faces.getClientWindow(node)

The client window id of the form or null (null without client window mode).

faces.contextpath, faces.separatorchar

The context path (/bookshelf) and the naming container separator (:).

h:commandScript

h:commandScript is the component for "call the server from a script". It renders a JavaScript function with the given name; calling the function (with an optional object of extra parameters) sends an Ajax request that behaves like a command button’s: the action and actionListener of the component run, and execute and render apply. The attributes (VDL documentation): name (required), autorun, action, actionListener, execute, render, resetValues, onevent, onerror, immediate, rendered, binding and id. It is a command component; it is not among the ClientBehaviorHolder implementations that the API documentation lists, so there is no f:ajax to nest in it, and its own execute, render and onevent attributes do that job.

src/main/webapp/cs.xhtml
<h:head><title>Command script</title></h:head>
<h:body>
  <script>var bookshelf = {};</script>
  <h:form id="f">
    <h:commandScript name="refreshReviews" action="#{jsBean.refresh}" render="out"/>
    <h:commandScript name="bookshelf.refreshStock" action="#{jsBean.refresh}" render="out"/>
    <h:outputText id="out" value="#{jsBean.last}"/>
  </h:form>
</h:body>
The action reads the extra parameter the script passed
public void refresh() {
    last = "refreshed:" + name + ":"
            + faces.getExternalContext().getRequestParameterMap().get("tag");
}

Calling refreshReviews({tag: 'A'}) and bookshelf.refreshStock({tag: 'B'}) from the console updated out to refreshed:start:A and refreshed:start:B on both implementations. A name without a period is declared with var; a namespaced name is assigned (bookshelf.refreshStock = function...), so on Mojarra the namespace object must already exist (MyFaces rendered a myfaces.reserveNamespace('bookshelf.refreshStock') call before the assignment). The function takes extra parameters from an object argument.

Two quirks were observed. MyFaces escaped an underscore in the name (name="bookshelf_refresh" became the function bookshelf_5Frefresh), so calls to the name you wrote failed; use names without underscores. With autorun="true", the function is called when the page has loaded; on Mojarra the load event object is passed as the parameter object, so the request also carried isTrusted=true, a harmless extra parameter. autorun is a one-shot at load. Polling is better done with setInterval calling the function, or with the WebSocket push page for server-initiated updates.

Events on the Client: onevent and addOnEvent

Two ways to observe a request: the onevent attribute of f:ajax (or the onevent option of faces.ajax.request) for one behaviour, and faces.ajax.addOnEvent for every Ajax request of the page. Both functions receive a data object. A request goes through three events:

Status When

begin

Immediately before the request is sent (for a queued request, when it leaves the queue).

complete

Immediately after the response arrived. For a successful response, immediately before faces.ajax.response is applied; for an unsuccessful one, immediately before the error callbacks.

success

Immediately after faces.ajax.response applied the response.

Property Value

type

event

status

begin, complete or success

source

The DOM element that triggered the request.

responseCode, responseXML, responseText

The HTTP status and the response (not present for begin).

The property sets observed in the browser agree: begin had the keys source, status, type; complete and success added responseCode, responseText and responseXML.

src/main/webapp/resources/js/bookshelf-ajax.js
(function () {
    const busy = () => document.getElementById('busy');
    const failure = () => document.getElementById('ajax-failure');

    faces.ajax.addOnEvent(function (data) {
        if (data.status === 'begin') {
            busy().hidden = false;
            failure().textContent = '';
        } else if (data.status === 'complete') {
            busy().hidden = true;
        }
    });

    faces.ajax.addOnError(function (data) {
        busy().hidden = true;
        if ((data.errorName || '').includes('ViewExpiredException')) {
            window.location.reload();
        } else {
            failure().textContent = 'The request failed. Please try again.';
        }
    });
})();
src/main/webapp/status.xhtml
<h:head>
  <title>Status</title>
  <h:outputScript library="jakarta.faces" name="faces.js" target="head"/>
  <h:outputScript library="js" name="bookshelf-ajax.js" target="head"/>
</h:head>
<h:body>
  <h:form id="f">
    <h:commandButton id="ok" value="OK" action="#{reviewBean.submit}">
      <f:ajax execute="@this" render="out"/>
    </h:commandButton>
    <h:commandButton id="fail" value="Fail" action="#{reviewBean.failing}">
      <f:ajax execute="@this" render="out"/>
    </h:commandButton>
    <h:outputText id="out" value="#{reviewBean.summary}"/>
  </h:form>
  <p id="busy" hidden="hidden">Working...</p>
  <p id="ajax-failure"></p>
</h:body>

In the test the OK button left ajax-failure empty and hid the indicator at complete, and the Fail button, whose action throws, showed The request failed. Please try again. on both implementations. The text is the application’s own; it does not echo the server’s exception, which is what Ajax Error Handling recommends. The handlers touch the DOM with textContent and hidden, never with innerHTML built from responseText or an error message.

The Partial Response

The response of an Ajax request is text/xml, with Cache-Control: no-cache (Mojarra) or Cache-control: no-cache plus Pragma: no-cache and Expires: -1 (MyFaces). The root is partial-response; the specification’s schema describes it, and faces.ajax.response documents how each element is applied. Elements are processed in the order they appear.

Element Effect on the client

<update id="x"><![CDATA[...]]></update>

Replaces the contents of the element x with the markup. The special ids: jakarta.faces.ViewRoot (replace the head and body), jakarta.faces.ViewHead, jakarta.faces.ViewBody, jakarta.faces.Resource (append resources missing from the head), and the ids ending in jakarta.faces.ViewState:n and jakarta.faces.ClientWindow:n, which update the hidden fields of the forms that were rendered.

<insert><before id="x"> and <insert><after id="x">

Insert markup before or after an element.

<delete id="x"/>

Removes the element.

<attributes id="x"><attribute name="n" value="v"/></attributes>

Sets attributes of an element.

<eval><![CDATA[...]]></eval>

Executes JavaScript.

<redirect url="..."/>

Navigates the browser to the URL.

<error><error-name>...</error-name><error-message>...</error-message></error>

Signals a serverError to the client error callbacks.

<extensions>

Implementation-specific.

Three of these appeared in the runs. A redirect from an action (return "js?faces-redirect=true") was answered by both implementations with <redirect url="/bookshelf/js.xhtml"></redirect>, no changes, and the browser’s location changed to that URL (on Mojarra and MyFaces). render="@all" produced a single update id="jakarta.faces.ViewRoot" holding the whole document. And PartialViewContext.getEvalScripts() produced an eval (Ajax from Java: PartialViewContext and AjaxBehavior).

Two implementation details are visible in the answers. MyFaces adds an id to the root (partial-response id="j_id__v_0") and writes the XML declaration with double quotes, and the view state element id differs (j_id1:jakarta.faces.ViewState:0 on Mojarra, j_id__v_0:jakarta.faces.ViewState:1 on MyFaces); the client matches by the jakarta.faces.ViewState part. When the render list is empty (@none), the response is a lone view state update:

<?xml version='1.0' encoding='UTF-8'?>
<partial-response><changes><update id="j_id1:jakarta.faces.ViewState:0"><![CDATA[8899450177001203622:2418558211659014539]]></update></changes></partial-response>

Ajax from Java: PartialViewContext and AjaxBehavior

PartialViewContext

FacesContext.getPartialViewContext() gives the object. Its methods (API documentation): isAjaxRequest(), isPartialRequest(), isExecuteAll(), isRenderAll(), getExecuteIds(), getRenderIds() (a mutable Collection), getEvalScripts() (a mutable List), getPartialResponseWriter(), processPartial(PhaseId) and isResetValues(). The two mutable collections let a listener change what the response contains. The listener below saved a title, added a panel outside the form to the render list and asked the browser to run a script:

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

import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.context.FacesContext;
import jakarta.faces.context.PartialViewContext;
import jakarta.faces.event.AjaxBehaviorEvent;
import jakarta.inject.Inject;
import jakarta.inject.Named;

@Named
@RequestScoped
public class PartialBean {

    @Inject
    private FacesContext faces;

    private String title = "Dune";
    private int counter;

    public void save(AjaxBehaviorEvent event) {
        counter++;
        PartialViewContext pvc = faces.getPartialViewContext();
        pvc.getRenderIds().add("stats");
        pvc.getEvalScripts().add("document.getElementById('stats').dataset.saved = 'yes';");
    }

    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public int getCounter() { return counter; }
}
src/main/webapp/pvc.xhtml
<h:form id="f">
  <h:inputText id="title" value="#{partialBean.title}"/>
  <h:commandButton id="save" value="Save">
    <f:ajax execute="title" render="out" listener="#{partialBean.save}"/>
  </h:commandButton>
  <h:outputText id="out" value="#{partialBean.title}"/>
</h:form>
<h:outputText id="stats" value="saved=#{partialBean.counter}"/>

The page asked only for out. The response contained updates for f:out and stats and an <eval>document.getElementById('stats').dataset.saved = 'yes';</eval>; in the browser, stats read saved=1 and had data-saved="yes" on both implementations. (The order of eval and the view state element differs: Mojarra writes eval after the view state, MyFaces before.) The id added to the render list is a client id. Never build a script for getEvalScripts() from user input: it is executed in the page, so a title concatenated into it is cross-site scripting.

Partial View Traversal

How does the server process only the components in the execute list and write only those in the render list? The specification calls it partial view traversal (section 13.4) and deliberately does not prescribe a strategy: it requires only that UIViewRoot.processDecodes, processValidators and processUpdates call PartialViewContext.processPartial(PhaseId) when the request is a partial one, and that the encoding of an Ajax request goes through the same method. processPartial works on the client ids in getExecuteIds() (for the three execute phases) and getRenderIds() (for Render Response); both collections are filled from the request parameters jakarta.faces.partial.execute and jakarta.faces.partial.render, which faces.js fills with the client ids that the search expressions resolved to (see execute and render). The API for such a walk is UIComponent.visitTree: it takes a VisitContext that carries the ids and a set of VisitHint values, and a callback that is called for every component the visit reaches, which is how a partial phase can run on selected components only. The hints are: VisitHint.EXECUTE_LIFECYCLE says that the visit is part of the execution of a lifecycle phase, SKIP_UNRENDERED visits only the rendered subtrees, SKIP_ITERATION asks components that normally visit their children several times (the iterating ones, such as UIData) to visit them once, and SKIP_TRANSIENT visits only non-transient subtrees. The VisitContext used for a partial request is an implementation class (PartialVisitContext in Mojarra’s com.sun.faces.component.visit and in MyFaces' org.apache.myfaces.component.visit, not part of the API); application code obtains one from VisitContext.createVisitContext(FacesContext, Collection<String> ids, Set<VisitHint> hints) and never names it.

The two shortcuts skip the id-based visit. @all in execute makes isExecuteAll() true and in render makes isRenderAll() true: the first runs the normal full-tree phases and the second renders the whole view, as the update of a single element jakarta.faces.ViewRoot in the response. @none contributes no id, so the visit has nothing to reach; with execute="@none" not even the f:ajax listener ran on Mojarra.

The probe below replays a request’s render ids through the same API an implementation uses. The listener builds a VisitContext from getRenderIds() and visits the view, collecting what a render of those ids would reach:

src/main/java/org/example/bookshelf/web/TraversalBean.java
@Named
@RequestScoped
public class TraversalBean {

    private String found = "";

    public void inspect(AjaxBehaviorEvent event) {
        FacesContext context = FacesContext.getCurrentInstance();
        PartialViewContext pvc = context.getPartialViewContext();
        List<String> visited = new ArrayList<>();
        if (!pvc.getRenderIds().isEmpty()) {
            VisitContext visit = VisitContext.createVisitContext(
                    context, pvc.getRenderIds(), EnumSet.of(VisitHint.SKIP_UNRENDERED));
            context.getViewRoot().visitTree(visit, (ctx, component) -> {
                visited.add(component.getClientId(ctx.getFacesContext()) + ":" + component.getClass().getSimpleName());
                return VisitResult.ACCEPT;
            });
        }
        found = "executeAll=" + pvc.isExecuteAll() + " renderAll=" + pvc.isRenderAll()
                + " execute=" + pvc.getExecuteIds() + " render=" + pvc.getRenderIds() + " visited=" + visited;
    }

    public String getFound() {
        return found;
    }
}
trav.xhtml — three buttons with different execute and render lists
<h:form id="f">
  <h:inputText id="title" value="#{partialBean.title}"/>
  <h:commandButton id="save" value="Save">
    <f:ajax execute="@form" render="out panel" listener="#{traversalBean.inspect}"/>
  </h:commandButton>
  <h:commandButton id="all" value="All">
    <f:ajax execute="@all" render="@all" listener="#{traversalBean.inspect}"/>
  </h:commandButton>
  <h:commandButton id="none" value="None">
    <f:ajax execute="@none" render="@none" listener="#{traversalBean.inspect}"/>
  </h:commandButton>
  <h:outputText id="out" value="#{partialBean.title}"/>
  <h:panelGroup id="panel" layout="block"><h:outputText id="found" value="#{traversalBean.found}"/></h:panelGroup>
</h:form>

Hand-made Ajax POSTs (with curl, sending the ids that faces.js would send, such as execute=f for @form) produced the following in found:

Table 1. Observed on Mojarra 4.1.17 and MyFaces 4.1.4
Button Mojarra 4.1.17 MyFaces 4.1.4

Save (@form, out panel)

execute=[f] render=[f:out, f:panel] visited=[f:out:HtmlOutputText, f:panel:HtmlPanelGroup]

execute=[f, f:save] (the source component is added), same render and visited

All (@all)

executeAll=true renderAll=true execute=[@all] render=[@all] visited=[]

executeAll=true renderAll=true execute=[] render=[jakarta.faces.ViewRoot] visited=[]

None (@none)

the listener did not run; the response contained only the view state

the listener ran with execute=[@none, f:none] render=[@none]

So code that reads the id collections must not assume what is in them: after @all the collections hold the keyword on Mojarra and nothing or the view root on MyFaces, the source component may or may not be in execute, and the visit reaches only components that exist and are rendered (a component inside a rendered="false" parent is skipped by SKIP_UNRENDERED). Use isExecuteAll() and isRenderAll() for the whole-view cases. Visiting the tree yourself is the right tool for tooling and diagnostics; to change what an Ajax request renders, add ids to getRenderIds() as the previous section did.

A PartialViewContext for Every Request

To do the same for every Ajax request, decorate the factory: PartialViewContextFactory and PartialViewContextWrapper are the extension points (registered in faces-config.xml). The wrapper below always adds the messages component to the render list, so a page cannot forget it:

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

import jakarta.faces.context.FacesContext;
import jakarta.faces.context.PartialViewContext;
import jakarta.faces.context.PartialViewContextFactory;
import jakarta.faces.context.PartialViewContextWrapper;
import jakarta.faces.event.PhaseId;

public class MessagesPartialViewContextFactory extends PartialViewContextFactory {

    public MessagesPartialViewContextFactory(PartialViewContextFactory wrapped) {
        super(wrapped);
    }

    @Override
    public PartialViewContext getPartialViewContext(FacesContext context) {
        return new MessagesPartialViewContext(getWrapped().getPartialViewContext(context));
    }

    static class MessagesPartialViewContext extends PartialViewContextWrapper {

        MessagesPartialViewContext(PartialViewContext wrapped) {
            super(wrapped);
        }

        @Override
        public void processPartial(PhaseId phaseId) {
            if (phaseId == PhaseId.RENDER_RESPONSE && isAjaxRequest() && !isRenderAll()) {
                getRenderIds().add("messages");
            }
            super.processPartial(phaseId);
        }
    }
}
WEB-INF/faces-config.xml
<faces-config xmlns="https://jakarta.ee/xml/ns/jakartaee"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-facesconfig_4_1.xsd"
              version="4.1">
  <factory>
    <partial-view-context-factory>org.example.bookshelf.web.MessagesPartialViewContextFactory</partial-view-context-factory>
  </factory>
</faces-config>

A page whose button used render="reviewer" only still received <update id="messages"> with Review 1 saved, on both implementations. The component with the id messages must exist in every page that uses the factory (here the id is a client id of a component outside any form); if one lacks it, Mojarra reports a malformedXML error to the client (What Can Be Rendered).

A Behaviour Added in Java

f:ajax is a tag handler around AjaxBehavior. Code can attach the same behaviour, using the id AjaxBehavior.BEHAVIOR_ID and ClientBehaviorHolder.addClientBehavior. A system event listener does it when a component enters the view, and the guard keeps a postback (when the event is published again) from adding it twice:

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

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.faces.component.UIComponent;
import jakarta.faces.component.behavior.AjaxBehavior;
import jakarta.faces.component.behavior.ClientBehaviorHolder;
import jakarta.faces.context.FacesContext;
import jakarta.faces.event.ComponentSystemEvent;
import jakarta.inject.Named;
import java.util.List;

@Named
@ApplicationScoped
public class AjaxAttacher {

    public void attach(ComponentSystemEvent event) {
        UIComponent component = event.getComponent();
        if (component instanceof ClientBehaviorHolder holder && !holder.getClientBehaviors().containsKey("keyup")) {
            FacesContext context = FacesContext.getCurrentInstance();
            AjaxBehavior ajax = (AjaxBehavior) context.getApplication().createBehavior(AjaxBehavior.BEHAVIOR_ID);
            ajax.setExecute(List.of("@this"));
            ajax.setRender(List.of("echo"));
            ajax.setDelay("200");
            holder.addClientBehavior("keyup", ajax);
        }
    }
}
src/main/webapp/prog.xhtml
<h:head>
  <title>Programmatic</title>
  <h:outputScript library="jakarta.faces" name="faces.js" target="head"/>
</h:head>
<h:body>
  <h:form id="f">
    <h:inputText id="q" value="#{jsBean.name}">
      <f:event type="postAddToView" listener="#{ajaxAttacher.attach}"/>
    </h:inputText>
    <h:outputText id="echo" value="#{jsBean.name}"/>
  </h:form>
</h:body>

The input rendered onkeyup="mojarra.ab(this,event,'keyup','@this','f:echo',{'delay':'200'})" on Mojarra and onkeyup="myfaces.ab(this,event,'keyup','f:q','f:echo',{'delay':'200'})" on MyFaces, and typing in a browser updated echo (abc, then abcd) on both. Without the h:outputScript, Mojarra did not load faces.js and the handler failed (Getting faces.js onto the Page). AjaxBehavior has setExecute, setRender, setOnevent, setOnerror, setDelay, setResetValues, setDisabled, setImmediate and addAjaxBehaviorListener, one for each tag attribute. PartialResponseWriter is for the rarer case of a component or a handler that writes its own partial response (startDocument, startUpdate(id), endUpdate(), delete(id), redirect(url), startEval(), startError(name) and so on); the specification asks implementations to use it so that nested CDATA sections are escaped correctly. A custom renderer needs none of this: it is rendered by the normal update machinery.

Ajax Error Handling

Errors reach the page through the error callbacks, and they come in four kinds. The tutorial and the specification list the values of status in the data object:

Status Meaning

httpError

The HTTP status is missing or outside 200 to 299.

serverError

The response contains an error element (an exception on the server).

malformedXML

The response does not follow the format, or an update cannot be applied (the missing element of What Can Be Rendered).

emptyResponse

The server sent nothing.

The data object has everything of the event payload and, in addition, description, errorName and errorMessage. The onerror attribute of f:ajax and the onerror option name a function for one request; faces.ajax.addOnError registers one for all. On the server side, an exception thrown in an action or listener is turned into the error element, by an ExceptionHandler installed for Ajax requests: its error-name is the class of the throwable and error-message the cause or message. Compare what the two implementations produced for reviewBean.failing(), which throws IllegalStateException("The review service is down") (the HTTP status is 200 in every case):

Case Mojarra 4.1.17 MyFaces 4.1.4

Exception in an action, Development

error-name class java.lang.IllegalStateException, message The review service is down

error-name org.apache.myfaces.view.facelets.el.ContextAwareELException, message jakarta.el.ELException: java.lang.IllegalStateException: The review service is down

Same, Production

the message is replaced by See your server log for more information

the same full message as in Development

Client data in the browser

status serverError, errorName as above

status an empty string; the data object also had serverErrorName, serverErrorMessage, typeDetails

Alert without a handler, Development

alert: serverError: class java.lang.IllegalStateException The review service is down (HTTP 200) [source: f:fail]

no dialog was raised

View state from another session

error-name class jakarta.faces.application.ViewExpiredException

error-name jakarta.faces.application.ViewExpiredException

A view state that is not valid base64 (ViewState=bogus)

ViewExpiredException

java.lang.IllegalArgumentException, message Last unit does not have enough valid bits

The lessons, in order of importance:

  • Do not show the server’s message to the user. On MyFaces 4.1.4 the Production stage still put the exception text in the response, and exception text is information about the application’s internals. Show your own text (as the example above does), log on the server, and, if the container’s default is not acceptable, install an Ajax ExceptionHandler (the exception handling page covers ExceptionHandler, ExceptionHandlerFactory and the OmniFaces FullAjaxExceptionHandler that renders an error page for Ajax requests).

  • Do not match errorName for equality. Mojarra prefixes the name with class `, and MyFaces wraps the exception in a `ContextAwareELException. includes('ViewExpiredException') worked on both for an expired view; the cause of a wrapped exception is not in errorName on MyFaces.

  • An expired view is the error you must handle. The session timed out, or the server restarted, and the user clicks. The response is an ordinary 200 with an error element, so the page has to react; the error handler above reloads the page.

  • Development shows an alert. The specification says that in the Development stage the client signals the error with a JavaScript alert; Mojarra did, MyFaces 4.1.4 did not. In Production there is no alert on either: register an onerror handler, or the user sees nothing.

  • A successful success event follows a server error. In both implementations, a serverError was followed by a success event (the response was applied; the error was an element in it), so success does not mean "the action succeeded". Use the error callback or a rendered message for that.

Security Notes

An Ajax request is a postback; the protections of postbacks apply and must not be bypassed for convenience.

  • The view state in jakarta.faces.ViewState is sent with every request, and its check on the server is what keeps a forged request from another site from being processed. Do not disable view state or CSRF protection to make an Ajax call work. The Faces-Request header is no protection by itself: the API documentation defines isAjaxRequest() through that header, so a script that sets it passes, but only with a valid view state of the user’s session (a bogus one failed with an error element).

  • A request executes only the components named in execute, and the server decodes only those. The hidden effect is useful: a forged value for an input that was not executed is ignored. A forged value for an input that was executed is converted and validated like any other.

  • Render only what you mean to. Everything in a render list is sent to the browser. Do not render components that contain data the user must not see just because they are in the same panel.

  • Scripts built for getEvalScripts(), for onevent handlers and for h:commandScript parameters must not concatenate user input. The values in params are request parameters and arrive as strings; validate them like any request parameter.

  • Output of user-supplied text through h:outputText is escaped in updates exactly as in full pages. escape="false" on such content is cross-site scripting in Ajax updates too.

  • Business logic belongs in beans and services, not in the JavaScript that calls them: authorize in the server method.

Mojarra and MyFaces

Same on both: the request format (Faces-Request, partial.execute, partial.render, behavior.event, source, ViewState), the partial lifecycle and phase order, the immediate behaviour, delay, the queue, resetValues, PartialViewContext and its factory, the events (begin, complete, success) and the payload keys of an ordinary event, h:commandScript calls with parameters, programmatic AjaxBehavior, the redirect and @all responses. Differences found in the scratch application:

  • Request body: Mojarra posts only the executed controls; MyFaces posts the whole form and the hidden formId_SUBMIT.

  • Order of the executed ids: source first on Mojarra, source last on MyFaces.

  • Raw requests (for example with curl): Mojarra ignores the source button unless it is in partial.execute; MyFaces invokes it anyway. The keyword @form sent in a raw request is not expanded by either of them.

  • Default DOM event of checkboxes and radios: onclick on Mojarra, onchange on MyFaces (The event Attribute and the Default Events).

  • A listener without parameters: works on Mojarra, MethodNotFoundException on MyFaces.

  • A render target with rendered="false": Mojarra answers malformedXML and stops applying updates; MyFaces skips it.

  • Nesting f:ajax in a group: one handler versus two for the same event, and the effect of a nested disabled (Grouping Components).

  • An AjaxBehavior added in Java: faces.js is added by MyFaces and not by Mojarra (Getting faces.js onto the Page).

  • h:commandScript: underscore escaping in the name on MyFaces; isTrusted=true with autorun on Mojarra; namespace reservation on MyFaces.

  • Errors: wrapped exception names, the Production message, the status of a server error, the development alert, and the error for an invalid view state (Ajax Error Handling).

  • Response shape: the id on partial-response (MyFaces), the position of eval, the view state element id and value format.

  • getExecuteIds() before the view is restored: a NullPointerException on Mojarra 4.1.17 (the MyFaces behaviour was not run).

  • The specversion and implversion values of faces.js are implementation-defined (Mojarra 40100 and 17, MyFaces 400000 and 0 in the test); do not branch on them.

What Changed

Version Change

Faces 1.x

No Ajax. Applications used hand-written XMLHttpRequest code, component libraries with their own script and response formats (ajax4jsf, Dynamic Faces) and no common contract.

Faces 2.0

f:ajax, AjaxBehavior, AjaxBehaviorEvent, AjaxBehaviorListener, ClientBehaviorHolder, PartialViewContext, PartialResponseWriter, the jsf.js resource and the jsf.ajax API (request, response, addOnEvent, addOnError), the partial-response format and the request queue. The API documentation says Since: 2.0 for these classes. onevent, onerror, execute, render, listener, immediate, disabled and event date from here.

Faces 2.2

The delay option and attribute (issue 1050) and resetValues (issue 1129), both listed in the 2.1 to 2.2 change list. ClientWindow also dates from 2.2; when a client window is in use, the JavaScript API adds jakarta.faces.ClientWindow to the request.

Faces 2.3

h:commandScript (issue 613), PartialViewContext.getEvalScripts() (issue 1412), search expressions in execute and render (issue 1238) and f:websocket, which accepts a nested f:ajax.

Faces 3.0

The namespace changed from javax.faces to jakarta.faces: the request parameters became jakarta.faces.partial.ajax, jakarta.faces.partial.execute, jakarta.faces.partial.render and jakarta.faces.ViewState, and the library of the script jakarta.faces.

Faces 4.0

The JavaScript resource and namespace were renamed: faces.js and faces.ajax.request, replacing jsf.js and jsf.ajax (the change list has "Rename JSF to Faces over all place", issue 1552; jsf.js returned 404 on both implementations of 4.1). ResourceHandler.JSF_SCRIPT_* are deprecated for removal. "Improve f:ajax behavior in composite components" (issue 1567). f:websocket gained an onerror attribute.

Faces 4.1

No Ajax-specific change in the change list. Full state saving is deprecated (it affects what a view state is, not the Ajax protocol).

Faces 5.0

Under development (the latest milestone at the time of writing is jakarta.faces-api:5.0.0-M9). No Ajax change is described here, because none was verified.

Where the Books Are Outdated

The books of the section’s bibliography that cover Ajax are of two generations. Burns and Schalk, and Leonard, describe Faces 2.0, where Ajax first became part of the specification; Jacobi and Fallows predate it.

  • Hand-written Ajax. Jacobi and Fallows build Ajax into components with their own XMLHttpRequest code, script resources and response formats, and Leonard shows ajax4jsf and Dynamic Faces. All of it is replaced by f:ajax, the standard queue, the standard partial response and ResourceHandler (Faces 2.0). The lasting idea is the one they teach: components that update parts of a page by asking the server for markup. The code to write today is none.

  • Old names. Burns and Schalk show the script jsf.js in the library javax.faces and the request parameters javax.faces.partial.*, and Leonard calls jsf.ajax.request. Today they are faces.js, faces.ajax.request, the library jakarta.faces and jakarta.faces.partial.*. Copying such examples fails twice: the script is missing (404 for jsf.js on both implementations) and the names no longer exist.

  • Newer features are missing. The books are older than delay and resetValues (2.2), h:commandScript, PartialViewContext.getEvalScripts(), search expressions (@next, @parent, @id(...)) and f:websocket (2.3), so none of them appears; delay, Queueing and disabled, resetValues, h:commandScript and Ajax from Java: PartialViewContext and AjaxBehavior cover them.

  • Libraries. ajax4jsf, Dynamic Faces and RichFaces have ended. PrimeFaces p:ajax and OmniFaces cover what they added; the pages on PrimeFaces Ajax and on OmniFaces describe them, and the RequestContext of the PrimeFaces books is PrimeFaces.current() today.

Common Mistakes

  • The message never appears. The h:messages or h:message is not in render. Add it, or use a PartialViewContext decoration for the whole application (A PartialViewContext for Every Request).

  • The model does not change. The input is not in execute. @this is the default and it executes only the trigger.

  • The action works in curl and not in the browser, or the reverse. A raw request must contain the source in partial.execute; copy the request from the browser’s network tab or from a request filter.

  • The target does not update. The element is absent from the page (rendered="false", a row that does not exist). Render a wrapper that is always present.

  • A script in the head says faces is not defined. List faces.js first with h:outputScript, put the script in a resource after it, or in the body.

  • A value reappears after a failed validation. The local value of a valid sibling stayed in the component tree; use resetValues="true" and list the inputs.

  • The listener should run for invalid input and does not. Validation failure skips Invoke Application. Do not use immediate to get around it; read the lifecycle page.

  • A double click runs the action twice. The queue serialises requests, it does not drop them. Make the action idempotent, or hide the trigger while the request is pending.

  • A listener works on one implementation only. A listener without the AjaxBehaviorEvent parameter fails on MyFaces.

  • A nested f:ajax sends two requests. A second f:ajax for the same event inside a group stacks on MyFaces 4.1.4.

  • escape="false" on rendered user text. It writes the text into the DOM unescaped.

  • Error handling shows the server’s text. Show a fixed message; log the rest. MyFaces 4.1.4 puts the full exception text in the response even in Production.

  • The page expires and nothing happens. Handle ViewExpiredException in addOnError and reload.

  • f:ajax on a non-existing event name. The 500 appears when the view is built. Names are keyup, change, click, action, valueChange; not onkeyup.