May 20, 2021

Building a library VIII - Extending rendering with functional zippers

Data is really cool to work with, and with the right tools at hand, you can make magic happen. One such tool, which is absolutely fantastic in the right circumstances, are functional zippers.

In short, a functional zipper is a way to navigate through one or several data structures, all the while making changes to them, and then finally realizing the changes at the end. It allows you to take a data structure, make changes to them as you see fit, and then return a completely new data structure based on the old one.

We’ll take a deep dive into a rendering implementation, which will allows us to expand the rendering functionality for the form as a whole, while leaving the rendering of the fields intact as it’s a boundary we wish to keep.

Expanding our rendering

So, we have our general rendering for table, list and paragraphs from part VI, but would like something a bit more flexible.

The really nice way was if we could feed the form how we wanted it to be rendered, telling it where to place the fields, the errors for the fields, etc. If it can be done declaratively, without us having to go into tedious implementations for supporting injections, all the better.

Since we already use hiccup thanks to reagent, we can use that as a basis. Consider the code below.

[:div :$key
 :$field
 [:span :$errors]]

It’s easy enough to follow. We have a div with the $key, $field and $errors declared. $errors is inside span, because that is what the insanity of the design from the designers demands and there is nothing you can do about it.

This would work for dictacting the rendering of all the related elements for each individual field. Add in some control over the container of all the fields, and you have something that’s fairly workable.

Expanding to the entire form layout

It falls a bit short if you need even more control of the form layout. Each field will look the same, and sometimes you come across situations where you need different visual implementations for different fields. We can expand the idea to the following.

[:table
 [:tr :$account.key
   [:td
     {:colspan 2}
     :$account.field
     [:div :$account.errors]]]
 [:tr
  [:td :$percent.label]
  [:td
   :$percent.field
   [:div :$percent.help]
   [:div :$percent.errors]]]]

Given two fields, account and percent we put them inside a table, but give them a slightly different layout. We follow the same pattern as established above, but can now go down to the individual level for each field.

Reasoning

By using reagent’s use of hiccup vectors, we have expanded the syntax to include our own fields. Two cases have emerged, one a template, the other a wholesome affair where all of the visual layout is spelled out in the hiccup format.

We put a $ marker in front of our expanded vocabularity for two reasons. One, it makes it easier to implement and two, it makes it easier to visually identify.

Zipper details

Note
Whenever you see a zip namespace in the code below, it’s been imported from clojure.zip.

First, we need to setup a zipper, that allows us to navigate a hiccup vector. There are some default implementations of zippers in clojure.zip, but this is the one I have used for quite some time to navigate clojure’s normal data structures as a sort of general purpose zipper.

(defmulti branch? type)
(defmethod branch? :default [_] false)
(defmethod branch? cljs.core/PersistentVector [v] true)
(defmethod branch? cljs.core/PersistentArrayMap [m] true)
(defmethod branch? cljs.core/List [l] true)
(defmethod branch? cljs.core/IndexedSeq [s] true)
(defmethod branch? cljs.core/LazySeq [s] true)
(defmethod branch? cljs.core/Cons [s] true)


(defmulti seq-children type)
(defmethod seq-children cljs.core/PersistentVector [v] v)
(defmethod seq-children cljs.core/PersistentArrayMap [m] (mapv identity m))
(defmethod seq-children cljs.core/List [l] l)
(defmethod seq-children cljs.core/IndexedSeq [s] s)
(defmethod seq-children cljs.core/LazySeq [s] s)
(defmethod seq-children cljs.core/Cons [s] s)

(defmulti make-node (fn [node children] (type node)))
(defmethod make-node cljs.core/PersistentVector [v children] (vec children))
(defmethod make-node cljs.core/PersistentArrayMap [m children] (into {} children))
(defmethod make-node cljs.core/List [_ children] children)
(defmethod make-node cljs.core/IndexedSeq [node children] (apply list children))
(defmethod make-node cljs.core/LazySeq [node children] (apply list children))
(defmethod make-node cljs.core/Cons [node children] (apply list children))

