I have three questions: - the init block in a comp...
# javascript
a
I have three questions: • the init block in a companion object is not running, is that correct? • how can I get the attributeChangedCallback running in a web component? • using esClasses=true I notice that the init blocks in the classes are not run, and class properties are undefined after initialization. See thread for example code.
Here is my example code, I am using the excellent kotlin-wrappers, but I can not get attributeChangedCallback working.
Copy code
class MyWebComponent : HTMLElement(), CustomElement.WithConnectedCallback, CustomElement.WithAttributeChangedCallback {

    companion object {
        @OptIn(ExperimentalJsStatic::class)
        @JsStatic
        @JsName("observedAttributes")
        val observedAttributes = arrayOf("counter")

        init {
            println("MyWebComponent init")
        }
    }

    override fun connectedCallback() {
        val shadow = this.attachShadow(ShadowRootInit(mode = ShadowRootMode.open))
        val root = document.createElement("div")
        root.id = "compose-root"
        shadow.appendChild(root)

        val actualRoot = shadow.getElementById("compose-root") as Element

        renderComposable(root = actualRoot) {
            Text("Hello World!")
        }
    }

    override fun attributeChangedCallback(name: String, oldValue: JsAny?, newValue: JsAny?) {
        println("Attribute changed: $name, $oldValue, $newValue")
    }
}

fun main() {
    customElements.define(HtmlTagName("my-web-component"), MyWebComponent::class.js)

    val myElement = document.createElement("my-web-component")
    myElement.setAttribute("counter", "1")

    document.body.appendChild(myElement)

    setInterval(timeout = 1000.milliseconds) {
        myElement.setAttribute("counter", Random.nextInt(1, 100).toString())
        println("Interval!")
    }
}
The 'interval!' is showing, but the 'MyWebComponent init' isn't. When doing this:
Copy code
companion object {
        @OptIn(ExperimentalJsStatic::class)
        @JsStatic
        @JsName("observedAttributes")
        val observedAttributes = arrayOf("counter")

        fun init() {
            MyWebComponent::class.js.asDynamic().observedAttributes = observedAttributes
        }
    }
And
Copy code
MyWebComponent.init()
customElements.define(HtmlTagName("my-web-component"), MyWebComponent::class.js)
It works... but that doesn't seem very nice. I made a bit more clean:
Copy code
companion object {
        val observedAttributes = arrayOf("counter")

        fun register() {
            MyWebComponent::class.js.asDynamic().observedAttributes = observedAttributes
            customElements.define(HtmlTagName("my-web-component"), MyWebComponent::class.js)
        }
    }
e
There doesn't seem to be a decent workaround unfortunately.
a
Extending from another class allows the init block in that class to run
Copy code
abstract class WebComponent : HTMLElement(), CustomElement.WithCallbacks {
    val observedAttributes = ObservedAttributes() // a helper class for me which generates statesflows for the observed attributes

    init {
        println("Initializing WebComponent!")
        println(observedAttributes.toString())
    }
}
The init block is run and the observedAttributes is working
I got Compose HTML with WebComponents working with reactive stateflows for attributes. Will share it later, but this way I can encapsulate a Compose HTML in a Web Component which is pretty neat!
🆒 1
K 1
kodee happy 1
e
But weren't you doing the same in the first example?
Copy code
class MyWebComponent : HTMLElement()
a
Well yes.... but then the
init
block of
MyWebComponent
isn't called. But any
abstract
class you place in between gets called.... Why? I have no clue
e
AHHHH, now I get it, so you're using
WebComponent
as your base class instead of
HTMLElement
a
Si
It is a hack, because it should work normally. But that it I think the bug mentioned above.
e
I wonder why with the prod run Webpack translates the code to a proper constructor
t
It's Kotlin/JS problem
Meta issue with blockers is here
✔️ 1
Issue about missed constructor (in development) should be in this list
a
You are awesome
t
How you register your component?
a
Copy code
interface WebComponentFactory<T : WebComponent> {
    val tagName: HtmlTagName<T>
    val clazz: CustomElementConstructor<T>
    val observedAttributes: Array<String>

    fun register() {
        clazz.asDynamic().observedAttributes = observedAttributes
        customElements.define(tagName, clazz)
    }
}

class WebComponents(vararg val factories: WebComponentFactory<*>) {
    fun registerAll() = factories.forEach { it.register() }
}

fun registerWebComponents(vararg factories: WebComponentFactory<*>) = WebComponents(*factories).registerAll()
And for every web component class I have the companion object implement the factory. The separate WebComponents is redundant I see now. But you get the gist.
Copy code
@OptIn(ExperimentalJsExport::class)
@JsExport
@JsName("CounterComponent")
class CounterComponent : ComposeWebComponent(COUNTER, MIN, MAX) {

    @JsExport.Ignore
    companion object : WebComponentFactory<CounterComponent> {
        // part of my typed stateflow attributes
        val COUNTER = ObservedAttributes.Attribute("counter", 0, cast = { it?.toInt() ?: 0 })
        val MIN = ObservedAttributes.Attribute("min", 0, cast = { it?.toInt() ?: 0 })
        val MAX = ObservedAttributes.Attribute("max", 100, cast = { it?.toInt()?: 100 })

        override val tagName = HtmlTagName<CounterComponent>("counter-component")
        override val clazz = CounterComponent::class.js
        override val observedAttributes = ObservedAttributes.of(COUNTER, MIN, MAX)
    }
 ...
}

fun main() {
  registerWebComponents(CounterComponent)
}
I just found out that when adding @JSExport the init blocks are working, also without extending from an intermediate abstract class.
e
I couldn't get the development version to run with
JsExport
, it was obviously complaining about the
export
keyword being used.
a
using esmodules?
e
Yup
a
strange... I am writing up my experiences so far. Hope to publish it today or tomorrow. You can steal my code from there and see what works.
e
Are you explicitly calling
useEsModules()
?
a
Copy code
tasks.withType<KotlinJsCompile>().configureEach {
    compilerOptions {
        moduleKind.set(JsModuleKind.MODULE_ES)
        useEsClasses.set(true)
    }
}
e
Mmh interesting. I'll wait for your code and test it out again, thanks!
a