HtmlFlow is unopinionated and eliminates the need for a special templating
dialect. All control flow is executed through the host language (i.e., Java),
fluently chained with HtmlFlow blocks using the of() or dynamic() builders (or dyn for Kotlin).
After introducing the core concepts of HtmlFlow, we present a couple of examples
demonstrating some of the most common usages with HtmlFlow. However, it’s
important to note that there are no limitations on the use of Java within HtmlFlow.
Core Concepts
HtmlFlow builders:
element builders (such as body(), div(), p(), etc) return the created element
text() and raw() return the parent element (e.g. .h1().text("...") returns the H1 parent). text()escapes HTML, while raw() doesn’t.
attribute builders - attr<attribute name>() - return their parent (e.g. .img().attrSrc("...") returns the Img).
__() in Java or l in Kolin - returns the parent element and emits the end tag of an element.
HtmlFlow provides both an eager and a lazy approach for building HTML.
This allows the Appendable to be provided either beforehand or later when
the view is rendered.
The doc() and view() factory methods follow each of these approaches:
An HtmlView is more performant than an HtmlDoc when we need to bind
the same template with different data models.
In this scenario, static HTML blocks are resolved only once, on HtmlView instantiation.
Given an HtmlView instance, e.g. view, we can render the HTML using one of the
following approaches:
The setOut() method accepts any kind of Appendable object.
The resulting HtmDoc or HtmlView from HtmlFlow.doc() or HtmlFlow.view() is configurable with the following options:
setIndent(boolean): Enables or disables indentation. It is on by default.
threadSafe(): Enables the use of the view in multi-threaded scenarios. It is off by default.
Note that HtmDoc and HtmlView are immutable, and the aforementioned methods return new instances.
Data Binding
Web templates in HtmlFlow are defined using functions (or methods in Java). The
model (or context object) may be passed as arguments to such functions.
Next, we have an example of a dynamic web page binding to a Track object.
The of() and dynamic() builders (or dyn for Kotlin) in HtmlDoc and HtmlView, respectively,
are utilized to chain Java code in the definition of web templates:
of(Consumer<E> cons) returns the same element E, where E is the parent HTML element.
dynamic(BiConsumer<E, M> cons) (or E.dyn(cons: E.(M) -> Unit): E for Kotlin)- similar to .of() but the consumer receives an additional argument M (model).
If/else
Regarding the previous template of trackDoc or trackView, consider, for
example, that you would like to display the year of the artist’s death for cases
where the artist has already passed away.
Considering that Track has a property diedDate of type LocalDate, we can interleave
the following HtmlFlow snippet within the ul to achieve this purpose:
You can utilize any Java loop statement in your web template definition. Next,
we present an example that takes advantage of the forEach loop method of
Iterable:
To ensure well-formed HTML, HtmlFlow needs to observe the completion of
asynchronous models. Otherwise, text or HTML elements following an asynchronous
model binding may be emitted before the HTML resulting from the asynchronous
model.
To bind an asynchronous model, one should use the builder
.await(parent, model, onCompletion) -> ...)
where the onCompletion callback signals to HtmlFlow that it can proceed to the
next continuation.
Alternatively, with Kotlin, you can avoid using the onCompletion callback.
Instead, you can use a suspending lambda with the suspending builder. This
allows your code to pause at the suspension point and resume
automatically when ready.
Next we present the asynchronous version of the playlist web template.
Instead of a List<Track> we are binding to a Flux,
which is a Reactive Streams Publisher with reactive operators that emits 0 to N elements.
HtmlFlow await feature works regardless the type of asynchronous model and can be used with
any kind of asynchronous API.
Layout and partial views (aka fragments)
HtmlFlow also enables the use of partial HTML blocks inside a template function, also know as fragments.
This is useful whenever you want to reuse the same template with different HTML fragments.
A fragment is constructed just like any template, that is a consumer of an HTML element.
Whereas a view template receives an HtmlPage corresponding the root element, a fragment template
may receive any kind of HTML element.
But, instead of a specific functional interface HtmlTemlate used for views, a fragment
is defined with the standard Consumer<T> where T is the type of the parent element.
For example, a fragment for a footer may be defined by a Consumer<Footer>.
The fragment template may receive additional arguments for auxiliary model objects (i.e. context objects).
Consider the following example from Spring Petclinic,
which creates a fragment for a div containing a label and an input.
Notice, the function partialOwner is in turn another fragment that receives a Div element.
Remember when calling a fragment to use a dynamic() block to avoid storing it internally as a static HTML block and make it render whenever you call it with a model. Do not use the builder of() to include a dynamic fragment.
This way of invoking a fragment is particularly useful when you need to use a smaller part (component) gathered together to produce a bigger template.
This is the most common usage of partials or fragments.
There is another way of using fragments, it’s to construct a layout.
The layout is a normal template, but with “holes” or placeholders, to be filled with fragments.
These fragments are consumers (i.e. Consumer<Element>) received as arguments of the layout.
The layout function may instantiate an HtmlView based on a template with closures over
those fragments arguments.
For example, the following layout uses an auxiliary navbar and content fragments to build the end view.
To chain the call of fragments fluently we take advantage of the auxiliary HtmlFlow builder of() that let us chain a consumer of the last created element.
Notice .of(navbar::accept) is equivalent to write in Java .of(nav -> navbar.accept(nav)).
Once defined the layout and considering the previous example of the partialOwner we may build
a owner view with: