How to create custom elements (Web Components) with ClojureScript?

Custom elements rely on ES6 classes to be defined, a simple example in JS:

class MyElement extends HTMLElement {}
customElements.define("my-element", MyElement)

See Using custom elements on MDN for a detailed explanation

Since ES6 classes cannot reliably be transpiled, and ClojureScript (as far as I know) doesn’t emit ES6 class syntax, the only way I can think of to generate a custom element is with js*, e.g.:

(def my-element (js* "class MyElement extends HTMLElement {}"))
(js/customElements.define "my-element" my-element)

Is there a better way? Or any plans to add support for ES6 classes in ClojureScript?

I played with web components a long while ago and I got it working fine, just needs a bit a macro sugar to make it look nicer. It might work differently nowadays since a lot may have changed in the 3 years since writing that.

Are you sure ClojureScript/Closure can’t emit ES6?

I haven’t tested it but the :language-in and :language-out options seem like they would work with ES6.

The Closure Compiler wiki definitely says it can handle ES6.

Closure handles ES6 just fine but ClojureScript has no built-in support to emit ES6 code (eg. class).

Thanks for the example, it is still working, the web component is created and attached to the DOM. :slight_smile:

But I’m still trying to figure out how to mock some ES6 features. In many web components examples, apart of extending HTMLElement, the constructor is overridden (and super() is called) . So far I didn’t find a way of doing that with ClojureScript. Do you have more ClojureScript - Web Components examples?

For context, what I’m really trying to do is to create a ClojureScript wrapper around LitElement

1 Like
(defn component []
  (js/Reflect.construct js/HTMLElement #js [] component))

component is the constructor function and the js/Reflect call is basically the super() call. So this is basically class component extends HTMLElement.

4 Likes

@theller’s reply helped me out a bunch on this topic. For posterity, here’s my simple “render reagent/re-frame component as a webcomponent’s shadow DOM” function built out

3 Likes

I just found that there is a ticket about adding support for ES6 classes to ClojureScript:
https://clojure.atlassian.net/browse/CLJS-3084

This is really cool @Mechrophile! Works well!

This seems very useful, but the gist is not available anymore. Does someone has the full snippet?

I might have found the Mechrophile full content, not sure if it’s exactly his version.

(ns simple-reagent-webcomponent (:require [reagent.core :as re]))
 
(defn webcomponent! [name view-component]
  (let [;; defines the constructor function, which is the "class" object used by the customElements api
        component (fn component [] (let [e
                                         ;; this is the equivalent of the call to "super"
                                         (js/Reflect.construct js/HTMLElement #js [] component)]
                           (js/console.log (str name ": Constructed!"))
                           (set! (.-shadow e) (.attachShadow e #js {:mode "open"}))
                           e))]
    (set! (.-prototype component)
          ;; establishes prototype hierarchy
          (js/Object.create (.-prototype js/HTMLElement)
                            #js {:connectedCallback
                                 #js {:configurable true
                                      :value        (fn []
                                                      (this-as this
                                                        ;; attaches the reagent process to the shadow dom
                                                        (re/render [view-component] (.-shadow this))
                                                        (js/console.log (str name ": Connected! ") this)))}}))
 
    ;;finally, defines the component with these values
    (js/window.customElements.define name component)
 
    component))
 
(def globstate (re/atom 0))
 
(defn main-panel []
  [:div
   [:p "yo, dis a shadow dom p"]
   [:form
    [:p "Changes: " @globstate] [:br]
    "First Name: " [:input {:type      :text
                            :name      :fname
                            :on-change (fn [e] (.preventDefault e)
                                         (js/console.log "change!")
                                         (swap! globstate inc))}] [:br]]])
 
(defn init [] (webcomponent! "test-comp" main-panel))