Creating a custom element / webcomponent

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: