This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-07-12
Channels
- # announcements (2)
- # babashka (22)
- # babashka-sci-dev (15)
- # beginners (62)
- # calva (2)
- # cider (8)
- # clj-kondo (33)
- # clojure (52)
- # clojure-europe (46)
- # clojure-losangeles (1)
- # clojure-norway (5)
- # clojure-spec (7)
- # clojurescript (31)
- # conjure (20)
- # data-science (4)
- # datalevin (16)
- # fulcro (28)
- # hyperfiddle (71)
- # introduce-yourself (3)
- # lsp (50)
- # off-topic (16)
- # polylith (8)
- # portal (3)
- # practicalli (1)
- # reitit (1)
- # releases (2)
- # tools-build (22)
- # vim (8)
- # xtdb (17)
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?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)
The code looks correct to me, could it be the same issue as your subsequent thread?
This works for me in the starter app repo:
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)
Thanks for your response. I think that it was a hot code reloading and forgetting the :require-macros issue. Now it works :)
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
Will Electric work with Quarkus / GraalVM? (`native-image`)
after incremental computation compilation is landed there should not be any technical blocker for AOT compilation anymore
This is definitely desired
FWIW after some initial grumbling, I'm not missing hiccup syntax as much as I thought I would
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?
> What’s the advantage of hiccup over the current syntax? I'm not convinced there are any
I think it's hard to understand why hiccup isn't an improvement until you start to become comfortable with the effectful rendering style
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.
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
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")))
But I have not thought about it in a while, it needs to be checked carefully for ambiguous edge cases
...
is "spread" operator, or perhaps use &
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
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.
@U09K620SG how about vectors instead of maps? [:text "hello " name]
?
is there an example about saving state per client?
(like sessions...)
server side session state and client UI state work the same way, just in e/server and e/client
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.
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?
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.
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
if you want to share state between clients you do that via global atoms, but that's optional
you're saying this code belongs in the client?
(let [!val (atom initial-val)
val (e/watch !val)]
...)
I thought you mean to put this in the server
but if you have a good canonical example I think it's valuable to attach it here so it's indexed by slack
@U7KDU667Q if you put this on the server, each client will have its own !val
this is an example: the counter state is per-client (always starts at 0, separate if you open two tabs)
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.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")))))
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)
• (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
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
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?
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 errorSo in your case, I expect that db
is only bound on the server and therefore we need to explicitly access it on the server
Resolving the ambiguity is WIP
I opened a public ticket to track https://github.com/hyperfiddle/electric/issues/46
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.
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
a dirty flag (i.e. a counter) is fine also
If you add me to your repo with your attempt (even messy scratch stuff) I can take a look
If you e/def
dynamically, how do you undef the var? (e/def x nil)
?
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.
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)
What electric version? I think that might have been fixed
Oh it might. I haven’t checked on the version.
it might be fixed only on master actually, if so better just to wait
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.i don’t see a problem here, try it in a fresh starter app to confirm
(the stack trace does not match the code, so it seems that more is in play than this snippet)