Fork me on GitHub
#hyperfiddle
<
2023-07-12
>
Hendrik08:07:34

I try to create a ref similar to dom/node . I tried to start with something simple, but I already get some weird errors:

(e/def some-ref)

(defmacro with-ref [ref & body]
  `(binding [some-ref ~ref]
     ~@body))

(e/defn bar []
  (println "mount" some-ref))

(e/defn Some []
  (e/client

   (binding [some-ref 24]
     (bar.))
    (with-ref 42 (bar.))

   ))
The manual (binding [some-ref 24] (bar.)) works. However, if I replace it with the with-ref macro, then I get (error in the answer). Can anybody explain what is happening here and what is causing this?

Hendrik09:07:15

Unbound var `app.todo-list/some-ref`

 in ( clojure.core/println mount <exception> ) in app/todo_list.cljc
 in reactive (defn bar [] ...) in app/todo_list.cljc line 46
 in ( app.todo-list/with-ref 42 <exception> ) in app/todo_list.cljc
 in reactive (defn Some [] ...) in app/todo_list.cljc line 49
 in (try ...) 

 Error: Unbound var `app.todo-list/some-ref`
    at Object.hyperfiddle$electric$impl$runtime$error [as error] (runtime.cljc:66:3)
    at eval (runtime.cljc:394:28)
    at eval (Latest.cljs:34:42)
    at missionary$impl$Latest$transfer (Latest.cljs:44:45)
    at Object.eval [as cljs$core$IDeref$_deref$arity$1] (Latest.cljs:10:17)
    at Object.cljs$core$_deref [as _deref] (core.cljs:688:12)
    at Object.cljs$core$deref [as deref] (core.cljs:1477:4)
    at eval (Latest.cljs:28:20)
    at missionary$impl$Latest$transfer (Latest.cljs:44:45)
    at Object.eval [as cljs$core$IDeref$_deref$arity$1] (Latest.cljs:10:17)

Dustin Getz17:07:42

The code looks correct to me, could it be the same issue as your subsequent thread?

Dustin Getz17:07:39

This works for me in the starter app repo:

Dustin Getz17:07:58

Possible gotchas are: • e/def duplication/ambiguous site • possibly hot code reloading issues related to forgetting the :require-macros the first time, causing the cljs build to end up in a bad state? This happens infrequently enough that there may be latent issues not yet detected at the electric/cljs/shadow touch points. We do have a custom hot code reloading strategy that forces the server to reload every .cljc file that shadow rebuilds. (That is necessary to keep your client/server code versions in sync)

Hendrik18:07:33

Thanks for your response. I think that it was a hot code reloading and forgetting the :require-macros issue. Now it works :)

👍 1
Dustin Getz19:07:48

Ok, yeah I think i saw the exact same issue when testing this (I also forgot the require-macros), which means we have repro steps for the next time we work on this

braai engineer10:07:19

Will Electric work with Quarkus / GraalVM? (`native-image`)

leonoel13:07:29

after incremental computation compilation is landed there should not be any technical blocker for AOT compilation anymore

Vincent00:07:00

incremental compilation? salivates uncontrollably

grounded_sage17:08:27

This is definitely desired

telekid13:07:42

FWIW after some initial grumbling, I'm not missing hiccup syntax as much as I thought I would

teodorlu14:07:34

What’s the advantage of hiccup over the current syntax? Are there specific use cases that you feel are lacking now that would improve with support for hiccup?

telekid14:07:43

> What’s the advantage of hiccup over the current syntax? I'm not convinced there are any

telekid14:07:03

but will pay attention as I go forward

telekid14:07:14

I think it's hard to understand why hiccup isn't an improvement until you start to become comfortable with the effectful rendering style

👍 2
jjttjj14:07:55

Agreed. I think it's a prime example of people irrationally preferring something that's familiar to them. Hiccup is for sure a great way to represent flat html files, but it's a poor fit imo for dynamic web apps. Personally I almost prefer the dom-elements-as-functions anyway. It's natural to me that a div would be a function that mutates the dom rather than a data structure created by a function that is handed off to some reconciliation engine. The overhead is pretty big (in terms of performance and/or complexity you take on) to get hiccup syntax into cljs dom libraries and at the end of the day you basically get to just use square brackets and keywords instead of symbols and parens. I really like the electric dom syntax. I kind of even like that it's a little more verbose, where in electric you must explicitly wrap props and text for example, because it makes sense to me that these should be explicit, since that is like, the granularity where things can change in your dom tree.

👍 3
Dustin Getz15:07:03

Yeah, abstractionist persona falls in love with the precision, frontend dev persona (high end frontends are markup heavy, as of 2023 at least) hates the boilerplate

👍 1
Dustin Getz15:07:20

The syntax I have in mind is this:

(dom/h1 :text "hello world")
(dom/h1 ... {:text "hello world"})
critically it separates static props (that are always present) from dynamic props (which may be present), e.g (dom/h1 ... (if x (assoc {} :text "hello world")))

👀 1
Dustin Getz15:07:23

But I have not thought about it in a while, it needs to be checked carefully for ambiguous edge cases

Dustin Getz15:07:44

... is "spread" operator, or perhaps use &

Dustin Getz15:07:17

plus there is reasonable sugar that can be added on top, something like :div.class1.class2#id can probably be hacked in somehow for the markup persona who writes a lot of static annotations

braai engineer08:07:44

Would be nice to have some sugar because typing out props ... :class is everywhere. Instead of:

(dom/div
  (dom/props {:class (string/join " " [(if active? "active) "other-class"])})
  (dom/text "hi" name))
would be nice if it could be:
(div.other-class {:class [(if active? :active)]}
  "hi" name) ;; can dom/text be assumed? probably edge cases.

braai engineer08:07:26

@U09K620SG how about vectors instead of maps? [:text "hello " name] ?

Reut Sharabani14:07:49

is there an example about saving state per client?

Reut Sharabani14:07:07

(like sessions...)

s-ol14:07:05

server side session state and client UI state work the same way, just in e/server and e/client

s-ol14:07:15

you just need an atom and e/watch:

s-ol14:07:45

(let [!val (atom initial-val)
      val (e/watch !val)]
  ...)

Reut Sharabani14:07:02

I needed to know where I can generate an "id" since the trivial examples it's transparent (there is no request). The example I attached shows where the websocket request data is stored if I understand corretly.

s-ol14:07:10

use swap! etc to update !val, val streams changes

Reut Sharabani14:07:32

I didn't try it yet but I see it has cookies and stuff so I expect it will be stable. Why do you think it won't be?

s-ol14:07:32

if you need it to mediate between clients

Reut Sharabani14:07:41

how can a client know what state belongs to them? You have to diffentiate them somehow. Maybe there is a pattern I'm missing here. An example would be helpful.

s-ol14:07:40

if the state is not persistant across reloads you don't need to, the electric entrypoint is "instantiated" server-side once for each websocket/client

s-ol14:07:04

so any local state declared is per-client already, no need for an id

s-ol14:07:43

if you want to share state between clients you do that via global atoms, but that's optional

Reut Sharabani14:07:42

you're saying this code belongs in the client?

(let [!val (atom initial-val)
      val (e/watch !val)]
  ...)

Reut Sharabani14:07:51

I thought you mean to put this in the server

Reut Sharabani14:07:38

but if you have a good canonical example I think it's valuable to attach it here so it's indexed by slack

s-ol14:07:05

@U7KDU667Q if you put this on the server, each client will have its own !val

s-ol14:07:06

this is an example: the counter state is per-client (always starts at 0, separate if you open two tabs)

Reut Sharabani14:07:49

I understand the "1-counter" example but I want a state per client on the server. I don't understand this statement:

if you put this on the server, each client will have its own !val (edited) 
From my experience it's shared for all clients it's not per-client. Maybe I don't understand how my code works 😄 This var (`mmd`) holds a graph description:
#?(:clj (defonce !mmd (atom "")))
(e/def mmd (e/server (e/watch !mmd)))
When I use it in e/client it's shared across clients. So I'm not sure what you mean.

s-ol15:07:16

this is an e/def, not the sample i shared

s-ol15:07:22

try this:

(ns user.tutorial-7guis-1-counter
  (:require [hyperfiddle.electric :as e]
            [hyperfiddle.electric-dom2 :as dom]
            [hyperfiddle.electric-ui4 :as ui]))

(e/defn Counter []
  (let [!state (atom 0)
        state (e/watch !state)]
    (e/client
      (dom/p (dom/text state))
      (ui/button (e/fn [] (e/server (swap! !state inc) nil))
        (dom/text "Count")))))

👍 1
s-ol15:07:39

this is per client state on the server

s-ol15:07:52

think about it this way: adding or removing e/server or e/client should never change the behaviour of the program. (In practice, there is a CLJ vs CLJS difference, so many things will actually break)

Dustin Getz15:07:41

(def !x (atom .)) global state, shared. (Clojure semantics) • (let [!x (atom .)] ...) local state, not shared. !x extent is bounded by the electric function's extent which is bounded by the electric program's extent which is tied to the websocket connection extent • (e/def !x (e/server (atom .))) local because e/def extent is tied to the program extent which is tied to the websocket connection extent. i think we are reconsidering this, it was an oversight. It might not change

🚀 3
Dustin Getz15:07:17

Another way to see it is that Electric bindings have an object lifecycle = dynamic extent. Clojure globals have indefinite extent = the entity continues to exist as long as the possibility of reference remains (consider garbage collection). See https://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node43.html

Hendrik15:07:13

Is it possible to use a (e/def db) in a callback? I added this to the starter app: (ui/button (e/fn [] (println (todo-count db))) (dom/text "click")) After clicking the button I get Unbound var app.todo-list/db`` . Is it possible to make (e/def db) available at callback time?

Dustin Getz16:07:15

It is possible and works, with a gotcha (that we're planning to fix): Today, (e/def x 1) creates a binding on both client and server, there are two bindings with the same name. Since it is ambiguous, you must be explicit from which site you resolve it from. This is in contrast with let bindings which are defined unambiguously at a single site and therefore the compiler can infer which site has the binding.

(e/def x 1) ; ambiguous, binding x exists at both places
(e/server
  (let [y 2] ; unambiguous, binding y located at server
    (e/client
      (println y) ; y inferred to be on server
      (println x) ; x is ambiguous so compiler assumes local access, here client
      (println (e/server x)) ; explicitly access x on server
      )))
the middle println will produce unbound var runtime error

Dustin Getz16:07:20

So in your case, I expect that db is only bound on the server and therefore we need to explicitly access it on the server

Dustin Getz16:07:37

Resolving the ambiguity is WIP

Hendrik19:07:14

Thanks again for the explanation. I did not see that db was bound server side. Now I got it to work. I chose db and the starter app to have a simple reproducible example. My usecase is still getting three.js opengl wrapped into electric. A rerender must be called explictly. So my idea is to have is_dirty state, which is created by the renderer and is passed to members of the scenegraph. Similar to e/dom where children and props can rely on something up the tree is setting it to the right value.

👀 1
Dustin Getz19:07:10

Is the scene graph mutable (threejs data structure) or is it clojure data? Clojure equality semantics are the perfect cachekey for establishing such a dependency in the DAG to cause render to happen

Dustin Getz19:07:59

a dirty flag (i.e. a counter) is fine also

Dustin Getz19:07:38

If you add me to your repo with your attempt (even messy scratch stuff) I can take a look

braai engineer23:07:38

If you e/def dynamically, how do you undef the var? (e/def x nil)?

Hendrik05:07:22

It is a mutable threejs data structure. Therefore the dirty flag. In theory I could create a immutable data structure by copy on write. That would require a recreation of the scene graph up to the root node. Maybe that is efficient enough. At least it requires no dirty flag. For now I have no repository, yet, just some code snippets. I’ll create one at the weekend.

grounded_sage17:07:36

Found a subtle error that could have a friendlier error message, when there is a space starting at the class list: (dom/props {:class " my-class") Error:

core.cljs:3953 Cannot read properties of null (reading 'cljs$core$IFn$_invoke$arity$0')

 in reactive (fn [] ...)
 in reactive (fn ClassList [node classes] ...)
 in reactive (fn [] ...)
 in reactive (defn WebsiteGreeting [] ...) in app/todo_list.cljc line 485
 in reactive (defn App [] ...) in app/todo_list.cljc line 503
 in (try ...) 

 TypeError: Cannot read properties of null (reading 'cljs$core$IFn$_invoke$arity$0')
    at eval (Relieve.cljs:34:27)
    at Object.missionary$impl$Relieve$ready [as ready] (Relieve.cljs:35:44)
    at G__40067 (Relieve.cljs:42:27)
    at Object.missionary$impl$Observe$run [as run] (Observe.cljs:40:13)
    at eval (core.cljc:564:27)
    at eval (Relieve.cljs:42:8)
    at Object.missionary$impl$Relieve$run [as run] (Relieve.cljs:42:7)
    at eval (core.cljc:659:34)
    at eval (runtime.cljc:601:13)
    at eval (Continuous.cljs:144:8)

Dustin Getz17:07:00

What electric version? I think that might have been fixed

grounded_sage21:07:30

Oh it might. I haven’t checked on the version.

Dustin Getz21:07:31

it might be fixed only on master actually, if so better just to wait

braai engineer23:07:42

Is this supposed to work?

(e/defn TextInput [value OnChange]
  (ui/input value OnChange
    (dom/props {:placeholder "some placeholder..."})))

(e/defn App []
  (let [!text (atom ""), text (e/watch !text)]
    (TextInput. text (e/fn [v] (reset! !text v)))))
ui/input works if I put it directly in App but does not seem to work if I move it to a subcomponent.

Dustin Getz00:07:46

i don’t see a problem here, try it in a fresh starter app to confirm

Dustin Getz00:07:38

(the stack trace does not match the code, so it seems that more is in play than this snippet)

braai engineer16:07:57

Resolved by bumping to latest Electric version.

🙌 2