Fork me on GitHub
#squint
<
2023-07-26
>
borkdude18:07:19

@zane about test-driving defclass: 🧵

👀 2
borkdude18:07:32

This works:

(defprotocol IFoo (foo [_]) (bar [_] [_ a b]))

(defclass Foo
  (field x)
  (constructor [this]
               )

  IFoo
  (foo [_] :foo)
  (bar [_] [:bar])
  (bar [_ a b] [:bar a b]))

(prn (bar (new Foo) 1 2))

borkdude18:07:45

also

(defclass Foo (extends ...))

borkdude18:07:06

but there are some edge cases I'm trying to fix and those are the hard parts, e.g. extending methods on Object is what doesn't work right now

borkdude18:07:40

If someone would help me test drive some examples, it would help make progress I think

zane18:07:52

Do you have specific use cases you want tested, or should I just go ahead and use it for the one I had in mind (making a simple web component)?

borkdude18:07:04

your use case would be good to start with

zane18:07:27

OK, will do.

zane18:07:03

I wonder if I can use (p)npm to install your branch, or if I have to clone it manually. Hm.

borkdude18:07:30

you will have to build squint locally since it has to be compiled to work on npm

zane18:07:36

Got it. OK!

borkdude18:07:03

branch: defclass-2

borkdude18:07:23

you can then do npm install to install the local project

zane18:07:32

I thought you might have something fancy set up with a post-install step.

borkdude18:07:02

no, I'm just running:

./node_cli.js
for local testing while developing

2
sergey.shvets18:07:28

Here is a simple lit hello world component if this helps. I got it to work with shadow.

(ns demo.index
  (:require [shadow.cljs.modern :refer (defclass js-template)]
            [goog.dom :as gdom]
            ["lit" :as lit]))


(defclass HelloWorld
  (extends lit/LitElement)

  (field name "Username")

  (constructor
   [this]
   (super))

  Object
  (render
   [this]
   (js/console.log "render is called" this)
   (js-template lit/html "<h1> Hello " name "!<h1>")))


(defn -main
  []
  (js/console.log "Hello, world!")
  (.define js/customElements "hello-world" HelloWorld)
  (let [app       (.getElementById js/document "app")
        custom-el (gdom/createElement "hello-world")]
    (gdom/appendChild app custom-el))
  )

borkdude18:07:48

ok, I think it should (almost) work with squint

borkdude18:07:59

except js-template doesn't work yet

borkdude18:07:57

but isn't lit/html just a function you can call?

borkdude18:07:39

gdom also isn't available, gotta use the normal browser dom methods

borkdude18:07:42

I think you can (for now) just call lit/html as:

(lit/html ["<h1> Hello", "!</h1>"], name)

borkdude18:07:57

although it is inconvenient, should be sufficient for testing

sergey.shvets18:07:01

That line doesn't work in shadow, let me figure out a proper way to call the template function from shadow and then I'll try to take squint to run that same code.

borkdude18:07:34

in shadow that should be #js [...] of course

borkdude18:07:41

but in squint [] compiles to normal arrays

sergey.shvets18:07:12

Yes, I tried it with JS, didn't do the trick. Even without variables. I'm googling it now: Error: invalid template strings array

borkdude18:07:35

> function foo(array, ...args) { return [ array, ...args]; }
undefined
> foo`dude`
[ [ 'dude' ] ]
> x = 1
1
> foo`dude ${x}`
[ [ 'dude ', '' ], 1 ]

borkdude18:07:04

foo`dude ${x}` is the same as writing:

foo([ 'dude ', '' ], x );

sergey.shvets18:07:01

Yes, it seems that lit/html doesn't like what I give to it.

borkdude18:07:05

perhaps debug with CLJS and print the args there

borkdude18:07:23

and then pass the same args in the other notation

borkdude19:07:12

it would be useful if someone could make a sample project with squint + lit, I have no lit experience but even a getting started project looks complicated

sergey.shvets19:07:02

I took on lit this morning, so that's why I'm struggling to figure out what am I doing wrong. Thanks for the tips. I have this in code

