Open mikaelstaldal opened 1 year ago
Possible solution:
@HtmlTagMarker
inline fun <T, C : TagConsumer<T>> C.fragment(crossinline block: TagConsumer<T>.() -> Unit): T {
try {
this.block()
} catch (err: Throwable) {
this.onTagError(HTMLTag("", this, emptyMap(), null, inlineTag = false, emptyTag = false), err)
}
return this.finalize()
}
Or no, that does not work properly, it ends up calling TagConsumer.finalize()
multiple times. Back to the drawing board.
"non-well-formed" sounds like the use of elements that are either missing an opening tag or a closing tag, but not both. If you only read the title of this issue, it sounds like you are trying to use a bad (non-well-formed) approach, but the thing you ask for is perfectly fine. "multiple elements with no containing element" is clearer IMO.
I also need a solution for this. Every time you want to reuse code (by using a function call), you are forced to use an unwanted container element, such as a div
. But I'm now in the situation where I need code reuse in the HTML head
, so I cannot use div
anymore....
I am using the XML definition of well-formedness: https://www.w3.org/TR/2008/REC-xml-20081126/#sec-well-formed
But OK, I tried to make it more clear.
You could do something like this:
val result: String = buildString {
appendLine(createHTML().option {})
appendLine(createHTML().option {})
appendLine(createHTML().option {})
}
println(result)
But these intermediate strings do limit performance and it's an ugly solution, especially when the number of root level elements grows.
Update: or better:
val result: String = buildString {
appendHTML().option { }
appendHTML().option { }
appendHTML().option { }
}
println(result)
I believe that sooner or later, everybody who uses kotlinx.html on an intermediate level, will sooner or later bump into this problem. The best solution would be if there was an attribute to signal that the root level element should be skipped, like:
val html: String = createHTML()
.select {
skipRoot = true // changed
for (model in models) {
option {
value = model.id
+model.name
}
}
}
or something like this:
val html: String = createHTML()
.container { // changed
for (model in models) {
option {
value = model.id
+model.name
}
}
}
This solutions works, but it requires a pull request.
Change kotlinx.html.Tag.tagName
from type String
to String?
. Then fix all usage of that property, which usually means code like:
if (tag.tagName == null) {
return
}
(Only about 5 files have to be modified.)
Then you can use this:
fun main() {
val r = createHTML().emptyRoot {
div { }
div { }
}
println(r) // prints: <div></div> <div></div>
}
inline fun <T, C : TagConsumer<T>> C.emptyRoot(classes : String? = null, crossinline block : EMPTY_ROOT.() -> Unit = {}) : T = EMPTY_ROOT(
attributesMapOf(),
this
).visitAndFinalize(this, block)
open class EMPTY_ROOT(initialAttributes : Map<String, String>, override val consumer : TagConsumer<*>) : HTMLTag(null, consumer, initialAttributes, null, false, false), HtmlBlockTag
Are there any updates? I'd like to see this implemented as well. I'm down to help out if you want.
I bumped into this issue when trying https://htmx.org with a ktor+kotlinx.html backend. Note that the issue does not only concern "multiple elements" as I might want to replace a single <li>
in a list. kotlinx.html does not allow me to build a <li>
without a containing <ol>
or <ul>
.
Luckily there is a workaround in https://htmx.org : using a hx-select
attribute I can ignore the containing <ol>
in the response and only "select" the <li>
element. This also works with multiple elements.
It's not perfect but you can copy the li
method onto FlowContent, e.g.
inline fun FlowContent.li(classes: String? = null, crossinline block: LI.() -> Unit = {}) =
LI(attributesMapOf("class", classes), consumer).visit(block)
My solution to the empty root problem:
inline fun partial(crossinline block: FlowContent.() -> Unit) = createHTML {
object : FlowContent {
override val attributes = DelegatingMap(emptyMap(), this) { this@createHTML }
override val attributesEntries: Collection<Map.Entry<String, String>>
get() = this.attributes.immutableEntries
override val consumer: TagConsumer<*>
get() = this@createHTML
override val emptyTag: Boolean
get() = false
override val inlineTag: Boolean
get() = false
override val namespace: String?
get() = null
override val tagName: String
get() = ""
}.block()
}
Came across this issue while researching. Posting my solution here if anyone needs it:
import kotlinx.html.*
class FRAGMENT(override val consumer: TagConsumer<*>) : HTMLTag(
tagName = "fragment",
consumer = consumer,
initialAttributes = emptyMap(),
emptyTag = false,
inlineTag = false,
namespace = null,
), FlowContent
@HtmlTagMarker
inline fun <T, C : TagConsumer<T>> C.fragment(
crossinline block: FRAGMENT.() -> Unit = {}
): T = FragmentAwareTagConsumer(this).let { FRAGMENT(it).visitAndFinalize(it, block) }
class FragmentAwareTagConsumer<T>(private val delegate: TagConsumer<T>) : TagConsumer<T> by delegate {
override fun onTagStart(tag: Tag) {
if (tag !is FRAGMENT) delegate.onTagStart(tag)
}
override fun onTagEnd(tag: Tag) {
if (tag !is FRAGMENT) delegate.onTagEnd(tag)
}
}
Usage:
buildString {
appendHTML().fragment {
div {
+"Hello, world from div!"
}
}
appendHTML().fragment {
p {
+"Hello, world from paragraph!"
}
}
}
Столкнулся с такой же проблемой. Моё решение:
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.server.application.*
import io.ktor.server.html.*
import io.ktor.server.response.*
import kotlinx.html.*
import kotlinx.html.consumers.filter
import kotlinx.html.stream.appendHTML
suspend fun <TTemplate : Template<FlowContent>> ApplicationCall.respondFragmentTemplate(
template: TTemplate,
status: HttpStatusCode = HttpStatusCode.OK,
body: TTemplate.() -> Unit
) {
template.body()
respondFragment(status) { with(template) { apply() } }
}
suspend fun ApplicationCall.respondFragment(status: HttpStatusCode = HttpStatusCode.OK, block: FlowContent.() -> Unit) {
val text = buildString {
appendHTML().filter { if (it is FRAGMENT) SKIP else PASS }.fragment(block = block)
}
respond(TextContent(text, ContentType.Text.Html.withCharset(Charsets.UTF_8), status))
}
@Suppress("unused")
private class FRAGMENT(override val consumer: TagConsumer<*>) :
HTMLTag("fragment", consumer, emptyMap(), null, false, false), HtmlBlockTag {
}
@HtmlTagMarker
private inline fun <T, C : TagConsumer<T>> C.fragment(crossinline block: FRAGMENT.() -> Unit = {}): T =
FRAGMENT(this).visitAndFinalize(this, block)
Использование:
get("/fragment_template") {
call.respondFragmentTemplate(
TestTemplate()
) {}
}
get("/fragment") {
call.respondFragment {
div {
+"test"
}
}
}
class TestTemplate : Template<FlowContent> {
override fun FlowContent.apply() {
div {
+"testTemplate"
}
}
}
It would be useful to be able to generate HTML fragments with multiple elements without any containing element.
Use case is to generate HTML snippets on server to be requested with AJAX and inserted into the DOM on the client.
This code:
Gives this:
But I want this:
(Same issue with
<tr>
etc.)