This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2022-12-24
Channels
- # adventofcode (25)
- # asami (39)
- # beginners (39)
- # biff (12)
- # clojure (53)
- # clojure-dev (4)
- # clojure-europe (6)
- # clojure-hungary (1)
- # clojure-norway (4)
- # clojure-spec (3)
- # conjure (2)
- # cursive (1)
- # dev-tooling (9)
- # emacs (4)
- # introduce-yourself (2)
- # juxt (4)
- # membrane (8)
- # off-topic (3)
- # polylith (8)
- # portal (4)
- # releases (1)
- # scittle (9)
- # sql (11)
- # squint (5)
- # xtdb (12)
I could use some guidance for a UI I'm trying to build with defui
components. This is a simplified version of the app I am trying to build.
In general there is a grid of photo thumbnails, and when you click on a thumbnail the large version of the image opens up in a carousel that can be navigated left/right.
While a photo is focused you can modify it (demoed here with a toggle) and it should update the photo data in the state atom.
In a CLJS/re-frame application I'm used to keeping my state in a more "normalized database" structure, but membrane.component
with its specter and for
support seems to advocate for keeping the data in a vector in a situation like this.
In the ::focus-photo
effect I'm peering into the specter path in way that seems unsatisfying and brittle to me
(defonce *example-state
(atom {:focused-index nil
:photos [{:name "img1" :toggled? false}
{:name "img2" :toggled? false}
{:name "img3" :toggled? false}
{:name "img4" :toggled? false}]}))
(defui photo-row
[{:keys [photo]}]
(let [{:keys [name toggled?]} photo]
(ui/horizontal-layout
(ui/on-click (fn [& _]
[[::focus-photo $photo]])
(ui/label name))
(ui/checkbox toggled?))))
(defui focused-photo
[{:keys [focused-index photo last-index]}]
(let [{:keys [name toggled?]} photo]
(ui/on-key-press
(fn [k]
(case k
(:right :space "n") [[:set $focused-index
(if (= focused-index last-index)
0
(inc focused-index))]]
(:left "p") [[:set $focused-index
(if (zero? focused-index)
last-index
(dec focused-index))]]
(:escape "q") [[:set $focused-index nil]]
"t" [[:update $toggled? not]]
nil))
(ui/horizontal-layout
(ui/label (str "Focus " name))
(ui/checkbox toggled?)))))
(defui example-root
[{:keys [photos focused-index]}]
(apply
ui/vertical-layout
(concat
(for [photo photos]
(photo-row {:photo photo
:focused-index focused-index}))
[(when focused-index
(let [photo (get photos focused-index)]
(focused-photo {:focused-index focused-index
:photo photo
:last-index (dec (count photos))} )))])))
(defeffect ::focus-photo
[$photo]
(prn "photo path >>>" $photo)
;; TODO improve this
(let [index (-> $photo second second)]
(dispatch! :set '[(keypath :focused-index)] index)))
(defonce example-app (mem.component/make-app #'example-root *example-state))
As a follow up I'm experimenting with a version where the state contains a "by id" (`:name`) map for the photos entities and then a separate vector for the list view:
(def initial-state
{:focused-photo-index nil
:photos-by-name {"img1" {:name "img1", :toggled? false},
"img2" {:name "img2", :toggled? false},
"img3" {:name "img3", :toggled? false},
"img4" {:name "img4", :toggled? false}}
:photos ["img1"
"img2"
"img3"
"img4"]})
(defonce *example-state
(atom initial-state))
(defui photo-row
[{:keys [photo]}]
(let [{:keys [name toggled?]} photo]
(ui/horizontal-layout
(ui/on-click (fn [& _]
[[::focus-photo name]])
(ui/label name))
(ui/checkbox toggled?))))
(defui focused-photo
[{:keys [photo]}]
(let [{:keys [name toggled?]} photo]
(ui/on-key-press
(fn [k]
(case k
"t" [[:update $toggled? not]]
nil))
(ui/horizontal-layout
(ui/label (str "Focus " name))
(ui/checkbox toggled?)))))
(defn- keep-index-in-bounds [list-count]
(fn [i]
(cond
(neg? i) (dec list-count)
(< (dec list-count) i) 0
:else i)))
(defui example-root
[{:keys [context photos photos-by-name focused-photo-index]}]
(apply
ui/vertical-layout
(concat
(for [photo-name photos]
(let [photo (get photos-by-name photo-name)]
(photo-row {:photo photo})))
[(when focused-photo-index
(let [focused-photo-name (get photos focused-photo-index)
photo (get photos-by-name focused-photo-name)]
(ui/wrap-on
:key-press
(fn [child-handler k]
(concat
(child-handler k)
(case k
(:right :space "n") [[:update $focused-photo-index (comp (keep-index-in-bounds (count photos)) inc)]]
(:left "p") [[:update $focused-photo-index (comp (keep-index-in-bounds (count photos)) dec)]]
(:escape "q") [[:update $focused-photo-index nil]]
nil)))
(focused-photo {:photo photo}))))])))
(defeffect ::focus-photo
[photo-name]
(swap! *example-state
(fn [state]
(let [index (.indexOf (:photos state) photo-name)]
(assoc state :focused-photo-index index)))))
(defonce example-app (mem.component/make-app #'example-root *example-state))
I'm interested in what people think about the two options in comparison> but membrane.component
with its specter and for
support seems to advocate for keeping the data in a vector in a situation like this.
Membrane intentionally tries to avoid imposing any sort of structure on your data model. If you use a value like seq, then it will get treated like a seq. If you use a value like a map, it will get treated like a map. If there are data types that feel awkward in membrane, then I think it's worth brainstorming solutions.
It seems like your second version is a good example of working with maps. It's also perfectly fine to do something like:
(for [k (keys m)]
(my-view (get m k)))
another option is to do something like:
;; child
(defui aview [{:keys [obj]}]
(ui/on
:mouse-down
(fn [_]
[[::select]])))
;; in parent
(for [[k v] m]
(ui/on
::select
(fn []
[[::select k]])
(aview {:obj v})))
There are ordered-map implementations that might be helpful as well.
I like your second version over the first. The only suggestions I might have are: • check to see if any of the ordered map implementations help • I would probably have the focus use the photo identifier (the photo's name in this case) rather than index . The idea is that if you add, rearrange, or remove photos, you probably want the focused element to refer to the same photo.
Thanks, @U7RJTCH6J. All of that is very helpful