Client Behaviors

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

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

This page is written against Jakarta Faces 4.1 (API jakarta.faces-api 4.1.2, part of Jakarta EE 11) with Eclipse Mojarra 4.1.17 and Apache MyFaces Core 4.1.4, Jakarta Servlet 6.1 and Java 17 or later (the examples were compiled with --release 21). Client behaviors exist since JSF 2.0 and the model has not changed since, so everything below also applies to Faces 4.0 apart from the jakarta.* names, but only 4.1 was run. The code in this page was deployed as an exploded Bookshelf WAR on Tomcat 11.0.26 with Weld 6.0.4.Final, once with Mojarra and once with MyFaces, and the scripts were driven in headless Chrome 154 (clicking, accepting or dismissing the confirm dialog, counting the POST requests). Everything marked as observed happened in those runs on 2026-10-08. PrimeFaces 16 is named once as an example of a third-party behavior and was not run.

A client behavior is an object that you attach to one client-side event of a component. At render time the component asks it for a piece of JavaScript and writes that script into the event handler it renders. When the user triggers the event in the browser, the script runs. That is all a behavior has to do, and it is how the standard f:ajax tag works: it creates an AjaxBehavior, attaches it to the component it is nested in, and the behavior’s script sends the Ajax request. A custom behavior is therefore the right tool when you want a reusable piece of client-side logic that you can attach to many components with one nested tag instead of copying an onclick attribute around: a confirmation dialog, a tracking call, a keyboard shortcut, a tooltip trigger. A behavior may also post back to the server and queue events of its own, which the page shows with a click tracker.

The Bookshelf scenario uses two small behaviors: bs:confirm asks "are you sure?" before a review is deleted, and bs:track tells the server when a visitor follows a link to the publisher’s page. A third piece, the bc:pill component, shows the other side of the contract: a component that accepts behaviors.

The Behavior Model

The model lives in the package jakarta.faces.component.behavior (plus a few types in jakarta.faces.event, jakarta.faces.render and jakarta.faces.view.facelets). Section 3.7 of the specification defines it; this table lists every type that a behavior author meets.

Type Role

Behavior

The root contract. It has one method, broadcast(BehaviorEvent), which a component calls to hand a queued behavior event back to the behavior. The Javadoc says that other behavior contracts may be added in the future; ClientBehavior is the only one.

BehaviorBase

A convenience base class that implements broadcast by delivering the event to the registered BehaviorListener objects, and implements PartialStateHolder. Listener registration is protected (addBehaviorListener, removeBehaviorListener), so a subclass decides whether and how to expose it.

ClientBehavior

Extends Behavior. getScript(ClientBehaviorContext) returns the script (or null for "no script this time"), decode(FacesContext, UIComponent) reads the request and may queue events, getHints() returns a set of ClientBehaviorHint.

ClientBehaviorBase

Extends BehaviorBase and implements ClientBehavior. Its getScript and decode delegate to the ClientBehaviorRenderer registered under getRendererType(); without a renderer type getScript returns null and decode does nothing. getHints() returns an empty set. This is the class to extend.

ClientBehaviorHolder

The component side: getEventNames(), getDefaultEventName(), addClientBehavior(String, ClientBehavior) and getClientBehaviors(). A component that does not implement it cannot carry a behavior. UIComponentBase already provides addClientBehavior and getClientBehaviors, so a component author only overrides the two event-name methods.

ClientBehaviorContext

An abstract class created with the static createClientBehaviorContext(FacesContext, UIComponent, String eventName, String sourceId, Collection<Parameter> parameters). It tells getScript which component and event the script is for and carries parameters that submitting behaviors should include. It also defines the constants BEHAVIOR_EVENT_PARAM_NAME (jakarta.faces.behavior.event) and BEHAVIOR_SOURCE_PARAM_NAME (jakarta.faces.source).

ClientBehaviorHint

An enum with one constant, SUBMITTING, meaning "this behavior posts back to the server".

ClientBehaviorRenderer

In jakarta.faces.render. An abstract class with getScript(ClientBehaviorContext, ClientBehavior) and decode(FacesContext, UIComponent, ClientBehavior). One instance serves the whole application, so it must be thread-safe.

AjaxBehavior

The only concrete behavior the specification defines. Its behavior id and renderer type are both jakarta.faces.behavior.Ajax, its hint is SUBMITTING, and it is exposed as the f:ajax tag.

BehaviorEvent, BehaviorListener

The server-side event and listener bases for behaviors that deliver events (AjaxBehaviorEvent and AjaxBehaviorListener are the standard pair).

@FacesBehavior, @FacesBehaviorRenderer

Register a behavior or a client behavior renderer by annotation. Application.createBehavior(String) creates a behavior from its id.

classDiagram class Behavior { <<interface>> broadcast(BehaviorEvent) } class ClientBehavior { <<interface>> getScript(ClientBehaviorContext) String decode(FacesContext, UIComponent) getHints() Set } class BehaviorBase { addBehaviorListener(BehaviorListener) } class ClientBehaviorBase { getRendererType() String } class AjaxBehavior class ClientBehaviorRenderer { getScript(ClientBehaviorContext, ClientBehavior) String decode(FacesContext, UIComponent, ClientBehavior) } class ClientBehaviorHolder { <<interface>> getEventNames() Collection getDefaultEventName() String addClientBehavior(String, ClientBehavior) getClientBehaviors() Map } Behavior <|-- ClientBehavior Behavior <|.. BehaviorBase BehaviorBase <|-- ClientBehaviorBase ClientBehavior <|.. ClientBehaviorBase ClientBehaviorBase <|-- AjaxBehavior ClientBehaviorBase ..> ClientBehaviorRenderer : delegates ClientBehaviorHolder o-- ClientBehavior : per event name

Two details are worth knowing before the first example. First, the specification says (3.7.1) that the standard HTML components are all "client behavior-ready". The h: tags found on both implementations are not: the next section gives the list, and a behavior nested in a tag that is not a holder fails. Second, nothing in the model is specific to confirmation dialogs or to Ajax. A behavior’s script is any JavaScript statement sequence, and whether the behavior takes part in the server-side lifecycle is a separate decision (decode, events and the SUBMITTING hint, covered below).

Where a Behavior Sits in the Lifecycle

A behavior touches the request processing lifecycle (see the phases) in four places:

  • View build (Restore View, or the first Render Response). The Facelets tag for the behavior creates the behavior with Application.createBehavior(id) and calls addClientBehavior(eventName, behavior) on the parent component. The tag runs every time the view is built: on the initial request, and again on each postback because the view is rebuilt from the templates before the saved state is applied (partial state saving). Properties set from tag attributes are therefore set again on every build. The state section below returns to this.

  • Render Response. The component’s renderer, or the component itself, asks each behavior of an event for its script through getScript(ClientBehaviorContext) and writes the script into the DOM event handler attribute.

  • In the browser. The script runs when the DOM event fires. A script that returns false stops the scripts after it in the chain and, for a submit button, stops the form submission (spec 14.6, faces.util.chain).

  • Apply Request Values, only for behaviors that post back. When the script sends a request, it must include the name of the behavior event in the parameter jakarta.faces.behavior.event and the id of the source component in jakarta.faces.source. The component’s renderer reads both, finds the behaviors attached to that event, and, if the source matches its own client id, calls decode(context, component) on each of them. decode may queue BehaviorEvent objects with component.queueEvent(...); the event is delivered in the phase set on it (AjaxBehavior uses Apply Request Values when immediate and Invoke Application otherwise). The component then calls the behavior’s broadcast and the behavior hands the event to its listeners.