(prefer-method make-node cljs.core/List cljs.core/IndexedSeq)
(prefer-method make-node cljs.core/List cljs.core/LazySeq)
(prefer-method branch? cljs.core/List cljs.core/IndexedSeq)
(prefer-method branch? cljs.core/List cljs.core/LazySeq)
(prefer-method seq-children cljs.core/List cljs.core/IndexedSeq)
(prefer-method seq-children cljs.core/List cljs.core/LazySeq)

(defn zipper [node]
  (zip/zipper branch? seq-children make-node node))

Next we require the code that navigates the zipper.

(defn unwrapper [loc markers]
  (let [rest-of-location (zip/rights loc)]
    (-> loc
        (zip/up)
        (zip/replace rest-of-location))))

(defn marker [node]
  (if (and (keyword? node)
           (str/starts-with? (str node) ":$"))
    node))

(defn wire [markers loc]
  (let [node (zip/node loc)
        mk (marker node)]
    (if (contains? markers mk)
      (let [value (get markers mk)]
        (if (fn? value)
          (value loc markers)
          (zip/replace loc value)))
      loc)))


(defn wiring [frame markers]
  (try
    (loop [loc (zipper frame)]
      (let [next-loc (zip/next loc)]
        (if (zip/end? next-loc)
          (zip/root loc)
          (recur (wire markers next-loc)))))
    (catch js/Exception e
      (.log js/console e))))

What the code does is really simple. We have a frame and some markers. The frame is our expanded hiccup syntax, while the markers is what we introduced before with our $marker (such as :$field). For each $marker, a corresponding key (e.g. :field) exists in markers and is used to replace our $marker in the frame.

In short, the wiring function goes through the entire data structure in frame by using zip/next. If it’s the end of the zipper, we realize our changes to the zipper (i.e., we return the new data structure) with zip/root. If not we move on to the next location in the frame. For each step forward we use the wire function.

The wire function takes our markers and the pointer to the current location in our frame. We realize the current location into a node with zip/node and then check if it’s a marker by using the marker function. If the $marker is in markers we replace the entire $marker with the value in markers. Unless it’s a function, in which case we run the function with the current location (i.e, we are giving the function the zipped frame with all the changes to it, to do with as the function wishes) and the markers.

There is also a function called unwrapper, which takes the the frame and removes that entire level of hiccup syntax. In short…​ if we had [:div "Hello World!"] and ran unwrapper on that, we would be left with "Hello world!".

Rendering setup

We now need to expand our common rendering functions to include our new advanced capabilities. We grab the :wiring key from our field for the advanced rendering and then run it through the wiring function with the wiring as our frame and the markers generated once in our Form-2 reagent component.

(defn render-wiring [{:keys [wiring] :as field} form]
  (let [rendered (wiring/wiring [:$wrapper
                                 wiring]
                                {:$wrapper wiring/unwrapper
                                 :$key     {:key (str "ez-wire-form-" (:id field))}
                                 :$label   (render-label field form)
                                 :$field   (render-field field form)
                                 :$errors  (render-error-element field form)})]
    (fn [_ _]
      (into [:<>] rendered))))

(defn assoc-wiring [{:keys [name] :as field} params]
  (let [params-wiring (:wiring params)]
    (assoc field :wiring
           (cond (contains? params-wiring name)
                 (get params-wiring name)

                 :else
                 (:wiring field)))))

