Search code examples
kotlinktorhtmx

Returning partial HTML and avoiding redundency code in Kotlin Ktor


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)

Solution

  • 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)
    }