The renderkit documentation states the decode rule and adds one more: behavior decoding applies to components that are readonly but not to components that are disabled (not run). Source for the rule: Standard HTML RenderKit documentation

The sequence below is the tracking behavior of this page, the one that does post back (the code follows in the renderer section):

sequenceDiagram autonumber participant V as View (build) participant R as Component renderer participant T as TrackBehavior participant B as Browser participant L as Lifecycle participant Bean as TrackerBean V->>V: tag creates TrackBehavior, addClientBehavior("click") Note over R,T: Render Response R->>T: getScript(ClientBehaviorContext) T-->>R: faces.ajax.request(this, event, {...}) R-->>B: onclick="faces.util.chain(...)" B->>L: POST jakarta.faces.behavior.event=click, jakarta.faces.source=pick, ViewState Note over L,R: Apply Request Values L->>R: decode(component) R->>T: decode(context, component) T->>L: queueEvent(TrackedEvent) Note over L,Bean: Invoke Application L->>T: broadcast(TrackedEvent) T->>Bean: onTracked(event) L-->>B: partial response without updates

The request that the script sends is an ordinary Faces Ajax request, so it carries the form’s view state. Observed on both implementations: the same POST sent by hand without jakarta.faces.ViewState was answered with HTTP 200 and a fresh view, the behavior was not decoded and the listener did not run.

Which Components Accept Behaviors

A component accepts behaviors if it implements ClientBehaviorHolder. Its event names are the logical events a behavior may attach to. For HTML components these are DOM event names without the on prefix (click, keyup, change), plus two logical names: action for components that are an ActionSource (the buttons and links that submit) and valueChange for components that are an EditableValueHolder (the inputs). The default event is what you get when the nested tag has no event attribute.

The holders were listed by creating each registered component type through Application.createComponent and reading getEventNames() and getDefaultEventName() (the sets are identical on both implementations unless noted):

Tag (component class) Default event Event names

h:commandButton (HtmlCommandButton)

action

action, click, dblclick, blur, focus, change, select, keydown, keypress, keyup, mousedown, mousemove, mouseout, mouseover, mouseup

h:commandLink (HtmlCommandLink)

action

the same list without change and select

h:inputText, h:inputSecret, h:inputTextarea, h:inputFile

valueChange

valueChange, change, select, blur, focus, click, dblclick and the key and mouse events

h:selectOneMenu, h:selectManyMenu, h:selectOneListbox, h:selectManyListbox

valueChange

valueChange, change, blur, focus and the click, key and mouse events (no select)

h:selectBooleanCheckbox, h:selectManyCheckbox, h:selectOneRadio

valueChange

as the inputs, including select

h:outputLink

Mojarra action, MyFaces none

click, blur, focus, the key and mouse events; Mojarra also lists action

h:link (HtmlOutcomeTargetLink)

Mojarra action, MyFaces none

as h:outputLink

h:button (HtmlOutcomeTargetButton)

none

click, blur, focus, the key and mouse events

h:form, h:panelGroup, h:panelGrid, h:dataTable, h:graphicImage

none

click, dblclick, the key events (keydown, keypress, keyup) and the mouse events

h:outputLabel

none

as h:outputLink without action

h:body

none

the form list plus load and unload

Not holders on either implementation: h:outputText, h:outputFormat, h:message, h:messages, h:column, h:head, h:inputHidden and h:commandScript. To attach a behavior to text, wrap it: an h:panelGroup (a span by default, a div with layout="block") is a holder. The API documentation also lists PassthroughElement and UIWebsocket (the component of f:websocket) among the implementing classes; the probe found UIWebsocket and the passthrough element only on MyFaces, because Mojarra does not create them by component type, so that part of the list is from the documentation, not from a run.

Nesting a behavior in a tag that is not a holder fails at render time on both implementations. Mojarra reports "Parent not an instance of ClientBehaviorHolder"; MyFaces reports "Parent not composite component or an instance of ClientBehaviorHolder".

Event names are checked differently. An event attribute that is an expression fails on both implementations, because the attribute must be a literal: Mojarra reports "The 'event' attribute for behavior tag must be a literal", MyFaces that the event name #{param.e} was not found on the component. A literal name that the component does not list (event="nonsense" on a button) is accepted silently by Mojarra, which answers with HTTP 200 and a button that has no script, and rejected by MyFaces with "eventName nonsense not found on component instance" (HTTP 500). A tag without event inside a holder that has no default event fails on both: Mojarra reports "The event name is not defined", MyFaces "null eventName could not be defined for client behavior" (observed with an h:panelGroup).

The rendered attribute is on plus the DOM name, with the two logical names mapped: action on a button or link and click both produce onclick, and the default valueChange of an input produces onchange (observed for the button, the link and h:inputText). There is one trap. On an h:outputLink Mojarra’s default event is action, and for that component the renderer writes the attribute onaction, which no browser fires. Observed: a confirm behavior without an event attribute rendered onaction="return confirm('Leave?')", and the same happened to a plain f:ajax (rendered onaction="mojarra.ab(this,event,'action',0,0)"). MyFaces has no default event for the component and fails with "null eventName could not be defined for client behavior". Both are avoided by always writing event="click" on an h:outputLink or h:link.

Attaching a Behavior

There are three ways to get a behavior onto a component.

A nested tag. A behavior is exposed to Facelets as a tag, declared in a tag library (shown in the next section). The tag is nested in the holder, its optional event attribute names the event, and every other attribute is set on the behavior as a bean property. The tag also understands binding, a value expression that supplies the behavior instance: the tag handler uses the instance the expression returns, and creates one and stores it through the expression when it returns null. The names event, binding and for are reserved: the handler does not set them as properties (read in Mojarra’s BehaviorTagHandlerDelegateImpl; for serves composite components). Observed on both implementations: <bs:confirm binding="#{attachBean.bound}" .../> rendered the expected script. Because the handler uses whatever instance the expression returns, a binding to a bean in a longer scope would hand the same behavior object to every view that references it (read in the source, not run), so keep the bean request scoped.

f:ajax. The standard tag, the same mechanism (it is the AjaxBehavior). Because it is a behavior, it composes with yours on the same event, as the Ajax delete button below does.

Java. Create the behavior with Application.createBehavior(id) and attach it with addClientBehavior. The usual place is a postAddToView listener on the component, so that the behavior is added once the component is in the tree:

attach.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core">
<h:head>
  <title>Attach from Java</title>
