Skip to content
This repository has been archived by the owner on Oct 24, 2019. It is now read-only.
/ flow Public archive

Lightweight library to help you write dynamic CLJS webapps

Notifications You must be signed in to change notification settings

jarohen/flow

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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.

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

  • 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!

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:

(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!"]))

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

(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.

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

(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

::f/on - Event Listeners

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

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!"])

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:

(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])]))

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

(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’.

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.

(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.

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 @jarohen.

Thanks!

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

License

Copyright © 2014 James Henderson

Distributed under the Eclipse Public License, the same as Clojure

About

Lightweight library to help you write dynamic CLJS webapps

Resources

Stars

Watchers

Forks

Packages

No packages published