Skip to content
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

Closed
ahungry opened this issue Jul 13, 2019 · 5 comments
Closed

Is it possible to multiple states/renderers in one view? #32

ahungry opened this issue Jul 13, 2019 · 5 comments

Comments

@ahungry
Copy link

ahungry commented Jul 13, 2019

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

@ahungry
Copy link
Author

ahungry commented Jul 13, 2019

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?

 it rejects using
     multiple stateful reactive atoms for state and instead prefers composing
     ui in more pure manner.

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.

@vlaaad
Copy link
Contributor

vlaaad commented Jul 13, 2019

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.
It's use looks like that:

{: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 string-input's instance, and all changes will be lost.

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.

@ahungry
Copy link
Author

ahungry commented Jul 14, 2019

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?

@ahungry
Copy link
Author

ahungry commented Jul 14, 2019

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

@vlaaad
Copy link
Contributor

vlaaad commented Jul 14, 2019

I think there may be many different solutions, and it's up to users to pick ones that suit them.
For example, in purer solution in my previous comment usage of component looks like this:

{: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 :on-string-input-key-pressed event be handled in "global" handler, but I think it's totally fine, if you use multimethods for event handling. That way, when writing isolated component, you define its internal events and handlers for them using defmethod in the same place. There is an example of such component, although it uses more advanced features to avoid passing state around explicitly.

@vlaaad vlaaad closed this as completed Jul 28, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants