Open felixscheinost opened 2 years ago
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)
}
}
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.
If
crossinline
was removed fromhtml
this would be possible.Why I don't want to call all
suspend
functions before rendering HTMLBecause 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
returnsMono<String>
which I would like to convert to asuspend
function usingawaitFirst
.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: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!