Skip to main content

Core Concepts

HTML Builders

A view is built by chaining method calls. There are four kinds of builder methods, and knowing what each one returns is the key to reading and writing HtmlFlow fluently:

  • Element builders (html(), body(), div(), p(), …) open a new element and return that child element, so the next call nests inside it.
  • Attribute builders (attrClass(...), attrSrc(...), attrHref(...), …) set an attribute and return the same element, so you can keep chaining attributes.
  • Text builders (.text(...), .raw(...)) add a text node and return the parent element; .text(...) escapes its content, while .raw(...) emits it unescaped.
  • Close builder (.__() in Java, .l in Kotlin) emits the element's end tag and returns its parent, stepping back out one level.
HtmlFlow.doc(System.out)
.html()
.body()
.div().attrClass("card") // element builder + attribute builder
.h1().text("Title").__() // text returns the <h1>, __() closes it
.__() // closes the <div>
.__() // closes the <body>
.__(); // closes the <html>

In Java the .__() calls make the nesting explicit; the trailing // div comments are a common convention to track which element each close belongs to. In Kotlin the trailing lambda braces close elements for you, and .l is available as a shorthand for .__() when chaining without a block.

Html Templates

HtmlFlow offers two ways to define a template, differing in when the HTML is produced.

  • HtmlFlow.doc(out)HtmlDoc (eager). The markup is written to the Appendable immediately, as you chain the builders. Best for a document you render once.
  • HtmlFlow.view(template)HtmlView<M> (lazy, reusable). The template, an HtmlTemplate lambda, is defined once and rendered many times, each time with a different model M.
// Lazily builds a reusable view. Created once and rendered multiple times.
static HtmlView<String> view = HtmlFlow.view(page -> page
.html().body().h1().<String>dynamic((h1, name) -> h1.text("Welcome " + name)).__().__().__());

static void printTwice(String name) {
// Builds and renders on the fly.
HtmlFlow.doc(System.out)
.html().body().h1().text("Welcome " + name).__().__().__();

// Binds and renders a pre-encoded view.
view.setOut(System.out).write(name);
}
Why a view is faster

An HtmlView separates static HTML (which never changes) from dynamic HTML (which depends on the model). The static blocks are resolved only once, when the view is instantiated, and reused on every subsequent render. Binding the same template to many models, the typical web-server workload, therefore avoids recomputing the parts that never change.

Models and Data Binding

A template renders dynamic data through a model object. In a reusable view the model is passed to write(model) or render(model), and you read it inside a dynamic block:

  • .dynamic((element, model) -> ...): used in an HtmlView; receives both the parent element and the model. In Kotlin this is .dyn { model -> ... }.
  • .of(element -> ...): used in an eager HtmlDoc; receives only the element (the data is captured directly from the surrounding scope).
record Track(String artist, String name) {}

HtmlView<Track> trackView = HtmlFlow.view(page -> page
.html()
.body()
.ul()
.<Track>dynamic((ul, track) -> ul
.li().text("Artist: " + track.artist()).__()
.li().text("Track: " + track.name()).__())
.__() // ul
.__() // body
.__() // html
);

// Render to a String...
String html = trackView.render(new Track("David Bowie", "Space Oddity"));
// ...or write straight to an Appendable
trackView.setOut(System.out).write(new Track("David Bowie", "Space Oddity"));

Only content inside a dynamic block is re-evaluated on each render; everything outside it is treated as a static block and emitted as-is. This split has a direct performance consequence: keep dynamic blocks as small as possible.

Anything inside a dynamic / dyn block, even tags and attributes that never change, is rebuilt by re-walking the builders on every render. Anything outside is pre-encoded once and reused. So put only the model-dependent values inside the block, and leave the surrounding structure static.

When the structure itself depends on the model, see Pre-encoding model-driven structure below.

Conditional Rendering and Loops

HtmlFlow has no special syntax for conditionals or iteration: you use ordinary Java or Kotlin control flow inside a dynamic (.dynamic / .dyn) or .of block. An if statement conditionally adds elements; a forEach loop repeats them.

HtmlView<List<Track>> playlistView = HtmlFlow.view(page -> page
.html()
.body()
.table()
.tr()
.th().text("Artist").__()
.th().text("Track").__()
.__() // tr
.<List<Track>>dynamic((table, tracks) -> tracks.forEach(trk -> table
.tr()
.td().text(trk.artist()).__()
.td().text(trk.name()).__()
.__() // tr
))
.__() // table
.__() // body
.__() // html
);
// Conditional: only render the <li> when the value is present
.<Track>dynamic((ul, track) -> {
if (track.diedDate() != null)
ul.li().text("Died in " + track.diedDate().getYear()).__();
})

Pre-encoding model-driven structure

Sometimes the HTML structure, not just a value, depends on the model: a loop body, or a branch that emits different markup. That markup has to live inside a dynamic block, so by the rule above it is re-walked on every render (and, in a loop, on every iteration), with its static tags rebuilt each time.

You can recover the pre-encoding: extract the model-driven fragment into its own reusable HtmlView (created once, at module level) and render it where you need it, inserting the result with raw(...). The fragment's static HTML is encoded once at instantiation and reused on every call, so each render only binds the dynamic values. A repeated table row is the clearest example:

// Created once — static <tr>/<td> structure is pre-encoded a single time
static final HtmlView<Track> rowView = HtmlFlow.view(page -> page
.tr()
.<Track>dynamic((tr, t) -> tr.td().text(t.artist()).__())
.<Track>dynamic((tr, t) -> tr.td().text(t.name()).__())
.__());

