kotlinx.html
kotlinx.html copied to clipboard
Would it be possible to remove `crossinline`?
What I am trying to do
I am trying to use kotlinx.html with Spring WebFlux.
I am trying to call a suspending function inside a rendering block.
An action which would roughly look like this is currently not possible.
@GetMapping("/")
suspend fun index(): String {
// Note: Using String + StringBuffer here only for simplicity
return StringBuffer().let { sb
sb.appendHTML().html {
// Cannot call a suspending function here as `block` is `crossinline`
// "Suspension functions can be called only within coroutine body"
someService.someSuspendingFunction()
}
sb.toString()
}
}
If crossinline was removed from html this would be possible.
Why I don't want to call all suspend functions before rendering HTML
Because I want to build components which use suspend functions.
A simple example would be a component which uses ResourceUrlProvider to get the URL to a resource.
ResourceUrlProvider#getForUriString returns Mono<String> which I would like to convert to a suspend function using awaitFirst.
It would be quite bothersome to do all those tasks inside the action first.
Would it be possible to remove crossinline?
I tested removing crossinline locally. I had to remove it from three places:
- https://github.com/Kotlin/kotlinx.html/blob/c2f8ee0df425e5188681ac821034c8781ee2a146/buildSrc/src/main/kotlin/kotlinx/html/generate/tagsgen.kt#L345
- https://github.com/Kotlin/kotlinx.html/blob/c2f8ee0df425e5188681ac821034c8781ee2a146/src/commonMain/kotlin/api.kt#L77
- https://github.com/Kotlin/kotlinx.html/blob/c2f8ee0df425e5188681ac821034c8781ee2a146/src/commonMain/kotlin/api.kt#L79
Then I included the project locally into my project using includeBuild and could successfully render HTML that way.
So the code would compile and I also think this would not be a breaking change as crossinline lambdas are a subset of all possible lambdas?
Thanks for your help!
I have the same need. Here you have another use case https://stackoverflow.com/q/73788797/1140754
I want to immediately start emitting static HTML (i.e. <html><body><h1>Artist Info</h1>), then suspend until data is available and then proceed.
suspend fun viewArtistInfo(fileName: String, cfArtist: CompletableFuture<Artist>) {
FileWriter(fileName).use {
it
.appendHTML()
.html {
body {
h1 { +"Artist Info" }
val artist = cfArtist.await() // ERROR Suspension functions can be called only within coroutine body
p { +"From: ${artist.from}" }
}
}
}
}
@felixscheinost cc @fmcarvalho
thanks to Kotlin extension functions, this is actually possible without a lib update. here is how i solved this in my framework, elide:
first, add your own methods to HTML for body and head, which have suspend, and redirect their calls to visitSuspend:
package kotlinx.html.tagext;
// imports...
/**
* Open a `<body>` tag with support for suspension calls.
*
* @Param classes Classes to apply to the body tag in the DOM.
* @param block Callable block to configure and populate the body tag.
*/
@HtmlTagMarker
public suspend inline fun HTML.body(
classes : String? = null,
crossinline block : suspend BODY.() -> Unit
) : Unit = BODY(
attributesMapOf("class", classes),
consumer
).visitSuspend(block)
/**
* Open a `<head>` tag with support for suspension calls.
*
* @param block Callable block to configure and populate the body tag.
*/
@HtmlTagMarker
public suspend inline fun HTML.head(
crossinline block : suspend HEAD.() -> Unit
) : Unit = HEAD(emptyMap, consumer).visitSuspend(
block
)
next, implement visitSuspend:
package kotlinx.html.tagext;
// imports...
// Visitor with suspension support.
public suspend inline fun <T : Tag> T.visitSuspend(crossinline block: suspend T.() -> Unit): Unit = visitTagSuspend {
block()
}
// Tag visitor with suspension support.
@Suppress("TooGenericExceptionCaught")
public suspend inline fun <T : Tag> T.visitTagSuspend(crossinline block: suspend T.() -> Unit) {
consumer.onTagStart(this)
try {
this.block()
} catch (err: Throwable) {
consumer.onTagError(this, err)
} finally {
consumer.onTagEnd(this)
}
}
then, you can just import your extensions...
import kotlinx.html.tagext.body
import kotlinx.html.tagext.head
import kotlinx.html.title
and use them regularly where you need suspend support:
@Get("/") suspend fun indexPage(request: HttpRequest<*>) = ssr(request) {
head {
title { +"Hello, Elide!" }
stylesheet(asset("styles.base"))
stylesheet("/styles/main.css")
script("/scripts/ui.js", defer = true)
}
body {
injectSSR(this@Index, request)
}
}