Creating a custom element / webcomponent

Hi,

Has anyone figured out how to create a custom element?

An example of this is documented here:

Google developer docs

and further documented here:

Mozilla docs

The principle is to call window.customElements.define(elementName, <class literal>)

In KotlinJS’ dom bindings, customElements.define is declared as

fun define(name: String, constructor: () -> dynamic, options: ElementDefinitionOptions = definedExternally): Unit

The following causes a compilation error:

    abstract class MyComponent : Element() {
      companion object {
        fun init() {
          console.log("registering")
          window.customElements.define("my-component", ::MyComponent )
          console.log("registered")
        }
      }
      init {
        this.textContent = "my-component"
      }
    }

Compilation fails at the line with ::MyComponent because MyComponent is abstract. It has to be abstract because it cannot extend from Element (and external abstract class!) without implementing all members.

Any thoughts on how this problem could be resolved please?

thanks
Fuzz.

Just as a thought - is the use of abstract redundant and perhaps not helpful, when declaring with external?

window.customElements.define("my-component", ::MyComponent )

The proper usage would be something like

window.customElements.define(
    "my-component",
    MyComponent::class.js.unsafeCast<() -> dynamic>()
)

For convenience, you can declare a function like this:

fun CustomElementRegistry.define(
    name: String, 
    constructor: KClass<in Element>
) {
    define(name, constructor.js.unsafeCast<() -> dynamic>())
}
2 Likes

Thanks Alexey, will try that and report back.

Being able to create first class web components as Kotlin classes is very compelling :slight_smile:

1 Like

Did you manage to create custom elements with KotlinJS ? I tried the code given by Alexey but unfortunately the generated code throws an error :

Uncaught TypeError: Failed to construct 'HTMLElement': Please use the 'new' operator, this DOM object constructor cannot be called as a function.
at new CounterElement (komponent_main.js:161)

Here is my code :

abstract class CustomElement : HTMLElement() {
	init {
		println("Creating custom element")
		textContent = "Hello world"
	}
}

fun <T : Element> defineElement(name: String, constructor: KClass<T>) {
	window.customElements.define(name, constructor.js.unsafeCast<() -> dynamic>())
}

fun main(args: Array<String>) {
	defineElement("custom-element", CustomElement::class)
}

I tried to create a custom element with hand-written ES5 code and it does not work:

function CustomElement() {
    HTMLElement.call(this);
    console.log("Custom element created")
}
CustomElement.prototype = Object.create(HTMLElement.prototype);
CustomElement.prototype.constructor = HTMLElement;

window.customElements.define("custom-element", CustomElement);

However, with ES6 everything works as expected:

class CustomElement extends HTMLElement {
    constructor() {
        super();
        console.log("Custom element created");
    }
}

window.customElements.define("custom-element", CustomElement);

It seems that web components don’t support ES6 classes, at least according to this discussion. And Kotlin/JS does not support ES6 target. We plan to support ES6 eventually, but currently we don’t have any current plans for that.

I actually managed to make it work :slight_smile: This is indeed because KotlinJS compiles to ES5, so we have to add an additional polyfill custom-elements-es5-adapterjs as described here : GitHub - webcomponents/webcomponentsjs: A suite of polyfills supporting the HTML Web Components specs

I started to write a library to write custom elements with KotlinJS (as well as KotlinJS wrappers for Polymer elements) and I think that it looks quite promising so I can keep you up-to-date on that if you’d like :slight_smile:

4 Likes

Has anyone of you a working example online that would help me getting started?

1 Like

I have a small working demo of a small POC I made a few months ago if you are still interested :

$ git clone https://github.com/jdemeulenaere/komponent.git
$ ./gradlew gulp_default
$ ./gradlew build -t

Fire up a local server and open index.html :slight_smile:

2 Likes

Hi i want this polymer lib

Thanks for getting back tp me. Took a while for me to notice. I’ll give it a try today!

I made this for fun :slight_smile:. Maybe somebody is still interested in this thread, so i posted it.

Kotlin code:

import org.w3c.dom.HTMLElement
import org.w3c.dom.OPEN
import org.w3c.dom.ShadowRootInit
import org.w3c.dom.ShadowRootMode
import kotlin.browser.window

fun main(args: Array<String>) {
    val xTestSpec = customElementSpec().apply {
        observedAttributes = arrayOf("prop")
        init = XTest::init.unsafeCast<(HTMLElement) -> Unit>()
        attributeChangedCallback = XTest::attributeChangedCallback.unsafeCast<(HTMLElement, String, String, String) -> Unit>()
    }
    val xTestConstructor = getCustomElementConstructor(xTestSpec)
    window.customElements.define("x-test", xTestConstructor)
}

abstract external class XTest : HTMLElement

fun XTest.init() {
    val shadow = attachShadow(ShadowRootInit(ShadowRootMode.OPEN)).unsafeCast<HTMLElement>()
    shadow.innerHTML = "<p>Works !!!</p>"
}

fun XTest.attributeChangedCallback(attrName: String, oldVal: String, newVal: String) {
    println("$attrName: $oldVal -> $newVal")
}

private fun customElementSpec(): CustomElementSpec = js("{}").unsafeCast<CustomElementSpec>()

private external interface CustomElementSpec {
    var observedAttributes: Array<String>?
    var init: ((receiver: HTMLElement) -> Unit)?
    var connectedCallback: ((receiver: HTMLElement) -> Unit)?
    var disconnectedCallback: ((receiver: HTMLElement) -> Unit)?
    var attributeChangedCallback: ((receiver: HTMLElement, attrName: String, oldVal: String, newVal: String) -> Unit)?
    var adoptedCallback: ((receiver: HTMLElement) -> Unit)?
}

private val getCustomElementConstructor: (spec: CustomElementSpec) -> () -> dynamic by lazy {
    function<(spec: CustomElementSpec) -> () -> dynamic>("spec", block = BLOCK)
}

@JsName("Function")
private external fun <T> function(vararg params: String, block: String): T

private const val BLOCK = """const myObservedAttributes = spec.observedAttributes;
const myInit = spec.init;
const myConnectedCallback = spec.connectedCallback;
const myDisconnectedCallback = spec.disconnectedCallback;
const myAttributeChangedCallback = spec.attributeChangedCallback;
const myAdoptedCallback = spec.adoptedCallback;

return class extends HTMLElement {

    static get observedAttributes() {
        if (myObservedAttributes) {
            return myObservedAttributes;
        }
    }

    constructor() {
        super();
        if (myInit) {
            myInit(this);
        }
    }

    connectedCallback() {
        if (myConnectedCallback) {
            myConnectedCallback(this);
        }
    }

    disconnectedCallback() {
        if (myDisconnectedCallback) {
            myDisconnectedCallback(this);
        }
    }

    attributeChangedCallback(attrName, oldVal, newVal) {
        if (myAttributeChangedCallback) {
            myAttributeChangedCallback(this, attrName, oldVal, newVal);
        }
    }

    adoptedCallback() {
        if (myAdoptedCallback) {
            myAdoptedCallback(this);
        }
    }
};"""

HTML code:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Test - Web Components</title>
    <script src="out/production/kotlin-webcomponents/lib/kotlin.js"></script>
    <script src="out/production/kotlin-webcomponents/kotlin-webcomponents.js"></script>
</head>
<body>

<x-test prop="test"></x-test>

</body>
</html>

Google Chrome 68.0.3440.106 (Build oficial) (64 bits) screenshot:

Firefox Developer Edition 63.0b1 (64-bits) screenshot:

Hi ! is there now a way to create a custom element ?
The solution proposed by @jdemeulenaere is not accessible anymore.

Thanks !

Unfortunately there doesn’t seem to be a way: Kotlin-js doesn’t currently compile to ES6. You can subscribe/vote for https://youtrack.jetbrains.com/issue/KT-8373 and other related issues, to vote for the Kotlin guys to implement this.

In the meantime, I have created a small project to allow creating web components in Kotlin JS
https://github.com/mvanassche/kotlin-web-component

1 Like

In Kotlin 1.9.0 ES6 is now experimental.
It is now possible to do web components in Kotlin! For example:

external interface WebComponent {
    fun connectedCallback()
    fun disconnectedCallback()
    fun adoptedCallback()
    fun attributeChangedCallback(name: String, oldValue: String, newValue: String)
}

@JsExport
abstract class CustomElement : HTMLElement(), WebComponent {
    init {
        println("Creating custom element")
        val shadow = this.attachShadow(ShadowRootInit(ShadowRootMode.OPEN)).unsafeCast<HTMLElement>()
        shadow.innerHTML = "<p>Hello</p>"
    }
    override fun connectedCallback() {
        println("connectedCallback")
    }
}
kotlin {
    js(IR) {
        useEsModules()
   [...]
   }
}

tasks.withType<KotlinJsCompile>().configureEach {
    kotlinOptions {
        useEsClasses = true
    }
}
1 Like