HtmlFlow

repository·master·Indexed 41 days ago

https://github.com/xmlet/htmlflow

A high-performance Java/Kotlin DSL for writing typesafe HTML templates as plain code. It provides full compiler support, eager and lazy building approaches, and support for asynchronous data models via .await(). Version 5.0.4 provides separate modules for Java and Kotlin developers.

Tokens
2.6K
Snippets
8
Records
12
Agent score
52%

What's inside HtmlFlow

  1. Eager vs Lazy HTML building

    master

    HtmlFlow supports two approaches for building HTML, depending on when the Appendable (the output destination) is provided:

    Eager Approach

    The Appendable is provided immediately. Use HtmlFlow.doc(Appendable).

    Java Example:

    HtmlFlow.doc(System.out).html().body().h1().text("Welcome").__()...

    Lazy Approach

    The template is defined first, and the Appendable is provided later during rendering. Use HtmlFlow.view(Function). This is more performant when binding the same template to different data models because static HTML blocks are resolved only once during instantiation.

    Java Example:

    var view = HtmlFlow.<Model>view(page -> page.html().body().text("Welcome").__()...);
    // Rendering:
    String html = view.render(model);
    // Or writing to an Appendable:
    view.setOut(System.out).write(model);
    // Eager
    HtmlFlow.doc(System.out).html().body().h1().text("Welcome").__();
    
    // Lazy
    var view = HtmlFlow.<Model>view(page -> page.html().body().text("Welcome").__());
    String html = view.render(model);
  2. How HtmlFlow builders and tags work

    master

    HtmlFlow uses a fluent DSL where different methods return different types of objects to allow for chaining:

    • Element builders (e.g., body(), div(), p()): Return the created element.
    • Text/Raw builders (text() and raw()): Return the parent element. Note that .text() escapes HTML, while .raw() does not.
    • Attribute builders (e.g., attrClass(), attrSrc()): Return the parent element.
    • End tag builders (__() in Java or l in Kotlin): Return the parent element and emit the closing tag for the current element.
  3. Install HtmlFlow for Java or Kotlin

    master

    To use HtmlFlow in a Gradle project, add the appropriate dependency based on your language. Starting from version 5, Java and Kotlin APIs are provided in separate modules.

    For Java developers: Use the com.github.xmlet:htmlflow dependency.

    For Kotlin developers: Use the com.github.xmlet:htmlflow-kotlin dependency to access the Kotlin-specific DSL.

    // Java
    implementation 'com.github.xmlet:htmlflow:5.0.4'
    
    // Kotlin
    implementation 'com.github.xmlet:htmlflow-kotlin:5.0.4'
  4. Using If/Else and Loops in templates

    master

    Since HtmlFlow is an internal DSL, you can use standard Java control flow statements directly within your templates using the .of() or .dynamic() builders.

    If/Else

    Wrap standard Java if statements inside a builder to conditionally render elements.

    .of(ul -> {
      if(track.getDiedDate() != null)
        ul.li().text("Died in " + track.getDiedDate().getYear()).__();
    })

    Loops

    Use standard Java loops (like forEach) to iterate over collections and generate repeating elements.

    .of(table -> tracks.forEach(trk ->
      table.tr()
        .td().text(trk.getArtist()).__()
        .td().text(trk.getName()).__()
      .__()
    ))
  5. Use HtmlViewSuspendHot for development hot-reloading

    master

    HtmlViewSuspendHot is a specialized view type designed for development scenarios where immediate reflection of template changes is required. Unlike optimized views, these dynamic views do not store static HTML blocks and instead recalculate the entire HTML structure on every rendering.

    Warning: These views are unoptimized and carry significant performance overhead. They are not recommended for production use.

    To use it, provide a template function that consumes an HtmlPage and an HtmlVisitorSuspending instance. You can configure whether the view is thread-safe or if the output should be indented.

  6. Data Binding with of() and dynamic()

    master

    To incorporate dynamic data into your templates, use the following builders:

    • of(Consumer<E> cons): Used in HtmlDoc. It allows you to chain Java code within an element. The consumer receives the element E as an argument.
    • dynamic(BiConsumer<E, M> cons): Used in HtmlView. Similar to of(), but the consumer receives both the element E and the data model M.

    Example (Java):

    void trackDoc(Appendable out, Track track) { 
      HtmlFlow.doc(out).html().body().ul()
        .li().of((li) -> li.text("Artist: " + track.getArtist())).__()
        .__()
      .__();
    }
    // Using of() in HtmlDoc
    .of((li) -> li.text("Artist: " + track.getArtist()))
    
    // Using dynamic() in HtmlView
    .dynamic((li, track) -> li.text("Artist: " + track.getArtist()))
  7. Bind to asynchronous data models using .await()

    master

    To ensure well-formed HTML when working with asynchronous data, HtmlFlow must observe the completion of the asynchronous model. This prevents subsequent HTML elements from being emitted before the asynchronous data is ready.

    Use the .await(parent, model, onCompletion) builder method. The onCompletion callback is critical: it signals to HtmlFlow that the asynchronous operation is finished and it can safely proceed to the next part of the HTML tree.

    This feature is type-agnostic and works with any asynchronous API, such as Project Reactor's Flux, Promises, or other Reactive Streams Publisher types.

    HtmlViewAsync<Flux<Track>> playlistView = HtmlFlow.viewAsync(view -> view
      .html()
        .body()
          .table()
            .tr()
              .th().text("Artist").__()
              .th().text("Track").__()
            .__()
            .<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
    );
  8. Handle duplicate view registration errors

    master

    The ClasspathLoader enforces a one-to-one mapping between a model type and a view. If the scanner finds multiple HtmlView implementations registered for the same model type, it will throw an IllegalStateException.

    Error Message Format: Multiple views found for model type '<ModelTypeName>'. Existing: <ExistingLocation>, New: <NewLocation>

  9. Configure HtmlViewSuspendHot thread safety and indentation

    master

    You can adjust the behavior of an HtmlViewSuspendHot instance using the following methods:

    • threadSafe(): Returns a new instance configured to be thread-safe.
    • threadUnsafe(): Returns a new instance configured to be thread-unsafe.
    • setIndented(isIndented: Boolean): Returns a new instance with the specified indentation setting.
    // Example of configuring a view
    val view = HtmlViewSuspendHot<MyModel>(template, visitor)
        .threadSafe()
        .setIndented(true)
  10. Scan for views using ClasspathLoader

    master

    Use scanForViews to search the classpath for HtmlView components that are compatible with a specific model type T. This returns a registry mapping model classes to their corresponding ViewInfo (which includes the view instance and its location).

    Parameters

    • basePackage: The package to start scanning from. Defaults to an empty string (root package).
    • hotReload: If set to true, the scanner will use a separate registry (hotViewRegistries) which allows for re-scanning views on subsequent calls. If false, results are cached in cachedViewRegistries for performance.
    • factory: An optional HtmlFlow.ViewFactory to be provided to the discovered views during inspection.
    // Assuming T is your model type, e.g., User
    val loader = ClasspathLoader(User::class.java)
    val viewRegistry = loader.scanForViews(basePackage = "com.myapp.views", hotReload = false)
  11. Render a model using HtmlViewSuspendHot.write()

    master

    The write method is a suspending function used to render the template with the provided model to an Appendable (such as a StringBuilder or Writer).

    If threadSafe is true, the visitor is cloned for the operation. If threadSafe is false, the existing visitor is configured with the Appendable and used directly.

    // Usage pattern for writing the view to an Appendable
    val out = StringBuilder()
    view.write(out, myModelInstance)
  12. Create a renderer with ClasspathLoader

    master

    The createRenderer method is the primary way to generate a high-level rendering function. It automates the scanning process and returns a lambda that accepts a model of type T and returns the rendered HTML string.

    Parameters

    • basePackage: The base package to scan for views. Defaults to the root package.
    • hotReload: If true, views will be re-scanned on every render call. Use this during development to pick up changes without restarting the application. If false, the view registry is cached.
    • factory: An optional HtmlFlow.ViewFactory to be passed to views.

    Return Value

    A function of type (T) -> String that, when called with a model, finds the compatible view and renders it.

    // Create a renderer for a 'User' model
    val userRenderer = ClasspathLoader(User::class.java).createRenderer(
        basePackage = "com.myapp.views",
        hotReload = true
    )
    
    // Use the renderer
    val html = userRenderer(myUserInstance)