# 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)