Tag Files and Tag Libraries
|
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 Java classes were compiled with --release 21). Faces 4.0
behaves the same for everything below, except for the schema version noted in What Changed per Version. The listings are files, or excerpts of
files, of a scratch Bookshelf WAR that was deployed on Apache Tomcat 11.0.26 with Weld 6.0.4.Final, once with Mojarra and
once with MyFaces, and requested with curl: the output and the error messages quoted come from those runs (checked on
2026-10-08). Where the two implementations disagree, the text says so. Faces 5.0 is still a milestone release and is not
described as current behaviour.
Facelets pages use tags such as h:inputText and ui:insert. Each tag belongs to a tag library, and the library is
found through the XML namespace the page declares. The standard libraries (jakarta.faces.html, jakarta.faces.core,
jakarta.faces.facelets, jakarta.faces.composite) are the libraries every implementation ships; the specification
describes the same mechanism for tag libraries written by the application author or by a third party. This page shows the
four things an application can put into such a library, in increasing order of power:
-
a tag file, an XHTML file that becomes a tag, with no Java at all
-
a tag that already exists in Faces, such as a converter, a validator or a component, under a name of your choice
-
a tag handler, a Java class that runs while the view is being built
-
an EL function, a static method that expressions can call
The page ends with a decision guide for choosing between these, a composite component and a custom component.
The Tag Library Descriptor
A library is described by an XML file whose name ends in .taglib.xml and whose root element is facelet-taglib. The
Bookshelf library uses one namespace, urn:org.example.bookshelf:tags, and any string is allowed there: it is only a key.
The file starts like this, and the entries that follow are shown one by one in the rest of the page:
<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:org.example.bookshelf:tags</namespace>
<short-name>bs</short-name>
...
</facelet-taglib>
Source: Facelet taglib schema web-facelettaglibrary_4_1.xsd and Jakarta EE Tutorial: Defining the Custom Component Tag in a Tag Library Descriptor
The elements that can appear, as defined by the 4.1 schema:
| Element | Meaning |
|---|---|
|
The namespace pages use to select the library. Required unless the library is given as a |
|
An advisory short name for tags of the library, "advisory" being the schema’s word. In both runtimes the prefix is whatever the page declares with |
|
One tag, with a |
|
An EL function ( |
|
Makes every composite component of one resource library (Choosing: Tag File, Composite, Component, Handler or Function) available under this namespace. |
|
An alternative to listing tags: the fully qualified name of a Java class that provides the library. The schema gives no further definition, so this is implementation specific; it was not used for this page. |
Registering a Library
The specification requires two ways to make a library known: a context parameter, and automatic discovery of descriptors packaged in JARs.
-
jakarta.faces.FACELETS_LIBRARIES. A semicolon separated list of paths, each starting with a slash and relative to the web application root. This is how the Jakarta EE Tutorial wires itsbookstore.taglib.xml. -
Auto-discovery. A file whose name ends in
.taglib.xml, placed in theMETA-INFdirectory of a JAR on the web application’s class path, is found without any configuration.
The Bookshelf registers its library in web.xml (a second, private descriptor used for the failing examples of this page is
listed the same way and is left out):
<context-param>
<param-name>jakarta.faces.FACELETS_LIBRARIES</param-name>
<param-value>/WEB-INF/bookshelf.taglib.xml</param-value>
</context-param>
Source: Jakarta Faces 4.1 specification, 11.1.3 Application Configuration Parameters and Jakarta EE Tutorial: Defining the Custom Component Tag in a Tag Library Descriptor
What the runs showed about discovery, identical on Mojarra and MyFaces:
-
A descriptor named
META-INF/bookshelf-ext.taglib.xmlin a JAR underWEB-INF/libwas found. So wasMETA-INF/cls.taglib.xmlplaced inWEB-INF/classes, which is where Maven putssrc/main/resources/META-INF. -
A descriptor in
WEB-INF/auto.taglib.xmlthat was not listed in the context parameter was not found. In the development stage the page then shows a warning and the tag is copied to the output as plain text. Mojarra says the page "calls for XML namespace … declared with prefix web but no taglibrary exists for that namespace". MyFaces says the page "declares namespace … and uses the tag web:flag , but no TagLibrary associated to namespace". -
A path in the context parameter that does not exist is only logged (Mojarra as a warning that mentions
jakarta.faces.CONFIG_FILES, MyFaces asSEVERE: Error loading library). The application starts, and the first page using one of the library’s tags or functions fails:Function [bs:formatIsbn] not found. -
The old Facelets 1.x parameter
facelets.LIBRARIESand the Faces 2.x namejavax.faces.FACELETS_LIBRARIESare not read by either runtime in 4.1: the Bookshelf pages failed with the same "not found" error when registered under those names.
The specification also says that a descriptor that cannot be parsed must make the application fail to deploy.
Tag Files
A tag file is the simplest thing a library can contain: an XHTML file, written like any other Facelets file, which a
tag entry turns into a tag. The entry’s source element names the file:
<tag>
<tag-name>bookCard</tag-name>
<source>tags/bookCard.xhtml</source>
<attribute>
<name>title</name>
<required>true</required>
</attribute>
</tag>
The file below is the tag. It is a ui:composition, so everything outside that element is ignored by Facelets (the
ui:composition VDL page says so), exactly as in a template client (see templates and template clients). Each attribute the page writes on the tag becomes a
variable of the same name inside the file, and the body of the tag is inserted where the file has an unnamed ui:insert:
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:ui="jakarta.faces.facelets">
<div class="book-card">
<h3>#{title}</h3>
<p>by #{author}<ui:fragment rendered="#{not empty year}">, #{year}</ui:fragment></p>
<ui:insert/>
</div>
</ui:composition>
A page binds the namespace and uses the tag like any other. The tag is used here inside ui:repeat with the EL functions of
EL Functions:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:ui="jakarta.faces.facelets"
xmlns:bs="urn:org.example.bookshelf:tags">
<h:head><title>Cards</title></h:head>
<h:body>
<ui:repeat value="#{catalogueBean.books}" var="b">
<bs:bookCard title="#{b.title}" author="#{b.author}" year="#{b.year}">
<p>ISBN #{bs:formatIsbn(b.isbn)} (#{bs:initials(b.author)})</p>
</bs:bookCard>
</ui:repeat>
<bs:bookCard title="Literal title" author="Someone"/>
<bs:panel heading="Panel one">Body text<ui:define name="footer">custom footer</ui:define></bs:panel>
<bs:panel heading="Panel two">Body two</bs:panel>
</h:body>
</html>
Source: Jakarta Faces 4.1 VDL: ui:composition, Jakarta Faces 4.1 VDL: ui:insert and Jakarta Faces 4.1 specification, 10.3.2 Facelet Tag Library mechanism
The response, identical on both implementations apart from the development-stage message div that MyFaces appends, was one card per book. The first was:
<div class="book-card">
<h3>Domain-Driven Design</h3>
<p>by Eric Evans, 2003</p>
<p>ISBN 978-0-32112-521-7 (EE)</p>
</div>
A tag file is resolved when the view is built (see Where a Handler Runs for what that means in the lifecycle). Nothing in it
runs in the later phases, but the components it creates do, so an h:inputText in a tag file converts, validates and
updates the model like any other input.
Where the source Path Is Resolved
The source path is resolved relative to the descriptor file, not to the web application root. With the descriptor at
/WEB-INF/bookshelf.taglib.xml, the entry tags/bookCard.xhtml is /WEB-INF/tags/bookCard.xhtml. An entry that
starts with a slash is not treated as an absolute path: <source>/WEB-INF/tags/panel.xhtml</source> failed on both
implementations with "Facelet //WEB-INF/tags/panel.xhtml not found at: file:/WEB-INF/tags/panel.xhtml". A path with ../
did work. For a library packaged in a JAR the same rule means a file next to the descriptor, so the JAR in Packaging a Library in a JAR
keeps its tag files in META-INF/tags/.
Attributes of a Tag File
The attributes a tag file receives are not declared anywhere: any attribute the page writes becomes a variable, and any
variable the file uses but the page does not pass is looked up as an ordinary EL name. A tag may declare its attributes in
the descriptor, with required, type and method-signature as optional children (type defaults to
java.lang.Object). The Bookshelf declares the one mandatory attribute of bs:bookCard (the entry above) and nothing
else. What happened with a missing mandatory attribute and with an undeclared one:
| Page | Mojarra 4.1.17 | MyFaces 4.1.4 |
|---|---|---|
|
HTTP 500, |
HTTP 200, the card is rendered with an empty title |
|
HTTP 200, the attribute is ignored |
HTTP 200, the attribute is ignored |
So required="true" on a tag-file tag is enforced by Mojarra and ignored by MyFaces. Do not rely on it for correctness; if
the file needs a value, guard against its absence inside the file (for example with rendered="#{not empty title}"), or
move to a composite component, whose cc:attribute declarations are part of its interface.
The Body and Named Slots
An unnamed <ui:insert/> receives the body of the tag. A tag file can also declare named slots, which the page fills with
ui:define exactly as in a template client; a slot that the page leaves out shows its default content:
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="jakarta.faces.facelets">
<section class="panel">
<header>#{heading}</header>
<div class="body"><ui:insert/></div>
<footer><ui:insert name="footer">default footer</ui:insert></footer>
</section>
</ui:composition>
used as:
<bs:panel heading="Panel one">Body text<ui:define name="footer">custom footer</ui:define></bs:panel>
<bs:panel heading="Panel two">Body two</bs:panel>
which rendered, on both implementations, a section whose footer was custom footer in the first case and default footer
in the second.
The same file works through ui:decorate and ui:include, which makes the difference between the three visible. With
<ui:decorate template="/WEB-INF/tags/bookCard.xhtml"> and ui:param children the body was inserted. With
<ui:include src="/WEB-INF/tags/bookCard.xhtml"> the parameters were passed but the body of the ui:include was
silently dropped, on both implementations. A tag file is therefore a template that has been given a name, a namespace and a
body.
Expressions Passed to a Tag File
An attribute value that is an EL expression is passed on as an expression, not as its value. Three consequences follow, all observed on both runtimes. The tag files:
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="jakarta.faces.facelets">
<p>first: #{title} / second: #{title}</p>
</ui:composition>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="jakarta.faces.facelets">
<p>catalogueBean: #{empty catalogueBean ? 'not set' : 'a CDI bean'}, name: [#{name}]</p>
</ui:composition>
and the page that uses them with the CatalogueBean of the scenario, whose expensiveTitle() increments a counter:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:bs="urn:org.example.bookshelf:tags">
<h:head><title>Expressions</title></h:head>
<h:body>
<bs:twice title="#{catalogueBean.expensiveTitle()}"/>
<bs:leak/>
</h:body>
</html>
Source: Jakarta Faces 4.1 VDL: ui:param and Jakarta Faces 4.1 specification, 5 Expression Language Facility
The output was first: Computed title 1 / second: Computed title 2 and catalogueBean: a CDI bean, name: []:
-
Every use re-evaluates.
#{title}used twice calledexpensiveTitle()twice. An expression with a cost or a side effect (a method call that queries a database, say) should not be passed to a tag file that uses it more than once; pass a cheap, cached bean property instead. -
An attribute the page did not pass is a plain EL name.
#{catalogueBean}was never an attribute ofbs:leak, so the expression found the CDI bean of that name. A page that forgets an optional attribute named like a bean or an implicit object therefore silently reads that. Choose attribute names that no bean uses (bookTitlerather thanbook), or test foremptybefore using the value. A missing attribute whose name nothing owns evaluates to nothing (name: []). -
The expression can be written to. The next file passes
valuestraight to an input. Becausevalueis the caller’s expression, the input reads and updates the caller’s property, which makes tag files a good way to build a labelled field.
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:ui="jakarta.faces.facelets">
<div class="field">
<h:outputLabel for="#{name}" value="#{label}"/>
<h:inputText id="#{name}" value="#{value}"/>
<h:message for="#{name}"/>
</div>
</ui:composition>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:bs="urn:org.example.bookshelf:tags">
<h:head><title>Ids</title></h:head>
<h:body>
<h:form id="f">
<bs:fieldId name="title" label="Title" value="#{formBean.title}"/>
<bs:fieldId name="author" label="Author" value="#{formBean.author}"/>
<h:commandButton id="save" value="Save" action="#{formBean.save}"/>
</h:form>
<p id="saved">#{formBean.saved}</p>
</h:body>
</html>
Posting f:title=Dune and f:author=Herbert updated formBean.title and formBean.author on both runtimes
(the page printed Saved: Dune / Herbert / null).
Ids: a Tag File Is Not a Naming Container
A tag file adds components to its parent and nothing else; it does not create a naming container. If the file gives its input a fixed id and the page uses the tag twice in one form, the two inputs collide. This variant of the field, with a fixed id, reproduced it:
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:ui="jakarta.faces.facelets">
<div class="field">
<h:outputLabel for="input" value="#{label}"/>
<h:inputText id="input" value="#{value}" required="#{required}"/>
<h:message for="input"/>
</div>
</ui:composition>
Using <bs:field label="Title" .../> and <bs:field label="Author" .../> in the same form failed with HTTP 500 on both
implementations. Mojarra: "Component ID f:input has already been found in the view." MyFaces: "Component with duplicate id
"f:input" found. The first component is /WEB-INF/tags/field.xhtml at line 6 and column 74". The cure, already used by the field of Expressions Passed to a Tag File, is to make the id an attribute (id="#{name}", with for="#{name}" on the label and the message), so each use
supplies its own. A composite component does not have the problem, because it is a naming container (Choosing: Tag File, Composite, Component, Handler or Function).
Method Expressions Do Not Pass Through
A tag file can forward a value expression, as the field does with value. It cannot forward a method expression. This
button wrapper is registered with a method-signature declaration, in the hope that it would help:
<tag>
<tag-name>saveButton</tag-name>
<source>tags/saveButton.xhtml</source>
<attribute>
<name>action</name>
<method-signature>java.lang.String action()</method-signature>
</attribute>
</tag>
and implemented by this tag file
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:ui="jakarta.faces.facelets">
<h:commandButton id="#{id}" value="#{label}" action="#{action}"/>
</ui:composition>
used as <bs:saveButton id="save" label="Save" action="#{formBean.save}"/>, failed on submit with HTTP 500 on both
implementations. Mojarra: action="#{action}": ... action="#{formBean.save}": Property [save] not found on type [org.example.bookshelf.web.FormBean]. MyFaces: the same message wrapped in ContextAwarePropertyNotFoundException. The
variable action holds the caller’s expression and the EL evaluates it as a property read of formBean.save, not as a
method call. Writing the call with parentheses, action="#{formBean.save()}", did not help either: both runtimes reported
"Identity [action] was null and was unable to invoke". The method-signature declaration in the descriptor made no
difference.
This is the first reason to leave tag files: anything that must pass an action, an action listener or a validator method
down to a component needs a composite component, whose cc:attribute method-signature was designed for it. The
equivalent of the button above worked on both implementations:
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:ui="jakarta.faces.facelets"
xmlns:cc="jakarta.faces.composite">
<cc:interface>
<cc:attribute name="label" required="true"/>
<cc:attribute name="action" method-signature="java.lang.String action()" required="true"/>
</cc:interface>
<cc:implementation>
<h:commandButton id="button" value="#{cc.attrs.label}" action="#{cc.attrs.action}"/>
</cc:implementation>
</ui:composition>
Posting the form with the button f:save:button printed Saved: Dune / null / null on Mojarra and MyFaces. The
composite component page covers cc:interface in full.
Tags for Converters, Validators, Components and Composites
A tag entry does not have to point at a file or a Java handler. It can also publish something that Faces already knows
under a name and namespace of the application’s choice. The children of the entry decide what:
Child of tag |
What it publishes |
|---|---|
|
A tag file (Tag Files). |
|
A tag handler written in Java (Custom Tag Handlers). |
|
A |
|
A |
|
A client behavior by its |
|
A |
The two entries below turn the Bookshelf’s validator and converter into tags. The classes are ordinary @FacesValidator and
@FacesConverter classes with a property each (see validators and converters for how to
write them):
<tag>
<tag-name>minWords</tag-name>
<validator>
<validator-id>bookshelf.minWords</validator-id>
</validator>
</tag>
<tag>
<tag-name>upper</tag-name>
<converter>
<converter-id>bookshelf.upper</converter-id>
</converter>
</tag>
package org.example.bookshelf.tags;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
import jakarta.faces.validator.FacesValidator;
import jakarta.faces.validator.Validator;
import jakarta.faces.validator.ValidatorException;
@FacesValidator("bookshelf.minWords")
public class MinWordsValidator implements Validator<String> {
private int min = 2;
public void setMin(int min) { this.min = min; }
public int getMin() { return min; }
@Override
public void validate(FacesContext context, UIComponent component, String value) {
if (value == null || value.isBlank()) {
return;
}
int words = value.trim().split("\\s+").length;
if (words < min) {
throw new ValidatorException(new FacesMessage(FacesMessage.SEVERITY_ERROR,
"Please write at least " + min + " words.", null));
}
}
}
package org.example.bookshelf.tags;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
import jakarta.faces.convert.Converter;
import jakarta.faces.convert.FacesConverter;
import java.util.Locale;
@FacesConverter("bookshelf.upper")
public class UpperConverter implements Converter<String> {
private boolean trim;
public void setTrim(boolean trim) { this.trim = trim; }
@Override
public String getAsObject(FacesContext context, UIComponent component, String value) {
if (value == null) {
return null;
}
String v = trim ? value.trim() : value;
return v.toUpperCase(Locale.ROOT);
}
@Override
public String getAsString(FacesContext context, UIComponent component, String value) {
return value == null ? "" : value;
}
}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:bs="urn:org.example.bookshelf:tags">
<h:head><title>VC</title></h:head>
<h:body>
<h:form id="f">
<h:inputText id="notes" value="#{formBean.notes}">
<bs:minWords min="3"/>
</h:inputText>
<h:inputText id="title" value="#{formBean.title}">
<bs:upper trim="true"/>
</h:inputText>
<h:commandButton id="save" value="Save" action="#{formBean.save}"/>
<h:messages/>
</h:form>
<p id="saved">#{formBean.saved}</p>
</h:body>
</html>
Source: Jakarta EE Tutorial: Creating and Using a Custom Validator and Jakarta Faces 4.1 VDL: f:validator
Posting f:notes=two words and f:title= dune +` answered on both runtimes with the message "Please write at least 3
words.", and the title converted to `+DUNE. Two things to know about such tags:
-
Attributes are wired to bean properties by name.
min="3"calledsetMin(3)andtrim="true"calledsetTrim(true), without anyattributedeclaration. An attribute that matches no property is silently ignored: withminimum="3"the validator kept its default and answered "Please write at least 2 words.". -
The
f:validatorandf:convertertags work without a library, but they have no attributes that set properties of the instance (bindingpoints at a bean instead). A tag is the way to give a validator or converter named, documented parameters.
A tag that publishes a component needs a component class, registered here with @FacesComponent. The smallest one writes its own markup and has the
entry shown here; the custom components page extends the idea with renderers and a StateHelper:
package org.example.bookshelf.tags;
import jakarta.faces.component.FacesComponent;
import jakarta.faces.component.UIOutput;
import jakarta.faces.context.FacesContext;
import jakarta.faces.context.ResponseWriter;
import java.io.IOException;
/** A minimal component with its own tag declared in bookshelf.taglib.xml. */
@FacesComponent("bookshelf.Greeting")
public class GreetingComponent extends UIOutput {
@Override
public void encodeEnd(FacesContext context) throws IOException {
ResponseWriter writer = context.getResponseWriter();
writer.startElement("span", this);
writer.writeAttribute("class", "greeting", null);
writer.writeText("Hello, " + getValue(), null);
writer.endElement("span");
}
}
<tag>
<tag-name>greeting</tag-name>
<component>
<component-type>bookshelf.Greeting</component-type>
</component>
</tag>
<tag>
<tag-name>compositeField</tag-name>
<component>
<resource-id>bookshelf/field.xhtml</resource-id>
</component>
</tag>
Using <bs:greeting value="Ada"/> printed <span class="greeting">Hello, Ada</span>, and <bs:compositeField .../>
rendered the composite component resources/bookshelf/field.xhtml under the library’s own namespace and tag name on both
implementations. The resource-id form was introduced in Faces 2.2 (What Changed per Version).
Source: Jakarta EE Tutorial: Defining the Custom Component Tag in a Tag Library Descriptor and Jakarta Faces 4.1 specification, 10.3.2 Facelet Tag Library mechanism
Without any entry at all, a composite component is reachable through the namespace jakarta.faces.composite/<library>.
The descriptor element composite-library-name gives the same library a namespace of its own, which is the form to choose
when the namespace should not reveal the resource folder:
<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:org.example.bookshelf:composites</namespace>
<composite-library-name>bookshelf</composite-library-name>
</facelet-taglib>
With this file registered, xmlns:bsc="urn:org.example.bookshelf:composites" and <bsc:field .../> rendered the
composite field.xhtml of resources/bookshelf on both implementations.
Custom Tag Handlers
The specification calls the role of the tag handler class in Facelets "greatly reduced and in most cases unnecessary". A handler is the right tool only when a tag has to act on the view as it is being built: set a variable for its body, decide whether the body exists at all, repeat it, or attach something to the parent. Everything that renders output or takes part in the lifecycle belongs in a component (a custom component), and everything that merely reuses markup belongs in a tag file.
Where a Handler Runs
Facelets compiles each XHTML file into a tree of FaceletHandler objects, one per element. Building the view means
calling apply(FaceletContext, UIComponent) on the tree, depth first (the specification describes this in 10.2.1, and
build time and render time describes the same two moments for c:forEach). A TagHandler has two moments:
-
its constructor runs when the file is compiled ("only called when the Facelets View is compiled", says the API), and receives a
TagConfig; -
its
applymethod runs every time the view is built, which is on the first request and again at Restore View of every postback (see the phases).
The Bookshelf handlers below increment two counters of a small bean, so a page can print how often each moment occurs:
package org.example.bookshelf.tags;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Named;
import java.util.concurrent.atomic.AtomicInteger;
@Named("tagStats")
@ApplicationScoped
public class TagStats {
public static final AtomicInteger CONSTRUCTED = new AtomicInteger();
public static final AtomicInteger APPLIED = new AtomicInteger();
public int getConstructed() { return CONSTRUCTED.get(); }
public int getApplied() { return APPLIED.get(); }
}
A page with one bs:times tag printed #{tagStats.constructed} and #{tagStats.applied}; the table shows them after each request:
| Request | Handler constructed | apply calls |
|---|---|---|
|
1 |
1 |
same: second GET |
1 |
2 |
same: postback of the form (the POST itself) |
1 |
one more than before |
default in the development stage: first, second, third GET |
1, 2, 3 |
1, 2, 3 |
Mojarra and MyFaces gave the same numbers. In the development stage the refresh period is zero, so the file is compiled on every
request and the handler is constructed every time. With -1 (the default in production) the constructor ran once and the
object served every later request. The consequences are the rules of a servlet: a handler instance is shared by all
requests and all users, so it must hold no per-request or per-user state in fields, and anything it needs from the request
must be read in apply from the FaceletContext.
The API
The API lives in jakarta.faces.view.facelets. A handler extends TagHandler, whose constructor takes the TagConfig:
| Type | Use |
|---|---|
|
The base class. The protected fields |
|
|
|
One attribute of the tag. |
|
Extends the EL context: |
|
Report a problem with the attribute, the tag, or the Facelet. The runtime adds the file, line and column to the message. |
Two Handlers
bs:today exposes today’s date to its body as a variable, the way c:set would, but scoped to the tag. The pattern is
the one every handler that publishes a variable needs: save the previous mapping, set the new one, apply the body, restore the
old one in a finally block.
package org.example.bookshelf.tags;
import jakarta.el.ValueExpression;
import jakarta.el.VariableMapper;
import jakarta.faces.component.UIComponent;
import jakarta.faces.view.facelets.FaceletContext;
import jakarta.faces.view.facelets.TagAttribute;
import jakarta.faces.view.facelets.TagConfig;
import jakarta.faces.view.facelets.TagHandler;
import java.io.IOException;
import java.time.LocalDate;
/** {@code <bs:today var="today">...</bs:today>} exposes the current date to the tag body. */
public class TodayHandler extends TagHandler {
private final TagAttribute var;
public TodayHandler(TagConfig config) {
super(config);
TagStats.CONSTRUCTED.incrementAndGet();
this.var = getRequiredAttribute("var");
}
@Override
public void apply(FaceletContext ctx, UIComponent parent) throws IOException {
TagStats.APPLIED.incrementAndGet();
String name = var.getValue(ctx);
VariableMapper mapper = ctx.getVariableMapper();
ValueExpression previous = mapper.resolveVariable(name);
ValueExpression value = ctx.getExpressionFactory().createValueExpression(LocalDate.now(), LocalDate.class);
mapper.setVariable(name, value);
try {
nextHandler.apply(ctx, parent);
} finally {
mapper.setVariable(name, previous);
}
}
}
bs:times applies its body a number of times, in the manner of c:forEach, and rejects a negative count with a
TagAttributeException, which becomes an error that names the page, the line and the attribute:
package org.example.bookshelf.tags;
import jakarta.faces.component.UIComponent;
import jakarta.faces.view.facelets.FaceletContext;
import jakarta.faces.view.facelets.TagAttribute;
import jakarta.faces.view.facelets.TagAttributeException;
import jakarta.faces.view.facelets.TagConfig;
import jakarta.faces.view.facelets.TagHandler;
import java.io.IOException;
/** {@code <bs:times count="3">...</bs:times>} applies its body count times, when the view is built. */
public class TimesHandler extends TagHandler {
private final TagAttribute count;
private final TagAttribute max;
public TimesHandler(TagConfig config) {
super(config);
TagStats.CONSTRUCTED.incrementAndGet();
this.count = getRequiredAttribute("count");
this.max = getAttribute("max");
}
@Override
public void apply(FaceletContext ctx, UIComponent parent) throws IOException {
TagStats.APPLIED.incrementAndGet();
int limit = max == null ? 10 : max.getInt(ctx);
int requested = count.getInt(ctx);
if (requested < 0) {
throw new TagAttributeException(count, "count must not be negative");
}
int n = Math.min(requested, limit);
for (int i = 0; i < n; i++) {
nextHandler.apply(ctx, parent);
}
}
}
The TagStats lines only feed the counters of Where a Handler Runs. Both are declared with handler-class; the optional attribute children document the attributes (type and
required):
<tag>
<tag-name>today</tag-name>
<handler-class>org.example.bookshelf.tags.TodayHandler</handler-class>
<attribute>
<name>var</name>
<required>true</required>
<type>java.lang.String</type>
</attribute>
</tag>
<tag>
<tag-name>times</tag-name>
<handler-class>org.example.bookshelf.tags.TimesHandler</handler-class>
</tag>
and used from a page:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:bs="urn:org.example.bookshelf:tags">
<h:head><title>Handlers</title></h:head>
<h:body>
<bs:today var="today"><p>Today is #{today}.</p></bs:today>
<p>Outside the tag: [#{today}]</p>
<bs:times count="3"><span>x</span></bs:times>
<bs:times count="#{catalogueBean.books.size()}" max="2"><span>y</span></bs:times>
</h:body>
</html>
The page printed Today is 2026-10-08., an empty Outside the tag: [] (the variable was restored), three x and
two y (the second count, 3, was capped by max="2") on both implementations. <bs:today/> without var failed with
Attribute 'var' is required and <bs:times count="-1"> with count="-1" count must not be negative, each prefixed with
the page, line and column (Mojarra /neg.xhtml @5,26, MyFaces /neg.xhtml at line 5 and column 26).
A Handler Decides When the View Is Built
A handler’s decision is taken when the view is built, before the action of a postback has run. bs:times and ui:repeat
were given the same count, which lives in a session scoped bean that an action increments:
package org.example.bookshelf.web;
import jakarta.enterprise.context.SessionScoped;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.List;
import java.util.stream.IntStream;
@Named
@SessionScoped
public class CountBean implements Serializable {
private int n = 2;
public void more() {
n++;
}
public int getN() {
return n;
}
public List<Integer> getRange() {
return IntStream.range(0, n).boxed().toList();
}
}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:ui="jakarta.faces.facelets"
xmlns:bs="urn:org.example.bookshelf:tags">
<h:head><title>Count</title></h:head>
<h:body>
<h:form id="f">
<p id="times">times: <bs:times count="#{countBean.n}"><span>t</span></bs:times></p>
<p id="repeat">repeat: <ui:repeat value="#{countBean.range}" var="i"><span>r</span></ui:repeat></p>
<p id="stats">applied #{tagStats.applied}</p>
<h:commandButton id="more" value="More" action="#{countBean.more}"/>
</h:form>
</h:body>
</html>
Both showed two items on the first request. After the postback of the button:
-
ui:repeatshowed three on both implementations, because it reads its collection while rendering, after the action. -
bs:timesshowed two on MyFaces, the stale value taken at Restore View, and three on the next request. Mojarra showed three straight away, because it applied the handler a second time in that response (the counter rose by two for that request, and by one when the count was the literalcount="2").
Nothing in the specification says how many times apply runs for a request, so a handler must be written to be called
repeatedly and must not depend on either behaviour. This is the same hazard as c:forEach and c:if (see
build time and render time), and the same rule applies: use a component such as ui:repeat or rendered for anything that can
change after the view is built, and a handler only for decisions that cannot.
Two further extension points exist and were not run for this page. MetaTagHandler and ComponentHandler (the base
class a handler for a component, converter, validator or behavior tag must extend, according to its API) wire tag attributes
to a Java object through MetaRuleset rules. TagDecorator rewrites a tag before its handler is chosen, and its
implementations are listed in the context parameter jakarta.faces.FACELETS_DECORATORS.
EL Functions
A library can also publish static methods as EL functions, written #{prefix:name(arguments)} with the prefix the page bound
to the library’s namespace. The function element names the function, the class and the Java signature:
<function>
<function-name>formatIsbn</function-name>
<function-class>org.example.bookshelf.tags.IsbnFunctions</function-class>
<function-signature>java.lang.String formatIsbn(java.lang.String)</function-signature>
</function>
<function>
<function-name>initials</function-name>
<function-class>org.example.bookshelf.tags.IsbnFunctions</function-class>
<function-signature>java.lang.String initials(java.lang.String)</function-signature>
</function>
package org.example.bookshelf.tags;
/** Static methods published as EL functions by bookshelf.taglib.xml. */
public final class IsbnFunctions {
private IsbnFunctions() {
}
public static String formatIsbn(String isbn) {
if (isbn == null || isbn.length() != 13) {
return isbn;
}
return isbn.substring(0, 3) + "-" + isbn.substring(3, 4) + "-" + isbn.substring(4, 9) + "-"
+ isbn.substring(9, 12) + "-" + isbn.substring(12);
}
public static String initials(String name) {
StringBuilder sb = new StringBuilder();
for (String part : name.trim().split("\\s+")) {
sb.append(Character.toUpperCase(part.charAt(0)));
}
return sb.toString();
}
}
The signature has the form ReturnType name(ParameterType, ...), with fully qualified class names (the schema borrows the
syntax of the JSP tag library descriptor). The cards page of Tag Files already used #{bs:formatIsbn(b.isbn)} and
#{bs:initials(b.author)}. The edge cases:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:bs="urn:org.example.bookshelf:tags">
<h:head><title>Edge</title></h:head>
<h:body>
<p>[#{bs:formatIsbn(null)}] [#{bs:formatIsbn('123')}] [#{bs:initials('ada king lovelace')}]</p>
</h:body>
</html>
printed [] [123] [AKL] on both runtimes: a null argument reaches the method as null, so the method must cope with it
(here it returns null, which prints as nothing), and an input of the wrong length is returned unchanged.
Source: Facelet taglib schema web-facelettaglibrary_4_1.xsd and Jakarta Faces 4.1 specification, 5 Expression Language Facility
How a wrong declaration fails differs between the runtimes, and a mistake in one function can take the whole library with it:
| Mistake | Mojarra 4.1.17 | MyFaces 4.1.4 |
|---|---|---|
|
The application fails to deploy ( |
The application starts; every page I requested that used the library answered 500 with |
|
Fails to deploy: "No Function Found on type … with signature …" |
Every such page answers 500 with the same message |
the method exists but is not |
Deploys; the call fails: |
Same |
a page calls |
HTTP 500, |
Same |
Deploy the library to a test environment before you ship it. An EL function is a good fit for a pure calculation with no
state (formatting an ISBN, building an initial); a method on a bean can already be called from EL (method expressions)
and needs no descriptor, so a function pays off only when the same helper is wanted in many views without a bean in scope. The
standard function library jakarta.tags.functions (conventionally fn:) is listed in the specification among the standard
Facelet tag libraries, and needs no registration.
Packaging a Library in a JAR
A library that several applications share is built as a JAR, and thanks to auto-discovery its users add a dependency and
nothing else. The Bookshelf extension below has this layout, which is the standard Maven src/main/resources layout; the
descriptor sits in META-INF and the tag file next to it, because source is relative to the descriptor
(Where the source Path Is Resolved):
bookshelf-tags-ext.jar
META-INF/bookshelf-ext.taglib.xml
META-INF/tags/badge.xhtml
org/example/bookshelf/... (handlers, functions, components, validators)
<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:org.example.bookshelf:ext</namespace>
<tag>
<tag-name>badge</tag-name>
<source>tags/badge.xhtml</source>
</tag>
</facelet-taglib>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="jakarta.faces.facelets">
<span class="badge">#{text}</span>
</ui:composition>
A page uses it with no entry in web.xml. The page below also uses a descriptor placed in WEB-INF/classes/META-INF
and one in WEB-INF that is not registered, to show the difference:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="jakarta.faces.html"
xmlns:ext="urn:org.example.bookshelf:ext" xmlns:cls="urn:org.example.bookshelf:cls" xmlns:web="urn:org.example.bookshelf:web">
<h:head><title>Auto</title></h:head>
<h:body>
<ext:badge text="from the jar"/>
<cls:tick text="from WEB-INF/classes/META-INF"/>
<web:flag text="WEB-INF/auto.taglib.xml"/>
</h:body>
</html>
On Mojarra and MyFaces the first two tags rendered (<span class="badge">from the jar</span> and the one from
WEB-INF/classes/META-INF), and the third was copied to the output as <web:flag text="WEB-INF/auto.taglib.xml"></web:flag>
with the warning quoted in Registering a Library. Components, converters and validators that the JAR declares by annotation are
subject to the packaging rules of the custom components page, which this page did not exercise.
Choosing: Tag File, Composite, Component, Handler or Function
All of them let a page say <bs:something/>. The runs showed concrete differences between them:
| Option | Needs | Good for | Cannot or does not |
|---|---|---|---|
|
XHTML. A path, |
A fragment used a few times; |
|
Tag file |
XHTML and a descriptor entry |
A named, namespaced fragment with a body and named slots; reading and writing the caller’s value expressions |
Attributes are undeclared variables (a missing one falls back to EL names). Not a naming container. Cannot forward method expressions |
Composite component ( |
XHTML in a resource library |
A declared interface ( |
Has no behaviour beyond what its XHTML and a backing component can express. Generated ids appear in client ids ( |
Tag handler |
Java class, descriptor entry |
Decisions taken while the view is built: variables, repetition, conditional inclusion, adding components |
Cannot render or take part in the lifecycle. Runs again on every build, in an implementation dependent number |
EL function |
Static Java method, descriptor entry |
Pure calculations inside expressions |
No access to the view or the request unless it reads them itself. A wrong declaration can stop the application |
Custom component and renderer |
Java classes ( |
State, |
The most code, and state must be saved correctly |
Source: Jakarta EE Tutorial: Determining Whether You Need a Custom Component or Renderer and Jakarta EE Tutorial: Composite Components
The same decision as a flow:
The Jakarta EE Tutorial gives the same advice in other words: a component that merely aggregates existing components should be a composite component, data manipulation belongs in a managed bean, and conversion, validation and listeners are registered with the standard tags, so a custom component is needed only for behaviour or markup that none of them offers. In the Bookshelf the labelled field of this page is a tag file because it only forwards a value, while the star rating of the composite components page is a composite component.
What Changed per Version
| Version | Change |
|---|---|
JSF 2.0 (Facelets 2.0) |
Facelets joined the specification. Section 10.2 of the specification explains that the original project did not separate API and implementation, and that only the classes that had to be public were kept, in |
JSF 2.2 |
|
JSF 2.3 |
The cardinality rules of |
Faces 3.0 |
|
Faces 4.0 |
JSP support was removed entirely (issue 1546), so the JSP tag handlers and TLDs of earlier books have no replacement in the view layer: a tag library is a Facelets taglib. |
Faces 4.1 |
The schema is |
Faces 5.0 |
Not described: the 5.0 milestone is not current behaviour. |
Where the Books Are Outdated
-
Facelets 1.x libraries. Material written for the stand-alone Facelets 1.x project, which the book on Facelets covers, declares tag libraries with a
facelet-taglibelement validated by the DTDfacelet-taglib_1_0.dtd, registers them with a project specific context parameter, and writes handlers that extend classes incom.sun.facelets. In Faces 4.1 the descriptor is validated by theweb-facelettaglibrary_4_1.xsdschema, the registration parameter isjakarta.faces.FACELETS_LIBRARIES(the old namesfacelets.LIBRARIESandjavax.faces.FACELETS_LIBRARIESwere not read by either runtime), and the handler API is injakarta.faces.view.facelets. A descriptor with the old DOCTYPE was still accepted by both runtimes in the test, but the specification no longer describes that form. Thecom.sun.faceletspackage names and the project’s customViewHandlercome from the issue’s description of the book and were not checked against the book here. -
JSP tag handlers and TLDs. The older Faces books (Jacobi and Fallows, Mann and Dudney and others) write a custom tag as a class that extends
UIComponentELTagplus a.tldfile. JSP support was removed in Faces 4.0; the replacement is atagentry with acomponentchild in a*.taglib.xml, or@FacesComponent(createTag = true)(custom components page). -
"Composition components". Early Facelets material builds reusable components as
ui:compositionfiles included withui:include. That still works, and a tag file is the same file given a tag name, but the composite component of Faces 2.0 replaced it where an interface, method expressions or unique ids are needed (Choosing: Tag File, Composite, Component, Handler or Function). -
The Tutorial’s validator tag. The Jakarta EE Tutorial shows a
validatorentry with avalidator-classchild. The 4.1 schema definesvalidator-idand an optionalhandler-classonly. Mojarra and MyFaces both loaded such a descriptor and ignored the extra element, so the snippet works by accident; write the entry as in Tags for Converters, Validators, Components and Composites.
Common Mistakes
-
Registering the library under the wrong parameter name. Only
jakarta.faces.FACELETS_LIBRARIES(or aMETA-INF/*.taglib.xmlin a JAR or inWEB-INF/classes) works. A missing library is only a warning, and the first tag or function that uses it fails. -
A tag file path that starts with a slash.
sourceis relative to the descriptor. -
The same fixed id in a tag file used twice in one form. Make the id an attribute, or use a composite component.
-
Relying on
requiredin the descriptor. Mojarra enforces it for tag files and MyFaces does not. -
An optional attribute named like a bean. The expression resolves to the bean when the page omits the attribute.
-
Passing an action or listener to a tag file. It evaluates as a property read and fails. Use a composite component with
method-signature. -
State in a handler field. One instance serves every request and user.
-
Assuming how often
applyruns. It runs per build, and Mojarra and MyFaces differ on a postback. -
Decisions in a handler about data an action changes. The view is built before the action; use
ui:repeatorrendered. -
A faulty EL function declaration. It can stop the whole application at start-up (Mojarra) or fail every page of the library (MyFaces).
-
Forgetting the null case in an EL function. Expressions pass
nullthrough.
References
-
Jakarta EE Tutorial: Creating Custom UI Components and Other Custom Objects
-
Jakarta EE Tutorial: Defining the Custom Component Tag in a Tag Library Descriptor
-
Jakarta EE Tutorial: Determining Whether You Need a Custom Component or Renderer
-
Jakarta EE Tutorial: Composite Components: Advanced Topics and an Example
-
Jakarta Faces 4.1 specification, 10 Facelets and its use in Web Applications
-
Jakarta Faces 4.1 specification, 10.3.2 Facelet Tag Library mechanism
-
Jakarta Faces 4.1 specification, 11.1.3 Application Configuration Parameters
-
Jakarta Faces 4.1 specification, B.1 Changes between 4.1 and 4.0
-
Jakarta Faces 4.1 specification, B.2 Changes between 4.0 and 3.0