Arjan van Wieringen
05/08/2025, 8:51 AMArjan van Wieringen
05/08/2025, 8:58 AMclass 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:
companion object {
@OptIn(ExperimentalJsStatic::class)
@JsStatic
@JsName("observedAttributes")
val observedAttributes = arrayOf("counter")
fun init() {
MyWebComponent::class.js.asDynamic().observedAttributes = observedAttributes
}
}
And
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:
companion object {
val observedAttributes = arrayOf("counter")
fun register() {
MyWebComponent::class.js.asDynamic().observedAttributes = observedAttributes
customElements.define(HtmlTagName("my-web-component"), MyWebComponent::class.js)
}
}
Edoardo Luppi
05/08/2025, 10:07 AMEdoardo Luppi
05/08/2025, 10:09 AMArjan van Wieringen
05/08/2025, 11:24 AMArjan van Wieringen
05/08/2025, 11:25 AMabstract 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 workingArjan van Wieringen
05/08/2025, 11:40 AMEdoardo Luppi
05/08/2025, 1:38 PMclass MyWebComponent : HTMLElement()
Arjan van Wieringen
05/08/2025, 2:07 PMinit
block of MyWebComponent
isn't called. But any abstract
class you place in between gets called....
Why? I have no clueEdoardo Luppi
05/08/2025, 2:09 PMWebComponent
as your base class instead of HTMLElement
Arjan van Wieringen
05/08/2025, 2:09 PMArjan van Wieringen
05/08/2025, 2:09 PMEdoardo Luppi
05/08/2025, 2:10 PMturansky
05/08/2025, 2:11 PMturansky
05/08/2025, 2:22 PMturansky
05/08/2025, 2:24 PMArjan van Wieringen
05/08/2025, 2:25 PMturansky
05/08/2025, 2:47 PMArjan van Wieringen
05/08/2025, 3:44 PMinterface 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.
@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)
}
Arjan van Wieringen
05/09/2025, 4:02 PMEdoardo Luppi
05/09/2025, 5:00 PMJsExport
, it was obviously complaining about the export
keyword being used.Arjan van Wieringen
05/09/2025, 5:22 PMEdoardo Luppi
05/09/2025, 5:22 PMArjan van Wieringen
05/09/2025, 5:22 PMEdoardo Luppi
05/09/2025, 5:23 PMuseEsModules()
?Arjan van Wieringen
05/09/2025, 5:23 PMtasks.withType<KotlinJsCompile>().configureEach {
compilerOptions {
moduleKind.set(JsModuleKind.MODULE_ES)
useEsClasses.set(true)
}
}
Edoardo Luppi
05/09/2025, 5:23 PMArjan van Wieringen
05/09/2025, 5:25 PMArjan van Wieringen
05/10/2025, 12:48 PM