Giter VIP home page Giter VIP logo

Comments (5)

ahungry avatar ahungry commented on May 25, 2024

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.

from cljfx.

vlaaad avatar vlaaad commented on May 25, 2024

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.

from cljfx.

ahungry avatar ahungry commented on May 25, 2024

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?

from cljfx.

ahungry avatar ahungry commented on May 25, 2024
         (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)).

from cljfx.

vlaaad avatar vlaaad commented on May 25, 2024

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.

from cljfx.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.