avatar

Enyert Vinas

Posted on 28th February 2022

Malli and HTML forms as data

news-paper Clojure | Software Development |

One of the so many great things offered by Clojure/Script to the community is the capacity of representing the structure and behavior of our applications, libraries, and tools as data.

This capacity is important because the data is the engine to be used in almost any process.

In this article, we want to present Malli and how to use this amazing library to represent HTML forms as data.

Malli

As we mentioned before data is the engine of almost any process, but this predicate works effectively only when we use data organized and well structured.

To maintain the data well structured, we have to assign names and domains to specific properties, in this way we can organize data and preserve it meaningful to our business processes.

Malli is a library that helps to create data schemas, so we can accomplish the objective of keeping the data integrity during our application life-cycle.

To accomplish this objective, Malli offers a ton of features but we will focus in the following ones:

  • Schemas, we want to create our own schemas so we can organize and create domains for our web forms
  • Validation, the validation process is super important when we talk about preserving the integrity of the data
  • Transformation, when we create web forms this will live in data that is recognized by the browser, so we will have to transform this data to make it compatible with out application environment.

HTML forms

Note: For this article we are assuming you understand three basic things: HTML, Reagent, Malli and Hiccup.

You probably already know that HTML forms are used to collect user input that is validated and sent to a server in the majority of the cases.

For this article we will be using a form like this one as reference:

(defn email-field
  ...
  ...)

(defn password-field
  ...
  ...)

(defn user-login-form
  []
  [:div#user-auth-form
    [:form {:method :post :action "/login"}
      [(email-field)]
      [(password-field)]
      [:button {:type :submit} "Press here to log in!"]]])

Here we have an email-field, a password-field and a button to submit our information. This is a very basic form but we hope this will help us to explain effectively our approach.

The field

We have the form we want to display in mind, we have to create some Malli schemas to represent the fields.

Fields are the different inputs of the form, so they are related with the entity Malli schemas for validation purposes.

The schema to be validated for the field can be the following:

(def MalliField
  [:map {:closed true}
   [:id  [:string {:min 1}]]
   [:name [:string {:min 1}]]
   [:class [:string {:min 1}]]
   [:description [:string]]
   [:error-message [:string]]
   [:default-error-mesage [:string]]
   [:type [:enum
           "text"
           "password"
           "checkbox"
           "email"
           "number"
           "file"
           "radio"
           "button"
           "color"
           "image"]]
   [:validation-group [:string]]])

This is a very basic schema but you can add more data such as default values and other relevant properties to be validated.

Let’s explain every property:

  • id: This is the CSS id of the input field
  • name: Field name. This property is important because it is mapped with the entity schema (We will explain this further)
  • class: CSS class of the input field
  • description: This field may not be required but can be useful for documentation generation.
  • error-message: This will be the error presented when the validation process fails.
  • default-error-message: This message will be rendered when error-message is empty.
  • type: Input type.
  • validation-group: If you want to evaluate the validation over different form groups. This will not be used for this particular example.

The form

We created a schema to validate our field, so now we need to create a new one for our forms.

The schema looks like the following one:

(defn valid-malli-field?
  [field]
  ...)

(def MalliForm
  [:and
   [:map {:closed true}
    [:fields [:vector :any]]
    [:validation-behavior [:enum "on-change" "grouped" "on-submit"]]
    [:schema [:vector :any]]
    [:validation-group-order [:vector :any]]]
   [:fn (fn [{:keys [fields]}]
          (every? #(:validity (valid-malli-field? %)) fields))]])

We are using the :and key here. This key is used by Malli to evaluate the conjunction of multiple blocks.

In this case we want to evaluate the validity of the :map schema and the :fn key. Remember you can find more specific information about Malli here, so you can add more complexity for your custom validations.

Generating our form structure

The form structure have two different components as a result of the generation process: reagent form and global state, so the main idea is binding these two components.

The reagent form is used by the reagent render function, so we need to create the behavior required by :on-submit or :on-change.

This behavior can be created using transformers and Malli provides a function called decode, another called encode and another called Transformer.

decode and encode functions controls the flow of the transformation and use a Transformer to do the process.

In code you can figure something like this:

(defn create-reagent-form
  [internal-form]
  ...)

(defn update-state 
  [internal-form state]
  ...)

(defn generate-form
  [internal-form state]
  (let [reagent-form (create-reagent-form internal-form)
          new-state (update-state internal-form state)
    {:result-form reagent-form
     :new-state new-state}))

With generate-form function we can access now to our reagent form(:result-form) and provide it to the reagent render function and we can update the state(:new-state).

Now we need to validate our form!

Validating data

After our structure generation process, we have to validate the user input. We already have the data binding between the state and the reagent form, so this is not difficult at all.

The tricky part here is the play with two functions provided by Malli: validate and explain.

The validate function verifies that our schema matches with the data recollected in the input and the explain function shows the errors collected in case the validation process fails.

Conclusion

In this article we presented the general idea of managing the life-cycle of our forms using Malli. This tool is so powerful so we can control the data consistency in our business processes.

We already have been doing some experiments with this use case, so the next step will be the construction of a library for this purpose because we love Clojure/Script and Clojure/Script loves data!

Thank you for the time reading this article, and happy coding journey! 🙂