This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-03-16
Channels
- # babashka (48)
- # beginners (72)
- # calva (65)
- # cider (10)
- # clerk (11)
- # clj-kondo (14)
- # clojure (85)
- # clojure-austin (11)
- # clojure-czech (1)
- # clojure-europe (26)
- # clojure-nl (1)
- # clojure-uk (6)
- # core-matrix (1)
- # cursive (8)
- # datomic (20)
- # docker (38)
- # emacs (2)
- # events (1)
- # fulcro (6)
- # funcool (6)
- # hyperfiddle (79)
- # introduce-yourself (1)
- # lsp (131)
- # malli (32)
- # off-topic (11)
- # pathom (3)
- # re-frame (11)
- # reagent (15)
- # releases (2)
- # shadow-cljs (49)
- # sql (3)
- # tools-deps (36)
Hi guys! There is a reason to not pass the event here https://github.com/hyperfiddle/electric/blob/master/src/hyperfiddle/electric_ui4.cljc#L99?
Because when a button
is inside a form
sometimes we want to preventDefault
the event.
This is a good question. We’ll discuss it.
In the meantime, <button role="button">…</button>
will not submit your form on click (default role for a form button is submit
).
Thanks @U2DART3HA 👍
@UHZPYLPU1 also you can use the lower dom/button + dom/on interface, example is https://github.com/hyperfiddle/electric/blob/master/src-docs/user/demo_chat.cljc#L25
The reasoning today (which can change) is: ui/input is a high level input that gives you the value not the event. ui/button is supposed to also be high level like that. Interacting with a dom/form would be a lower level concern in this worldview.
I’m curious about incorporating electric into a serverless architecture. I think the big question is how to externalize the server-side memoize caches to something like Redis? Of course, there are other issues like being at to associate a websocket with a stable client id but these are outside electric’s scope and have googleable solutions.
You want server state to be durable across restarts? Memo state is stored in Missionary, so we'd likely move the missionary heap into something durable as you suggest or are you thinking about something else? Today the server runtime state can theoretically be reconstructed from the client runtime state, so this may not actually be an issue. We POC'ed this already, there's a blocker, it will land after the next missionary design lands
The first one: I am thinking of durable server-side state across restarts. Separately, I have a question related to recreating server-side state from the client
Unrelated to servlerless electric, I’m curious about the performance issues around a single client-side atom for its database and a server-side watch on that atom. Will electric stream all updates to the client-side atom? Or only those changes that server cares about? For example, suppose the atom contains
{:client/cursor-location "blah blah", :shared/key "key"}
The server contains functions like (get db :shared/key)
but never reference :client/cursor-location
oh, well that answers that 🙂
only scope (e.g. lexical/dynamic/global/local) is streamed, as reified in the DAG
ah, i think i got it. so, if the server could watch a client atom, then update to :client/cursor-location
in the atom would trigger a recalc but since none of the server’s scope is changed, no application code would be executed
any change to an atom will fire watches (this is how clojure atom watches work - see core/add-watch and core/remove-watch). what happens next depends on what your cursor does. From electric's perspective, at any point if a reactive value is the same as it was before, we skip the pointless recomputation and use the memoized value. So any work downstream of that would be skipped.
does that answer your question?
Yes, it does.
Now I’m curious why the server cannot watch a client-side atom.
you can't move/serialize references
I don’t follow but I think I should re-ground myself in the code before continuing my fanciful architecture ideas 🙂
you can do this
#?(:cljs (def !x (atom {:deep {:thing 1}})))
(e/server
(let [x (e/client (e/watch !x))]
(println x))) ; entire value was transferred
but !x cannot transfer because it can't be serialized, and e/watch needs to attach directly to the reference, so it needs to be co-located
I understand that part. But suppose the code was this:
#?(:cljs (def !x (atom {:client-only {:thing 1}
:shared {:something "blah"}})))
(e/server
(let [{shared :shared} (e/client (e/watch !x))]
(println shared)))
e/watch returns a reactive value so that’s allowed
If I understand correctly, if the client executes (swap! !x update :client-only f)
then the bytes will flow across the websocket to the server and electric/missionary would run enough to determine that the server state hasn’t changed
we aren’t that smart, the destructuring here happens on the server
So, there is an opportunity for optimization but it is not currently implemented
I imagine a poor man’s solution might be
#?(:cljs (def !x (atom {:client-only {:thing 1}
:shared {:something "blah"}})))
(e/server
(let [shared (e/client (e/watch-in !x [:shared]))]
(println shared)))
(btw, I’m not criticizing, I’m just trying to understand the current state of the world)
you should move the destructure to the client and stream the leaf
ah, that makes sense
perfect
sure we could optimize let, haven’t thought about it much
I don’t have a strong opinion on it. The fact that I can move the destructure to the client gives me exactly what I was looking for
I could use some feedback on the following. I'm trying to setup a project that uses js-modules and woud like to abstract the mounting of the program so I can call this from the init-functions of the modules with their root-component. I now have:
(defonce ^:private reactor nil)
(defonce ^:private current-app nil)
(defn ^:private mount-current-app []
(set! reactor (current-app
#(js/console.log "Reactor success:" %)
#(js/console.error "Reactor failure:" %))))
(defn initialize-app [app]
(set! current-app app)
(mount-current-app))
(defn ^:dev/after-load start! []
(assert (nil? reactor) "reactor already running")
(mount-current-app))
(defn ^:dev/before-load stop! []
(when reactor (reactor))
(set! reactor nil))
and then for each module a file with something like:
(defn ^:export init! []
(current-app/initialize-app
(e/boot
(binding [dom/node js/document.body]
(hello-world/HelloWorld.)))))
They're all cljs files.
This seems to work fine at first. But I noticed hot-reload is now broken. As in, it still reloads, I see the stop! and start! logs but it mounts the old-program. I'm also wondering if it's possible to abstract this last bit of duplication as well, the call to e/boot, so I can just pass the root-component. I tried, but the results of e/defn seem to be nil when passed around like that in normal cljs, not sure if that suprises me or not, still trying to get my head around the basics. Any hints appreciated 🙏try lifting out the e/boot to a def
(def root
(e/boot
(binding [dom/node js/document.body]
(hello-world/HelloWorld.))))
(defn ^:export init! []
(current-app/initialize-app root))
do you have a ^:dev/always
on the user ns https://github.com/hyperfiddle/electric-starter-app/blob/a8463f499f05397d7a567491bca1e12f512ee05d/src/user.cljs
the fact that it mounts the old program suggest your changes might not be getting picked up for some reason
Yes, I've got ^:dev/always.
Actually on both the current-app ns and the init ns as I wasn't sure what was necessary . I just now tried removing it from either one of them, but it all gives the same result. I also see in my browser logs that they all got reloaded. That is: hello-world, current-app and init. One thing that stands out is that the order is: hello-world > current-app > init, while I would expect hello-world > init > current-app. Im not sure if the printing order is guaranteed to be the reloading order, but this seems to be consistent on all reloads. If this had anything to do with the observed behaviour I would actually expect the old update to show up when I trigger another reload with a new change, but it doesnt... it just always loads the first version. Complete refresh obviously updates to the latest version.
I'm also still very interested in the second part of my question. Not so much because I necessarily need to pass electric-components around in cljs file, Im fine with having it refactored to this point, Im just trying to understand what is/isnt possible and why. In my cljs-repl eval of HelloWorld gives nil, so thats inline with the errors I get when Im trying to pass it around as a value. Macroexpanding also result in nil. Eval of (e/boot ...) gives me an object, so it makes sense Im able to pas that around in cljs-land. Macroexpanding it gives... well, something that doesnt really help my understanding 🙂 I guess its cljs side of the reactive-runtime. Not sure how to integrate my findings into actual understanding.
I just meant evaling the var and checking the result in the repl. Another way to put it (def x hello-world/HelloWorld)
whats the value of x in a cljs file, nil apparently
ok, understand. The reason for that is e/def
doesn't actually do anything 🙂 It stores the code as metadata on the var. The electric analyzer will look at that metadata and compile your program
I see. When I eval (meta #'hello-world/HelloWorld)
I find your meta-data in the result. I'll have to think about the implication for a bit, but this definitely gives me a better understanding, thanks
https://github.com/avisi-apps/tech-testing-ground/tree/master/prototypes/electric. Start repl, function to start shadow-watch is in dev > user. Run mount/start in src > server > server. Locahost:3000 now shows hello world. Changing something in src > components > hello-world and saving triggers reload but doesnt update whats shown in the browser.
Thank you for the repo. It helps a lot. We are swamped today, sorry, we will look into it first thing on Monday morning 🤞
I understand you want multiple js modules with one electric program per module. Shadow's :init-fn
runs once when the module is loaded, but not during hot code reload. As a result, the init!
functions in main-page
and current-app
won't be called on hot code reload. You can either:
• invert the dependency between current-app
and main-page
or item-view
• add ^:dev/after-load
to one of your init!
functions.
Ok, makes sense. I think it's actually connected to the second part of my initial post. I tried to copy this structure from the way we do it in our fulcro apps, but there I'm able to pass the component as an arg to inititialize-app and changes to it get picked-up on hot-reload. As I'm unable to pass electric-components around like that and have to boot them first I got this situation. After learning last friday that for electric-components it's about the var/meta-data and not the value, I tried sending it as a symbol with the intent of finding the corresponding var from wihtin the other ns but this doesnt seem possible in cljs (I might be wrong about that). I guess it should be possible to extract the relevant meta-data, pass that as an arg and than reconstruct a var in the other ns which can be passed to e/boot, but it feels a bit contrived so havn't tried (and I now actually think it wouldnt solve the reload problem either). As for the reload-problem itself. The first option probably works but breaks the code structure, so I went with the second and it works fine, thanks for looking into it.
Hi! Is this code valid:
(dom/on "click" (e/fn [e]
(.preventDefault e)
(try
(e/server
(e/offload
#(let [foo "foo"]
(prn "BAZ")
(Thread/sleep 10000)
(prn "FOO")
foo)))
(catch Pending _
(swap! !state assoc :status :submited)
(e/on-unmount #(swap! !state assoc :status :idle))))
I never see the FOO
print. Like if the Thread/sleep
was ignored. I don’t understand why.hmmm I dont know, and can reproduce
still thinking
In case you're stuck, just don't silence the Pending exception inside the callback and it will work
dom/on is listening for Pending to know when the callback has successfully run on the server
you're catching it (i.e. not rethrowing it)
Thanks @U09K620SG
You can use this pattern to hook the loading state
(case (e/server ...) X)
is the interesting part, the case
here only runs the body X
when the pending state is resolved
Does that make sense?
We are still thinking about this usage of case
and how to do this better
in this case the try/catch is not needed at all. When the event handler finishes it is unmounted, so you can use on-unmount
hello! How can I move that button to left button? I checked some CSS tutorials but everything didn't work that's what I tried until now.
Do you use flexbox
css api? (https://css-tricks.com/snippets/css/a-guide-to-flexbox/)
(e/defn SliderApp []
(e/client
(let [!state (atom {:in "" :v 0 :v-state 0 :placeholder "Write a number please..."})]
(let [in (get (e/watch !state) :in) v (get (e/watch !state) :v)]
(dom/div (dom/props {:style {:display :grid
:width "40em"
:grid-gap "0.5em"
:align-items :center}})
(dom/h1 (dom/text "Slider Example")
(dom/props {:style {:grid-row 1 :align-items :center
}}))
(ui4/range v (e/fn [newv] ((swap! !state assoc :v newv)))
(dom/props {:min 0, :max 100, :style {:grid-row 2}}))
(ui4/input in (e/fn [v] (swap! !state assoc :in v))
(dom/props {:placeholder (get (e/watch !state) :placeholder)
:style {:background-color (get (e/watch !state) :bg-color2)
:width "47em" :height "1em" :align-items :center :grid-row 3}})
(dom/on "keydown" (e/fn [enter]
(when (= "Enter" (.-key enter))
(when-some [givenValue (contrib.str/empty->nil (-> enter .-target .-value))]
(swap! !state assoc :v givenValue)
(set! (.-value dom/node)
)))))
(dom/on "keyup" (e/fn [keyup]
(when-some [givenValue (contrib.str/empty->nil (-> keyup .-target .-value))]
(swap! !state assoc :v-state givenValue)
)))
)
(dom/button (dom/on "click" (e/fn [click] (swap! !state assoc :v (get (e/watch !state) :v-state))))
(dom/text "Insert Num!!!")
(dom/props {:style {:grid-row 4 :width "15em" :height "2em"
:grid-gap "10em" :align-items :auto
}})
)
(dom/button (dom/on "click" (e/fn [click] (swap! !state assoc :v "")
(swap! !state assoc :in "")))
(dom/text "Reset!!!")
(dom/props {:style {:grid-row 4 :width "15em" :height "2em"
:grid-gap "1em" :align-items :auto
}})
)
(dom/h1 (dom/p (dom/text "result is: " v))
(dom/props {:style {:grid-row 5 :align-items :center
}}))
)
)
)
)
)