squint-cljs / cherry

Experimental ClojureScript to ES6 module compiler
https://squint-cljs.github.io/cherry
553 stars 22 forks source link

Support EcmaScript classes #97

Closed jakub-stastny closed 1 year ago

jakub-stastny commented 1 year ago

Sibling of the same issue on Squint https://github.com/squint-cljs/squint/issues/288

Reason: Web Components are defined via the class syntax, there's no other way.

jakub-stastny commented 1 year ago

As a workaround, as suggested by @borkdude on #cherry Slack:

None of these unfortunately works for me (my project is based on Web Components, so there will be a lot of classes), but maybe it helps someone.

borkdude commented 1 year ago

The main thing blocking me to support this is the syntax that should be chosen. Any proposals?

jakub-stastny commented 1 year ago

@borkdude I don't have a strong opinion on this, but it might be a good idea to be compatible with classes that ShadowCLJS has https://clojureverse.org/t/modern-js-with-cljs-class-and-template-literals/7450 in order to prevent further fragmentation of the ecosystem.

That might make it easier to borrow that code from ShadowCLJS as well :)

borkdude commented 1 year ago

@jakub-stastny Yeah, probably! Alright, let's do that.

shvets-sergey commented 1 year ago

I've implemented Web Components tutorial using Lit and there are a few extra considerations that aren't supported by Shadow-cljs syntax above.

Static class properties: Lit and Web Components require them for reactivity. You can set a static property with what fields on class to watch and it will fire callback every time they change.

export class SimpleGreeting extends LitElement {
  static styles = css`p { color: blue }`;

  static properties = {
    name: {type: String},
  };

  constructor() {
    super();
    this.name = 'Somebody';
  }

  render() {
    return html`<p>Hello, ${this.name}!</p>`;
  }
}

Workaround that works in shadow and will work here as well: Call after defclass the following code.

(set! ClassName "propName" "propValue")

Template literals support: It will be nice to support (js-template) from shadow link above. Calling lit html template just as a function is problematic as it doesn't accept the regular JS array with templates. It expects a TemplateStringsArray which expects to have a raw property on the object. Faking it with the following code does the trick, but isn't sustainable.

(defn template-array
  [string-seq]
  (let [arr (js/Object.assign #js [] string-seq)]
    (set! (.-raw arr) string-seq)
    arr))

Super calls in Object override methods Lit hooks up to the Web Components standard callbacks for adding/removing elements from DOM. In order for all machinery work, you need to be able to call super's method or otherwise your render won't event work.

How it looks in javascript:

connectedCallback() {
  super.connectedCallback()
  addEventListener('keydown', this._handleKeydown);
}

In ClojureScript there is not much you can do as even (js* "js-code") doesn't help because call to super.connectedCallback() must be the first, but generated function has some (I assume) assignment to hook up prototypes implementations.

Very Ugly workaround is to go into prototype chain and call parent manually, but I'm not exactly sure if it replicates what super does and if it will work forever or even at every browser.

(connectedCallback
   [this]
   ;; Object.getPrototypeOf(Dog.prototype).speak.call(this);
   (.call (.-connectedCallback (.getPrototypeOf js/Object lit/LitElement.prototype)) this)
   (js/console.log "Hello from Dom" this)))

There is an issue in shadow-cljs to address the second part and it seems that for the first one the workaround is acceptable for now. Since squint generates ES6 compliant code, probably better to solve both problems right away?

Thanks!

borkdude commented 1 year ago

Can you test the current defclass-2 branch?

jakub-stastny commented 1 year ago

Sorry haven’t seen the notification until now. Will check.

borkdude commented 1 year ago

It's already been merged and published.