flow icon indicating copy to clipboard operation
flow copied to clipboard

Lightweight library to help you write dynamic CLJS webapps

  • Flow

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.

#+BEGIN_SRC clojure [jarohen/flow "0.3.0-alpha3"]

(:require [flow.core :as f :include-macros true]) #+END_SRC

For an 'all-batteries-included' template (including Flow), you might want to try SPLAT:

#+BEGIN_SRC shell lein new splat #+END_SRC

** The Flow DSL

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.

*** Tutorials

If you want to dive straight into tutorials, there are a number of tutorials/example apps in this repository:

  • [[samples/counter][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
  • [[samples/todomvc][TodoMVC]] example application - no webapp library would be complete without it!

*** Basic Elements

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:

#+BEGIN_SRC clojure (f/el [:p "Hello world!"]) #+END_SRC

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:

#+BEGIN_SRC clojure (f/root js/document.body (f/el [:p "Hello world!"])) #+END_SRC

Elements can be given an ID by appending it to the tag keyword with a hash:

#+BEGIN_SRC clojure (f/root js/document.body (f/el [:p#hello "Hello world!"])) #+END_SRC

*** =<<= - Reacting to dynamic state

Flow uses standard ClojureScript atoms to store state, so I'll assume here we have an atom containing the current value of a 'counter':

#+BEGIN_SRC clojure (def !counter (atom 0)) #+END_SRC

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:

#+BEGIN_SRC clojure (f/el [:p "The current value of the counter is " (<< !counter)]) #+END_SRC

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.

*** =::f/styles= & =::f/classes= - Styling

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):

#+BEGIN_SRC clojure (f/el [:div.container [:p#hello.copy "Hello world!"]]) #+END_SRC

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:

#+BEGIN_SRC clojure (f/el (let [counter (<< !counter)] [:p {::f/classes [(when (even? counter) "even")]} "The value of the counter is " counter])) #+END_SRC

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=:

#+BEGIN_SRC clojure (f/el [:p {::f/style {:text-align :center :margin "1em 0"}} "Hello world!"]) #+END_SRC

Style values here can be keywords or strings.

N.B. Each of the Flow keywords is namespace-qualified, so uses a double colon

*** =::f/on= - Event Listeners

Event listeners are attached with the =::f/on= attribute:

#+BEGIN_SRC clojure (f/el [:button {::f/on {:click #(js/alert "Hello!")}} "Click me!"]) #+END_SRC

There is also a helper function, =f/bind-value!=, which binds the value of an HTML input to a dynamic value or atom:

#+BEGIN_SRC clojure (let [!textbox-value (atom nil)] (f/el [:input {:type :text ::f/on {:keyup (f/bind-value! !textbox-value)}}])) #+END_SRC

*** Lifecycle callbacks

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:

#+BEGIN_SRC clojure (f/el [:p {::f/on {::f/mount (fn [$p-element] ;; ... )}} "Hello world!"]) #+END_SRC

*** Subcomponents

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:

#+BEGIN_SRC clojure (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])])) #+END_SRC

*** =!<<= - Passing dynamic values outside of the =f/el=

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 =!<<=:

#+BEGIN_SRC clojure (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)]))) #+END_SRC

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:

#+BEGIN_SRC clojure (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])]))) #+END_SRC

Here, we can't wrap 'caption', because it's a string, so we tell =!<<= that it needs to wrap the ':caption' key of 'todo'.

*** Collection keys - =f/keyed-by=

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.

#+BEGIN_SRC clojure (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)])))

#+END_SRC

=f/keyed-by= takes a key function, and a collection cursor.

** Current Limitations

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.

** Questions/suggestions/bug fixes?

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 [[https://twitter.com/jarohen][@jarohen]].

Thanks!

** Thanks

A big thanks to [[https://github.com/malcolmsparks][Malcolm Sparks]], [[https://github.com/lsnape][Luke Snape]] and [[https://github.com/henrygarner][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 [[https://github.com/matlux][Mathieu Gauthron]] and [[https://github.com/n8dawgrr][Nathan Matthews]] whose feedback on Clidget helped shape the direction of Flow.

Cheers!

James

** License

Copyright © 2014 James Henderson

Distributed under the Eclipse Public License, the same as Clojure