Programmatic Facelets
|
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, on Apache Tomcat 11.0.26 with Weld 6.0.4.Final, with the project compiled for
Java 21. The versions were checked on 2026-10-08. The feature this page covers is new in Faces 4.0 (Jakarta EE 10), so
everything below applies to 4.0 and 4.1 alike; there is nothing to look for in Faces 3.0 or in the javax-era 2.x
releases. Faces 5.0 is still a milestone and is mentioned nowhere else on this page.
The Bookshelf example in this page was compiled and run for real: the exact class shown below was built into a WAR with
Maven, served by Apache Tomcat 11.0.26 (embedded) with Weld, once with Mojarra 4.1.17 and once with MyFaces 4.1.4, and a
first request, a button click (a postback) and a view that has a physical file of the same name were driven with
curl. The HTML shown is the output of that run. The signatures of View and Facelet were read from the API jar
and, for the lookup mechanism, from the MyFaces 4.1.4 source; the Mojarra internals were not inspected, so every
statement about how Mojarra behaves is something the run showed, not something read in its code.
A normal Facelets view is an XHTML file (see the Facelets overview):
the implementation compiles it into a tree of handlers and, at the first
request, runs those handlers to fill the UIViewRoot with components. Faces 4.0 adds a second way to reach the same
point: a Java class that fills the UIViewRoot itself. This page shows how to write one, when it is worth the extra
code, and what it cannot do.
What It Is
A programmatic Facelet is a CDI bean that
-
extends the abstract class
jakarta.faces.view.facelets.Facelet, and -
carries the qualifier annotation
jakarta.faces.annotation.View, whose only element is the view id the bean answers to, for example@View("/catalogue.xhtml").
When a request names /catalogue.xhtml, the Facelets view handler asks CDI for a bean of type Facelet qualified by
@View("/catalogue.xhtml"). If it finds one, it calls that bean’s apply method instead of compiling a file; if it
finds none, it looks for the XHTML file as usual. The user-visible result is the same as for an XHTML view: the same
FacesServlet mapping, the same lifecycle, the same
state saving, the same renderers.
with @View('/catalogue.xhtml')?"} java["CatalogueView.apply()
fills the UIViewRoot in Java"] xhtml["Compile catalogue.xhtml
into handlers and apply them"] tree["UIViewRoot with its component tree"] lifecycle["Normal lifecycle:
restore, decode, validate, invoke, render"] request --> cdi cdi -->|"yes"| java cdi -->|"no"| xhtml java --> tree xhtml --> tree tree --> lifecycle
Source: BalusC: What’s new in Faces 4.0 (the section on the new API to build Facelets programmatically) and the MyFaces 4.1.4 sources DefaultFaceletFactory and ViewIdSupport, where the lookup CDIUtils.get(bm, Facelet.class, true, View.Literal.of(uri)) is made
The API
The API is small on purpose. Both types were read from jakarta.faces-api-4.1.2.jar.
// jakarta.faces.annotation.View
@Qualifier
@Retention(RUNTIME)
@Target({ TYPE, METHOD, PARAMETER, FIELD })
public @interface View {
String value(); // the view id, e.g. "/catalogue.xhtml"
// plus a nested AnnotationLiteral: View.Literal.of("/catalogue.xhtml")
}
// jakarta.faces.view.facelets.Facelet
public abstract class Facelet {
public void applyMetadata(FacesContext context, UIComponent root) throws IOException; // has a default body
public abstract void apply(FacesContext context, UIComponent root) throws IOException;
}
-
Viewis a CDI qualifier, not a marker the container scans for. This is why the class must be a bean: an@ApplicationScoped(or other scope) annotation makes it one in a WAR whosebeans.xmlusesbean-discovery-mode="annotated". The nestedView.Literallets code, including the implementation, build the qualifier for a given view id and look the bean up programmatically. -
Facelethas one abstract method.applyreceives theFacesContextand the component to populate, which for a view is theUIViewRoot. The Javadoc describes it as "the parent or root object in aFaceletHandlercomposition": it takes care of populating the parent in relation to the create and restore lifecycle of Faces. -
applyMetadatais optional. It is not abstract, so a subclass that has no view parameters or other metadata does not need to override it. This page does not override it, and a view that carries metadata (the equivalent off:metadata) was not tried.
Both the abstract class and the annotation are in the standard API, so the same code compiles against Mojarra and MyFaces without implementation-specific imports.
Source: View (API 4.1) and Facelet (API 4.1)
A View Written in Java
The Bookshelf catalogue is rendered by a view with no XHTML file. First, the data it lists comes from an ordinary CDI bean, which the view injects like any other bean, and which EL can reach by name.
package org.example.bookshelf.service;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Named;
import java.util.List;
@Named
@ApplicationScoped
public class BookService {
private final List<String> titles = List.of("Dune", "Neuromancer", "Solaris <draft>");
public List<String> getTitles() {
return titles;
}
public int getCount() {
return titles.size();
}
}
Then the view itself. It writes the <head> as literal markup, creates a body and a form, produces one list item
per book from the injected data, binds the status text to an EL expression and gives the button a listener.
package org.example.bookshelf.web;
import jakarta.el.ELContext;
import jakarta.el.ExpressionFactory;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.faces.annotation.View;
import jakarta.faces.application.Application;
import jakarta.faces.application.StateManager;
import jakarta.faces.component.UIComponent;
import jakarta.faces.component.UIOutput;
import jakarta.faces.component.html.HtmlBody;
import jakarta.faces.component.html.HtmlCommandButton;
import jakarta.faces.component.html.HtmlForm;
import jakarta.faces.component.html.HtmlOutputText;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.facelets.Facelet;
import jakarta.inject.Inject;
import java.io.IOException;
import org.example.bookshelf.service.BookService;
@View("/catalogue.xhtml")
@ApplicationScoped
public class CatalogueView extends Facelet {
@Inject
private BookService books;
@Override
public void apply(FacesContext context, UIComponent root) throws IOException {
if (!context.getAttributes().containsKey(StateManager.IS_BUILDING_INITIAL_STATE)) {
return;
}
Application app = context.getApplication();
ELContext el = context.getELContext();
ExpressionFactory ef = app.getExpressionFactory();
root.getChildren().add(markup(app, "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\">"
+ "<title>Catalogue</title></head>"));
HtmlBody body = create(app, HtmlBody.COMPONENT_TYPE);
root.getChildren().add(body);
body.getChildren().add(markup(app, "<h1>Catalogue</h1><ul>"));
for (String title : books.getTitles()) {
body.getChildren().add(markup(app, "<li>"));
HtmlOutputText text = create(app, HtmlOutputText.COMPONENT_TYPE);
text.setValue(title);
body.getChildren().add(text);
body.getChildren().add(markup(app, "</li>"));
}
body.getChildren().add(markup(app, "</ul>"));
HtmlForm form = create(app, HtmlForm.COMPONENT_TYPE);
form.setId("form");
body.getChildren().add(form);
HtmlOutputText status = create(app, HtmlOutputText.COMPONENT_TYPE);
status.setId("status");
status.setValueExpression("value", ef.createValueExpression(el,
"#{bookService.count} books in the catalogue", String.class));
form.getChildren().add(status);
HtmlCommandButton button = create(app, HtmlCommandButton.COMPONENT_TYPE);
button.setId("hello");
button.setValue("Say hello");
button.addActionListener(event -> status.setValue("Hello from Java"));
form.getChildren().add(button);
root.getChildren().add(markup(app, "</html>"));
}
@SuppressWarnings("unchecked")
private static <T extends UIComponent> T create(Application app, String type) {
return (T) app.createComponent(type);
}
private static UIOutput markup(Application app, String html) {
HtmlOutputText out = create(app, HtmlOutputText.COMPONENT_TYPE);
out.setEscape(false);
out.setValue(html);
return out;
}
}
Nothing else is needed: no faces-config.xml entry, no web.xml mapping beyond the one that already sends
*.xhtml to the FacesServlet, and no file called catalogue.xhtml. A request for /bookshelf/catalogue.xhtml
produced this response on both implementations (Mojarra shown; the MyFaces output has different whitespace and different hidden
fields):
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Catalogue</title></head><body><h1>Catalogue</h1>
<ul><li>Dune</li><li>Neuromancer</li><li>Solaris <draft></li></ul>
<form id="form" name="form" method="post" action="/bookshelf/catalogue.xhtml;jsessionid=...">
<span id="form:status">3 books in the catalogue</span><input id="form:hello" type="submit" name="form:hello" value="Say hello" />
<input type="hidden" name="jakarta.faces.ViewState" ... />
</form>...</body></html>
Posting the form with the button parameter returned the same page with <span id="form:status">Hello from Java</span>:
the listener ran in the Invoke Application phase and the changed value was rendered. The title Solaris <draft>
was escaped, as it is for any h:outputText.
Source: BalusC: What’s new in Faces 4.0 and View (API 4.1)
How the Code Works
Declaring the View
@View("/catalogue.xhtml") binds the bean to a view id, not to a URL. The value is the same string a navigation
outcome or UIViewRoot.getViewId() would hold, with the leading slash and the .xhtml suffix. A request for any URL
that the FacesServlet maps to that view id reaches the bean.
The class also needs a scope annotation. In the run @ApplicationScoped was used: the bean is a single instance that
builds the tree for every request and keeps no state of its own, which is the safe reading of this API. A programmatic
Facelet that kept per-request fields on an application-scoped bean would share them between users; keep the state in the
components or in other beans, as above.
Building the Tree
apply receives the parent to populate. The code adds children with the same API a custom component or a
ComponentSystemEvent listener would use: Application.createComponent(String) with the component type, the setters
of the concrete class, and getChildren().add(...). Four patterns appear in the example.
-
Markup between components. A page needs
<html>,<head>and so on. There is no literal-text node in the API, so the example creates anHtmlOutputTextwithescapeset tofalseand the markup as its value. WithoutsetEscape(false)the markup is escaped and shows as visible text instead of being interpreted by the browser; this is the usual surprise reported with this approach. Because the closing</html>is added to theroot, after thebodycomponent, the document stays well formed. -
Components with ids.
form.setId("form")andstatus.setId("status")give stable client ids (form:status). Components without an explicit id receive generated ids, which work, but are less convenient in a test or a stylesheet. -
Expressions.
setValueExpression("value", ...)is the code equivalent ofvalue="#{...}"in an XHTML view. TheExpressionFactorycomes from theApplication, and theELContextfrom theFacesContext; evaluating expressions from Java is covered on the Expression Language page. The expression above is a composite one, with a literal part and a#{}part, and it was evaluated normally. -
Behaviour.
addActionListenertakes anActionListener, so a lambda is enough. An action method expression is set withsetActionExpressionand aMethodExpressionfrom the same factory; that variant was not run on this page.
The Initial State Guard
The first statement of apply returns early unless the context attributes contain
StateManager.IS_BUILDING_INITIAL_STATE. Faces sets that attribute while it builds the view for the first time and
while it rebuilds the initial view to restore a saved one, and clears it afterwards. The guard is taken from the
example in the Faces 4.0 announcement, and it expresses the intent: create the components only when the initial tree is
built, and never touch the tree when the view is being rendered or processed from saved state.
In the scratch run the page rendered correctly, and the button postback worked, with the guard removed as well, so with these two implementations the guard did not change the result of this example. Keep it anyway, as the cost is one map lookup, and the rule it states is the correct one for components that are created in code: a tree built outside the initial-state phase is a tree whose changes partial state saving does not know how to restore.
State and Postbacks
The components created in apply are part of the view and are saved like any others. The value set by the listener,
Hello from Java, survived to the render phase because it is stored as the component’s local value in the saved state.
The listener itself is not saved: it is attached again every time the initial tree is built, which is why it must not
capture per-request data. The lambda above captures only status, a component of the tree being built.
Expressions are also rebuilt each time. If a value must change between requests, write it to a bean or to a component
value, never to a field of the Facelet bean.
Source: the scratch run described at the top of this page, and the Jakarta Faces 4.1 specification on the view declaration language and state saving
When It Is Useful
For a typical page, XHTML is the better tool: it is shorter, a designer can read it and the tag library covers the common cases. A programmatic Facelet pays off where the shape of the page is data rather than layout.
-
Generated UIs. An admin console that shows one input per configured field, a form produced from a schema, a report with a column set chosen at runtime, a wizard defined by a configuration file. A loop in Java that creates the components is easier to test and to follow than the same loop expressed with
ui:repeator JSTL tags, because it can use any Java construct. -
Dynamic trees you would otherwise build with a binding. (A component built from a tag in XHTML is described on the component tree section; a reusable component with its own renderer is covered on the custom components page.) Code that populates a container in a
@PostConstructor in abindingsetter can be moved into the view class, where its purpose is clear. -
Views packaged in a library. A shared module can contribute a complete page as a jar with only classes, with no resource files to ship. Because the lookup is by bean qualifier, an application can also choose between two implementations of the same view id with CDI alternatives; that combination was not tried here.
-
Tests. The view is a bean with a plain method, so a test can in principle call
applywith a mockedFacesContextand assert on the components, without parsing a file. This was not tried.
The Faces 4.0 announcement calls the new API "very minimal" and expects convenience methods or tree builders on top of
it; no such builder exists in the 4.1 API, so any DSL you want is code you write yourself. The example’s create and
markup helpers are the beginning of one.
Java View Versus a File
When an XHTML file and a bean answer to the same view id, the Java view won in the scratch run on both Mojarra 4.1.17
and MyFaces 4.1.4: the response contained the markup built by CatalogueView, not the text of the file. In the MyFaces
source this is by design, as the factory asks CDI first and compiles the file only when no bean is found. Do not rely
on it to override a file in production; keeping one source for each view id avoids the question.
Limits
-
No templating. There is no
ui:composition,ui:define,ui:includeorui:decoratefor a Java view (the tags are described on the templating page). If the page must share a layout with the XHTML pages, the Java code reproduces it by hand (the example writes its own<html>), or includes another Facelet through theFaceletContext, which this page did not try. A reader on the Faces 4.0 announcement asked how to combine the two and the thread has no answer. -
Tag libraries do not apply.
h:*,f:*,c:*and custom tag handlers are XML tags interpreted while compiling a file. A Java view creates the underlying component classes directly, so a feature that exists only as a tag handler with no component behind it (for examplec:if,ui:param; see the build time and render time page) has no direct equivalent. In Java the loop and the condition are ordinary code that runs whenapplyruns. -
Verbose and untyped. Each element is three or four lines, and
createComponentreturns aUIComponentthat the helper casts. A mistake in a component type string is found at runtime, not at compile time. -
Not editable while the server runs. An XHTML view is re-read when the refresh period elapses in development; a view class is compiled Java and needs a rebuild and a redeploy or a hot-swap tool. This follows from how Java works and was not measured.
-
CDI is required. The lookup goes through the bean manager (the MyFaces source checks that CDI is available before asking for the bean). In a container without CDI, or a WAR whose bean archive does not include the class, the bean is not found and the request is treated as a request for a view that does not exist. Check
beans.xmland the scope annotation first; this failure was not reproduced on this page. -
Designers cannot read it. The page is no longer a document, and tools such as IDE previews and design systems that parse XHTML do not see it.
Common Mistakes
-
Markup shown as text. The markup component was left with its default
escapeoftrue. SetsetEscape(false)on every component that holds literal HTML. -
The view is not found. The class is not a bean (no scope annotation in a bean archive with
bean-discovery-mode="annotated"), CDI is not present, or the@Viewvalue does not match the view id, which starts with/and includes the suffix. -
State kept on the
Faceletbean. The bean is shared; per-user data in its fields is visible to everyone. Keep the bean stateless. -
A listener that captures request data. The listener is rebuilt for each request, not saved, so a lambda that captured a value from the first request does not see it again on the postback.
-
Components without ids in a form. Generated ids differ if the tree is built in a different order. Give explicit ids to every component a test, a script or a message refers to.
-
Expecting
f:metadataand view parameters to work unchanged.applyMetadatais a separate method, and this page did not test view parameters on a Java view. -
Mixing a layout in two places. A header written in Java and the same header in a template drift apart. If most pages are XHTML, generate only the part that is dynamic.
What Changed Between Versions
| Version | Change relevant to programmatic Facelets |
|---|---|
JSF 2.x and Faces 3.0 |
No equivalent: a view is an XHTML file, or a JSP page before Faces 4.0. Trees built in Java were attached to a view through |
Faces 4.0 |
The feature is introduced: the |
Faces 4.1 |
The same API (the 4.1 API documentation of both types was read for this page); nothing about it changes in the 4.1 behaviour measured here. |
Faces 5.0 (upcoming, milestone |
The milestone issue list names no change to |
Where the Books Are Outdated
None of the eight books consulted for this guide covers this feature: all of them predate Faces 4.0. Two older ideas are close to it.
Aranda and Wadia’s Facelets Essentials (2008) describes writing custom Facelets tag handlers in Java, which is the
tag handler API still available, but it builds nothing for a whole view. The
books on JSF 2.0 build dynamic component trees in a bean through binding or in listeners; that approach still works, and a
programmatic Facelet is the cleaner place for it when the whole view is generated. In all of them the view is an XHTML or JSP
file, and the javax.faces classes they use are jakarta.faces today.
Related Pages
The XHTML views, templating, expression language, HTML5-friendly markup and the difference between build-time and render-time tags are the counterparts of what a Java view replaces: the Facelets and XHTML views page, the templating page, the Expression Language page, the HTML5-friendly markup page and the build time and render time page. The Bookshelf catalogue shown here is one of the views that can be built either way.
References
-
BalusC: What’s new in Faces 4.0, the announcement of the feature and the origin of the initial-state guard
-
jakarta.faces.annotation.Viewandjakarta.faces.view.facelets.Facelet, the two API types -
The Facelets view handling sections of the Jakarta Faces 4.1 specification for the
ViewDeclarationLanguagethat callsapply