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.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.instead of returning clojure vector and diffing, you can return the reactive table directly using e/amb
this also more faithfully models the fact that these commands are concurrent and succeed/fail separately
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
)))))goal is to get a callback on blur?
> instead of returning clojure vector and diffing, you can return the reactive table directly using e/amb Noted, thanks!
goal is to get a callback on blur?I want to be able to run effects on blur, but , 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 Input! is wrapped by Form! , which I have not studied sufficientlydom/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.also, what is the business level use case you are trying to achieve (see https://xyproblem.info/ )
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).
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).
Related to adjusted Service above, just looked into Interpreter, nice!
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
in the mean time, compositional state management is a very experimental situation