vrac

Template-based web library [WIP] - https://github.com/green-coder/vrac Zulip archive: https://clojurians.zulipchat.com/#narrow/stream/180378-slack-archive/topic/vrac Clojureverse archive: https://clojurians-log.clojureverse.org/vrac
2020-08-29T03:40:12.277Z

đź‘‹

2020-08-29T03:57:19.282300Z

Today’s brainstorming will be about giving an example of how a user would use Vrac to build forms. Although there are many ways to do it, I am going to use https://github.com/reagent-project/reagent-forms as a reference, as well as ideas from https://www.youtube.com/watch?v=IekPZpfbdaI which, similarly to Vrac, is using a declarative approach. Then I will attempt to describe how it would translate in Vrac by taking advantage of the fact that templates are highly readable data.

2020-08-29T05:57:06.289700Z

I am watching the video and I take notes. At 7:00 in the video, we see

{:title "Patient Intake Form"
 :metadata {:form-name "patient-intake-form"}
 :model {:height {:path "patient/vital/height"}
         :weight {:path "patient/vital/weight"}
         :bmi {:path "patient/vital/bmi"}}
 :view {:default {:form [[:widget {:type :section
                                   :title "Vitals"}
                          [:widget {:type :numeric
                                    :title "Height (cm)"
                                    :path :height}]
                          [:widget {:type :numeric
                                    :title "Weight (kg)"
                                    :path :weight}]
                          [:widget {:type :numeric
                                    :title "BMI"
                                    :path :bmi}]]]}}}
There are a few things to notice; 1. The model’s properties and the view’s properties are decoupled. 2. The link between the model and the view is a path which identify parts of the model. 3. The approach is data driven, the edn above is data which is fed into a function which will output hiccup with some plumbing on how to update the data from the user’s interaction with the form, and how to update the form from data changes.

2020-08-29T05:59:49.291600Z

In Vrac, there are multiple ways to do it, depending on how data driven you want/need to be.

2020-08-29T06:40:36.293Z

Let’s see the most simple way first, by hardcoding the form’s structure into the Vrac components:

(defc numeric [title data]
  (let [form-id (gen-id "form-")] ;; non-reactive binding to a generated unique string
    [:div
     [:label {:for form-id} title]
     [:input {:type :number
              :id form-id
              :value (:value data)
              :on-change (dispatch [:input-change data])}]]))

(defc section [title & children]
  [:div
   [:h1 title]
   (for [child children]
     [child])])

(defc patient-intake-form [patient]
  (let [{:keys [height weight bmi]} (:vital patient)]
    [:form
     [section "Vitals"
      [numeric "height (cm)" height]
      [numeric "Weight (kg)" weight]
      [numeric "BMI" bmi]]]))
Here we have the view part of the form. The binding between “which data” and “which view” is done in the same way Clojure does when putting together functions and arguments, with some optional use of let to keep things tidy. As a side note, not using let has no impact on performances.
;; Alternative formulation, no impact on performance.
(defc patient-intake-form [patient]
  [:form
   [section "Vitals"
    [numeric "height (cm)" (-> patient :vital :height)]
    [numeric "Weight (kg)" (-> patient :vital :weight)]
    [numeric "BMI" (-> patient :vital :bmi)]]])

2020-08-29T07:11:38.298Z

Now, if we don’t want the component patient-intake-form to be strongly linked to the specific components section and numeric , we can pass them in parameters.

(defc patient-intake-form [patient form-comps]
  (let [{:keys [height weight bmi]} (:vital patient)
        {:keys [section numeric]} form-comps]
    [:form
     [section "Vitals"
      [numeric "height (cm)" height]
      [numeric "Weight (kg)" weight]
      [numeric "BMI" bmi]]]))
form-comps is an hash-map whose values are component ids. For example:
;; "Rainbow form theme"
{:section ::rainbow/section
 :numeric ::rainbow/numeric}

2020-08-29T07:15:02.299100Z

The form components can also be accessed in a global way rather than passed in parameters:

(defc patient-intake-form [patient]
  (let [{:keys [height weight bmi]} (:vital patient)
        {:keys [section numeric]} (:form-comps global)]
    [:form
     [section "Vitals"
      [numeric "height (cm)" height]
      [numeric "Weight (kg)" weight]
      [numeric "BMI" bmi]]]))

