-
Notifications
You must be signed in to change notification settings - Fork 152
Description
Summary
Add a zio-blocks-htmx module that provides a type-safe, ergonomic Scala DSL for all htmx HTML attributes — matching or exceeding the quality level of the existing zio-http-datastar-sdk.
Depends on zio-blocks-template (#1164 / #996).
Motivation
The current zio-http-htmx module is extremely primitive — it wraps every htmx attribute as PartialAttribute[String], provides zero type safety, no modifier support, no builder patterns, and has exactly 1 test. Meanwhile, the zio-http-datastar-sdk in the same repo provides a fully typed DSL with modifier ADTs, builder patterns, signal types, and comprehensive tests.
Problems with the current zio-http htmx module:
| Problem | Current State | Expected State |
|---|---|---|
| Swap strategies | hxSwapAttr := "innerHTML" (string) |
hxSwap := HxSwap.InnerHTML.swap(1.second).settle(500.millis) |
| Trigger modifiers | hxTriggerAttr := "click delay:500ms" (string) |
hxTrigger := HxTrigger.click.delay(500.millis).once |
| Event handlers | ~100 boilerplate hxOn*Attr defs |
hxOn.click := js"alert('hi')" builder |
| Target selectors | hxTargetAttr := "closest div" (string) |
hxTarget := HxTarget.closest("div") |
| JSON vals | hxValsAttr := """{"key":"value"}""" (string) |
hxVals := HxVals.from[MyCase](value) with Schema |
| Tests | 1 test | Comprehensive |
| Template version | Old template |
New zio-blocks-template |
Scope
In Scope (zio-blocks-htmx)
Core HTTP Method Attributes — typed URL values:
hxGet,hxPost,hxPut,hxPatch,hxDelete
Swap Strategy — HxSwap sealed trait/enum with modifiers:
sealed trait HxSwap {
def swap(duration: Duration): HxSwap // swap:1s
def settle(duration: Duration): HxSwap // settle:500ms
def transition: HxSwap // transition:true
def scroll(position: ScrollPosition): HxSwap
def show(position: ShowPosition): HxSwap
def ignoreTitle: HxSwap
def focusScroll(enabled: Boolean): HxSwap
}
object HxSwap {
case object InnerHTML extends HxSwap // innerHTML (default)
case object OuterHTML extends HxSwap // outerHTML
case object TextContent extends HxSwap // textContent
case object BeforeBegin extends HxSwap // beforebegin
case object AfterBegin extends HxSwap // afterbegin
case object BeforeEnd extends HxSwap // beforeend
case object AfterEnd extends HxSwap // afterend
case object Delete extends HxSwap // delete
case object None extends HxSwap // none
}Trigger Builder — HxTrigger with modifier chains:
HxTrigger.click // "click"
HxTrigger.click.delay(500.millis) // "click delay:500ms"
HxTrigger.click.throttle(1.second) // "click throttle:1s"
HxTrigger.click.once.changed // "click once changed"
HxTrigger.click.from("#other") // "click from:#other"
HxTrigger.load // "load"
HxTrigger.revealed // "revealed"
HxTrigger.intersect.threshold(0.5) // "intersect threshold:0.5"
HxTrigger.every(2.seconds) // "every 2s"
HxTrigger.click.filter(js"ctrlKey") // "click[ctrlKey]"
HxTrigger.click.queue(QueueStrategy.Last) // "click queue:last"
// Multiple triggers
HxTrigger(HxTrigger.load, HxTrigger.click.delay(1.second))Target Selectors — HxTarget with extended selectors:
HxTarget.this_ // "this"
HxTarget.closest("div") // "closest div"
HxTarget.find(".result") // "find .result"
HxTarget.next // "next"
HxTarget.next(".sibling") // "next .sibling"
HxTarget.previous // "previous"
HxTarget.css("#my-id") // "#my-id" (regular CSS selector)Event Handler Builder — hxOn (NOT 100 defs):
hxOn.click := js"doSomething()" // hx-on:click="doSomething()"
hxOn.submit := js"validate()" // hx-on:submit="validate()"
hxOn("custom-event") := js"handle()" // hx-on:custom-event="handle()"
// Uses Js type from zio-blocks-template for the expressionOther Typed Attributes:
hxBoost— boolean attributehxPushUrl/hxReplaceUrl— boolean or URLhxSelect/hxSelectOob— CSS selectorhxSwapOob— boolean or swap strategyhxConfirm/hxPrompt— stringhxDisable— booleanhxDisabledElt— CSS selectorhxIndicator— CSS selectorhxInclude— CSS selector with extended selectorshxParams—HxParamsenum (All, None, Not(params), Only(params))hxSync—HxSyncstrategy typehxValidate— booleanhxPreserve— booleanhxEncoding—HxEncodingenum (Multipart)hxExt— extension namehxHeaders— JSON (with Schema integration like HxVals)hxDisinherit— attribute name listhxHistory— boolean
Schema Integration:
hxValswithSchema[A]for type-safe JSON encodinghxHeaderswith same pattern
Also In Scope (via http-model dependency)
Since zio-blocks already has http-model with Header, Headers, Request, Response:
Response Headers — typed htmx response header values:
HX-Location(JSON — client-side redirect with swap options)HX-Push-Url/HX-Replace-Url(URL)HX-Redirect(URL — full page redirect)HX-Refresh(boolean)HX-Reswap(swap strategy — reusesHxSwaptype)HX-Retarget(CSS selector)HX-Reselect(CSS selector)HX-Trigger/HX-Trigger-After-Settle/HX-Trigger-After-Swap(JSON/string — trigger client events)
Request Headers — typed htmx request header parsing:
HX-Request(boolean — always sent by htmx)HX-Boosted(boolean)HX-Current-URL(URL)HX-Target(element ID)HX-Trigger/HX-Trigger-Name(element ID/name)HX-History-Restore-Request(boolean)HX-Prompt(string — user response to hx-prompt)
These integrate directly with http-model's Header.Typed[H] pattern.
Out of Scope (belongs in zio-http)
- Endpoint integration — generating htmx URLs from
Endpointdefinitions - SSE/WebSocket extensions — require zio-http transport
Design Principles
- Match datastar SDK quality — the datastar-sdk in zio-http is the reference for API design quality.
- Use
zio-blocks-templatetypes —Dom,PartialAttribute,Jsfrom PR feat(template): zio-blocks-template — type-safe HTML templating with compile-time optimizations #1164. - Zero additional dependencies — only depend on
zio-blocks-templateandzio-blocks-schema(for JSON vals). - Cross-platform — JVM + JS.
- Render to valid htmx — all typed values must render to correct htmx attribute strings.
Example Usage (Target API)
import zio.blocks.template._
import zio.blocks.htmx._
val searchForm = form(
hxPost := "/search",
hxTarget := HxTarget.find("#results"),
hxSwap := HxSwap.InnerHTML.settle(200.millis),
hxTrigger := HxTrigger.submit,
hxIndicator := css"#spinner"
)(
input(
typeAttr := "text",
nameAttr := "q",
hxGet := "/suggest",
hxTrigger := HxTrigger("input").changed.delay(300.millis),
hxTarget := HxTarget.next(".suggestions")
),
button(typeAttr := "submit")("Search")
)
val infiniteScroll = div(
hxGet := s"/items?page=$nextPage",
hxTrigger := HxTrigger.revealed,
hxSwap := HxSwap.AfterEnd,
hxIndicator := css".spinner"
)("Loading...")