Fork me on GitHub
#hyperfiddle
<
2023-04-14
>
tobias05:04:57

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.

2
👀 2
yes 2
xificurC06:04:15

Sure! We don't have an example of that, it would require a different entrypoint

tobias08:04:17

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.

xificurC08:04:13

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.

tobias09:04:35

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.

🙂 2
chromalchemy14:04:27

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..)?

Dustin Getz14:04:36

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)

🔥 2
Dustin Getz14:04:11

Solid IIUC contains implementation mistakes but in concept is similar

chromalchemy15:04:23

Cool. I dont want to miss out on any quanta 🤓

🙂 4
chromalchemy15:04:56

I had the wrong discussion link earlier. Here is the right one https://clojurians.slack.com/archives/CKCBP3QF9/p1677614613185429

👀 2
andersmurphy15:04:17

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.))))

2
Dustin Getz15:04:24

what is Failure

xificurC15:04:57

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

andersmurphy15:04:03

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.)))))

andersmurphy15:04:25

That renders both the GameScreen and NewGameScreen components

andersmurphy15:04:13

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?

xificurC15:04:35

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

Dustin Getz15:04:10

the catch here is on the server so catch Exception or IllegalStateException

xificurC15:04:18

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)))) 

Dustin Getz15:04:00

> 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

xificurC15:04:02

can't NewGameScreen be a modal popup that unmounts after pressing something?

andersmurphy15:04:02

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.

Dustin Getz15:04:08

There is surely a cleaner way to do this, i will try to find a good example of routing

Dustin Getz15:04:40

why do you want to use throwing an exception to signal a route change?

andersmurphy15:04:07

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.

andersmurphy15:04:44

Maybe, I’m still shackled to rest/http concepts

Dustin Getz15:04:03

to me, conceptually, clicking a hyperlink (click callback) or setting document location all map conceptually to reset! on an atom

andersmurphy15:04:50

So this isn’t at the route, it’s in a parent component. But on error I want to show a different component.

andersmurphy16:04:15

(Game (if success ShowGame ShowCreateNewGame))

andersmurphy16:04:44

(if (e/server ;; return false on error
           )
         (GameScreen.)
         (NewGameScreen.))

Dustin Getz16:04:18

> 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))

xificurC16:04:37

Indeed, you're asking for a router

andersmurphy16:04:54

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?

andersmurphy16:04:59

You wouldn’t use routes for that but conceptually it shows A or B. Right?

andersmurphy16:04:11

Btw as an aside I’m absolutely loving electric, it makes a lot of really hard things easy. I also appreciate the help.

🙂 2
Dustin Getz16:04:34

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

2
Dustin Getz16:04:37

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

xificurC16:04:43

> 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)))

👀 2
andersmurphy07:04:13

https://clojurians.slack.com/archives/C7Q9GSHFV/p1681488258907999?thread_ts=1681484777.404339&amp;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!

andersmurphy07:04:57

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&amp;cid=C7Q9GSHFV

xificurC07:04:56

The case ensures the second arg runs only if the first one didn't throw an exception

🙏 2
andersmurphy08:04:13

Would it behave differently (in electric) to (when (foo) (Ok.)) or (and (foo) (Ok.))assuming (foo) returns truethy on success?

xificurC08:04:07

It would behave the same. case makes no assumption about the return value

👍 2
Dag Norberg20:04:05

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))

2
Dag Norberg20:04:38

Obviously you could just pass a vector but still

Dustin Getz20:04:46

Not yet, sorry about the confusion

Dag Norberg21:04:17

ok thanks for clarifying!

tobias22:04:14

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")))))

noonian03:04:57

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?

tobias03:04:23

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")))))

👍 2
Dustin Getz11:04:02

@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)

💯 2
Dustin Getz11:04:49

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

noonian17:04:27

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?

👍 2
Dustin Getz17:04:47

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

💯 2