static final HtmlView<List<Track>> playlistView = HtmlFlow.view(page -> page
.html().body().table()
.<List<Track>>dynamic((table, tracks) ->
tracks.forEach(t -> table.raw(rowView.render(t))))
.__().__().__());

This pays off only with pre-encoding enabled (the default) and enough static markup in the fragment to pay off the extra render(...) call. The gain comes from reusing the fragment's pre-encoded static blocks rather than re-walking them. Keep the fragment view in a static final field (Kotlin: a top-level val); creating it inside the loop, or inside the enclosing template, would re-run pre-encoding every time and erase the benefit.

Configuring Views

A view's behaviour is configured through HtmlFlow.ViewFactory. The builder exposes the common options; view(...)/doc(...) use sensible defaults when called without a factory.

HtmlView<Model> view = HtmlFlow.ViewFactory.builder()
.indented(true) // pretty-print output (default true)
.threadSafe(false) // make the view safe to share across threads (default false)
.preEncoding(true) // pre-encode static blocks for faster rendering (default true)
.mfeEnabled(false) // enable the mfe() micro-frontend builder (default false)
.build()
.view(template);
Views are immutable

HtmlDoc and HtmlView never mutate in place. Configuration methods such as setIndented(boolean), threadSafe(), and setOut(Appendable) return a new instance with the change applied, so assign the result rather than relying on a side effect.

Indentation is on by default; turn it off in production to emit minified HTML. Views are not thread-safe by default. Call threadSafe() (or set threadSafe(true)) before sharing one view across request threads.

Reusable Components

Templates compose. A fragment (also called a partial) is just a function that takes a parent element and adds children to it: a Consumer of an HTML element in Java, or a receiver function in Kotlin. Because it accepts any element, not only the page root, the same fragment can be reused anywhere that element type is valid.

// A reusable input-field fragment
static void inputField(Div<?> container, String label, String id, Object value) {
container
.div().attrClass("form-group")
.label().text(label).__()
.input()
.attrClass("form-control")
.attrType(EnumTypeInputType.TEXT)
.attrId(id).attrName(id)
.attrValue(value.toString())
.__() // input
.__(); // div
}

// A template that builds a form from a reusable fragment
public static HtmlView<Owner> ownerView() {
return HtmlFlow.view(page -> page
.html()
.body()
.form().attrMethod(EnumMethodType.POST)
.<Owner>dynamic((form, owner) -> {
inputField(form.div(), "Name", "name", owner.getName());
inputField(form.div(), "Address", "address", owner.getAddress());
})
.__() // form
.__() // body
.__() // html
);
}
Call dynamic fragments inside a dynamic block

When a fragment depends on the model, invoke it from inside a dynamic() / dyn block, as above, not from of(). Content added via of() is captured once as a static block, so a model-dependent fragment placed there would render only the first model's data and reuse it forever.

Layouts

Fragments also compose into layouts. A layout is an ordinary template with "holes": rather than fixed markup, it accepts fragments as parameters (Consumer<Nav<?>>, Consumer<Div<?>>, …) and drops each one into place with the .of(fragment::accept) builder.

.of(navbar::accept) is shorthand for .of(nav -> navbar.accept(nav)): it chains a consumer of the most recently created element, so the fragment fills exactly that spot.

// A layout with "navbar" and "content" holes
static HtmlView<Owner> layout(Consumer<Nav<?>> navbar, Consumer<Div<?>> content) {
return HtmlFlow.view(page -> page
.html()
.head()
.title().text("PetClinic").__()
.__() // head
.body()
.nav().of(navbar::accept).__() // fill the navbar hole
.div().attrClass("container")
.of(content::accept) // fill the content hole
.__() // div
.__() // body
.__() // html
);
}

// Build a concrete view by passing fragments into the layout
HtmlView<Owner> ownerView = layout(
nav -> nav.a().attrHref("/").text("Home").__(),
div -> div.h1().text("Owner").__());

Because the layout is itself a reusable HtmlView, its static skeleton (head, nav/div structure) is pre-encoded once; only the inserted fragments change between the views you build from it.

Dynamic Content

Some content is not known at render time and must be loaded from another service. The v5 mfe() builder embeds a micro-frontend: an HTML fragment fetched from a remote URL, supporting both server-side rendering (SSR) and client-side rendering (CSR) with optional event wiring between fragments. It requires a view built with mfeEnabled(true).

HtmlView<?> view = HtmlFlow.ViewFactory.builder()
.mfeEnabled(true)
.build()
.view(page -> page
.html()
.head().__()
.body()
.div()
.mfe(cfg -> {
cfg.setMfeUrlResource("http://localhost:8081/bikes");
cfg.setMfeName("mfe1");
cfg.setMfeElementName("some-element");
cfg.setMfeListeningEventName("triggerBikeEvent");
cfg.setMfeTriggersEventName("triggerCartEvent");
cfg.setMfeScriptUrl("http://localhost:8081/js/mfe-bikes.js");
cfg.setMfeStylingUrl("http://localhost:8081/css/style.css");
})
.__() // div
.__() // body
.__() // html
);

For content that arrives asynchronously (reactive streams, futures, or Kotlin suspend functions) and for streaming the HTML out progressively, see Advanced → Asynchronous Rendering and Streaming HTML.