Creating a custom element / webcomponent


#1

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.


#2

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


#3
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>())
}

#4

Thanks Alexey, will try that and report back.

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


#5

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)
}

#6

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.


#7

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 : https://github.com/webcomponents/webcomponentsjs#user-content-custom-elements-es5-adapterjs

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:


#8

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


#9

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:


#10

Hi i want this polymer lib


#11

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


#12

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: