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:

  • /* eager */ HtmlFlow.doc(System.out).html().body().div().table()...
  • /* lazy */ var view = HtmlFlow.view(page -> page.html().body().div().table()...)

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:

  • String html = view.render(tracks)
  • view.setOut(System.out).write(tracks)

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.

void trackDoc(Appendable out, Track track) {
  HtmlFlow.doc(out)
    .html()
      .body()
        .ul()
          .li().text(format("Artist: %s", track.getArtist())).__()
          .li().text(format("Track: %s", track.getName())).__()
        .__() // ul
      .__() // body
    .__(); // html
}
...
trackDoc(System.out, new Track("David Bowie", "Space Oddity"));
HtmlView<Track> trackView = HtmlFlow.view(view -> view
  .html()
    .body()
      .ul()
      .<Track>dynamic((ul, track) -> ul
        .li().text(format("Artist: %s", track.getArtist())).__()
        .li().text(format("Track: %s", track.getName())).__()
      )
      .__() // ul
    .__() // body
  .__() // html
);
...
trackView.setOut(System.out).write(new Track("David Bowie", "Space Oddity"));
import htmlflow.*

fun Appendable.trackDoc(track: Track) {
  doc {
    html {
      body {
        ul {
          li { text(format("Artist: %s", track.artist)) }
          li { text(format("Track: %s", track.name)) }
        } // ul
      } // body
    } // html
  } // doc
}
import htmlflow.*

val trackView = view<Track> {
  html {
    body {
      ul {
        dyn { track: Track ->
          li { text("Artist: ${track.artist}")) }
          li { text("Track: ${track.name}")) }
        }
      } // ul
    } // body
  } // html
}

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:

void trackDoc(Appendable out, Track track) {
    ...
        .ul()
        ...
        .of(ul -> {
            if(track.getDiedDate() != null)
            ul.li().text(format("Died in %d", track.getDiedDate().getYear())).__();
        })

}
HtmlView<Track> trackView = HtmlFlow.view(view -> view
    ...
        .ul()
        ...
        .<Track>dynamic((ul, track) -> {
            if(track.getDiedDate() != null)
            ul.li().text(format("Died in %d", track.getDiedDate().getYear())).__();
        })
        ...
import htmlflow.*
  ...
  ul {
    ...
    if (track.diedDate != null) {
      li { text(format("Died in %d", track.diedDate.year)) }
    }
    ...
  }
  ...

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:

void playlistDoc(Appendable out, List<Track> tracks) {
  HtmlFlow.doc(out)
    .html()
      .body()
        .table()
          .tr()
            .th().text("Artist").__()
            .th().text("Track").__()
          .__() // tr
          .of(table -> tracks.forEach( trk ->
            table
              .tr()
                .td().text(trk.getArtist()).__()
                .td().text(trk.getName()).__()
              .__() // tr
          ))
        .__() // table
      .__() // body
    .__(); // html
}
HtmlView<List<Track>> playlistView = HtmlFlow.view(view -> view
  .html()
    .body()
      .table()
        .tr()
          .th().text("Artist").__()
          .th().text("Track").__()
        .__() // tr
        .<List<Track>>dynamic((table, tracks) -> tracks.forEach( trk ->
          table
            .tr()
              .td().text(trk.getArtist()).__()
              .td().text(trk.getName()).__()
            .__() // tr
          ))
      .__() // table
    .__() // body
  .__() // html
);
import htmlflow.*

fun Appendable.playlistDoc(tracks: List<Track>) {
  doc {
    .html {
      body {
        table {
          tr {
            th { text("Artist") }
            th { text("Track") }
          }
          tracks.forEach { track ->
            tr { td { text(track.artist) } }
            tr { td { text(track.name) } }
          }
        } // table
      } // body
    } // html
  } // doc
}
import htmlflow.*

val playlistView = view<List<Track>> {
  html {
    body {
      table {
        tr {
          th { text("Artist") }
          th { text("Track") }
        }
        dyn { tracks: List<Track> -> tracks
          .forEach { track ->
              tr { td { text(track.artist) } }
              tr { td { text(track.name) } }
          }
        }
      } // table
    } // body
  } // html
}

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.

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.

HtmlViewAsync<Flux<Track>> playlistView = HtmlFlow.viewAsync(view -> view
  .html()
    .body()
      .table()
        .tr()
          .th().text("Artist").__()
          .th().text("Track").__()
        .__() // tr
        .<Flux<Track>>await((table, tracks, onCompletion) -> tracks
          .doOnComplete(onCompletion::finish)
          .doOnNext( trk ->
            table
              .tr()
                .td().text(trk.getArtist()).__()
                .td().text(trk.getName()).__()
              .__()
        ))
      .__() // table
    .__() // body
  .__() // html
);
import htmlflow.*

val playlistView = viewAsync<Flux<Track>> {
  html {
      body {
          table {
              tr {
                  th { text("Artist") }
                  th { text("Track") }
              }
              await { tracks: Flux<Track>, resume -> tracks
                  .doOnComplete(resume)
                  .doOnNext{ track ->
                      tr { td { text(track.artist) } }
                      tr { td { text(track.name) } }
                  }
              }
          } // table
      } // body
  } // html
}
import htmlflow.*

val playlistView = viewSuspend<Flux<Track>> {
  html {
      body {
          table {
              tr {
                  th { text("Artist") }
                  th { text("Track") }
              }
              suspending { tracks: Flux<Track> -> tracks
                  .asFlow()
                  .collect { track ->
                      tr { td { text(track.artist) } }
                      tr { td { text(track.name) } }
                  }
              }
          } // table
      } // body
  } // html
}

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
}
import htmlflow.*

fun Div<*>.partialInputField(label: String, id: String, value: Any) {
    div {
        attrClass("form-group")
        label { text(label) }
        input {
            attrClass("form-control")
            attrType(EnumTypeInputType.TEXT)
            attrId(id)
            attrName(id)
            attrValue(value.toString())
        } // input
    } // div
}

This partial could be used inside another partial.

static void partialOwner(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()))
    ...
fun Div<*>.partialOwner() {
    h2 { text("Owner") }
    form {
        attrMethod(EnumMethodType.POST)
        div {
            attrClass("form-group has-feedback")
            dyn { owner: Owner ->
                partialInputField("Name", "name", owner.name)
                partialInputField("Address", "address", owner.address)
            }
        } // div
    } // form
}

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.

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
    ); // view
  }
}
import htmlflow.*

fun ownerView(navbar: (Nav<*>) -> Unit, content: Div<*>.() -> Unit): HtmlView<Owner> {
    return view<Owner> {
        html {
            head {
                title { text("PetClinic :: a Spring Framework demonstration") }
                link { attrRel(EnumRelType.STYLESHEET).attrHref("/resources/css/petclinic.css") }
            }
            body {
                nav { navbar(this) }
                div {
                    attrClass("container xd-container")
                    content(this)
                } // div
            } // body
        } // html
    } // 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:

public class OwnerView {

  static final HtmlView view = Layout
    .view(Navbar::navbarFragment, OwnerView::partialOwner);
  ...
}
class OwnerView {

    val view = ownerView(::navbarFragment) { partialOwner() }
    ...
}

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);
}
@GetMapping("/owners/{ownerId}/edit")
@ResponseBody
fun initUpdateOwnerForm(@PathVariable("ownerId") ownerId: Int) {
    val owner = ownersRepository.findById(ownerId);
    return view.render(owner);
}