https://kotlinlang.org logo
#kotlinx-html
Title
# kotlinx-html
r

Reuben Firmin

11/29/2023, 10:06 PM
i have another example of the same exception - "java.lang.IllegalStateException: You can't change tag attribute because it was already passed to the downstream at kotlinx.html.consumers.DelayedConsumer.onTagAttributeChange(delayed-consumer.kt:16)". in this case the component renders fine when it's part of the
createHTMLDocument
consumer (i.e. the original render) but fails when it's part of the
appendHTML
consumer. source in thread. why is this happening?
source:
Copy code
class TabBar(private val tabs: List<Tab>, consumer: TagConsumer<*>): DIV(mapOf(
    "class" to "text-xl font-medium text-center text-gray-500 border-b border-gray-200 dark:text-gray-400 " +
            "dark:border-gray-700 pb-8"), consumer) {

    private val activeClasses = "inline-block p-4 text-blue-600 border-b-2 border-blue-600 rounded-t-lg " +
            "active dark:text-blue-500 dark:border-blue-500"

    private val inactiveClasses = "inline-block p-4 border-b-2 border-transparent rounded-t-lg " +
            "hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300"

    fun render(block: TabBar.() -> Unit) {
        ul("flex flex-wrap -mb-px") {
            this@TabBar.tabs.forEach { tab ->
                li("me-2") {
                    form {
                        a(classes = if (tab.current) {
                            this@TabBar.activeClasses
                        } else {
                            this@TabBar.inactiveClasses
                        }) {
                            href = "#"
                            htmxClick(this@form, tab.path, tab.target)
                                .params(tab.params)
                            if (tab.current) {
                               this@a.attributes["aria-current"] = "page"
                            }
                            +tab.label
                        }
                    }
                }
            }
        }
        block()
    }
}

fun FlowContent.tabBar(tabs: List<Tab>, block: TabBar.() -> Unit = {}) {
    TabBar(tabs, consumer).visit {
        render(block)
    }
}

data class Tab(val label: String,
               val current: Boolean,
               val path: String,
               val target: String,
               val params: Map<String, String>)
it fails on the
this@a.attributes["aria-current"]
line, but again only when under
appendHTML
another weirdness: if I move the href below the tab.current check, it will fail in the
createHTMLDocument
consumer
c

Cies

11/30/2023, 12:06 PM
Why
this@a.attributes
? why not just
Copy code
if (tab.current) { attributes["aria-current"] = "page"}
directly under the
href = "#"
i dont know what htmxClick does... but you cannot add HTML arguments to a tag once you have started filling in the HTML tag's content.
r

Reuben Firmin

11/30/2023, 12:08 PM
I did that first, same issue - the
this@a
was just superstition to make sure I was adding attributes to the right tag. What constitutes content in this case, href? Or the label?
Here's my htmxClick function:
Copy code
fun HTMLTag.htmxClick(path: String, target: String): HTMLTag {
        attributes["hx-post"] = path
        attributes["hx-trigger"] = "click"
        attributes["hx-target"] = target
        attributes["hx-swap"] = "innerHTML"
        return this
    }
I pasted the wrong htmxClick function, and that gave me an idea. This works:
Copy code
fun render(block: TabBar.() -> Unit) {
        ul("flex flex-wrap -mb-px") {
            this@TabBar.tabs.forEach { tab ->
                li("me-2") {
//                    form {
                        a(classes = if (tab.current) {
                            this@TabBar.activeClasses
                        } else {
                            this@TabBar.inactiveClasses
                        }) {
                            href = "#"
                            htmxClick(tab.path, tab.target)
//                            htmxClick(this@form, tab.path, tab.target)
//                                .params(tab.params)
                            if (tab.current) {
                               attributes["aria-current"] = "page"
                            }
                            +tab.label
//                        }
                    }
                }
            }
        }
        block()
    }
So it's something about the form, and the form version of htmxClick, which is:
Copy code
fun HTMLTag.htmxClick(form: FORM, path: String, target: String): FORM {
        attributes["hx-post"] = path
        attributes["hx-trigger"] = "click"
        attributes["hx-target"] = target
        attributes["hx-swap"] = "innerHTML"
        return form
    }

    fun FORM.params(params: Map<String, String>) {
        params.entries.forEach {
            input {
                type = InputType.hidden
                name = it.key
                value = it.value
            }
        }
    }
So I think what you're saying is because I'm adding these hidden inputs into the a tag itself (which is admittedly janky) then the subsequent setting of attributes fails.
c

Cies

11/30/2023, 12:37 PM
<tag1 agr1="value arg1" arg2="value arg2">content of tag1<tag2>content tag 2</tag2></tag1>
all of tag2 is also content of tag1
im not sure why htmxClick returns
this
, we do not do that... also if htmxClick only works on a tags, put it on
A
instead of on all htmlTags
r

Reuben Firmin

11/30/2023, 12:47 PM
it can actually go on any tag. i was returning
this
just so i could stick params on in a builder pattern. with some lightweight refactoring, the form approach now works:
Copy code
fun render(block: TabBar.() -> Unit) {
        ul("flex flex-wrap -mb-px") {
            this@TabBar.tabs.forEach { tab ->
                li("me-2") {
                    form {
                        htmxParams(tab.params) // <<<<<<
                        a(classes = if (tab.current) {
                            this@TabBar.activeClasses
                        } else {
                            this@TabBar.inactiveClasses
                        }) {
                            href = "#"
                            htmxClick(this@form, tab.path, tab.target)
                            if (tab.current) {
                               attributes["aria-current"] = "page"
                            }
                            +tab.label
                        }
                    }
                }
            }
        }
        block()
    }
so now the hidden inputs go into the form above the
a
that was helpful, thanks
c

Cies

12/04/2023, 1:50 PM
welcome! glad it helped!!
6 Views