---
# System prepended metadata

title: 'SQUID Forms API Proposal: Fn-based Validation API v2:'

---

# SQUID Forms API Proposal: Fn-based Validation API v2:

**Please read original [PoA/Design Doc](https://docs.google.com/document/d/10WCeiclxUiLCbGrWW-k3cS3hap0yBTBvt5wEUYjH-zk)**

## High Level:

Reminder of priority order of api improvement features from original GDoc:

1. Nested field
2. Cross field
- *-- the line --*
4. Group-level
- *-- bonus --*
6. Multiple error messages
7. Field Coercion

# End State:

```clojure
{:default-values {:group-1 {:group-1-1 {:number-1-1-1 33
                                        :number-1-1-2 33
                                        :number-1-1-3 34}
                            :text-1-1 "hello"}
                  :group-2 {:text-2-1 "hello"}
                  :number-1 2}
 :validator (make-validator 
             {:& (fn [values _form-values]
                    "At the top-level :&, `values` == `form-values`"
                    (if (invalid? values) "It's all invalid!"))
             :group-1 {:group-1-1 {:& (fn [values _form-values]
                                          "Now `values` is the sub-map at [:group-1 :group-1-1]"
                                          (when (not= 100 (apply + (vals values)))
                                            "The sum of all the numbers must be 100."))}}
             :group-2 {:text-2-1 (coerce 
                                   str/trim 
                                   (fn [value form-values]
                                     (when (= value (get-in form-values [:group-1 :text-1-1]))
                                       "Cannot be same as :text-1-1.")))}
             :number-1 (fn [value]
                           (if (nil? value)
                             "Required."
                             [(when (even? value) "Must be odd")
                              (when-not (< 1 value) "Must be greater than 2.")]))}})
```

## ***Now let's zoom in***:

# 1. `:validator (make-validator {...})`:

## A. The `:validator` prop will now accept a simple flexible function with this signature:

> (and thus no longer accept a `map`)

```clojure 
(fn [form-values] ;; a map in :default-values's shape
    {:values {...}
     :errors {...}})
```
**Args**: `[form-values]`
- `form-values` will be the current form values. 
- It will already be `clj->js`-ified.

**Return**: `{:values {...} :errors {...}}`
- `:values`, 
    - if no errors, will be used as the `values` passed to `handle-submit`/`on-submit`
        - Used to coerce the values of form fields 
        - eg `string` -> `number` for field `[:foo :bar]`
        - eg apply `string/trim` to field `[:foo]`
- `:errors` 
    - validation errors

The dev will be responsible for matching `values` and `errors` shapes to the `:default-values` map. (or a subset, for `:errors`)

We then `clj->js`-ify the return value and pass that to `react-hook-form`

## B. Introducing **`(make-validator)`** :tada:: how people will *likely* actually use `:validator`

This helper will construct the right function for `:validator`, with a familiar API.

`make-validator` is a function with this signature:
```clojure 
(fn make-validator 
    [validators-map]
    (fn [form-values]
        {:values {...}
         :errors {...}})
```
**Args**: `[validators-map]` 
- `validators-map`
    - An arbitrarily nested map of field names -> validator/coercion fns
    - Resembles `:default-values`, but instead of values at each node, they're functions that validate and/or coerce those fields' values.


**Return**: `(fn [form-values] {:values {...} :errors {...}})`
- A fn of the exact signature that `:validator` will want (see above)

All of this is already the exact same thing we *currently* do internally for `:validator`'s map-based API

**Example usage:**
```clojure
{:default-values {:foo {:bar "boo"}}
 :validator (make-validator {:foo {:bar (fn [val] 
                                            (when (= val "boo") 
                                                "Invalid value"))}})
```

- The rest of this document will articulate the vision for `make-validator`'s new features that it adds to the existing `:validators` map conveniences.

# 2. Nested field validation: 

Currently, `:validators`'s map can only be flat/1-level deep. So `make-validator` will fix that:

```clojure    
{...
 :validator (make-validator 
              {:group-2 {:text-2-1 (fn [value]
                                     (when (invalid? value)
                                       "[:group-2 :text-2-1] is Invalid"))}})}
```

# 3. Cross field validation: 

For each field's validation fn, add a 2nd argument (eg `form-values`) that simply contains the whole form should a field need it: 

```clojure
{...
 :validator (make-validator 
              {:text-2-1 (fn [value form-values]
                           (when (= value (get-in form-values [:group-1 :text-1-1]))
                             ":text-2-1 cannot be same as :text-1-1"))})}
```

- From `(fn [value] ...)`
- To `(fn [value form-values] ...)`

**OPEN QUESTION:** should `form-values` be pre- or post-coercion?
> Maybe offer a 3rd arg that is the uncoerced whole map?

# 4. Group/whole-form-level: 

`make-validator` will accept an optional self-referential `:&` key at any level of the map.
It's value is a validator/coercion function just like above, but applied to a sub-map instead of a single value:
- first arg is the **sub-map of values *at that depth/path***
- second arg is the whole form

If `:&` used at the highest depth, the first and second arg will be the same.

**Example: Whole-form validation:**
```clojure 
{...
 :validator (make-validator 
              {:& (fn [values _form-values]
                (when (whole-form-invalid? values) "It's all invalid!"))})}
```

**Example: Group-level validation:**
```clojure
{...
 :validator (make-validator
              {:group-1 {:group-1-1 {:& (fn [values _form-values]
                (when (not= 100 (apply + (vals values)))
                  "The sum of these number inputs' values must be 100."))}}})}
```

# 4. Multiple error msgs per field: 

Accept a `coll` of error msg `string`s as return value of validation fns:
- (in addition to existing single-`string` return value)

```clojure 
{...
 :validator (make-validator
              {:number-1 (fn [value]
                (if-not value
                  "Required."
                    [(when (even? value) "Must be odd")
                     (when-not (< 1 value) "Must be greater than 1.")]))})}
```
# 5. Field Coercion

Introducing... `forms/coerce` 🎉:

It's a function that allows devs to define a coercion fn for a field, either instead of, or in addition to, a validation fn.

It already existed in `react-hook-forms`, but our abstractions neither supported it nor allowed it.

Coercion is the transformation or parsing of field data at 2 phases:

1. 

**Note about coercion in react-hook-form**:
> (and the `:values` map)
