-
-
Notifications
You must be signed in to change notification settings - Fork 47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Is it possible to multiple states/renderers in one view? #32
Comments
https://github.com/ahungry/scratch/blob/master/blog/cljfx/counter-gui/src/counter_gui/core.clj My attempt thus-far - I think it may not be possible by design judging from this line in the readme?
Although, the reason for providing such a feature could be - lets say I want to make a 'widget' (maybe a text-area with vim + emacs key bindings and modes) that others could use - such a widget would likely require sufficient state management, but said state should be entirely abstracted from the consumer or user of such a widget. |
Yes, it's not supported by design: using multiple stateful sources of data brings incidental complexity. Having local mutable state is both blessing and a curse, I know. JavaFX components have some hidden mutable state that is very convenient to use. For example, you might want to have text input with notion of "submitting input". Declarative way to do it looks like this (which I use): (ns example
(:require [cljfx.api :as fx]
[clojure.pprint :as pprint])
(:import [javafx.scene.input KeyEvent KeyCode]))
;; 2 flavors of state in atom:
;; - "db" which is domain-related data
;; - "ui" which is local state for components
(def *state
(atom
{:db {:user {:name "vlaaad"}}
:ui {}}))
(defn update-state [state e]
(case (:event/type e)
:edit
(update state :ui assoc-in (:path e) (:fx/event e))
:submit
(-> state
(update :db assoc-in (:path e) (:fx/event e))
(update :ui assoc-in (:path e) nil))
:on-string-input-key-pressed
(condp = (.getCode ^KeyEvent (:fx/event e))
KeyCode/ENTER
(update-state state (assoc (:on-value-changed e) :fx/event (:state e)))
KeyCode/ESCAPE
(update-state state (assoc (:on-state-changed e) :fx/event nil))
state)))
;; components have both local state (`state`) and domain data (`value`), and a way to
;; update both (`on-state-changed` for ui state, `on-value-changed` for domain data)
(defn string-input [{:keys [value on-value-changed state on-state-changed]}]
{:fx/type :text-field
:text (or state value)
:on-text-changed on-state-changed
:on-key-pressed {:event/type :on-string-input-key-pressed
:state state
:on-value-changed on-value-changed
:on-state-changed on-state-changed}})
(defn root-view [{:keys [db ui] :as state}]
{:fx/type :stage
:showing true
:width 620
:height 250
:scene {:fx/type :scene
:root {:fx/type :v-box
:padding 20
:spacing 10
:children [{:fx/type string-input
:value (get-in db [:user :name])
:on-value-changed {:event/type :submit
:path [:user :name]}
:state (get-in ui [:user :name])
:on-state-changed {:event/type :edit
:path [:user :name]}}
{:fx/type :label
:font "monospace"
:wrap-text true
:text (with-out-str (pprint/pprint state))}]}}})
(def renderer
(fx/create-renderer
:opts {:fx.opt/map-event-handler #(swap! *state update-state %)}
:middleware (fx/wrap-map-desc #(root-view %))))
(fx/mount-renderer *state renderer) Alternatively, you can (ab)use the fact that JavaFX has some local mutable state inside (which I also use because it's convenient): (ns example
(:require [cljfx.api :as fx]
[clojure.pprint :as pprint]))
(def *state
(atom {:user {:name "vlaaad"}}))
(defn update-state [state e]
(case (:event/type e)
:submit
(assoc-in state (:path e) (:fx/event e))))
(defn string-input [{:keys [value on-value-changed]}]
{:fx/type :text-field
;; text-formatter hides local mutation underneath
:text-formatter {:fx/type :text-formatter
:value value
:value-converter :default
:on-value-changed on-value-changed}})
(defn root-view [state]
{:fx/type :stage
:showing true
:width 620
:height 250
:scene {:fx/type :scene
:root {:fx/type :v-box
:padding 20
:spacing 10
:children [{:fx/type string-input
:value (get-in state [:user :name])
:on-value-changed {:event/type :submit
:path [:user :name]}}
{:fx/type :label
:font "monospace"
:wrap-text true
:text (with-out-str (pprint/pprint state))}]}}})
(def renderer
(fx/create-renderer
:opts {:fx.opt/map-event-handler #(swap! *state update-state %)}
:middleware (fx/wrap-map-desc #(root-view %))))
(fx/mount-renderer *state renderer) ...But it's not reliable. Most of the time, it'll work. Sometimes, it'll behave bad. For example, imagine we have a setting that changes layout of UI from horizontal to vertical, and user can turn it on and off. {:fx/type (if (get-in state [:settings :layout :horizontal]) :h-box :v-box)
:children [{:fx/type string-input
:value (get-in state [:user-name])
:on-value-changed {:event/type :submit
:path [:user :name]}}]} Type of the component is changed, and cljfx has to recreate all the components inside it, because for different types props with same keys might have different meanings, so we can't just reuse them. It will recreate That's why I decided to not introduce local mutable state for components in hierarchy, and instead suggest you to keep your component's local state in main state atom. |
Thanks - the comment is very helpful! I'm going to work out a sample where a stateful widget has a privatized state that the calling namespace is (mostly) unaware of, by doing something like treating event handlers as functions that pass state through many layers of event handlers (ala ring middlewares I guess) but ultimately end up tacking their privatized state into the global state atom (nested under prefixes or something). I think having some common pattern to do this would be beneficial for making isolated/distributable 'things' that could exist in the gui. Is there a way to signal an event manually? |
(def *state (atom {:clicked 0}))
(defn inc-or-make [n] (if n (inc n) 0))
(defn event-handler [event state]
(case (:event/type event)
::stub (update-in state [:clicked] inc-or-make)
state))
(defn make-button-with-state
"Wrapper to generate a stateful widget."
[prefix]
(let [handler (fn [event state]
"The event dispatching for received events."
[event state]
(if (= prefix (:prefix event))
(case (:event/type event)
::clicked (update-in state [prefix :clicked] inc-or-make)
state)
state))
view (fn [state]
(let [{:keys [clicked]} (prefix state)]
{:fx/type :button
:on-action {:event/type ::clicked
:prefix prefix}
:text (str "Click me more! x " clicked prefix)}))]
;; Send the handler and view back up to the caller.
{:handler handler
:view view}))
(def bws-1 (make-button-with-state ::bws-1))
(def bws-2 (make-button-with-state ::bws-2))
(def event-handlers
[event-handler
(:handler bws-1)
(:handler bws-2)])
(defn run-event-handlers
"If we have many event handler layers, we want to run each one in sequence.
This could let us have `private` widgets that maintain a state."
([m]
(prn "REH received only one arg? " m))
([state event]
(let [f (reduce comp (map #(partial % event) event-handlers))]
(f state))))
(defn root [{:keys [clicked] :as state}]
{:fx/type :stage
:showing true
:title "Counter"
:width 300
:height 300
:scene {:fx/type :scene
:stylesheets #{"styles.css"}
:root {:fx/type :v-box
:children
[
{:fx/type :label :text (str "Root state is: " clicked)}
((:view bws-1) state)
((:view bws-2) state)
]}
}
})
(defn renderer []
(fx/create-renderer
:middleware (fx/wrap-map-desc assoc :fx/type root)
:opts {:fx.opt/map-event-handler #(swap! *state run-event-handlers %)}))
(defn main []
(fx/mount-renderer *state (renderer))) In that code - if some more of the boilerplate/wiring was able to be abstracted away, the caller could use a widget with "internalized" state with no more than a few lines (then, the last part would be a way to make a generic abstraction/wrapper around it, and have a way to push events outwards to the users of the widget (to choose to maybe do something or maybe not)). |
I think there may be many different solutions, and it's up to users to pick ones that suit them. {:fx/type string-input
:value (get-in db [:user :name])
:on-value-changed {:event/type :submit
:path [:user :name]}
:state (get-in ui [:user :name])
:on-state-changed {:event/type :edit
:path [:user :name]}} As you see, there is a lot of repetition, and since cljfx operates on just data, you can make it really short: (defn string-input-for [state path]
{:fx/type string-input
:value (get-in (:db state) path)
:on-value-changed {:event/type :submit
:path path}
:state (get-in (:ui state) path)
:on-state-changed {:event/type :edit
:path path}})
;; then use it like that:
{:fx/type :v-box
:children [(string-input-for state [:user :name])]} This still might feel like its not very encapsulated, because it needs |
I would like to make a 'widget' (a component with an encapsulated state, using the event dispatching, but usable as an fx/type in a larger component).
Is such a thing possible?
It seems if I do the setup work around a 'button' - make a unique atom, bind it to watchers with create-renderer etc. but then click the actual button in the main 'root' wrapper, the state is never updated on the child element (well, the event never seems to dispatch at all).
The text was updated successfully, but these errors were encountered: