May 14, 2021

Building a library VI - Rendering

We have most of our internal machinery in place. It is time to look at visual representations of our forms.

Visual representations is one of those things that are really sticky. Complect is the word the Clojure community likes to use. The idea being that we intertwine two or more things into something that is complex. I personally prefer sticky in this instance, if only for the visual of something that you cannot touch without it getting all sticky on your fingers, and now everything else you touch will stick to your fingers.

We have two choices here really. Pick one implementation for the visuals and stick with it, or hide the implementation details behind polymorphism. I went with the first option for ez-wire, and we’ll go into the details of that choice, but first it might be interesting to see how the second option plays out.

Approach two: Polymorph

So, we first need a protocol for rendering. Since Clojure dispatches on the type of the first argument for protocols, we should probably think a bit about this a bit more deeply.

Using the form itself might seem tempting, as we so far have built (if we exclude the RAtom) something that is not really tied to a specific implementation. We could change the RAtom to an ordinary Atom, keep everything else, and add manual rendering for updated fields.

Alternatively we might want to dispatch on the field, and send along any required data, such as the form, as extra arguments.

(defprotocol IRender1
  (render [form]))

(defprotocol IRender2
  (render [field form]))

IRender1 or IRender2 would then have to tie in with the functions that the watcher (part V) handles.

IRender1 looks to be more work, as you would need to create a new implementation of the Form record for each new graphic library you want to use.

IRender2 would have the advantage of being more surgical, and could be implemented with minimal changes to the watcher functions as they currently are.

Another thing to consider as that the visual representation of a form happens in two places. The first one is the field itself, and what element (or widget) should visually represent the field. But the second part a form needs to be visually represented is in how the fields are grouped, in which order they come, if all are shown at once, etc.

For IRender1 this means the current definition of the protocol needs to handle both cases, which causes its own set of problems. IRender2 is a bit more flexible, and a single arity to the render function could easily be added.

We run into a third problem very quickly however. Say we want to render the form as a table, or an unordered list, or a list of paragraphs, or decide how it is to be rendered ourselves? How do we do that with the protocols?

We could have helper functions, that added required meta data for the form, but this quickly leads to all kind of edge cases once you start supporting more than one graphical library for the rendering part. We could expand IRender2 maybe, or we add more protocols.

(defprotocol IRenderField1
  (render-field [field form]))

(defprotocol IRenderForm1
  (render-form [visual form]))

This could be made to work. For render-field and the reagent implementation, we could have RAtom sitting per individual field. Other implementations could have their own way of doing it. And for each visual representation of the form, such as a table, an unordered list, etc, we could use render-form with a record for each type of visual. Add in some helper functions and you shield the programmer from all the mess.

Personally I didn’t go down this route, because my gut feeling is that there are dragons ahead.

Approach one: Only one implementation

A much easier and simpler way to approach visual representations of the forms.

We still need to show the fields (handled already via reagent), and show the form itself, along with the grouping, ordering, visual reprsenation (table, ul, …​), etc.

When showing the form we have a choice of how flexible we want to make these. A standard set of often used representations are often a good choice. You probably want to make it possible to visually represent the form however you want as well.

(defn- table-row [{:keys [active?] :as field} {{label? :label?} :options :as form}]
  [:tr {:key (str "tr-" (:id field))}
        (common/render-label field form)]
        (common/render-field (dissoc field :label) form)
        (common/render-error-element field form)
        (common/render-text field form)
        (common/render-help field form)]])

(defn as-table [params {:keys [id form-key] :as form} & [content]]
   {:display-name "as-table"

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

    (fn [params form & [content]]
      (let [{:keys [style class]
             :or {style {}
                  class ""}} params]
        [:table {:key (util/slug "form-table" (str @form-key))
                 :style style
                 :class class}
          [common/get-body table-row params form]
          (if content

Couple of things to note. We have a common namespace for common rendering that can be shared between all of the table rendering implementations. We use form-key to force re-rendering of the table if we use the reset-form! utility function. We alternate between using reagent rendering and running the functions from common.

Reagent rendering and running functions was tricky to get right. We want the minimum amount of rendering to happen, but some things might need setting up first, and so we want to run normal functions, and then return a hiccup vector for reagent.

And we finally arrive at the common functions. This is where the boundary between reagent and the inner workings of the form meet.

(defn render-field [{:keys [field-fn name] :as field} form]
  [field-fn (-> field
                (assoc :model (reagent/cursor (:data form) [name]))
                (dissoc :wiring :template :label))])

(defn render-text [{:keys [field-fn text css] :as field} form]
  (if text
    [:div {:class (get css :text "text")} (t text)]))

(defn render-help [{:keys [field-fn help css] :as field} form]
  (if help
    [:div {:class (get css :help "help")} (t help)]))

(defn render-label [{:keys [css id label name] :as field} form]
  (if (false? label)
    [:label {:for id :class (get css :label "label")} (t (or label name))]))

(defn get-body [row-fn params form]
  (let [fields (map #(get-in form [:fields %]) (:field-ks form))]
    (fn [row-fn params form]
       (for [field fields]
         ^{:key (:id field)}
         [row-fn field form])])))

Common’s get-body takes a row-fn, params and form. table-row from as-table in table is our row-fn, and utilizes render-field, render-text, render-help and render-label as normal functions which then return a hiccup vector. Running them as normal functions ensures they only run once, after that reagent is responsible for re-running them when a reagent reaction occurrs.

Notice that render-field adds the :model key that used in our integer-adapter in part III, and that is is a cursor.

Unordered lists and paragraphs are also implemented in a similar fashion as table.

Free visual representation the form will be covered in another part, as the implementation is a bit more advanced.

Putting it all together

We now have all the pieces to have a first version of a form library. And we utilize it thus.

(defn integer-adapter [{:keys [element] :as field}]
  (let [f (r/adapt-react-class element)]
    (fn [{:keys [model value] :as data}]
      [f (merge {:value (str (or @model value))
                 :on-change #(let [value (-> % .-target .-value)
                                   int-value (js/parseInt value)]
                               (if-not (js/isNaN int-value)
                                 (reset! model int-value)
                                 (reset! model value)))}
                (select-keys data [:id :placeholder]))])))

(defvalidation :percent-above-0
  #(>= % 0)
  "The percent cannot go lower than 0%")
(defvalidation :percent-below-100
  #(<= % 100)
  "The percent cannot go higher than 100%")

(defform my-form-name
  [{: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 {} {:percent 0})]
    [as-table {} form]))

Once we start re-using forms, elements, adapters, validations, i18n, etc the library quickly starts to pay itself off. We have successfully reduced the amount of code we need to do. Now, if only the boss will pay extra for doing the job faster…​

Next up…​

We will continue by looking at boundaries.

Tags: programming