Skip to main content

Integrations

data-*

DataStar is a hypermedia framework that builds interactive web applications by extending HTML with declarative data-* attributes. Rather than introducing a template language or a client-side component model, it enhances standard HTML with reactive behavior, making it a natural fit for HtmlFlow.

The htmlflow-datastar module extends htmlflow-kotlin with a type-safe DSL for DataStar's attributes and expressions. Instead of writing data-* attributes and JavaScript expressions as strings, they are represented through Kotlin APIs that provide compile-time validation, IDE completion, and seamless integration with the existing HtmlFlow Kotlin DSL.

1. Dependencies

1. Dependency

Add the htmlflow-datastar module to your project.

implementation 'com.github.xmlet:htmlflow-datastar:1.3.0-alpha'

2. Type-safe signals and attributes

DataStar manages reactive state through signals, which are created and consumed by different data-* attributes. In HtmlFlow Datastar, a signal is created once using dataSignal and represented as a typed Signal<T> object that can be reused throughout the view. Instead of referencing signals through string-based data-* attributes, builders such as dataText, dataShow, and dataClass accept Signal<T> objects directly, providing type safety, IDE completion, and consistent signal references. As shown below, the count signal is created once and then passed to dataText to display its value.

div {
val count: Signal<Int> = dataSignal("count", 0)
div {
span {
attrId("counter")
dataText { +count }
}
}
}

This produces the equivalent DataStar markup:

<div data-signals:count="0">
<span id="counter" data-text="$count"></span>
</div>

Reusing the same Signal<T> object avoids misspelled signal names and keeps references consistent.

3. Type-safe events and modifiers

User interactions are defined through data-on:* attributes. HtmlFlow Datastar exposes these as type-safe event builders, allowing the event type (such as Click or Input), the action, and any modifiers to be expressed using Kotlin APIs instead of manually constructing strings.

In the following example, the input field is bound to the search signal. Every Input event triggers a GET request, but only after the user has stopped typing for 200 ms, thanks to the debounce modifier. This reduces the number of requests sent while the user is typing and improves the overall user experience.

Notice that the DataStar get action accepts a Kotlin function reference. During rendering, this reference is resolved to the URL declared in the function's route annotation, eliminating the need to hardcode endpoint paths and ensuring they remain synchronized with the server-side route definitions.

div {
val search: Signal<String> = dataSignal("search", "")
input {
attrType(EnumTypeInputType.TEXT)
attrPlaceholder("Search...")
dataBind(search)
dataOn(Input) {
get(::search)
modifiers { debounce(200.milliseconds) }
}
}
}

Which produces:

<div data-signals:search="''">
<input type="text" placeholder="Search..." data-bind:search data-on:input__debounce="@get('/examples/search')" />
</div>

Typed events and modifiers eliminate invalid event names and malformed modifier syntax.

4. Type-safe expressions

Many DataStar attributes accept JavaScript-like expressions. Instead of writing these expressions as strings, HtmlFlow Datastar represents them as Kotlin objects that can be combined using familiar operators such as and, or, and not.

In the following example, dataIndicator creates a signal named _fetching. The leading _ follows a DataStar convention indicating that the signal is local and is therefore not sent to the server when an HTTP request is made. The signal's value is automatically set to true while a request is in progress and back to false when the request completes.

The dataAttr attribute binds the value of any HTML attribute to an expression, keeping it automatically synchronized with the expression's result. In this case, binding the button's disabled attribute to _fetching ensures that the button is disabled while the request is in progress.

The same signal is also used in the dataOn expression to prevent multiple requests from being sent if the user clicks the button before the browser has applied the disabled attribute. This provides an additional safeguard against duplicate requests.

button {
val fetching: Signal<Boolean> = dataIndicator("_fetching")
dataAttr("disabled") { +fetching }
dataOn(Click) {
!fetching and get(::clickToLoadMore)
}
text("Load More")
}

Which generates:

<button
data-indicator:_fetching
data-attr:disabled="$_fetching"
data-on:click="!$_fetching && @get('/examples/click_to_load/more')"
>
Load More
</button>

Expressions can be composed and refactored using Kotlin operators instead of string concatenation.

HtmlFlow Datastar generates standard DataStar HTML, enabling developers to build reactive user interfaces declaratively with a fully type-safe Kotlin DSL while preserving compatibility with the DataStar runtime.

http4k

http4k is a Kotlin HTTP toolkit built on the idea of handlers as plain functions ((Request) -> Response). HtmlFlow plugs into http4k's templating SPI through the htmlflow-view-loader module (com.github.xmlet:htmlflow-view-loader), so HtmlFlow views can be used wherever http4k expects a template engine: alongside, or instead of, Pebble, Thymeleaf, or Handlebars.

The view loader does convention-based resolution: you expose your views as HtmlView<MyViewModel> members (a val property or a function), point the loader at a package, and it builds a registry that maps each model type to its view (matching superclasses and interfaces as a fallback). The result is a (ViewModel) -> String function, which is exactly http4k's TemplateRenderer.

1. Dependencies

Add the Kotlin DSL, the view loader, and http4k's template core:

implementation 'com.github.xmlet:htmlflow-kotlin:5.0.4'
implementation 'com.github.xmlet:htmlflow-view-loader:5.0.4'
implementation 'org.http4k:http4k-template-core'

2. A view model and its view

The model implements http4k's ViewModel; the view is an HtmlView typed by that model. To find a view, the loader scans each class in the package for a zero-argument member that returns an HtmlView — a val property or a function both work. It just needs an instance to read that member from, and it tries several ways to get one: a Kotlin object, a top-level (file-level) declaration, a class with a no-argument constructor, a companion object, a getInstance() singleton, or a constructor that takes an HtmlFlow.ViewFactory. A Kotlin object is the simplest, so this example uses one:

import htmlflow.*
import org.http4k.template.ViewModel
import org.xmlet.htmlapifaster.body
import org.xmlet.htmlapifaster.h1

data class Greeting(val name: String) : ViewModel

object Views {
val greeting: HtmlView<Greeting> = HtmlFlow.view {
it.html {
body {
dyn { model: Greeting -> h1 { text("Hello, ${model.name}!") } }
}
}
}
}

3. Build the renderer

ClasspathLoader(ViewModel::class.java).createRenderer(package) scans the package and returns a renderer that resolves the right view from the model's type at call time:

import htmlflow.viewloader.ClasspathLoader
import org.http4k.template.TemplateRenderer

val renderer: TemplateRenderer =
ClasspathLoader(ViewModel::class.java).createRenderer("com.example.views")

4. Wire it into an http4k handler

Use http4k's viewModel body lens to render a model straight into a response. No explicit view lookup is needed. The loader picks Views.greeting because the model is a Greeting:

import org.http4k.core.*
import org.http4k.core.ContentType.Companion.TEXT_HTML
import org.http4k.template.viewModel

val htmlView = Body.viewModel(renderer, TEXT_HTML).toLens()

val app: HttpHandler = { _: Request ->
Response(Status.OK).with(htmlView of Greeting("World"))
}

Set hotReload = true on createRenderer(...) during development to re-scan views on each render (this also disables pre-encoding); keep it false (the default) in production so views are pre-encoded once and cached. See Core Concepts → Configuring Views.