2020-08-29T07:36:45.300300Z

For a better genericity, the form properties could be passed to form components via an hash-map:

(defc patient-intake-form [patient]
  (let [{:keys [height weight bmi]} (:vital patient)
        {:keys [section numeric]} (:form-comps global)]
    [:form
     [section (val {:title "Vitals"})
      [numeric (val {:title "Height (cm)"
                     :data height})]
      [numeric (val {:title "Weight (kg)"
                     :data weight})]
      [numeric (val {:title "BMI"
                     :data bmi})]]]))

2020-08-29T09:01:03.303900Z

Now, If we wanted to really go data-driven and not hard-code the structure into components, we could do it via a recursive component. That’s a little bit complicated to show here, but it seems doable.

2020-08-29T09:26:23.305500Z

The bridge between a form description as data and a component composition may look like that:

(defc form-builder-comp [form-desc data]
  (let [{:keys [type data-path children]} form-desc
        data (get-in data data-path)]
    [(-> global :form-comps type)
     (val {:data data})
     (for [child-form-desc children]
       [form-builder-comp child-form-desc data])]))

2020-08-29T09:27:56.306700Z

Side note: to avoid using val in the template, we can also do like this: [my-comp {} value1 value2 ...]

2020-08-29T10:04:39.316300Z

I did not mention about the event handling yet, but the idea is to provide the event process with the path of the data as seen outside of the template. Then, the event handler is wrapped inside a function which provides it with the input data via get-in , and then use the output effect to possibly update the local-db using update-in . It may sound familiar, re-frame has a way to do that already. The difference is that in re-frame the path is static and provided by the library’s user. It can only be used for 1 place in the DB, while in Vrac the data paths depends on which data the event is dispatched. The benefits are: 1. we decouple event handler from data locations, so that there are possibly less use of get-in and update-in in the event handler. 2. we are able to reuse the event handlers for similar processing in different locations in the db.

đź‘Ť 1
2020-08-29T11:16:09.322200Z

In Re-frame, the same is achieved by the component by passing in the event what the documentation refers to as "identity" information. In Vrac the determination of the identity is implicitly done by the system in the shape of a path in the db. I means that it is no longer the component's job to do it, and not even the event handler.

2020-08-29T11:27:53.324400Z

It looks like having canonical paths is something important after all.

2020-08-29T11:32:09.327400Z

The cool thing is that the user can do things like (dispatch [:update my-data f arg1 arg2]).

2020-08-29T18:17:45.344700Z

The data model which is used inside the template is different from the data in the local db. Explanations. In the template, we are in the semantic world. The data has the shape of a general directed graph (possibly with some cycles) of data entities (e.g. person, blog, article, etc …). In the general case, you can get from one entity to another one by calling a function. Such function can have one or multiple parameters, it can also be a keyword acting as a hash-map look up (because keywords are functions). The result of those functions can be other entities which are in the local db, or it could be a computed value. From the component’s point of view, both will be used in the same way, as a base for further navigation in the graph or to define new computed values. Outside of the template, we are in the Clojure world. The language’s immutable data structures are trees, not graphs. Between the 2 worlds, there is a gap, a semantic illusion which is built on some very concrete data structure. In order to apply the data access and operations described in the template onto the local db, we need to provide some translation from the semantic world to the Clojure world. For instance, (-> user :written-articles (get 0) :authors (get 0) :name) is navigating from a person to an articles, then back to a person. If the data was really represented like that in a Clojure data structure, it would mean that some persons only exist thanks to the articles they wrote - but that’s not how we model data in real life !

2020-08-29T18:36:01.352600Z

Because they are in 2 different worlds, keywords which are used as function do not function the same as in Clojure. Any content of a Vrac template is data representing some user-helping semantic for doing front end. It means that it’s all subject to interpretation, and it would not be anything without an interpretation (because that’s not even Clojure code). The template function :written-articles is mapped to a user-provided Clojure function which is doing a look up of any article which have an author that matches the provided user. It could looks like that:

;; This time, this is really some Clojure code.
(defn person-id->written-articles [db person-id]
  (into #{}
        (filter (fn [article]
                  (contains? (:authors article) person-id)))
        (:articles db)))