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.
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()
returns its parent element (e.g..h1().text("...")
returns theH1
parent).- attribute builders -
attr<attribute name>()
- return their parent (e.g..img().attrSrc("...")
returns theImg
). __()
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 in HtmlDoc
and HtmlView
, respectively,
are utilized to chain Java code in the definition of web templates:
of(Consumer<E> cons)
returns the same elementE
, whereE
is the parent HTML element.dynamic(BiConsumer<E, M> cons)
- similar to.of()
but the consumer receives an additional argumentM
(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:
Loops
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
:
Binding to Asynchronous data models
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.
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
.
static void partialInputField(Div<?> container, String label, String id, Object value) {
container
.div().attrClass("form-group")
.label()
.text(label)
.__() //label
.input()
.attrClass("form-control")
.attrType(EnumTypeInputType.TEXT)
.attrId(id)
.attrName(id)
.attrValue(value.toString())
.__() // input
.__(); // div
}
This partial could be used inside another template.
static void ownerTemplate(Div<?> container) {
container
.h2()
.text("Owner")
.__() //h2
.form()
.attrMethod(EnumMethodType.POST)
.div().attrClass("form-group has-feedback")
.<Owner>dynamic((div, owner) -> partialInputField(div, "Name", "name", owner.getName()))
.<Owner>dynamic((div, owner) -> partialInputField(div, "Address", "address", owner.getAddress()))
...
Notice, the function ownerTemplate
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.
public class Layout {
public static HtmlView view(Consumer<Nav> navbar, Consumer<Div> content) {
return HtmlFlow.view(page -> page
.html()
.head()
.title()
.text("PetClinic :: a Spring Framework demonstration")
.__() //title
.link().attrRel(EnumRelType.STYLESHEET).attrHref("/resources/css/petclinic.css")
.__() //link
.__() //head
.body()
.nav()
.of(navbar::accept)
.__() //nav
.div().attrClass("container xd-container")
.of(content::accept)
.__() //div
.__() //body
.__() //html
); // return 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 ownerTemplate
we may build
a owner view with:
public class OwnerView {
static final HtmlView view = Layout
.view(Navbar::navbarFragment, OwnerView::ownerTemplate);
...
}
Given this view we may render it with a Owner
object.
For example, the OwnerController
edit handler may be defined as:
@GetMapping("/owners/{ownerId}/edit")
@ResponseBody
public String initUpdateOwnerForm(@PathVariable("ownerId") int ownerId) {
Owner owner = ownersRepository.findById(ownerId);
return OwnerView.view.render(owner);
}