</h:head>
<h:body>
  <h:form id="attach">
    <h:commandButton id="clear" value="Clear" action="#{reviewsBean.reset}">
      <f:event type="postAddToView" listener="#{attachBean.attach}"/>
    </h:commandButton>
  </h:form>
</h:body>
</html>
AttachBean.java
package org.example.bookshelf.web;

import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.component.UIComponent;
import jakarta.faces.component.behavior.ClientBehavior;
import jakarta.faces.component.behavior.ClientBehaviorHolder;
import jakarta.faces.context.FacesContext;
import jakarta.faces.event.ComponentSystemEvent;
import jakarta.inject.Inject;
import jakarta.inject.Named;

@Named
@RequestScoped
public class AttachBean {

    @Inject
    FacesContext context;

    private ClientBehavior bound;

    public void attach(ComponentSystemEvent event) {
        UIComponent component = event.getComponent();
        ConfirmBehavior behavior = (ConfirmBehavior) context.getApplication().createBehavior("bookshelf.confirm");
        behavior.setMessage("Clear all reviews?");
        ((ClientBehaviorHolder) component).addClientBehavior("click", behavior);
    }

    public ClientBehavior getBound() {
        return bound;
    }

    public void setBound(ClientBehavior bound) {
        this.bound = bound;
    }
}

The bound property of the bean is what the binding attribute above pointed at. Observed on both implementations: the button had exactly one confirm script, both before and after a postback (the listener runs again on each build). createBehavior throws a FacesException for an id that is not registered; observed through a tag whose behavior-id had no registration: Mojarra reported "Expression Error: Named Object: bookshelf.ghost not found.", MyFaces "Could not find any registered behavior-class for behaviorId : bookshelf.ghost" (both HTTP 500).

A Custom Confirm Behavior

The first behavior is bs:confirm: the browser’s confirm dialog before the event’s normal work. Cancelling the dialog stops the click. It is a script-only behavior, so it extends ClientBehaviorBase and overrides getScript instead of naming a renderer.

The message is text from the page author, and in the next section it becomes text from the database (a book title), so it cannot be pasted into a JavaScript string literal. A title such as O'Reilly's "Guide" </script> would end the literal and run whatever follows. The helper below escapes the characters that matter in a single-quoted literal that is itself inside an HTML attribute or a script element. Faces escapes the attribute value for HTML afterwards, but that is a different language.

JsStrings.java
package org.example.bookshelf.web;

/** Escapes text for use inside a single-quoted JavaScript string literal that is itself inside an HTML attribute or script. */
final class JsStrings {

    private JsStrings() {
    }

    static String escape(String text) {
        StringBuilder out = new StringBuilder(text.length() + 16);
        for (int i = 0; i < text.length(); i++) {
            char c = text.charAt(i);
            switch (c) {
                case '\\' -> out.append("\\\\");
                case '\'' -> out.append("\\'");
                case '"' -> out.append("\\u0022");
                case '<' -> out.append("\\u003c");
                case '>' -> out.append("\\u003e");
                case '&' -> out.append("\\u0026");
                case '\n' -> out.append("\\n");
                case '\r' -> out.append("\\r");
                case '
' -> out.append("\\u2028");
                case '
' -> out.append("\\u2029");
                default -> {
                    if (c < 0x20) {
                        out.append(String.format("\\u%04x", (int) c));
                    } else {
                        out.append(c);
                    }
                }
            }
        }
        return out.toString();
    }
}

The behavior itself, registered with @FacesBehavior under the id bookshelf.confirm:

ConfirmBehavior.java
package org.example.bookshelf.web;

import jakarta.faces.component.behavior.ClientBehaviorBase;
import jakarta.faces.component.behavior.ClientBehaviorContext;
import jakarta.faces.component.behavior.FacesBehavior;
import jakarta.faces.context.FacesContext;

/** A client behavior that asks "OK or Cancel?" before the event's default work (a postback, an Ajax request) runs. */
@FacesBehavior("bookshelf.confirm")
public class ConfirmBehavior extends ClientBehaviorBase {

    private String message = "Are you sure?";

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
        clearInitialState();
    }

    @Override
    public String getScript(ClientBehaviorContext context) {
        if (message == null || message.isBlank()) {
            return null;
        }
        return "return confirm('" + JsStrings.escape(message) + "')";
    }

    @Override
    public Object saveState(FacesContext context) {
        if (initialStateMarked()) {
            return null;
        }
        return new Object[] {super.saveState(context), message};
    }

    @Override
    public void restoreState(FacesContext context, Object state) {
        if (state == null) {
            return;
        }
        Object[] values = (Object[]) state;
        super.restoreState(context, values[0]);
        message = (String) values[1];
    }
}
  • getScript returns null when there is nothing to say; the Javadoc allows that and the component then renders no script for the event.

  • The script is return confirm('...'). The return is what lets a false result cancel the default action of a submit button.

  • setMessage calls clearInitialState(), as every setter of a state-holding behavior must (see state saving).

The tag library declares the tag. A behavior tag has a tag-name and a behavior element whose behavior-id is the id used in the annotation (the optional handler-class is used in the next section):

bookshelf.taglib.xml
<?xml version="1.0" encoding="UTF-8"?>
<facelet-taglib 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-facelettaglibrary_4_1.xsd"
                version="4.1">
    <namespace>urn:bookshelf:behaviors</namespace>
    <tag>
        <tag-name>confirm</tag-name>
        <behavior>
            <behavior-id>bookshelf.confirm</behavior-id>
        </behavior>
    </tag>
    <tag>
        <tag-name>confirmDynamic</tag-name>
        <behavior>
            <behavior-id>bookshelf.confirmDynamic</behavior-id>
            <handler-class>org.example.bookshelf.web.ExpressionBehaviorHandler</handler-class>
        </behavior>
    </tag>
    <tag>
        <tag-name>track</tag-name>
        <behavior>
            <behavior-id>bookshelf.track</behavior-id>
            <handler-class>org.example.bookshelf.web.ExpressionBehaviorHandler</handler-class>
        </behavior>
    </tag>
</facelet-taglib>

A library under WEB-INF is listed in web.xml, as a semicolon-separated list of paths starting with a slash; a library inside a JAR is found by itself when the file is META-INF/*.taglib.xml (spec chapter 10):

    <context-param>
        <param-name>jakarta.faces.FACELETS_LIBRARIES</param-name>
        <param-value>/WEB-INF/bookshelf.taglib.xml</param-value>
    </context-param>

Using it on the Bookshelf reviews page, once with a plain submit and once combined with f:ajax:

reviews.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:bs="urn:bookshelf:behaviors">
<h:head>
  <title>Reviews</title>
</h:head>
<h:body>
  <h:form id="reviews">
    <h:dataTable id="list" value="#{reviewsBean.reviews}" var="r">
      <h:column>#{r.title}</h:column>
      <h:column>
        <h:commandButton id="delete" value="Delete" action="#{reviewsBean.delete(r)}">
          <bs:confirm message="Delete this review?"/>
        </h:commandButton>
      </h:column>
      <h:column>
        <h:commandButton id="deleteAjax" value="Delete (Ajax)" action="#{reviewsBean.delete(r)}">
          <bs:confirm message="Delete this review?"/>
          <f:ajax render="@form"/>
        </h:commandButton>
      </h:column>
    </h:dataTable>
  </h:form>
</h:body>
</html>

The rendered buttons differ between implementations but do the same thing (copied from the responses):

Mojarra 4.1.17
<input id="reviews:list:0:delete" type="submit" name="reviews:list:0:delete" value="Delete" onclick="return confirm('Delete this review?')" />
<input id="reviews:list:0:deleteAjax" type="submit" name="reviews:list:0:deleteAjax" value="Delete (Ajax)" onclick="faces.util.chain(this,event,'return confirm(\'Delete this review?\')','mojarra.ab(this,event,\'action\',0,\'@form\')');event.preventDefault()" />
MyFaces 4.1.4
<input id="reviews:list:0:delete" name="reviews:list:0:delete" type="submit" value="Delete" onclick="return faces.util.chain(this, event,function(event){return confirm('Delete this review?')});" />
<input id="reviews:list:0:deleteAjax" name="reviews:list:0:deleteAjax" type="submit" value="Delete (Ajax)" onclick="faces.util.chain(this, event,function(event){return confirm('Delete this review?')}, function(event){myfaces.ab(this,event,'action','','@form',{})}); return false;" />

With a single behavior on a plain button, Mojarra renders the script alone; with f:ajax next to it, both implementations wrap the scripts in faces.util.chain and suppress the default submit, so Ajax replaces it. In the Ajax button the confirm script comes first (the order of the nested tags). Observed in Chrome on both implementations, for the plain and the Ajax button: dismissing the dialog sent no request and left the three reviews, accepting it sent exactly one POST and removed the first review.

The Ajax button uses render="@form" because of a difference outside the scope of this page: written as the table’s id (list or :reviews:list), Mojarra resolved the render target of a button in a table row to the row (reviews:list:0, seen in the mojarra.ab call) and refreshed only that row, whereas MyFaces refreshed the table. The Ajax page returns to this.

A confirm dialog is a courtesy to the user, not a security control. The request that deletes a review can be sent without the script, so delete must authorize the caller on the server like any other action.

State Saving and Behaviors

BehaviorBase implements PartialStateHolder. With partial state saving (the default) the framework rebuilds the view from the Facelets templates on every postback and saves only what changed after the initial state was marked. The tag handler runs again during that rebuild, so the properties set from tag attributes are set again by the page itself and need no saving. State that is changed afterwards, in Java, has to be saved, and the contract is:

  • call clearInitialState() in each setter, so that the behavior is no longer in its initial state and the framework asks for its state;

  • return null from saveState while initialStateMarked() is true;

  • save the superclass state and your own fields otherwise, and restore them in restoreState.

ConfirmBehavior above follows this. The property survives full state saving as well (jakarta.faces.PARTIAL_STATE_SAVING=false, deprecated for removal since 4.1): observed on both implementations, the rendered script of bs:confirm was identical before and after a postback with it switched off. A behavior without saveState and restoreState loses its properties in that mode: the next section’s bs:confirmDynamic showed the failure before the two methods were added (the script, and so the confirmation, silently disappeared from every button after the first postback; getScript returned null because the message was null).

A ValueExpression is saved with UIComponentBase.saveAttachedState and restored with restoreAttachedState, as the code in the next section does. Keep only values that can be saved (serializable values and attached objects); never keep a reference to the FacesContext or to a component.

Expressions in Behavior Attributes: a Message per Row

The reviews page above has a generic message. A better dialog names the review: "Delete the review of Dune?". Written with the obvious tag, it does not work:

<h:commandButton value="Delete" action="#{reviewsBean.delete(r)}">
  <bs:confirm message="Delete the review of #{r.title}?"/>
</h:commandButton>

The cause is how Facelets sets a tag attribute on a behavior. Unlike a component, a behavior does not receive the attribute as a value expression: the tag handler evaluates the expression once, when it applies the tag, and calls setMessage(String) with the result. The tag is applied when the view is built, not once per table row, so r is not defined then. Observed in the rendered buttons of both implementations: onclick="return confirm('Delete the review of ?')" on every row. EL that does not depend on the row (a bundle entry, a bean property) works, and it is evaluated at build time.

To get the evaluation at render time, per row, the behavior needs the unevaluated expression. A setter that takes ValueExpression is not enough by itself: with the default tag handler the same page failed on both implementations with "Cannot convert [Delete the review of ?] of type [class java.lang.String] to [class jakarta.el.ValueExpression]" (Mojarra’s message; MyFaces gave the same cause). The default rule for a bean property evaluates the attribute and then tries to coerce the result. The fix is a small tag handler that adds a Facelets MetaRule for setters of type ValueExpression (and, for the click tracker below, MethodExpression). A handler is declared with the handler-class element of the tag, as in the tag library above, and is a subclass of BehaviorHandler that overrides createMetaRuleset. MetaRuleset.addRule appends a rule, and the rules added last are tried first (read in Mojarra’s MetaRulesetImpl.finish).

ExpressionBehaviorHandler.java
package org.example.bookshelf.web;

import jakarta.el.MethodExpression;
import jakarta.el.ValueExpression;
import jakarta.faces.event.BehaviorEvent;
import jakarta.faces.view.facelets.BehaviorConfig;
import jakarta.faces.view.facelets.BehaviorHandler;
import jakarta.faces.view.facelets.FaceletContext;
import jakarta.faces.view.facelets.MetaRule;
import jakarta.faces.view.facelets.MetaRuleset;
import jakarta.faces.view.facelets.Metadata;
import jakarta.faces.view.facelets.MetadataTarget;
import jakarta.faces.view.facelets.TagAttribute;
import jakarta.faces.view.facelets.TagAttributeException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * A tag handler for behaviors whose setters take a ValueExpression or a MethodExpression: the attribute is handed over
 * unevaluated, so the behavior can evaluate it each time it renders its script or receives an event.
 */
public class ExpressionBehaviorHandler extends BehaviorHandler {

    public ExpressionBehaviorHandler(BehaviorConfig config) {
        super(config);
    }

    @Override
    protected MetaRuleset createMetaRuleset(Class type) {
        return super.createMetaRuleset(type).addRule(new ExpressionRule());
    }

    private static final class ExpressionRule extends MetaRule {

        @Override
        public Metadata applyRule(String name, TagAttribute attribute, MetadataTarget target) {
            Method setter = target.getWriteMethod(name);
            if (setter == null) {
                return null;
            }
            Class<?> parameter = setter.getParameterTypes()[0];
            if (parameter != ValueExpression.class && parameter != MethodExpression.class) {
                return null;
            }
            return new Metadata() {
                @Override
                public void applyMetadata(FaceletContext ctx, Object instance) {
                    try {
                        Object expression = parameter == ValueExpression.class
                                ? attribute.getValueExpression(ctx, Object.class)
                                : attribute.getMethodExpression(ctx, void.class, new Class[] {BehaviorEvent.class});
                        setter.invoke(instance, expression);
                    } catch (InvocationTargetException | IllegalAccessException e) {
                        throw new TagAttributeException(attribute, e);
                    }
                }
            };
        }
    }
}

The behavior then keeps the expression, evaluates it each time getScript runs (the table is being iterated at that moment, so r is bound) and saves it:

DynamicConfirmBehavior.java
package org.example.bookshelf.web;

import jakarta.el.ValueExpression;
import jakarta.faces.component.UIComponentBase;
import jakarta.faces.component.behavior.ClientBehaviorBase;
import jakarta.faces.component.behavior.ClientBehaviorContext;
import jakarta.faces.component.behavior.FacesBehavior;
import jakarta.faces.context.FacesContext;

/** Like ConfirmBehavior, but the message is a ValueExpression evaluated every time the script is rendered. */
@FacesBehavior("bookshelf.confirmDynamic")
public class DynamicConfirmBehavior extends ClientBehaviorBase {

    private ValueExpression message;

    public void setMessage(ValueExpression message) {
        this.message = message;
        clearInitialState();
    }

    @Override
    public String getScript(ClientBehaviorContext context) {
        if (message == null) {
            return null;
        }
        Object text = message.getValue(context.getFacesContext().getELContext());
        return text == null ? null : "return confirm('" + JsStrings.escape(text.toString()) + "')";
    }

    @Override
    public Object saveState(FacesContext context) {
        if (initialStateMarked()) {
            return null;
        }
        return new Object[] {super.saveState(context), UIComponentBase.saveAttachedState(context, message)};
    }

    @Override
    public void restoreState(FacesContext context, Object state) {
        if (state == null) {
            return;
        }
        Object[] values = (Object[]) state;
        super.restoreState(context, values[0]);
        message = (ValueExpression) UIComponentBase.restoreAttachedState(context, values[1]);
    }
}
rows.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:bs="urn:bookshelf:behaviors">
<h:head><title>table</title></h:head>
<h:body>
  <h:form id="f">
    <h:dataTable id="t" value="#{reviewsBean.reviews}" var="r">
      <h:column><h:outputText value="#{r.title}"/></h:column>
      <h:column>
        <h:commandButton id="static" value="static" action="#{reviewsBean.delete(r)}">
          <bs:confirm message="Delete the review of #{r.title}?"/>
        </h:commandButton>
        <h:commandButton id="dynamic" value="dynamic" action="#{reviewsBean.delete(r)}">
          <bs:confirmDynamic message="Delete the review of #{r.title}?"/>
        </h:commandButton>
      </h:column>
    </h:dataTable>
  </h:form>
</h:body>
</html>

Observed on both implementations: the static buttons rendered Delete the review of ? and the dynamic buttons Delete the review of Dune?, Delete the review of O\'Reilly\'s "Guide" </script>? and Delete the review of Emma?. In Chrome the second dialog read Delete the review of O'Reilly's "Guide" </script>? on both implementations, so the escaping holds. The scripts also survived a postback with partial state saving and with full state saving.

A Behavior with a Renderer: the Click Tracker

ClientBehaviorBase offers a second way to produce the script: return a renderer type from getRendererType() and let a ClientBehaviorRenderer do the work. This is the same split as for components. It pays off when the script depends on the render kit, or when you want the behavior class to stay a plain bean and the output logic to be replaceable; the standard AjaxBehavior is built that way (its renderer type is its id). For a script-only behavior like confirm it is more code than the class above.

The second Bookshelf behavior, bs:track, reports a click to the server in the background, for example when a visitor follows the link to the publisher’s page. It needs the server side of the contract: a script that sends a request, a decode that turns the request into an event, and a listener. The pieces are:

  • TrackBehavior (the behavior): the id bookshelf.track, the renderer type bookshelf.track, an isbn property, and a listener method expression; it overrides broadcast to call that expression after the registered listeners.

  • TrackBehaviorRenderer (registered with @FacesBehaviorRenderer): produces the script, and decode queues the event.

  • TrackedEvent and TrackedListener: the event and listener types (BehaviorEvent and BehaviorListener).

TrackBehavior.java
package org.example.bookshelf.web;

import jakarta.el.MethodExpression;
import jakarta.faces.application.ResourceDependency;
import jakarta.faces.component.behavior.ClientBehaviorBase;
import jakarta.faces.component.behavior.FacesBehavior;
import jakarta.faces.context.FacesContext;
import jakarta.faces.event.AbortProcessingException;
import jakarta.faces.event.BehaviorEvent;

/** Reports clicks to the server in the background; script and decoding are delegated to the renderer. */
@ResourceDependency(library = "jakarta.faces", name = "faces.js")
@FacesBehavior("bookshelf.track")
public class TrackBehavior extends ClientBehaviorBase {

    public static final String RENDERER_TYPE = "bookshelf.track";

    private String isbn;
    private MethodExpression listener;

    public String getIsbn() {
        return isbn;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    public void setListener(MethodExpression listener) {
        this.listener = listener;
    }

    public void addTrackedListener(TrackedListener trackedListener) {
        addBehaviorListener(trackedListener);
    }

    @Override
    public String getRendererType() {
        return RENDERER_TYPE;
    }

    @Override
    public void broadcast(BehaviorEvent event) throws AbortProcessingException {
        super.broadcast(event);
        if (event instanceof TrackedEvent && listener != null) {
            listener.invoke(FacesContext.getCurrentInstance().getELContext(), new Object[] {event});
        }
    }
}

The renderer. The @FacesBehaviorRenderer attribute rendererType must equal the behavior’s renderer type; renderKitId defaults to HTML_BASIC. The class needs a public no-argument constructor and one instance is shared by all requests, so it holds no state:

TrackBehaviorRenderer.java
package org.example.bookshelf.web;

import jakarta.faces.component.UIComponent;
import jakarta.faces.component.behavior.ClientBehavior;
import jakarta.faces.component.behavior.ClientBehaviorContext;
import jakarta.faces.context.FacesContext;
import jakarta.faces.render.ClientBehaviorRenderer;
import jakarta.faces.render.FacesBehaviorRenderer;

/** One instance serves every request, so it must be stateless. */
@FacesBehaviorRenderer(rendererType = TrackBehavior.RENDERER_TYPE)
public class TrackBehaviorRenderer extends ClientBehaviorRenderer {

    @Override
    public String getScript(ClientBehaviorContext context, ClientBehavior behavior) {
        String isbn = ((TrackBehavior) behavior).getIsbn();
        return "faces.ajax.request(this,event,{'jakarta.faces.behavior.event':'" + JsStrings.escape(context.getEventName())
                + "','isbn':'" + JsStrings.escape(isbn == null ? "" : isbn) + "'})";
    }

