Thursday, April 23, 2026

OmniFaces 5.3 released!

OmniFaces 5.3 has been released!

This is a relatively small feature release on top of 5.2. One new component has been added, and the rest of the changes are under the hood for better long-term maintenance: automated code formatting, a simpler and faster JavaScript build, and reorganized TypeScript sources. You can find the complete list of additions, changes and fixes at What's new in OmniFaces 5.3? in the showcase.

New: <o:lazyPanel>

Ever had a page with an expensive region below the fold which the user may never scroll to, but which gets built on every page load anyway? The traditional workaround is to wire up an IntersectionObserver in custom JavaScript and fire an ajax request yourself, which is a lot of boilerplate for something so common.

The new <o:lazyPanel> defers rendering of its children until the panel has scrolled into view:

<o:lazyPanel>
    <h:dataTable value="#{bean.expensiveList}" var="row">
        ...
    </h:dataTable>
</o:lazyPanel>

On initial render, the component writes a wrapper element with an optional placeholder and schedules a viewport intersection listener on it via OmniFaces.js, which uses IntersectionObserver when available and falls back to scroll/resize/orientationChange listeners otherwise. As soon as the wrapper intersects the viewport, a single faces.ajax.request targeting its own client id is fired. The component then flips its loaded flag, optionally invokes a listener bean method with a LazyPanelEvent, and renders its children in place of the placeholder.

The loaded attribute is a server-side escape hatch: when true, the children are rendered immediately without any client side observer. This is useful for print views, SEO crawlers, or tests.

<o:lazyPanel loaded="#{bean.printPreview}">
    ...
</o:lazyPanel>

Nested <f:param> or <o:param> children are sent along with the lazy panel ajax request, so that a single listener can serve multiple panels by distinguishing on an entity id, filter key, or page number:

<o:lazyPanel listener="#{bean.preload}">
    <f:param name="productId" value="#{product.id}" />
    ...
</o:lazyPanel>

The listener can read them via Faces#getRequestParameter(). Parameter values are evaluated at initial render (snapshot semantics), consistent with UIParameter usage elsewhere in Faces.

The closest equivalent in PrimeFaces is <p:outputPanel deferred="true" deferredMode="visible">, which loads its contents once the panel is scrolled into view. Under the hood however it uses jQuery scroll handlers on the window combined with $.offset() and window height math, which fires on every scroll event and scales poorly when you have multiple deferred panels on the same page. <o:lazyPanel> uses the native IntersectionObserver which is browser-native, more efficient, and only observes the panel itself; it falls back to scroll/resize/orientationChange listeners only when IntersectionObserver is unavailable. <o:lazyPanel> also has no jQuery or PrimeFaces runtime dependency, it's just a standard faces.ajax.request, so it works in vanilla Faces applications without PrimeFaces. On top of that, <o:lazyPanel> supports <f:param>/<o:param> for passing context to the listener, which <p:outputPanel> does not natively offer.

The homegrown alternative is to wire up an IntersectionObserver yourself which then calls a <h:commandScript> or <p:remoteCommand> from the intersection callback, and to manually swap placeholder markup on response. This works, but it's imperative JavaScript scattered across the view, and you'll have to repeat it for every lazy region. <o:lazyPanel> is the declarative equivalent: one tag, no JavaScript, and the placeholder and listener are just regular Faces markup.

Under the hood

Relatively a lot of things have been cleaned up in the build and source tree. These have no impact on runtime behavior, but they do make the project easier to maintain and contribute to:

  • Automated code formatting via Spotless and Stylistic; all Java, XML, XHTML and TypeScript sources are now formatted consistently on every build. This avoids inconsistently formatted source code coming in with pull requests.
  • The TypeScript sources have been reorganized into their own src/main/ts subfolder. This keeps the context of src/main/webapp clean.
  • The JavaScript build has been improved: browserify and closure-compiler-maven-plugin have been replaced by esbuild for performance and simplicity.
  • Vdlgen now also runs during Eclipse incremental builds, so workspace resolution into sandbox projects continues to work.

Fixes

MultiViews welcome file resolution failed on Windows-based servers due to wrong parent path handling. This has been fixed (#949).

<o:validateBean> did not collect nested properties of @Valid-annotated beans, so validation could silently skip nested constraints. This has been fixed (#951).

@ViewScoped unload threw a NullPointerException during pending view state removal in the specific combination of Spring WebFlow with MyFaces. This has been fixed (#952).

Installation

Non-Maven users: download OmniFaces 5.3.1 JAR and drop it in /WEB-INF/lib the usual way, replacing the older version if any.

Maven users: add below entry to pom.xml, replacing the older version if any.

<dependency>
    <groupId>org.omnifaces</groupId>
    <artifactId>omnifaces</artifactId>
    <version>5.3.1</version>
</dependency>

The 5.3.1 fixes have also been backported to 4.x and 3.x, so OmniFaces 4.7.7 and OmniFaces 3.14.18 have been released as well.

No comments: