I'm trying to build an HTMX app in Kotlin Ktor with the lib HTML-DSL. I'm having difficulties wrapping my head around how to return elegantly partial HTML and avoid code duplication.
As per usage in HTMX, let's say I have a search form:
fun Application.configureRouting() {
routing {
get("/") {
call.respondHtml { fullPage() }
}
}
}
With:
@HtmlTagMarker
fun HTML.fullPage(searchResults: List<SearchResult> = listOf()) {
headerWithHTMXJS()
body {
main {
mySearchBar { }
mySearchResults(results=searchResults)
}
}
}
and my components being:
@HtmlTagMarker
inline fun FlowContent.mySearchBar(classes: String? = null): Unit =
div(classes) {
input {
this.classes += "form-control"
type = InputType.search
name = "search"
attributes["hx-post"] = "/search"
attributes["hx-trigger"] = "input changed delay:500ms, search"
attributes["hx-target"] = "#search-results"
attributes["hx-swap"] = "outerHTML"
attributes["hx-indicator"] = ".htmx-indicator"
}
}
@HtmlTagMarker
fun FlowContent.mySearchResults(classes: String? = null, results: List<SearchResult>): Unit {
return table {
id = "search-results"
this.classes += "table"
thead {
tr {
th { +"First Name" }
th { +"Last Name" }
th { +"Email" }
}
}
tbody {
results.map {
tr {
td { it.firstName }
td { it.lastName }
td { it.email }
}
}
}
}
}
The important bit is attribute["hx-post"]="/search"
that is expecting to receive back the html element to replace #search-results
, which is a table
. Let's implement this in Application.configureRouting()
:
post("/search"){
val params = call.receiveParameters()
val textInput = params["search"]
val searchResults = mySearchService.search(textInput)
// I don't think I can call `call.respondHtml {}`
// Because it expect a function on HTML
// So I can't return `call.respondHtml { div {} }`
// I would need to return `call.respondHtml { body { div{} } }
call.respondText(
createHTML().mySearchResults(results=searchResults),
ContentType.Text.Html.withCharset(Charsets.UTF_8))
}
To implement createHTML().mySearchResults
, I need to create this function:
HtmlTagMarker
fun <T, C : TagConsumer<T>> C.mySearchResults(classes : String? = null, results: List<SearchResult>) : T {
return table {
id = "search-results"
this.classes += "table"
thead {
tr {
th { +"First Name" }
th { +"Last Name" }
th { +"Email" }
}
}
tbody {
results.map {
tr {
td { it.firstName }
td { it.lastName }
td { it.email }
}
}
}
}
}
As you can see, the body of these 2 methods:
fun FlowContent.mySearchResults(classes: String? = null, results: List<SearchResult>): Unit
fun <T, C : TagConsumer<T>> C.mySearchResults(classes : String? = null, results: List<SearchResult>) : T
is exactly similar and pure duplication, but I don't seem to be able to extract them into a function that would fulfill both method signature. Any idea how to achieve that? I am also happy to hear any feedback if my overall approach is wrong (I am very new at using Kotlin HTML DSL)To remove the code duplication, use the consumer
property of the FlowContent
to call the <T, C : TagConsumer<T>> C.mySearchResults
method:
fun FlowContent.mySearchResults(classes: String? = null, results: List<SearchResult>): Unit {
this.consumer.mySearchResults(classes, results)
}