hyperfiddle

Stef Coetzee 2025-01-22T12:40:48.794209Z

I suspect I might not be alone in coming to Electric from re-frame, with its opinions around effects and state management. In consulting the tutorials, one can similarly pick up the Electric approach to effects and state management. E.g.:

;; Centralized state
(e/declare !state)
(e/declare state)
(def init-state { ... })

...

(e/defn Effects []
  (e/client
    {`Sample-fx Sample-fx ; `Sample-fx` is defined via `e/defn`
     ... }))

(e/defn Page []
  (binding [!state        (atom init-state)
            cqrs/effects* (Effects)] ; `hyperfiddle.electric-forms0` referenced as `cqrs`
    (binding [state (e/watch !state)] ; `state` cannot be in the same `binding` form as `!state`
      ...)))
Effects can be managed via the provided Service pattern in hyperfiddle.electric-forms0. For example, Button! from that namespace takes a "directive" in the form of [Some-effect & args]`. (Be sure to return ::cqrs/ok from Some-effect upon successful handling of the user action.) Q: How might multiple directives be dispatched from a single user action? Separately, the https://electric.hyperfiddle.net/tutorial/todomvc has the most code content around effects and state management. The following is noted: > How can the todo edit be cancelled upon clicking away? We'd need a more complicated dom strategy for click away, i think our production combobox has this now. that modal edit state has a lot in common semantically with a popover, it may actually be a semantic popover with inline layout strategy Q: Is there a resource to learn more about the solution mentioned above? It might be that custom form implementations are the answer to both of these questions (e.g. see the commentary at the end of the https://electric.hyperfiddle.net/tutorial/token_explainer), but there is leverage in using blessed patterns of course.

Stef Coetzee 2025-01-28T17:57:48.813639Z

I played around a little to allow for multiple effects to be dispatched from a single button click (via the e/Token API):

;; Adjusted `Service`
(e/defn ServiceMultiple [forms]
  (e/client ; client bias, t doesn't transfer
   (js/console.log "Forms:" forms)
   (e/for [form forms]
     (let [[t effect-vec _guess]  form
           [effect & args]       effect-vec
           Effect                ((or cqrs/effects* {})
                                  effect (e/fn Default [& _args]
                                           (doto ::effect-not-found (prn effect))))
           res                   (e/Apply Effect args)] ; effect handlers span client and server
       (prn 'final-res res)
       (case res
         nil (prn `res-was-nil-stop!)
         :cqrs/ok (t) ; sentinel, any other value is an error
         (t :cqrs/rejected))))))

;; Call `Page` from inside `Main`'s mandatory `div`
(e/defn Page []
  (binding [cqrs/effects* {`Some-effect Some-effect}]
    (dom/div
     (dom/text "Multiple effects dispatch (using tokens)")
     (dom/div
      (ServiceMultiple
       (dom/button
        (dom/text "test")
        (let [e (dom/On "click" identity nil)
              [t1 _] (e/Token e)
              [t2 _] (e/Token e)]
          (e/When (some some? [t1 t2])
                  (e/amb [t1 [`Some-effect 1] {}] [t2 [`Some-effect 2] {}])))))))))
Note: changed based on @dustingetz's feedback below.

👏 1
Dustin Getz (Hyperfiddle) 2025-01-28T18:00:13.278999Z

instead of returning clojure vector and diffing, you can return the reactive table directly using e/amb

Dustin Getz (Hyperfiddle) 2025-01-28T18:00:54.492829Z

this also more faithfully models the fact that these commands are concurrent and succeed/fail separately

Stef Coetzee 2025-01-28T18:01:08.732139Z

Separately, I've made less progress on blur effects from inputs. This works for what I use it for, but does not properly leverage the Electric effect system:

;; Modified `Input!`
(e/defn BlurInput! [field-name ; fields are named like the DOM,  - for coordination with form containers
                    v & {:keys [maxlength type parse edit-monoid Validate blur-handler]
                         :as   props
                         :or   {maxlength   100
                                type        "text"
                                parse       identity
                                edit-monoid hash-map
                                Validate    (e/fn [_])}}]
  (e/client
   (dom/input
    (dom/props (-> props (dissoc :parse :Validate :blur-handler) (assoc :maxLength maxlength :type type)))
    (let [e        (dom/On "input" identity nil)
          [t err]  (e/Token e) ; reuse token until commit
          editing? (dom/Focused?)
          waiting? (some? t)
          error?   (some? err)
          dirty?   (or editing? waiting? error?)
          blur-e   (dom/On "blur" identity nil)]

      (when-not dirty? (set! (.-value dom/node) v)) ; todo - submit must reset input while focused
      (when error? (dom/props {:aria-invalid true})) ; not to be confused with CSS :invalid. Only set from failed tx (err in token). Not set if form fail to validate.
      (when waiting? (dom/props {:aria-busy true}))

      (e/When (and blur-handler (some? blur-e))
              (blur-handler blur-e)) ; call `blur-handler` on blur if present

      (e/When waiting? ; return nothing, not nil - because edits are concurrent, also helps prevent spurious nils
              (let [v'                 ((fn [] (-> e .-target .-value (subs 0 maxlength) parse)))
                    validation-message (Validate v')
                    edit               (edit-monoid field-name v')] ; named field edit, a KV structure
                (cqrs/InputValidity validation-message)
                [t edit validation-message]) ; edit request, bubbles upward to service
              )))))

Dustin Getz (Hyperfiddle) 2025-01-28T18:02:10.570599Z

goal is to get a callback on blur?

Stef Coetzee 2025-01-28T18:02:32.183469Z

> instead of returning clojure vector and diffing, you can return the reactive table directly using e/amb Noted, thanks!

Stef Coetzee 2025-01-28T18:04:27.470999Z

goal is to get a callback on blur?I want to be able to run effects on blur, but Input! is wrapped by Form! , which I have not studied sufficiently, but I'm not sure how to go about that with the forms3 forms. From earlier in the thread, this kind of functionality is a work in progress. It's easy enough to use dom/input to achieve the basic result (which can be used to define a custom input, of course). E.g.:

(Service
 (e/amb
  (dom/input
   (dom/props {:placeholder "foo"})
   (let [e (dom/On "blur" identity nil)
         [t err] (e/Token e)]
     (e/When (some? t)
             [t [`Some-effect] {}])))))
Hard to resist the forms3 forms, though. They come with a lot of functionality.

Dustin Getz (Hyperfiddle) 2025-01-28T18:04:43.474529Z

also, what is the business level use case you are trying to achieve (see https://xyproblem.info/ )

Stef Coetzee 2025-01-28T23:08:12.787219Z

Use case: cancel out of editing a field (represented by an input field) by clicking away (e.g. as is often done as part of TodoMVC demos, but I think this functionality is quite common when working with data more generally).

Stef Coetzee 2025-01-28T23:14:40.526679Z

I.e., change field into editable state via double click, exit out of editable state via Enter (to submit), Esc (to cancel), or loss of focus (also submit).

Stef Coetzee 2025-01-29T00:46:11.899139Z

Related to adjusted Service above, just looked into Interpreter, nice!

Dustin Getz (Hyperfiddle) 2025-01-22T13:19:18.507159Z

The form patterns are evolving rapidly, we just published forms3 (which was backwards compatible with forms0) and are working again on the next version, which has e.g. table pickers. Basically you're already at the bleeding edge, at this point the next step is for us to get them into production with one of our commercial partners, get the learnings, probably refactor again, and then document

🚀 2
Dustin Getz (Hyperfiddle) 2025-01-22T13:20:22.966249Z

in the mean time, compositional state management is a very experimental situation

👍 2