This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-04-14
Channels
- # ai (24)
- # announcements (36)
- # babashka (15)
- # babashka-sci-dev (8)
- # beginners (18)
- # biff (4)
- # calva (24)
- # cider (13)
- # clj-kondo (1)
- # clj-on-windows (2)
- # clojars (15)
- # clojure (120)
- # clojure-dev (13)
- # clojure-europe (69)
- # clojure-nl (1)
- # clojure-norway (8)
- # clojure-uk (2)
- # clojurescript (4)
- # core-logic (2)
- # cursive (6)
- # datomic (193)
- # dev-tooling (4)
- # emacs (1)
- # hyperfiddle (57)
- # lsp (56)
- # malli (11)
- # missionary (15)
- # nbb (61)
- # off-topic (8)
- # polylith (8)
- # practicalli (2)
- # proletarian (1)
- # reitit (3)
- # releases (2)
- # remote-jobs (1)
- # shadow-cljs (13)
- # spacemacs (1)
- # specter (2)
- # sql (17)
- # tools-deps (3)
- # vim (38)
Just curious: can Electric be used for purely client-side apps, as a sort of Reagent alternative? I'm guessing not as the client would try to establish a websocket connection with the server? I have no particular use case for this, just interested in understanding what's possible.
That's neat. I remember a few years ago there was talk of making something like Reagent that didn't rely on React (e.g. Mr Clean https://www.reddit.com/r/Clojure/comments/abc4pi/mr_clean_is_a_reagent_compatible_clojurescript/), and it seems like you guys have created that as part of Electric. I understand that the goals of Electric are much larger of course.
thanks for the link. Seems to be a reagent compatible library swapping out react. Electric is a reactive language. What this means is the DOM stuff is ~300loc with some deprecated stuff / tech debt. But we could do the same with any other UI toolkit, e.g. Swing or JavaFX. The loc would be similar.
Wow that's cool that the UI layer can in principle be swapped out for something else. I'm still wrapping my head around the reactive language idea.
I understand Electric is fine-grained across the network boundary in a sense that functions that don’t need to be re-run are not (given the omniscience of the cross-boundary DAG). I am curious about the nature of reactivity when focused on just the client/Dom state. @U0PUGPSFR #matrix Talks of “fine-grained”-ness in terms of reactivity operating at the property/attribute level (rather than view function) of a UI component. It also maintains a Dag to achieve omniscience, enabling next level developer UX. https://github.com/kennytilton/matrix/wiki/introduction#property-oriented-data-flow Heres an interesting discussion on some of these dynamics. https://clojurians.slack.com/archives/CKCBP3QF9/p1677614613185429 This stuff appeals to me for wrangling view design dynamics (as opposed to focus on cross network state). Dustin mentioned Electric offers local reactive semantics similar to solid.js in this respect. I am not very familiar with it. Does Electric/solid.js encompass this property-level granularity? Or is it a different scope or approach? Or (just thinking out lout)… If the meaningful state for reaction resides on the server (and Electric enables using this fabulously), maybe this comprehensive client-only reactivity is not a fully applicable in this case? Either way, from the above I assume there might not be problem to add something like Matrix to cljs side for extra client semantics (even if it is macro heavy and probably has some overlapping concerns..)?
focusing on just dom rendering: both matrix and electric promise fine-grained reactivity and the ability to express maximum possible granularity at the quanta of a single point in the AST (attribute, expression, whatever)
Solid IIUC contains implementation mistakes but in concept is similar
I had the wrong discussion link earlier. Here is the right one https://clojurians.slack.com/archives/CKCBP3QF9/p1677614613185429
So I’m trying to render something else when there’s a Failure. i.e show the NewGame Screen instead of GameScreen if for whatever reason there’s an error updating state. Currently this doesn’t seem to work. Any ideas?
(try
(e/server
(swap! !state add-session-to-presence game-id session-id)
(e/on-unmount
#(swap! !state remove-session-from-presence
game-id session-id)))
(e/client (GameScreen. game-id))
(catch Failure _
(e/client (NewGameScreen.))))
what is Failure
https://github.com/hyperfiddle/electric/blob/master/src/hyperfiddle/electric.cljc#L26-L31
if it's hyperfiddle.electric.Failure
that's an internal type you'll never see in userspace. If you want a catch-all you need Throwable
on the server and :default
on the client
So those swap! can throw IllegalStateException so something like this?
(e/server
(try
(e/server
(swap! !state add-session-to-presence game-id session-id)
(e/on-unmount
#(swap! !state remove-session-from-presence
game-id session-id)))
(e/client (GameScreen. game-id))
(catch :default _
(e/client (NewGameScreen.)))))
That renders both the GameScreen and NewGameScreen components
I guess I’m expecting try catch to behave like it does on Clojure i.e stop execution the moment something fails. I understand that’s not how it works in Electric. Is there a way to bring that back? Or is there an electric convention for this?
indeed, that's how try/catch works, a catch handler doesn't unmount the try body, because an exception can resolve later, which would cause flicker
the catch
here is on the server so catch Exception
or IllegalStateException
The https://electric-examples-app.fly.dev/user.demo-chat!Chat has some explanation of reactive exceptions
I don't know what the 2 components do/look like, but off the top of my head you could reset! an atom and if on that
(let [!threw? (atom false), threw? (e/watch !threw?)]
(try (if threw? (NewGameScreen.) (case (GameScreen. game-id) (reset! !threw? false)))
(catch :default _ (reset! !threw? true))))
> behave like it does on Clojure i.e stop execution the moment something fails > Is there a way to bring that back? just don't catch the exception might do what you want
I guess I’m modeling it as screens cause that’s similar to how we do things on our non electric mobile first stuff. I could probably hack it with a modal. But conceptually this is similar to routing. Basically If error show A otherwise B seems like something quite common? @U09K620SG That’s the tutorial I was working off (they are super helpful). @U09FL65DK I’ll try that atom for tracking the exception, feels a bit clunky compared to how easy everything else is in electric.
There is surely a cleaner way to do this, i will try to find a good example of routing
why do you want to use throwing an exception to signal a route change?
Kinda like a redirect conceptually I guess. I’ve been using the contrib goog.history stuff and for the most part that’s been fine.
Maybe, I’m still shackled to rest/http concepts
to me, conceptually, clicking a hyperlink (click callback) or setting document location all map conceptually to reset! on an atom
So this isn’t at the route, it’s in a parent component. But on error I want to show a different component.
(Game (if success ShowGame ShowCreateNewGame))
(if (e/server ;; return false on error
)
(GameScreen.)
(NewGameScreen.))
> On error I want to show a different component in that case, this seems appropriate to me, as it's the exception that represents a request for state change
(catch Exception _ (reset! !route `NewGameScreen))
Right, but what about at a more granular level. Show button A when error, show button B when not error. That’s not really any different?
You wouldn’t use routes for that but conceptually it shows A or B. Right?
Btw as an aside I’m absolutely loving electric, it makes a lot of really hard things easy. I also appreciate the help.
this is a bit meta but i would question the essential difference between single-state-atom and document.location. for example we have an experimental router that looks like an atom but puts state into the route
so imo the decision of whether to put state in the route or in an atom is arbitrary, do what feels nice to you in your app
> Show button A when error, show button B when not error > Errors usually add a button. There's also another common pattern
(try (case (foo) (Ok.)) (catch :default e (Error. e)))
https://clojurians.slack.com/archives/C7Q9GSHFV/p1681488258907999?thread_ts=1681484777.404339&cid=C7Q9GSHFV
Using an atom as a location pointer for routing is really interesting. Is there a reason you are passing the symbol?
I’ve tried (catch Exception _ (reset! !route NewGameScreen))
without the quote and it means your top level routing doesn’t need a case lookup. Now the top level of the app looks super simple and doesn’t need to change when new (internal) routes are added:
(e/defn Game []
(e/client
(dom/link (dom/props {:rel :stylesheet :href "/styles.css"}))
(if route
(route.)
(NewGameScreen.))))
NewGameScreen in this case is whatever you want you’re home/starting screen to be. Obviously, this isn’t much good for url/path based external links and you’d need some sort of look up for that (I’m sure there are other limitations I haven’t considered). But for internal “screen” changes this seems to work quite nicely. Thanks again for all the help!I don’t quite understand the use of case with two arguments like that? Is it being used as a when
/ and
.
https://clojurians.slack.com/archives/C7Q9GSHFV/p1681491343538329?thread_ts=1681484777.404339&cid=C7Q9GSHFV
The case
ensures the second arg runs only if the first one didn't throw an exception
Would it behave differently (in electric) to (when (foo) (Ok.))
or (and (foo) (Ok.))
assuming (foo)
returns truethy on success?
Maybe I’m missing something basic but does Electric support variadic functions? This is what I’m trying to do
(e/defn Debugger [& xs]
(dom/div
(dom/text (str xs))))
(e/defn App []
(Debugger. watcher-1 watcher-2 watcher-3))
Obviously you could just pass a vector but still
Not yet, sorry about the confusion
ok thanks for clarifying!
This gist for working with promises works great:
https://gist.github.com/dustingetz/8823e47c13f780d18938363d1d641b5b
But why doesn't it work if I try to put the (new (e/task->cp ...))
inside the function definition?
(defn await-promise "Returns a task completing with the result of given promise"
[p]
(let [v (m/dfv)]
(.then p
#(v (fn [] %))
#(v (fn [] (throw %))))
(m/absolve v)))
(e/defn compact-await-promise
[p]
(new (e/task->cp (await-promise p))))
(e/defn Todo-list []
(dom/p (dom/text "Promise value using original method: ")
;; This displays "hi" as expected:
(dom/text (new (e/task->cp (await-promise (.resolve js/Promise "hi"))))))
(dom/p (dom/text "Promise value using compact function: ")
;; This displays nothing
(dom/text (compact-await-promise (.resolve js/Promise "hi")))))
Since compact-await-promise
is an electric function, you need to call it with new
e.g. (new (compact-await-promise ...))
or (compact-await-promise. ...)
using dot notation. Does it work after making that change?
That works, thank you!
(e/defn Todo-list []
(dom/p (dom/text "Promise value using original method: ")
;; This displays "hi" as expected:
(dom/text (new (e/task->cp (await-promise (.resolve js/Promise "hi"))))))
(dom/p (dom/text "Promise value using compact function: ")
;; This displays nothing
(dom/text (compact-await-promise (.resolve js/Promise "hi"))))
(dom/p (dom/text "Promise value using compact function: ")
;; This displays "hi" as expected. Note the dot after compact-await-promise
(dom/text (compact-await-promise. (.resolve js/Promise "hi")))))
@U052PH695 minor tweak for your understanding -
• (Compact-await-promise. ...)
-- correct
• (new (Compact-await-promise ...))
-- incorrect
• (new Compact-await-promise ...)
-- correct
I've also capitalized the electric fns to help make clear that capitalized things are called with new (as if a class)
See the recently published https://electric-examples-app.fly.dev/user.tutorial-lifecycle!Lifecycle which describes new
and how electric fns have object-like aspects
Thank you. My bad on the incorrect syntax in the (new (Fn ..))
example.
As far as capitalization goes, that is just a convention to help differentiate between Clojure and Electric functions right? I.e. if you don't capitalize the function name that doesn't change how electric interprets the code?
correct
it also helps you remember where the new goes, because (new (f x)) can be a thing too if (f x) returns a missionary flow