(defn get-body [row-fn params form]
  (let [fields (map (comp #(assoc-wiring % params)
                          #(get-in form [:fields %])) (:field-ks form))]
    (fn [row-fn params form]
      [:<>
       (for [field fields]
         (if (:wiring field)
           ^{:key (:id field)}
           [render-wiring field form]
           ^{:key (:id field)}
           [row-fn field form]))])))

We can now add our form rendering functions. In ez-wire they got named as-template and as-wire.

as-template

(defn row [{:keys [wiring]} _]
  wiring)

(defn- adapt-wiring [{:keys [template] :as params} form]
  (assoc params :wiring
         (reduce (fn [out [_ {:keys [name]}]]
                   (assoc out name template))
                 {} (:fields form))))

(defn as-template [{:keys [template/element]
                    :or {element :div}
                    :as params}
                   {:keys [id form-key] :as form} content]
  (let [{:keys [template]} params]
    (r/create-class
     {:display-name "as-template"

      :component-will-unmount
      (fn [this]
        (when (util/select-option :form/automatic-cleanup? form params)
          (rf/dispatch [:ez-wire.form/cleanup id])))

      :reagent-render
      (fn [params form content]
        (let [{:keys [style
                      class]
               :or {style {}
                    class ""}} params]
          [element {:key (util/slug "form-template" @form-key)
                    :style style
                    :class class}
           [common/get-body row (adapt-wiring params form) form]
           (if content
             [content])]))})))

This piece of code will allow us to create a form like this, with each field being rendered according to the :template wiring in the definition of the form.

(defform my-form-name
  {:template [:<>
              :$field
              :$errors]}
  [{:name :percent
    :element Input ;; from some popular React component library
    :adapter integer-adapter
    :validation [:percent-above-0 :percent-below-100]}])

(defn view []
  (let [form (my-form-name {} {})]
    [as-template {} form]))

as-wire

(defn- ->kw [name k]
  (keyword (str "$" (subs (str name) 1) "." (clojure.core/name k))))

(defn wrapped-render [f field form]
  [f field form])

(defn- assemble-body [{:keys [wiring]} {:keys [fields] :as form} content]
  (let [default-map (if content
                      {:$content [content]}
                      {})]
    (wiring/wiring wiring (reduce (fn [out [_ {:keys [name] :as field}]]
                                    (merge out
                                           {(->kw name :wrapper) wiring/unwrapper
                                            (->kw name :key)     {:key (str "ui-wire-form-wire" (:id field))}
                                            (->kw name :label)   (common/render-label field form)
                                            (->kw name :field)   [wrapped-render common/render-field field form]
                                            (->kw name :errors)  (common/render-error-element field form)}))
                                  default-map fields))))

(defn as-wire [{:keys [wiring/element]
                :or {element :div}
                :as params}
               {:keys [id form-key] :as form} & [content]]
  (let [body (assemble-body params form content)]
    (r/create-class
     {:display-name "as-wire"

      :component-will-unmount
      (fn [this]
        (when (util/select-option :form/automatic-cleanup? form params)
          (rf/dispatch [:ez-wire.form/cleanup id])))

      :reagent-render
      (fn [params form & [content]]
        (let [{:keys [style
                      class]
               :or {style {}
                    class ""}} params]
          [element (merge {:key (util/slug "form-wire" @form-key)}
                          (if (not= element :<>)
                            {:style style
                             :class class}))
           body]))})))

as-wire requires a bit more work, as we here have to handle each field individually in the frame to be sent to the wiring function. Once that is done, the code we have setup can still be used as is.

With what we currently have, we can now render our form as such. Notice that we now add the options with the as-wire function, instead of defining it as part of the form as we did above with as-template.

(defn view []
  (let [form (my-form-name {} {})]
    [as-wire {{:wiring/element :span
               :class "foobar"
               :wiring
               [:div.wire
                :$percent.label
                :$percent.field
                :$percent.errors]}}
     form]))

Repeat

Functional zippers are one of those things that is a bit of a mind bender. In short, we take a hiccup vector [:div :$field], navigate through it, find which part of the vector is a :$field and replace it with what we have for that field.

So…​ we start at the top of the vector. We navigate next at our current position, which is the entire vector [:div :$field]. That moves us into the vector, and our position is now at :div. We check to see if is our field, and it’s not, so we navigate next. Since :div is not a collection, next will move us right inside the collection. We now end up at :$field. This is our field, and so we replace it with what we have in markers, which also happens to be pure data. Our check to see if we are at the end of the vector hits true and we realize the changes (i.e., we get back our changed hiccup vector). The changed hiccup vector will now look something like [:div [my-field-fn]].

Next up…​

We will finish this series with taking a look at branching logic and wizards.

Tags: programming