    @Override
    public void decode(FacesContext context, UIComponent component, ClientBehavior behavior) {
        String isbn = context.getExternalContext().getRequestParameterMap().get("isbn");
        component.queueEvent(new TrackedEvent(context, component, behavior, isbn));
    }
}

The event. BehaviorEvent requires the component and the behavior. setPhaseId decides when the event is delivered:

TrackedEvent.java
package org.example.bookshelf.web;

import jakarta.faces.component.UIComponent;
import jakarta.faces.component.behavior.Behavior;
import jakarta.faces.context.FacesContext;
import jakarta.faces.event.BehaviorEvent;
import jakarta.faces.event.FacesListener;
import jakarta.faces.event.PhaseId;

/** Queued when the browser reports a tracked click. */
public class TrackedEvent extends BehaviorEvent {

    private static final long serialVersionUID = 1L;

    private final String isbn;

    public TrackedEvent(FacesContext context, UIComponent component, Behavior behavior, String isbn) {
        super(context, component, behavior);
        this.isbn = isbn;
        setPhaseId(PhaseId.INVOKE_APPLICATION);
    }

    public String getIsbn() {
        return isbn;
    }

    @Override
    public boolean isAppropriateListener(FacesListener listener) {
        return listener instanceof TrackedListener;
    }

    @Override
    public void processListener(FacesListener listener) {
        ((TrackedListener) listener).processTracked(this);
    }
}
TrackedListener.java
package org.example.bookshelf.web;

import jakarta.faces.event.AbortProcessingException;
import jakarta.faces.event.BehaviorListener;

public interface TrackedListener extends BehaviorListener {

    void processTracked(TrackedEvent event) throws AbortProcessingException;
}

The bean that receives the events, and the tag declaration (the track tag in the tag library above uses the same handler as confirmDynamic, so that the listener attribute arrives as a MethodExpression):

TrackerBean.java
package org.example.bookshelf.web;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Named;
import jakarta.faces.event.BehaviorEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@Named
@ApplicationScoped
public class TrackerBean {

    private final List<String> hits = Collections.synchronizedList(new ArrayList<>());

    public void onTracked(BehaviorEvent event) {
        TrackedEvent tracked = (TrackedEvent) event;
        hits.add(tracked.getComponent().getClientId() + " isbn=" + tracked.getIsbn() + " phase=" + tracked.getPhaseId());
    }

    public List<String> getHits() {
        return hits;
    }
}
track.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:bs="urn:bookshelf:behaviors">
<h:head>
  <title>Publisher</title>
</h:head>
<h:body>
  <h:form id="f">
    <h:outputLink id="pub" value="https://example.org/dune" target="_blank">Publisher page
      <bs:track event="click" isbn="9780441013593" listener="#{trackerBean.onTracked}"/>
    </h:outputLink>
  </h:form>
  <pre id="hits">#{trackerBean.hits}</pre>
</h:body>
</html>

Observed on both implementations, in Chrome: clicking the link produced one POST with jakarta.faces.behavior.event=click, jakarta.faces.source=f:pub, isbn=9780441013593 and jakarta.faces.partial.ajax=true, and the bean recorded f:pub isbn=9780441013593 phase=INVOKE_APPLICATION (the phase prints as INVOKE_APPLICATION 5 on Mojarra and INVOKE_APPLICATION(5) on MyFaces). The link opens the publisher’s page in a new tab (target="_blank") and the background request is not cancelled.

Points that the example shows:

  • The event name. context.getEventName() is the behavior event name of the holder, so the script does not hard-code click. For a button or link with the default event it is action, for an input valueChange (observed).

  • The source. faces.ajax.request(this, event, ...) sends jakarta.faces.source itself. getSourceId() of the context was null for every standard renderer probed on both implementations, so the script cannot rely on it.

  • Decoding. The renderer’s decode ran only for the request that came from this link, because the component’s renderer compares jakarta.faces.source with its client id before it calls decode (read in Mojarra’s HtmlBasicRenderer).

  • Untrusted input. The isbn read in decode is whatever the browser sent. The event carries it, and the listener must treat it as data from an attacker: validate it, and do not write it to a log line or a page without escaping. TrackerBean above only stores it, which is enough for a demonstration and not enough for production.

  • The component renders the handler. h:commandLink and h:commandButton chain the script with their own submit, so that a tracking call on a button still submits the form (observed in Chrome on both implementations: an Ajax POST and then a normal form POST). The tracker is therefore attached to an h:outputLink, which has no submit of its own.

  • Parameters. context.getParameters() held the f:param children of a command button and link (observed p=1). For h:outputLink, h:link, h:button, inputs and the other holders, Mojarra passed an empty collection and MyFaces passed null, which the Javadoc says never happens: a script that iterates the collection must test for null (a first version of TrackBehaviorRenderer that did not raised a NullPointerException at render time on MyFaces, so the renderer below reads no parameters).

  • faces.js. The script calls faces.ajax.request, which exists only if the page loads the Faces script. The standard components that need it add it themselves, a bare custom component does not (see the pill component).

The Behavior Context

ClientBehaviorContext is the argument of getScript. It is only valid during that call, and the Javadoc forbids keeping a reference. The standard renderers build it with createClientBehaviorContext(context, component, behaviorEventName, null, params) (read in Mojarra’s RenderKitUtils); a component that renders behaviors itself builds one the same way, as the pill component below does. Its properties, as seen by a probe behavior that printed them into the script:

Property Observed value

getEventName()

The behavior event name: action for a command button or link without an event attribute, valueChange for an input or select menu, the DOM name (click) when the attribute names it.

getComponent()

The holder: HtmlCommandButton, HtmlOutputLink, HtmlInputText, HtmlPanelGroup, HtmlGraphicImage and so on.

getSourceId()

null for every standard holder on both implementations. The Javadoc describes it as an id a component can pass when the script cannot find the source from the DOM.

getParameters()

The f:param children for h:commandButton and h:commandLink. Empty on Mojarra and null on MyFaces for the others (see above).

ClientBehaviorHint.SUBMITTING tells a renderer that the behavior posts back, so the renderer may leave out the script it would otherwise add for the same event. AjaxBehavior returns it. What the renderers did with it, observed with a behavior that claimed to be submitting although it only asked for confirmation:

  • On a command button, Mojarra rendered onclick="return confirm('...');event.preventDefault()" and MyFaces onclick="faces.util.chain(...); return false;". On MyFaces the form was never submitted, even after accepting the dialog. On Mojarra the return comes before preventDefault(), so accepting still submitted the form and cancelling did not.

  • On a command link, both implementations dropped their submit script, and nothing was submitted after accepting.

The hint is a promise: a behavior that sets it must perform the postback itself. A behavior that does not post back must not set it.

A Component That Accepts Behaviors

The other half of the contract is a component that implements ClientBehaviorHolder. UIComponentBase supplies addClientBehavior and getClientBehaviors; the component declares the event names, renders each event’s scripts and decodes the behaviors. The component below is a small label, bc:pill, that exposes click and dblclick. It renders itself (a renderer would be cleaner, see the page on custom components and renderers) to keep the example in one class:

Pill.java
package org.example.bookshelf.web;

import jakarta.faces.application.ResourceDependency;
import jakarta.faces.component.FacesComponent;
import jakarta.faces.component.UIOutput;
import jakarta.faces.component.behavior.ClientBehavior;
import jakarta.faces.component.behavior.ClientBehaviorContext;
import jakarta.faces.component.behavior.ClientBehaviorHolder;
import jakarta.faces.context.FacesContext;
import jakarta.faces.context.ResponseWriter;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Map;

/** A small label component that exposes "click" and "dblclick" to client behaviors. */
@ResourceDependency(library = "jakarta.faces", name = "faces.js")
@FacesComponent(value = "bookshelf.Pill", createTag = true, namespace = "urn:bookshelf:components", tagName = "pill")
public class Pill extends UIOutput implements ClientBehaviorHolder {

