Skip to content

Commit

Permalink
Merge pull request #4 from halgari/fixes
Browse files Browse the repository at this point in the history
Fixes
  • Loading branch information
halgari committed Mar 18, 2016
2 parents c37b3cb + 1417c4e commit 3324ca0
Show file tree
Hide file tree
Showing 12 changed files with 277 additions and 96 deletions.
167 changes: 126 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,60 +1,145 @@
# fn(fx)
This library provides a functional, declarative wrapper around JavaFX. The goals are to provide a "Virtual DOM" like
This library provides a functional, declarative wrapper around JavaFX. The goals are to provide a "Virtual DOM"
interface over the OOP mutability JavaFX embrances.

# Rationale
While the web has taken over many aspects of GUI programming that normally would have been implemented in JavaFX, its
still important to notice that a certain amount of complexity is involved in adopting a web based GUI. Programmers must
While the web has taken over many aspects of GUI programming that normally would have been implemented in JavaFX, it's
still important to recognize that a certain amount of complexity is involved in adopting a web based GUI. Programmers must
now write in several other languages, setup web servers, and handle network data transfers, when all that was required
was a GUI to some backend process. Sometimes a desktop UI really is the simplest option.

However, clojure developers have tranditionally shied away from adopting technologies such as Swing and JavaFX for fear
However, clojure developers have traditionally shied away from adopting technologies such as Swing and JavaFX for fear
of delving into the mess of mutability that is GUI programming.

This is the niche that fn(fx) attempts to fill, by providing a functional interface over JavaFX
This is the niche that fn(fx) attempts to fill: providing a functional interface over JavaFX

# How it works
A developer describes a interface via a nested datastructure that maps quite closely to the naming conventions of JavaFX:
# Basic Overview
fn(fx) requires that users express their ui via data, and calls to a function known as "ui". This function constructs
a quasi-immutable datastructure that can easily be diffed against other components. We say "quasi-immutable", since
some of the fields on the structure are mutated, but only once, from nil to a known value, never from a value
to another value. This tree of components can then be handedled by several functions:

* `(fn-fx.fx-dom/render component event-callback)` - This function takes a virtual dom (component tree) and
renders it, returning an opaque structure that can be used to later up date the UI with a new virtual dom.
`event-callback` is a function that will be handed events from the UI, more on that later.
* `(fn-fn.fx-dom/update-dom prev-state new-dom)` - Given a value returned from a previous call to `render`
or `update-dom` this function will diff `new-dom` against the dom used to create `prev-state` the resulting diff
will be used to make minimal changes to the UI where required.

## Event handling
Events are data, and are attached to components where `EventHandler` instances would normally be used. Thus creating
a button with the property `:on-action {:event :clicked!}` would result in a button that sent `{:event :clicked!}` to
the event-callback handed to the initial call to `render`

## User components
The `defui` macro generates a "user component" that is not a UI component, but a rendering function, and an
optional differ function. The render method on this component is only invoked when the properties to the component
change. `defui` is most often used to optimzie re-rendering as whole sections of the UI can be ignored during rendering
and diffing if the properties of the component haven't changed since the last render cycle.

# Example

```clojure
(ns getting_started.hello_world
(:require [fn-fx.render :as render]))

;; Describe the GUI
(def init-state
{:type :Stage
:fn-fx/children #{:root}
:title "Hello World!"
:scene {:type :Scene
:width 300
:height 250
:fn-fx/children #{:root}
:root {:type :StackPane
:fn-fx/children #{:children}
:children [{:type :Button
;; When this action is fired, provide the tag data
;; as part of the event handed to the event handler
:onAction {:tag :say-hello}
:text "Say Hello World"}]}}})


(let [c (render/create-root init-state)]
;; Attach a handler
(render/update-handler! c (fn [evt]
;; When we get an event, resize the window
(render/update! c (update-in init-state [:scene :width] + 10))
(println "Hello world! : " evt)))
(render/show! c))
```

In this example the root is considered a window, the root can be updated by applying a new description datastructure
to the component. The new state and the old state are diffed and the smallest changes are made to the GUI.
(ns getting-started.02-form
(:require [fn-fx.fx-dom :as dom]
[fn-fx.diff :refer [component defui render should-update?]]
[fn-fx.render :refer [ui]]))

