Flow is a library to help you write dynamic ClojureScript webapps in a declarative style, without worrying how/when the DOM needs to be updated.
[jarohen/flow "0.3.0-alpha3"]
(:require [flow.core :as f :include-macros true])
For an ‘all-batteries-included’ template (including Flow), you might want to try SPLAT:
lein new splat <your-project>
The main part of Flow is its dynamic element DSL - a language with the following main design aims:
- It’s as close to ClojureScript as possible, to minimise the
learning curve.
let
(including destructuring),for
*,case
,if
,when
,when-let
, etc are all designed to work as they do in ClojureScript, albeit having been extended to handle continuous dynamic updates. If you know CLJS, the Flow DSL should be a natural extension. (see ‘Current Limitations’ for a couple of minor differences’) - Its element syntax is based on Hiccup - again, a well-known and widely used DSL in itself.
- It only adds two new constructs to the language -
<<
, to extract a value from a vanilla CLJS atom; and!<<
, to turn an extracted value back into an atom. This is covered in more detail below. - Flow is entirely declarative - so there’s no need for you to specify how or when to update the elements; simply state what they should look like given a certain state, and Flow will do the rest.
If you want to dive straight into tutorials, there are a number of tutorials/example apps in this repository:
- Flow in 5 Minutes - writing a ‘Hello World’/simple counter application
- Colour Picker (to come) - an intermediate tutorial introducing
!<<
- Contacts (to come) - an intermediate tutorial covering splitting your Flow application into components
- TodoMVC example application - no webapp library would be complete without it!
Flow elements are created inside the f/el
macro - this transpiles
the declarative Flow DSL into the imperative ClojureScript required to
create and update the elements.
Elements are created in the traditional Hiccup style:
(f/el
[:p "Hello world!"])
The result of f/el
is a standard, albeit possibly dynamic, DOM
element and can be passed around as such.
Flow also includes f/root
, a helper function to include an element
in the page. f/root
first clears the container element, and then
attaches the Flow component:
(f/root js/document.body
(f/el
[:p "Hello world!"]))
Elements can be given an ID by appending it to the tag keyword with a hash:
(f/root js/document.body
(f/el
[:p#hello "Hello world!"]))
Flow uses standard ClojureScript atoms to store state, so I’ll assume here we have an atom containing the current value of a ‘counter’:
(def !counter
(atom 0))
The ‘!’ before the name is an optional naming convention - it doesn’t have any effect on Flow. Personally, as a developer, I like using it because it makes a clear distinction in my code between stateful variables and immutable values.
We now need to tell Flow to include the current value of the counter
in our element, which we do using Flow’s (<< ...)
operator. It’s
similar in nature to ‘@’/’deref’, and it’s used as follows:
(f/el
[:p "The current value of the counter is " (<< !counter)])
As it’s part of the Flow DSL, <<
only works inside the f/el
macro.
When I’m reading Flow code, I’ll usually read (<< ...)
as ‘comes
from’, or ‘unwrap’.
Flow is fundamentally declarative in nature. We don’t specify any imperative behaviour here; no ‘when the counter updates, then update this element’ - we simply say ‘this element contains the up-to-date value of my atom’ and Flow does the rest.
Classes can be added in one of two ways. Static classes can be added in the same way as Hiccup, by appending them to the tag keyword (after the ID, if present):
(f/el
[:div.container
[:p#hello.copy "Hello world!"]])
If a class should only be applied to an element in certain situations,
use ::f/classes
. Using the example in the previous section, if we
wanted the counter to be of class .even
when the counter is even,
we’d write:
(f/el
(let [counter (<< !counter)]
[:p {::f/classes [(when (even? counter)
"even")]}
"The value of the counter is " counter]))
Notice here that we’ve also moved the (<< counter)
into a ‘let’
block, the same way as we would in vanilla Clojure. Even though
they’re not wrapped with (<< ...)
, Flow knows that the ‘counter’
variable came from an atom, and therefore that it needs to update the
classes and the text when the !counter
atom changes.
Finally, for inline styles, we use ::f/style
:
(f/el
[:p {::f/style {:text-align :center
:margin "1em 0"}}
"Hello world!"])
Style values here can be keywords or strings.
N.B. Each of the Flow keywords is namespace-qualified, so uses a double colon
Event listeners are attached with the ::f/on
attribute:
(f/el
[:button {::f/on {:click #(js/alert "Hello!")}}
"Click me!"])
There is also a helper function, f/bind-value!
, which binds the
value of an HTML input to a dynamic value or atom:
(let [!textbox-value (atom nil)]
(f/el
[:input {:type :text
::f/on {:keyup (f/bind-value! !textbox-value)}}]))
Lifecycle callbacks can also be added to the ::f/on
attribute. Flow
only supports one lifecycle callback at the moment, ::f/mount
, which
is called once whenever the element is first mounted.
It expects a function of one argument - the DOM element that is about to be mounted:
(f/el
[:p {::f/on {::f/mount (fn [$p-element]
;; ...
)}}
"Hello world!"])
Flow components can be easily composed - simply call them as you would a normal function (with any necessary parameters) but, rather than parens, enclose them in a vector:
(defn render-list-item [elem]
(f/el
[:li
[:span "Item: " elem]]))
(defn render-list [elems]
(f/el
[:ul
(for [elem elems]
[render-list-item elem])]))
The values extracted from an atom with <<
are only dynamic within
the scope of the f/el
in which they are extracted. To ensure that
subcomponents also react to dynamic values, we re-wrap them using
!<<
:
(defn render-todo-item [!todo]
;; '!todo' is an atom here
(f/el
(let [{:keys [caption]} (<< !todo)]
[:li caption])))
(defn render-todo-list [!todos]
(f/el
(for [todo (<< !todos)]
[render-todo-item (!<< todo)])))
Notice here that we don’t need to re-wrap the original atom for the dynamic value to be propagated - we can just as easily wrap sub-keys of a map, or elements in a vector.
Unfortunately, Flow can only re-wrap maps, vectors, lists and sets in
this way. For primitives, you can use !<<
to wrap the containing
collection, and provide an extra key to find the corresponding value:
(defn render-todo-item [!caption]
;; Here '!caption' is an atom containing a string.
(f/el
[:li caption]))
(defn render-todo-list [!todos]
(f/el
(for [todo (<< !todos)]
;; we only want to pass the ':caption' key as a dynamic value
[render-todo-item (!<< todo [:caption])])))
Here, we can’t wrap ‘caption’, because it’s a string, so we tell
!<<
that it needs to wrap the ‘:caption’ key of ‘todo’.
We can also specify the key of a collection, using f/keyed-by
. This
helps Flow with calculating the difference between one state and the
next - it means that Flow can track individual elements in the
collection in the presence of insertions, deletes and shuffles.
(defn render-todo-item [!todo]
(f/el
(let [{:keys [caption]} (<< !todo)]
[:li caption])))
(defn render-todo-list [!todos]
(f/el
(for [todo (->> (<< !todos)
(f/keyed-by :todo-id))]
[render-todo-item (!<< todo)])))
f/keyed-by
takes a key function, and a collection cursor.
There are a couple of features still to be implemented:
- Flow’s ‘for’ doesn’t yet support ‘:when’, ‘:let’ or ‘:while’ clauses - support for this will be added in the future.
Yes please!
If you have any questions, or would like to get involved with Flow’s development, please get in touch! I can be contacted either through Github, or on Twitter at @jarohen.
Thanks!
A big thanks to Malcolm Sparks, Luke Snape and Henry Garner for their
feedback and advice on early versions of the library, and their help
through numerous design/implementation conversations. Thanks also to
Henry for his excellent suggestion of the <<
syntax.
Also, thanks to Mathieu Gauthron and Nathan Matthews whose feedback on Clidget helped shape the direction of Flow.
Cheers!
James
Copyright © 2014 James Henderson
Distributed under the Eclipse Public License, the same as Clojure