(.html lit #js [ "<h1> Hello" "" "!</h1>"] name)
And that's what shadow generates.
return module$node_modules$lit$index.html(["<h1> Hello","","!</h1>"],self__.name);
Error is the following:
lit-html.js:56 Uncaught (in promise) Error: invalid template strings array
    at P (lit-html.js:56:60)
    at V (lit-html.js:80:11)
    at new N (lit-html.js:94:20)
    at R._$AC (lit-html.js:280:49)
    at R.g (lit-html.js:270:43)
    at R._$AI (lit-html.js:249:189)
    at exports.render (lit-html.js:431:12)
    at appspark$index$HelloWorld.update (lit-element.js:66:106)
    at appspark$index$HelloWorld.performUpdate (reactive-element.js:300:16)
    at appspark$index$HelloWorld.scheduleUpdate (reactive-element.js:286:17)
So I guess the problem isn't with how I call it, but the lit itself.

borkdude19:07:27

try (lit/html ...)

borkdude19:07:10

> foo`<h1> Hello ${x}!<h1>`
[ [ '<h1> Hello ', '!<h1>' ], 1 ]

borkdude19:07:31

so perhaps remove the empty string?

borkdude19:07:00

if I'm not mistaking, the template strings array should always have one more element than the amount of variable argument

borkdude19:07:06

because they are interleaved

borkdude19:07:12

and in your case, you have 3 vs 1

borkdude19:07:24

instead of 2 vs 1

borkdude19:07:06

so:

(lit/html #js [ "<h1> Hello" "!</h1>"] name)

sergey.shvets19:07:58

Didn't help. I think the problem is it expects https://lit.dev/docs/v1/api/lit-html/templates/ TemplateStringsArray and I give it just a js array which is missing raw property.

borkdude19:07:54

I think that "TemplateStringsArray" is just a typescript thing

sergey.shvets19:07:20

This function did the thing, but that's a big hack 😄

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

sergey.shvets19:07:34

ok, ready to test squint with the same component. What's the easiest way to set it up?

borkdude19:07:51

you tell me

borkdude19:07:53

just npm install squint

borkdude19:07:07

and call squint compile <file.cljs>

borkdude19:07:35

but like I explained to @zane in the #cherry channel, the experimental branch is defclass-2 and you need to compile it locally

borkdude19:07:58

I was actually talking to @zane, but somehow the conversation switched to you 😂

borkdude19:07:23

all good though, more testers the merrier

sergey.shvets19:07:51

Sorry for stealing it 🙂 do I just run bb build to compile locally on a defclass-2 branch?

borkdude19:07:59

bb dev I would do

borkdude19:07:22

I can continue tomorrow, afk now

sergey.shvets19:07:46

ok, thanks for the help!

zane20:07:23

> I think that “TemplateStringsArray” is just a typescript thing @U4EFBUCUE This seems right to me. From what I can tell the actual runtime value is just a JavaScript array. What error are you getting exactly?

zane20:07:50

Oh, wait! I’m wrong.

sergey.shvets20:07:17

I "emulated" the TemplateStringsArray and lit ate it. But I don't know a proper workaround )

zane20:07:28

Emulated how?

sergey.shvets20:07:40

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

zane20:07:53

Ah, great. That’s what I was going to suggest.

sergey.shvets20:07:00

added a dumb raw prop

zane20:07:47

Did that work? You may want the array value to be the same as the value of the raw property.

sergey.shvets20:07:46

Yes, it worked within shadow and I'm still setting up squint to test it there

2
sergey.shvets21:07:28

So, I tried the code and it crashed because super() isn't the first call in constructor. Web Components enforce it. The code and simple dev setup is here: https://github.com/shvets-sergey/web-components-squint super() must be called before const self__ in line 12 on screenshot: Browser error for reference:

borkdude21:07:57

Thanks, I'll have a look tomorrow

sergey.shvets21:07:48

Also static property (field) in shadow isn't in compiled file (line 18 on screenshot above). Not sure if that is suppose to work yet.

borkdude21:07:29

you mean the default value: no I haven't made that to work yet, but I think you can assign it in the constructor, I'll fix it tomorrow

borkdude18:07:34

I didn't get to work on it a whole lot today, but only for a few hours. Still wrestling with passing "this" arguments etc

borkdude18:07:42

OO is pretty frustrating

zane18:07:14

It is deeply disappointing that web components require classes.

borkdude18:07:47

This example now works:

(defprotocol IFoo (foo [_]) (bar [_] [_ a b]))

(defclass Foo
  (field x) ;; TODO: support defaults;
  (constructor [this]
               (set! x "this-is-x"))

  Object
  (toString [this] (str x :dude))

  IFoo
  (foo [_] :foo)
  (bar [_] [:bar])
  (bar [this a b] [(str this) :bar a b]))

(prn (bar (new Foo) 1 2))

(prn (str (new Foo)))

borkdude18:07:09

so I may have solved the this trouble. the only thing left is maybe the field default and perhaps some other stuff

🎉 4
borkdude18:07:30

and the super problem mentioned above I think

sergey.shvets19:07:20

I found an extra problem with super yesterday as I was diving more into lit. Shadow doesn't have a syntax support for it and there is a pretty ugly workaround. More details are here: https://github.com/thheller/shadow-cljs/issues/1137 TLDR: sometimes you need to call super.parentMethod(). For example, lit uses it to extend standard lifecycle methods of web components and if you want to extend them in your component you have to call the lit's method or otherwise your render will never be called. CLJS compiler doesn't have a way to do it and even spitting it with (js* "super.parentCallback()") doesn't work because JavaScript requires super to be the first call in a class method and cljs compiler ads that this.self assignment as on the super problem above.

sergey.shvets19:07:22

But the good side that I made all the lit tutorial stuff to work on shadow and even use normal cljs structures, so I can test this part on squint as soon as super problem is solved 🙂

borkdude19:07:30

I pushed another commit that fixes the order for super() and the self init

borkdude19:07:54

The super call doesn't support any other args, it's just super() right now. now afk, to be continued tomorrow.

sergey.shvets19:07:17

Base example (without default value) for lit now works like a charm.

👍 2
borkdude19:07:32

you can use (set! instance-field constructor-arg)

borkdude19:07:45

or (set! instance-field "static-propr") in the constructor

sergey.shvets19:07:51

Yeap, that works.

sergey.shvets19:07:30

Btw, another thing to consider that isn't supported by shadow syntax and has to be work around with (set! ClassName ...) is static arguments for class. It seems like some libraries rely on them to deliver some functionality (at least lit does). Screenshot for explanation (see static styles and static properties).

borkdude19:07:03

Cool, could you please post this stuff in a Github issue in squint (I think there's one about defclass)

borkdude19:07:13

then I'll have another go at it tomorrow

sergey.shvets20:07:44

Done! Thank you very much!

👍 2
borkdude10:07:32

I released a new version of squint, but note that defclass isn't in there yet. I'm getting back to defclass now for the coming few hours

borkdude14:07:04

This example works now in the latest commit:

$ ./node_cli.js -e '(defclass Class1 (field _x) (constructor [this x] (set! _x x))) (defclass Class2 (extends Class1) (constructor [this x y] (super (+ x y)))) (prn (new Class2 1 2))'
{"_x":3}

borkdude14:07:00

The last step is adding the field defaults

borkdude15:07:59

pushed field defaults, please test. I'll add a unit test for this all soon based on what I tested here:

$ ./node_cli.js --show -e '(defclass Class1 (field _x) (field __secret :dude) (constructor [this x] (set! _x x))) (defclass Class2 (extends Class1) (field _y 1) (constructor [this x y] (super (+ x y))) Object (dude [this] _y)) (def c (new Class2 1 2)) (prn [c (.dude c)])'

👍 1
sergey.shvets15:07:04

Cool, I'll test it later today and report back. Thank you!

👍 2
sergey.shvets20:07:19

Tested and default fields now working. Is super in Object's override method suppose to work?

(connectedCallback
   [this]
   (super (connectedCallback))
   (js/console.log "Hello from Dom" this))
Right now it generates this and breaks:
(HelloWorld2["prototype"]["set-checked"] = f__26357__auto__5);
let f__26357__auto__6 = (function (this$) {
this$ = this; const self__ = this;
super$(connectedCallback());
return console.log("Hello from Dom", this$);
})

sergey.shvets20:07:25

Uncaught ReferenceError: super$ is not defined
    at HelloWorld2.f__26357__auto__6 [as connectedCallback] (index.mjs:52:1)
    at add_component_to_app (index.mjs:61:13)
    at start (index.mjs:67:8)
    at index.mjs:70:1

sergey.shvets20:07:06

Also, it seems like my hack with template string doesn't work when template-array contains code variables.

borkdude20:07:15

thanks. does the override method work with shadow?

sergey.shvets20:07:37

No, it doesn't. I talked with Thomas about it and he created an issue: https://github.com/thheller/shadow-cljs/issues/1137. There is a workaround I described in the github issue, but it's very hacky.

borkdude20:07:29

ok, I'll follow shadow's issue

borkdude20:07:04

is there anything else not working that does work with shadow? my initial goal is to support whatever shadow supports in squint

borkdude20:07:15

I'll implement the js-literal thing later

sergey.shvets20:07:36

js-template literal

sergey.shvets20:07:58

The rest seem to work.

borkdude10:07:48

I'll take a look at that super stuff in impl methods

borkdude10:07:40

the issue is that you can't write:

super.getNameSeparator()
right?

borkdude10:07:46

Right: > Should most likely lift Object method declarations out of the extend-type and directly into the class body. Will think about it for a bit. This is what I would do too

borkdude11:07:17

@U4EFBUCUE This example now works in the branch:

(ns scratch
  (:require [squint.core :refer [defclass]]))

(defclass Class1
  (field _x)
  (field __secret :dude)
  (constructor [this x] (set! _x x))

  Object
  (getNameSeparator [_] "-"))

(defclass Class2
  (extends Class1)
  (field _y 1)
  (constructor [this x y] (super (+ x y)))

  Object
  (dude [this] (str _y (.getNameSeparator (js* "super")))))

(def c (new Class2 1 2)) (prn [c (.dude c)])

borkdude12:07:37

I'll do js-literal after my break

borkdude14:07:58

pushed js-template (js-literal was again a typo)

borkdude14:07:06

I'm sure there are edge cases so please test :)

borkdude14:07:51

I guess I'll have to figure out how to test this with a proper lit project...

sergey.shvets16:07:15

Thanks, I'll test more later today.

borkdude19:07:36

pushed some minor fixes. I've got this example working: https://github.com/squint-cljs/squint-lit-example/blob/main/my_element.cljs almost... When I click the button, the counter increases, but a re-render isn't triggered

borkdude19:07:05

I don't know a lot about lit, any pointers are welcome

sergey.shvets19:07:31

you need to set static properties to the class. add this (probably without #js in squint case) after class definition before registering component.

(set! (.-properties MyElement) #js {"count" #js {}})
And it should do the trick. I'm going to test lit-tutorial components on your branch now.

sergey.shvets19:07:48

Seems like methods with dash in name don't work.

(handle-click
   [this event]
   (js/console.log "click" this)
   (js/console.log "event" event)
   (set! fname (.. event -target -value))
   nil) 
This one throws error.

sergey.shvets19:07:13

(.connectedCallback (js* "super")) worked and js template too!

borkdude19:07:08

cool, (set! (.-properties MyElement) #js {"count" #js {}}) worked!

borkdude19:07:16

#js is allowed in squint, but it's a no-op

borkdude19:07:46

ah yes, I'll add handle-click to my list, I also bumped into it. try naming it handleClick

sergey.shvets19:07:47

Yeah, I renamed and it worked. Testing further. Am I supposed to get error for doall? Here is what I'm trying to do:

(defn js-map
  [f js-seq]
  (let [jsArr #js []]
    (doall
     (for [item js-seq]
       (let [processed (f item)]
         (.push jsArr processed))))
    jsArr))
And getting the error:

borkdude19:07:22

yes, just use vec here, it's fixed in a branch which I will soon merge

borkdude19:07:35

or for side effects, just use doseq

borkdude19:07:47

(doseq [item js-seq] ...)

sergey.shvets19:07:03

That worked. aclone also not supported, right? I'll rewrite it with js clone function.

borkdude19:07:16

why don't you just use (map ...) here? it returns a new array

borkdude19:07:47

(mapv f ...)

sergey.shvets19:07:03

It's a different case where I need to add an item to js array and "replace" it, so the lit reactivity kicks in.

borkdude19:07:45

right, I think we have conj! for this, but no we don't have aclone yet, I think

borkdude19:07:53

feel free to post a separate issue for that

sergey.shvets19:07:33

ok, everything seems to work. Atom's add-watch aren't supported by squint, right?

sergey.shvets19:07:29

I pushed components I tested here: https://github.com/shvets-sergey/web-components-squint These are all from lit basic tutorial.

borkdude20:07:24

That's awesome!

borkdude20:07:40

atom add-watch not supported yet, but we could do, issue welcome.

sergey.shvets21:07:47

I had exactly the same thought in mind. I'm thinking about it, but haven't tried to do anything yet. Probably in a few days. I think hiccup will struggle with non-standard html tags that are component names? So, somehow needs to support something like ["hello-word" {:prop "name} & children]

borkdude21:07:40

[:hello-world ...] should just work?

sergey.shvets21:07:18

Oh ok, then it's not an issue at all.

borkdude15:08:58

@U4EFBUCUE can you remind me of the remaining things in this thread that weren't done yet? one being atoms + add-watch

sergey.shvets16:08:44

I only faced doall and add-watch, but they're kind of unrelated to the defclass/web-components. I've seen the issue on doall somewhere already, not sure about add-watch. There is nothing related to web-components that is left out.

borkdude16:08:57

doall is already in

borkdude16:08:11

ok, I'll have a look at add-watch

sergey.shvets04:08:57

I made a proof of concept working for this: https://twitter.com/MikeMargerum/status/1686122808064032769 but it uses cljs.analyzer. Can I use cljs.analyzer within squint/cherry macros? I think this can be done without cljs-analyzer but cljs analyzer does tons of heavy-lifting and I'd like to keep it if possible.

sergey.shvets05:08:03

Simply copying hiccup's implementation doesn't work, as we need to mix-and-match string templates with clojure code and then merge consecutive strings, so we get the max performance out of browser's template implementation. It seems that browsers have some optimizations for diffing template literals that allowing lit to have almost 50% faster performance then React's VDOM for common use cases.

borkdude06:08:37

Well cljs analyzer probably isn’t going to be in squint because of size but where’s the code? I also did an attempt in a branch but ran into some edge cases

borkdude06:08:20

Shall we start a new thread about this?

sergey.shvets16:08:58

yeah, let me work a bit more on this and I'll start a new thread when I have a code to present. It's now a long spaghetti one exploring what's possible. It also has a bunch of other stuff to generate web components, beyond a js template literal, which for sure should be separate.

sergey.shvets16:08:33

I only use cljs analyzer during macroexpansion to write the cljs-code. Easier to walk forms and handle some edge cases. I don't need that as runtime dependency. But I'm not sure how squint/cherry macros is different from regular cljs one and if I can do the same there. It works on shadow right now.

sergey.shvets16:08:45

Regular vectors is relatively easy to do without analyzer, but if/for is more challenging.

borkdude17:08:50

Well squint can already emit JSX via hiccup so in that sense it’s close

sergey.shvets18:08:46

Do you think adding some template literal like #jst template-fn? [:tag props? & children]

sergey.shvets18:08:52

Or reader tags can only affect the next argument? I don't know anything about their internals.

borkdude18:08:38

I have played around with this, with a generic #html [:a ...] reader conditional which returns a vector of strings and expressions, much like a template function would receive, such that you can do:

(js-template lit/html #html [:a ...])

borkdude18:08:04

yes, reader tags only apply to the next element

borkdude18:08:13

of course you could do something like #tmpl [lit/html [:a ...]]

sergey.shvets18:08:16

I like your approach, but in lit's case you need to have some conditionals for properties. E.g. if you want to pass javascript argument you need to generate a property name with some chars like this <div .yourProperty={js object} ?checked=true @click=(fn [e]) . So it's not just plain generic html. Also they have directives that are just an expression instead of argument=value pair.... That's another headache.

borkdude18:08:08

I also looked into if there was a jsx -> lit-html thing, but couldn't find it

sergey.shvets18:08:09

I was thinking about hicada approach where people can create a macro and pass some template-specific things when calling a compiler function. But ideally this should be avoided if possible.

borkdude18:08:40

don't know hidica

sergey.shvets18:08:52

Their idea that they have a compiler code, but user has to create a macro and pass things like js/createElement, custom tags etc. Macro is quite simple, example is

(defmacro html
  [body]
  (hicada.compiler/compile body {:create-element 'js/React.createElement
                                 :transform-fn (comp)
                                 :array-children? false}
                                 {} &env))

sergey.shvets18:08:30

Their compiler is very react specific, so I'm thinking how to generalize it for typical js-templates. Ideally to capture css`` templates too.