    private static final List<String> EVENTS = List.of("click", "dblclick");

    public Pill() {
        setRendererType(null);
    }

    @Override
    public Collection<String> getEventNames() {
        return EVENTS;
    }

    @Override
    public String getDefaultEventName() {
        return "click";
    }

    @Override
    public void decode(FacesContext context) {
        super.decode(context);
        Map<String, String> params = context.getExternalContext().getRequestParameterMap();
        String event = params.get(ClientBehaviorContext.BEHAVIOR_EVENT_PARAM_NAME);
        String source = params.get(ClientBehaviorContext.BEHAVIOR_SOURCE_PARAM_NAME);
        if (event != null && getClientId(context).equals(source)) {
            for (ClientBehavior behavior : getClientBehaviors().getOrDefault(event, List.of())) {
                behavior.decode(context, this);
            }
        }
    }

    @Override
    public void encodeBegin(FacesContext context) throws IOException {
        ResponseWriter writer = context.getResponseWriter();
        String clientId = getClientId(context);
        writer.startElement("span", this);
        writer.writeAttribute("id", clientId, "id");
        writer.writeAttribute("class", "pill", null);
        for (Map.Entry<String, List<ClientBehavior>> entry : getClientBehaviors().entrySet()) {
            String event = entry.getKey();
            ClientBehaviorContext behaviorContext = ClientBehaviorContext.createClientBehaviorContext(
                    context, this, event, clientId, null);
            StringBuilder chain = new StringBuilder("faces.util.chain(this,event");
            int scripts = 0;
            for (ClientBehavior behavior : entry.getValue()) {
                String script = behavior.getScript(behaviorContext);
                if (script != null) {
                    chain.append(",'").append(JsStrings.escape(script)).append('\'');
                    scripts++;
                }
            }
            if (scripts > 0) {
                writer.writeAttribute("on" + event, chain.append(')').toString(), null);
            }
        }
        Object value = getValue();
        if (value != null) {
            writer.writeText(value, "value");
        }
    }

    @Override
    public void encodeEnd(FacesContext context) throws IOException {
        context.getResponseWriter().endElement("span");
    }
}
pill.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:bs="urn:bookshelf:behaviors"
      xmlns:bc="urn:bookshelf:components">
<h:head>
  <title>Pill</title>
</h:head>
<h:body>
  <h:form id="shelf">
    <bc:pill id="pick" value="Staff pick">
      <bs:confirm message="Remove the staff pick label?"/>
      <bs:track event="dblclick" isbn="9780441013593" listener="#{trackerBean.onTracked}"/>
    </bc:pill>
  </h:form>
</h:body>
</html>

What the component has to do, because no renderer does it for it:

  • Event names. getEventNames and getDefaultEventName give the names that nested tags may use. Without a default event a tag with no event attribute fails (see above).

  • Render the scripts. For each event, create a ClientBehaviorContext, ask every behavior for its script, skip null, and chain the rest with faces.util.chain(this, event, script, ...), which runs them in order and stops at the first one that returns false (spec 14.6). The component passes its own client id as the source id here.

  • Decode. Read jakarta.faces.behavior.event and jakarta.faces.source, and call decode on the behaviors of that event when the source is the component. This is the rule the standard renderers apply.

  • Load faces.js. Mojarra adds the script to a page only when a component that needs it is rendered. On a page with only the pill, faces.util was undefined: Chrome reported "faces is not defined" for each click and no confirm dialog appeared, while MyFaces had added the script anyway. The @ResourceDependency annotation on the component fixes it; the ClientBehavior Javadoc asks for the same annotation on a behavior class whose script calls into another resource, and TrackBehavior carries it. Observed: with the annotation only on the behavior, a page with the pill and the tracker rendered faces.js, and with the annotation only on the pill, a page with the pill and bs:confirm did.

With the annotation, observed on both implementations in Chrome: a click on the pill showed the dialog "Remove the staff pick label?"; a double click sent the tracking request and the bean recorded isbn=9780441013593 (a double click also fires click twice, so the dialog appeared twice).

Registering Behaviors and Renderers

Both pieces can be registered by annotation or in faces-config.xml; the two ways are equivalent and work on both implementations (observed with the XML form below, a behavior and its renderer, used from a tag):

What Annotation faces-config.xml

Behavior

@FacesBehavior("id") on the class

<behavior> with <behavior-id> and <behavior-class>

Client behavior renderer

@FacesBehaviorRenderer(rendererType = "type") on the class

<render-kit> containing <client-behavior-renderer> with <client-behavior-renderer-type> and <client-behavior-renderer-class>

faces-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<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">
    <behavior>
        <behavior-id>bookshelf.greet</behavior-id>
        <behavior-class>org.example.bookshelf.web.XmlGreetBehavior</behavior-class>
    </behavior>
    <render-kit>
        <client-behavior-renderer>
            <client-behavior-renderer-type>bookshelf.greet</client-behavior-renderer-type>
            <client-behavior-renderer-class>org.example.bookshelf.web.XmlGreetRenderer</client-behavior-renderer-class>
        </client-behavior-renderer>
    </render-kit>
</faces-config>
XmlGreetBehavior.java
package org.example.bookshelf.web;

import jakarta.faces.component.behavior.ClientBehaviorBase;

/** Registered in faces-config.xml instead of with an annotation. */
public class XmlGreetBehavior extends ClientBehaviorBase {

    @Override
    public String getRendererType() {
        return "bookshelf.greet";
    }
}
XmlGreetRenderer.java
package org.example.bookshelf.web;

import jakarta.faces.component.behavior.ClientBehavior;
import jakarta.faces.component.behavior.ClientBehaviorContext;
import jakarta.faces.render.ClientBehaviorRenderer;

public class XmlGreetRenderer extends ClientBehaviorRenderer {

    @Override
    public String getScript(ClientBehaviorContext context, ClientBehavior behavior) {
        return "alert('hello from " + context.getEventName() + "')";
    }
}

The registered ids and types are plain strings, and a mismatch is silent. Observed on both implementations: a behavior whose getRendererType() named a renderer that did not exist rendered no script and logged nothing, even in the Development project stage. A tag whose behavior-id is not registered fails with an error on both implementations (see the attaching section).

The specification’s text for renderer registration by annotation (section 8.3.1) spells the annotation @FacesClientBehaviorRenderer, which does not exist. The annotation in the API is jakarta.faces.render.FacesBehaviorRenderer, and that is the one that worked.

Managed behaviors. @FacesBehavior(value = "id", managed = true) makes the behavior a CDI bean, so it can use @Inject (spec 5.6.1 lists it with managed converters and validators). The class needs a bean-defining annotation such as @Dependent when beans.xml uses bean-discovery-mode="annotated", as for the other managed Faces artifacts. Observed with a ClientBehaviorBase that read an injected bean in getScript: MyFaces rendered the expected script ("Delete one of 3 reviews?"), but Mojarra 4.1.17 rendered the button with no script at all and no message. Mojarra’s CDI wrapper for managed behaviors (com.sun.faces.cdi.CdiBehavior) implements only Behavior, not ClientBehavior, and the tag handler adds only ClientBehavior instances to the holder, which explains it. A managed client behavior is therefore not portable today; keep the behavior unmanaged and give it a ValueExpression that resolves the bean, as bs:confirmDynamic does.

Behaviors in Other Libraries and in Composite Components

f:ajax is the behavior everyone uses. PrimeFaces documents p:confirm as "a behavior element used to integrate with global confirm dialog/popup": it is nested in a p:commandButton or p:commandLink and works with p:confirmDialog, and its message can also be a facet of the parent. The PrimeFaces documentation warns that escape="false" on the message exposes an XSS vector unless the content is trusted. Use the library’s behavior where you use the library. Write your own when you need something the library does not cover, or when the page has no component library.

A composite component can pass a behavior on to one of its inner components with cc:clientBehavior in the interface, which is how a composite can accept f:ajax from its caller. The page on composite components covers it.

Mojarra and MyFaces

Everything below was observed in the runs described at the top of the page:

Topic Mojarra 4.1.17 MyFaces 4.1.4

Script of one behavior on a command button

onclick="return confirm('...')"

onclick="return faces.util.chain(this, event,function(event){return ...});"

Two script behaviors (or a user onclick and one behavior) on a command button

onclick="faces.util.chain(...)" with no return: dismissing the dialog does not stop the submit (observed in Chrome, one POST after cancel)

return faces.util.chain(...): cancel stops the submit

Two script behaviors on a command link

Cancel stops everything

Cancel stops everything

Script behavior plus f:ajax on a button

chain(...);event.preventDefault(), cancel stops the request

chain(...); return false;, cancel stops the request

SUBMITTING hint on a button, dialog accepted

return script;event.preventDefault(): the form is still submitted

chain(...); return false;: nothing is submitted

h:outputLink, h:link default event

action, rendered as onaction

none: error "eventName could not be defined"

ClientBehaviorContext.getParameters() for non-command holders

empty collection

null

@FacesBehavior(managed = true) on a client behavior

no script rendered (the CDI wrapper is not a ClientBehavior)

works

Faces script on a page with a bare custom holder

not added: faces is not defined

added

Render id of the enclosing table from a button in one of its rows

resolved to the row (reviews:list:0)

resolved to the table

Phase text of PhaseId

INVOKE_APPLICATION 5

INVOKE_APPLICATION(5)

The two rows about the chain matter most in practice: the order and the result of the scripts differ, so test cancel and accept on both implementations and in a real browser if you support both.

What Changed in Which Version

  • 2.0: the component behavior model: Behavior, ClientBehavior, ClientBehaviorHolder, ClientBehaviorBase, ClientBehaviorContext, ClientBehaviorHint, ClientBehaviorRenderer, @FacesBehavior, @FacesBehaviorRenderer and AjaxBehavior (all Since: 2.0 in the API documentation).

  • 2.3: the constants ClientBehaviorContext.BEHAVIOR_EVENT_PARAM_NAME and BEHAVIOR_SOURCE_PARAM_NAME are added (Since: 2.3); the CDI section of the specification lists @FacesBehavior(managed = true) among the objects that must support @Inject.

  • 3.0: the packages move from javax.faces to jakarta.faces, and the reserved request parameter names carry the new prefix (jakarta.faces.behavior.event, jakarta.faces.source).

  • 4.0: FacesBehavior.Literal is added for inline use of the qualifier (Since: 4.0); the script resource is faces.js instead of jsf.js.

  • 4.1: full state saving (jakarta.faces.PARTIAL_STATE_SAVING=false) is deprecated for removal (StateManager Javadoc); the behavior API itself is unchanged.

  • 5.0 (upcoming): the milestone releases remove deprecated APIs; this page was not checked against them and nothing here should be read as describing 5.0.

Where the Books Are Outdated

The oldest books in the bibliography (JavaServer Faces in Action by Mann, Mastering JavaServer Faces by Dudney and others, and Pro JSF and Ajax by Jacobi and Fallows, all from 2004 to 2006) were written before JSF 2.0, so they cannot describe the behavior model. In JSF 1.x client-side logic reached a component through its renderer (or a JSP tag that emitted the script), through hand-written XMLHttpRequest code, or through the Ajax libraries of the time; a behavior attaches the same logic from the page, without touching the component or its renderer. The books that can cover behaviors, Burns and Schalk’s reference and Leonard’s cookbook for JSF 2.0, use the javax.faces packages, so every type on this page needs the jakarta.faces name and the Jakarta namespaces. The Jacobi and Fallows book treats Ajax-enabled components through renderers, and the XUL and HTC render kits it uses are gone. The PrimeFaces books describe component libraries that have moved on; for the confirmation dialog, PrimeFaces 16 documents the p:confirm behavior described above (not run), so read the library’s current documentation rather than the book. Everything on this page was checked on 4.1.

Pitfalls

  • Pasting text into a JavaScript string. Escape every value that reaches a script (JsStrings above). A title, a user name or a message from a resource bundle can contain a quote. The rendered handler is an inline attribute (observed on both implementations); a strict Content-Security-Policy that forbids inline handlers affects every behavior and was not tested here.

  • A confirm dialog as authorization. It only asks the user. The server must check permissions for the action regardless.

  • Two script behaviors on one command button on Mojarra. Cancelling does not stop the submit. Use one behavior, or an h:commandLink, or f:ajax (which always prevents the default submit), or test on the implementation you deploy.

  • EL with a loop variable in a plain property. It is evaluated once at build time. Take a ValueExpression and a handler that passes it through.

  • Properties without state. They disappear under full state saving, and the failure is a missing script, not an error.

  • No event on h:outputLink or h:link. Write event="click": Mojarra renders onaction, MyFaces fails.

  • The SUBMITTING hint without a postback. The button stops submitting.

  • A renderer type that does not match. No script, no message.

  • Null context values. getParameters() can be null on MyFaces, and getSourceId() is null on both.

  • No faces.js on pages that use only your component (Mojarra). Add @ResourceDependency.

  • Trusting decode input. Everything in the request is chosen by the client, including the behavior event and the source. A request without a valid view state is not decoded at all (observed), but that is not input validation.

  • A session-scoped bean behind binding. One behavior instance would be shared by every view of the session. Keep it request scoped.

  • A managed client behavior on Mojarra. The behavior is silently ignored.