Liking cljdoc? Tell your friends :D

Build Status JavaFX build on Clojars OpenJFX build on Clojars

Assistance / contributions welcome! Please see the contribution guide for more details.

fn(fx)

This library provides a functional, declarative wrapper around JavaFX / OpenJFX. The goals are to provide a "Virtual DOM" interface over the OOP mutability JavaFX / OpenJFX embraces.

Rationale

While the web has taken over many aspects of GUI programming that normally would have been implemented in JavaFX / OpenJFX, 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 traditionally shied away from adopting technologies such as Swing and JavaFX / OpenJFX for fear of delving into the mess of mutability that is GUI programming.

This is the niche that fn(fx) attempts to fill: providing a functional interface over JavaFX / OpenJFX.

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 handled 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 update 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 optimize 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.

Usage

Tested Versions

Before using fn-fx, please make sure you're using a JRE version that has been tested and is known to work. Here's the current test matrix:

JRE 1.8 (Oracle)JRE 1.8 (OpenJDK)JRE 11 (Oracle)JRE 11 (OpenJDK)
Clojure 1.7.011,211
Clojure 1.8.02
Clojure 1.9.02
Clojure 1.10.02

1 For now we've decided to only test back as far as Clojure 1.8.0. If anyone needs this tested on older versions of Clojure, PRs are welcome!

2 Currently, there is no easy way to obtain OpenJFX for OpenJDK 1.8, so it is not supported. If anyone has ideas on how to easily add this support, please comment on issue #71.

A Note on JavaFX vs OpenJFX

JavaFX was included in Oracle JRE versions 1.7u6 through 10, but has never been bundled in any version of OpenJDK, nor is it bundled in any edition of the JRE (Oracle or OpenJDK) from version 11 onward. In JRE 11 and up, these capabilities are instead provided by a separate library called OpenJFX that is not part of the default JRE installation.

Although we've attempted to hide this compatibility mess from the library user to the maximum extent possible, we were forced to provide multiple fn-fx artifacts in Clojars:

  1. fn-fx-javafx - for code that targets JREs that bundle JavaFX (i.e. Oracle JRE versions 1.7u6 through 10)
  2. fn-fx-openjfx## - for code that targets JREs that do not bundle JavaFX (i.e. OpenJDK and Oracle JRE versions 11 and up). Note that "##" is a specific number (currently only "11" is provided, yielding fn-fx-openjfx11)

Although these artifacts are code-identical, they have different upstream dependencies that are JRE-version-specific, so please make sure you select the correct artifact based on the version of the JRE that will be used at both development and runtime.

The regrettably tight coupling between OpenJFX and JRE versions makes it more challenging for the project to maintain both forward and backward compatibility across JRE editions and versions, but our intent is to maintain the broadest practical compatibility, at least until JRE 11+ is widely deployed. Whether this continues to be done via multiple artifacts, or some other mechanism is an open question. If you have suggestions / comments / preferences on this, please let us know!

Trying the Library Out

With lein-try

Kicking the tyres is a snap with the handy lein-try plugin:

$ lein try fn-fx/fn-fx-openjfx11 "0.5.0-SNAPSHOT"   # If you're running JRE 11 (Oracle or OpenJDK)
$ lein try fn-fx/fn-fx-javafx "0.5.0-SNAPSHOT"      # If you're running on an Oracle JRE, versions 1.7u6 to 10

With a Placeholder Project

Or, if you'd rather not use lein-try, you could create a new folder and put a project.clj file in it, something like this:

(defproject your-name/my-first-fn-fx-project "0.1.0-SNAPSHOT"
  :description      "My first fn(fx) project"
  :min-lein-version "2.8.1"
  :dependencies     [[org.clojure/clojure "1.9.0"]
                     ; Pick one, and only one, of the following dependencies:
                     [fn-fx/fn-fx-openjfx11 "0.5.0-SNAPSHOT"]    ; If you're running JRE 11 (Oracle or OpenJDK)
                     [fn-fx/fn-fx-javafx "0.5.0-SNAPSHOT"]       ; If you're running on an Oracle JRE, versions 1.7u6 to 10
])

Then start a REPL from that directory:

$ lein repl
nREPL server started on port 55554 on host 127.0.0.1 - nrepl://127.0.0.1:55554
REPL-y 0.3.7, nREPL 0.2.12
Clojure 1.9.0
OpenJDK 64-Bit Server VM 11.0.1+13
    Docs: (doc function-name-here)
          (find-doc "part-of-name-here")
  Source: (source function-name-here)
 Javadoc: (javadoc java-object-or-class-here)
    Exit: Control+D or (exit) or (quit)
 Results: Stored in vars *1, *2, *3, an exception in *e

user=> 

Using the Clojure Command Line Tools

Not yet implemented, though this is being tracked as issue #49. PRs welcome!

Making API Calls from the REPL

Once you're in a REPL you can make standard fn-fx API calls:

user=> (require '[fn-fx.fx-dom :as dom])
nil
user=> (require '[fn-fx.controls :as ui])
nil
user=> ; Do awesome GUI stuff here!

API documentation is published here, but note that these are currently *cough* "limited" due to a lack of docstrings in the code. Issue #s 21, 26, 27, and 28 go into more detail on this, and any additional comments / feedback / PRs for documentation are welcome!

Running the Examples

Note that the examples are not deployed to Clojars, so to run those you'll need to clone the project locally, and run a REPL from within the cloned directory:

$ cd <directory where you like to put your GitHub clones>
$ git clone https://github.com/fn-fx/fn-fx.git
Cloning into 'fn-fx'...
remote: Enumerating objects: 47, done.
remote: Counting objects: 100% (47/47), done.
remote: Compressing objects: 100% (27/27), done.
remote: Total 550 (delta 23), reused 39 (delta 20), pack-reused 503
Receiving objects: 100% (550/550), 301.67 KiB | 4.02 MiB/s, done.
Resolving deltas: 100% (264/264), done.
$ cd fn-fx
$ lein repl
nREPL server started on port 52005 on host 127.0.0.1 - nrepl://127.0.0.1:52005
REPL-y 0.3.7, nREPL 0.2.12
Clojure 1.9.0
OpenJDK 64-Bit Server VM 11.0.1+13
    Docs: (doc function-name-here)
          (find-doc "part-of-name-here")
  Source: (source function-name-here)
 Javadoc: (javadoc java-object-or-class-here)
    Exit: Control+D or (exit) or (quit)
 Results: Stored in vars *1, *2, *3, an exception in *e

user=>

Once you have a REPL up within the cloned directory, the examples may be run as follows:

  • 01 Hello world: (require '[getting-started.01-hello-word :as hello]) (hello/-main)
  • 02 Form: (require '[getting-started.02-form :as form]) (form/-main)
  • Shapes 3D: (require '[other-examples.shapes-3d :as shapes-3d]) (shapes-3d/-main)
  • Todo: (require '[other-examples.todo :as todo]) (todo/-main)
  • Menu Bar: (require '[other-examples.menubar :as menu]) (menu/-main)
  • WebView: (require '[other-examples.webview :as webview]) (webview/-main)

Each example will open a window that can be closed at any time (and reopened by calling -main again).

License

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. By using this software in any fashion, you are agreeing to be bound by the terms of this license. You must not remove this notice, or any other, from this software.

Can you improve this documentation?Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close