Introduce TagConsumer downcast extension point for DOM interop#296
Conversation
| fun finalize(): R | ||
| } | ||
|
|
||
| interface InteroperableTagConsumer<I, out R> : TagConsumer<R> { |
There was a problem hiding this comment.
Realistically this is only available on the JSDOMBuilder (js and wasmjs). I guess it's not a terrible idea to have the interface though.
| private fun HTMLElement.asR(): R = this.asDynamic() | ||
|
|
||
| override fun onRenderedContent(content: Node) { | ||
| path.last().appendChild(content) |
There was a problem hiding this comment.
This sort of assumes that all use cases of this would be appending to the DOM. That might be fine but part of me wondered if instead of a method like this, the DOM tag consumers should just expose path.last() somehow letting users modify the current last DOM leaf however they want?
| @OptIn(ExperimentalContracts::class) | ||
| inline fun FlowContent.dom(crossinline block: () -> Node) { | ||
| contract { callsInPlace(block, InvocationKind.AT_MOST_ONCE) } | ||
| val consumer = when (val topConsumer = consumer) { |
There was a problem hiding this comment.
This I really don't like all that much. We have no way to offer this method since all the DSL methods dangle off HTMLTag which contains a TagConsumer<*>. Unless there wasa whole distinct set of Tags for JS which contained a more specific type of consumer. However that would be a lot more bloat. This method is only available on js and wasmJs which limits the blast radius.
|
|
||
| wrapper.append.div { | ||
| dom { | ||
| document.createElement("svg") |
There was a problem hiding this comment.
This is a standin for what a sister library could do.
| import kotlin.contracts.InvocationKind | ||
| import kotlin.contracts.contract | ||
|
|
||
| interface TagConsumer<out R> { |
There was a problem hiding this comment.
I am thinking maybe the best way to go here would be to introduce a val out: Any? = null here where this points to path.last() in JSDOMBuilder and the generic appendable in the streaming one (and delegates in the delegating ones). This should give us something to easily hook into for HTMLElement functions using as? / is operators.
I think you could introduce this here with an opt-in annotation, since we can probably come up with something better in a later release when significant breaking changes are on the table.
There was a problem hiding this comment.
I agree. I actually was planning to refactor into something like that. It probably makes more sense to keep some of this awkward implementation in consumers for now.
I assume this library will be significantly refactored for context params. Might be better to consider options then.
There was a problem hiding this comment.
Done. I think it's a lot less problematic this way.
| override fun finalize() = lastLeaved ?: throw IllegalStateException("No tags were emitted") | ||
|
|
||
| @ExperimentalKotlinxHtmlApi | ||
| override val last: Any? get() = path.lastOrNull() |
There was a problem hiding this comment.
Noticed that the JVM dom consumer could also use this. I never even knew this existed before. Interesting!
bjhham
left a comment
There was a problem hiding this comment.
Yeah, this approach looks good to me 👍
| override fun finalize(): R = lastLeaved?.asR() ?: throw IllegalStateException("We can't finalize as there was no tags") | ||
|
|
||
| @ExperimentalKotlinxHtmlApi | ||
| override val last: Any? get() = path.lastOrNull() |
There was a problem hiding this comment.
We can use HtmlElement return type here and replace references to path.last() to tidy things here.
There was a problem hiding this comment.
I figured preventing a throw was ideal, though since this is an experimental opt in I am not against it. Realistically I would be hanging this off of a Tag function when I use it which means it shouldn't ever throw. I will follow the standards you deem ideal for this library though! Just wanted to explain my thought process.
There was a problem hiding this comment.
It has been updated to use last().
I am working on an implementation of the SVG 1.1 protocol. One thing I would really like to be able to do is seamless inter-operation with
kotlinx-html. However I ran into an issue. For most targets, where the HTML being generated is really just text, this can be accomplished by simply using the unsafe raw text API:I could easily refactor this into an extension function that simply taps into the raw call. In fact, there are quite a few examples of libraries that have done this. However on targets like JS or WasmJS, kotlinx-html actually generates DOM elements. If I attempt to tap into the raw(...) calls I'll just be adding text to DOM elements and not adding DOM elements!
I want the ability for one DSL to seamlessly interoperate with
kotlinx-htmlon those targets! Obviously this was created due to a need for a library I am developing, but this should be generic enough to be used any library that would like to interop this way.Right now, if I want to create an extension function it look something like this:
Essentially I create a
divso that I can get what that method returns as I have no way to grab the HTML element inside of the DSL. I brought this up on slack as well:https://kotlinlang.slack.com/archives/CKWA2MV8U/p1771194798125389
This solution "works" but I'll be honest that it has some aspects I don't love, I'll get into those with comments.