Comments (5)
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.
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.
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.
(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.
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)
- Spinner Example
- controlsfx, CheckComboBox HOT 7
- depstar is deprecated and archived; switch to tools.build HOT 2
- The example project fails with (UnsupportedOperationException) HOT 6
- Mouseclick and textfield ?
- Support for CheckBoxTreeItem/CheckBoxTreeCell
- Method for adding another context-like property HOT 1
- Runnning examples/e20_markdown_editor results in NullPointerException: HOT 1
- How do I use StyledTextArea or any new class? HOT 2
- Broken encoding for Sanskrit text in WebView HOT 2
- How do I exit the packaged app on window close or menu quit command HOT 2
- Unable to open DISPLAY HOT 4
- Trying to understand the lifecycle again
- Using cljfx with Gradle HOT 2
- Problem using leiningen HOT 2
- how to pass args to constructors HOT 3
- how to set the background of a region HOT 5
- Children: duplicate children added: when using pie-chart and dynamic rendering HOT 3
- Memory reuse with `fx/ext-instance-factory` HOT 5
- behavior of tree cell factory
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from cljfx.