(defn firebrick []
(ui :color :red 0.69 :green 0.13 :blue 0.13))

;; The main login window component, notice the authed? parameter, this defines a function
;; we can use to construct these ui components, named "login-form"
(defui LoginWindow
(render [this {:keys [authed?]}]
(ui :grid-pane
:alignment :center
:hgap 10
:vgap 10
:columns 2
:rows 6
:padding (ui :insets
:bottom 25
:left 25
:right 25
:top 25)
:children [(ui :text
:text "Welcome"
:font (ui :font
:family "Tahoma"
:weight :normal
:size 20)
:grid-pane/column-index 0
:grid-pane/row-index 0
:grid-pane/column-span 2
:grid-pane/row-span 1)

(ui :label
:text "User:"
:grid-pane/column-index 0
:grid-pane/row-index 1)

(ui :text-field
:grid-pane/column-index 1
:grid-pane/row-index 1)

(ui :label :text "Password:"
:grid-pane/column-index 0
:grid-pane/row-index 2)

(ui :password-field
:grid-pane/column-index 1
:grid-pane/row-index 2)

(ui :h-box
:spacing 10
:alignment :bottom-right
:children [(ui :button :text "Sign in"
:on-action {:event :auth})]
:grid-pane/column-index 1
:grid-pane/row-index 4)

(ui :text
:text (if authed? "Sign in was pressed" "")
:fill (firebrick)
:grid-pane/column-index 1
:grid-pane/row-index 6)])))

;; Wrap our login form in a stage/scene, and create a "stage" function
(defui Stage
(render [this args]
(ui :stage
:title "JavaFX Welcome"
:shown true
:scene (ui :scene
:root (login-window args)))))

(defn -main []
(let [;; Data State holds the business logic of our app
data-state (atom {:authed? false})

;; handler-fn handles events from the ui and updates the data state
handler-fn (fn [{:keys [event]}]
(println "UI Event" event)
(case event
:auth (swap! data-state assoc :authed? true)
(println "Unknown UI event" event)))

;; ui-state holds the most recent state of the ui
ui-state (agent (dom/app (stage @data-state) handler-fn))]

;; Every time the data-state changes, queue up an update of the UI
(add-watch data-state :ui (fn [_ _ _ _]
(send ui-state
(fn [old-ui]
(dom/update-app old-ui (stage @data-state))))))))

```

If this process sounds alot like projects such as ReactJS, that's not surprising as this entire project could probably be
described as "ReactJS for JavaFX".

# License
Copyright (c) Timothy Baldridge. All rights reserved.
Copyright (c) 2016 Timothy Baldridge. All rights reserved.
The use and distribution terms for this software are covered by the
Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
which can be found in the file epl-v10.html at the root of this distribution.
Expand Down
21 changes: 21 additions & 0 deletions examples/getting_started/01_hello_word.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
(ns getting-started.01-hello-word
(:require [fn-fx.fx-dom :as dom]
[fn-fx.diff :refer [component defui render should-update?]]
[fn-fx.render :refer [ui]]))



(defn -main []
(let [u (ui :stage
:title "Hello World!"
:shown true
:scene (ui :scene
:width 300
:height 250
:root (ui :stack-pane
:children [(ui :button
:text "Say 'Hello World'"
:on-action {:say "Hello World!"})])))
handler-fn (fn [evt]
(println "Received Event: " evt))]
(dom/app u handler-fn)))
93 changes: 93 additions & 0 deletions examples/getting_started/02_form.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
(ns getting-started.02-form
(:require [fn-fx.fx-dom :as dom]
[fn-fx.diff :refer [component defui render should-update?]]
[fn-fx.render :refer [ui]]))

(defn firebrick []
(ui :color :red 0.69 :green 0.13 :blue 0.13))

;; The main login window component, notice the authed? parameter, this defines a function
;; we can use to construct these ui components, named "login-form"
(defui LoginWindow
(render [this {:keys [authed?]}]
(ui :grid-pane
:alignment :center
:hgap 10
:vgap 10
:columns 2
:rows 6
:padding (ui :insets
:bottom 25
:left 25
:right 25
:top 25)
:children [(ui :text
:text "Welcome"
:font (ui :font
:family "Tahoma"
:weight :normal
:size 20)
:grid-pane/column-index 0
:grid-pane/row-index 0
:grid-pane/column-span 2
:grid-pane/row-span 1)

(ui :label
:text "User:"
:grid-pane/column-index 0
:grid-pane/row-index 1)

(ui :text-field
:grid-pane/column-index 1
:grid-pane/row-index 1)

(ui :label :text "Password:"
:grid-pane/column-index 0
:grid-pane/row-index 2)

(ui :password-field
:grid-pane/column-index 1
:grid-pane/row-index 2)

(ui :h-box
:spacing 10
:alignment :bottom-right
:children [(ui :button :text "Sign in"
:on-action {:event :auth})]
:grid-pane/column-index 1
:grid-pane/row-index 4)

(ui :text
:text (if authed? "Sign in was pressed" "")
:fill (firebrick)
:grid-pane/column-index 1
:grid-pane/row-index 6)])))

;; Wrap our login form in a stage/scene, and create a "stage" function
(defui Stage
(render [this args]
(ui :stage
:title "JavaFX Welcome"
:shown true
:scene (ui :scene
:root (login-window args)))))

(defn -main []
(let [;; Data State holds the business logic of our app
data-state (atom {:authed? false})

;; handler-fn handles events from the ui and updates the data state
handler-fn (fn [{:keys [event]}]
(println "UI Event" event)
(case event
:auth (swap! data-state assoc :authed? true)
(println "Unknown UI event" event)))

;; ui-state holds the most recent state of the ui
ui-state (agent (dom/app (stage @data-state) handler-fn))]

;; Every time the data-state changes, queue up an update of the UI
(add-watch data-state :ui (fn [_ _ _ _]
(send ui-state
(fn [old-ui]
(dom/update-app old-ui (stage @data-state))))))))
4 changes: 1 addition & 3 deletions project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.7.0-alpha5"]
[org.clojure/core.memoize "0.5.6"]
[org.clojure/core.async "0.1.346.0-17112a-alpha"]
:dependencies [[org.clojure/clojure "1.8.0"]
[org.clojure/core.match "0.3.0-alpha4"]
[org.reflections/reflections "0.9.10"]]
:profiles {:dev {:source-paths ["src" "examples"]
Expand Down
2 changes: 0 additions & 2 deletions src/fn_fx/component.clj

This file was deleted.

1 change: 0 additions & 1 deletion src/fn_fx/data_context.clj

This file was deleted.

19 changes: 12 additions & 7 deletions src/fn_fx/diff.clj
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
(nil? a) :nil
(instance? Component a) :comp
(satisfies? IUserComponent a) :ucomp
:else (assert false (str "Bad value type " (type a)))))
:else (assert false (str "Bad value type " (type a) " " a))))

(defn needs-update? [from to]
(let [{:keys [props]} to]
Expand Down Expand Up @@ -132,16 +132,21 @@
(reset! has-should-update? true))
(conj acc fn))
[]
fns)]
fns)
fn-name (symbol (util/camel->kabob nm))]
`(let [kw# (keyword (name (.getName ^clojure.lang.Namespace *ns*)) ~(name nm))]
(defquasitype ~nm [~'type ~'props ~'render-result]
IUserComponent
~@mp
~@(when (not @has-should-update?)
`[(should-update? [this# old-props# new-props#]
(= old-props# new-props#))]))
(defn ~(symbol (util/camel->kabob nm)) [& {:as props#}]
(~(symbol (str "->" (name nm)))
kw#
props#
nil)))))
(defn ~fn-name
([] (~fn-name {}))
([k# v# & props#]
(~fn-name (apply hash-map k# v# props#)))
([props#]
(~(symbol (str "->" (name nm)))
kw#
props#
nil))))))
2 changes: 0 additions & 2 deletions src/fn_fx/file_gen.clj

This file was deleted.

2 changes: 1 addition & 1 deletion src/fn_fx/fx_dom.clj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
IDom
(create-component! [this type spec]
(run-and-wait
(println type spec)
(binding [render/*handler-fn* handler-fn]
(render/create-component type spec))))

Expand Down Expand Up @@ -41,7 +42,6 @@




(defrecord App [prev-state dom root handler-fn])

(defn default-handler-fn [data]
Expand Down

0 comments on commit 3324ca0

Please sign in to comment.