Tying fields together in a record, so a function operating on one triggers an action on the other

I’ve got an architecture issue in Formless that I could use some help on.

It’ll take a little quick background before I can get to the question:

Background

These are the core types I’ll be discussing:

  • a Form newtype you define as a user, which is a newtype around a record of fields that are in your form. Each field has an error, input, and output type associated.
  • This newtype takes an argument, f, which is filled in with types like FormField. As you can see below, FormField takes the same error, input, and output types and produces a record from it.

Don’t worry too much about these; these types are unrelated to the probelm I’m discussing, but are still good to know for context as you see code snippets.

-- For context
newtype Form f = Form { name :: f Error String ValidName }
newtype FormField e i o = FormField { input :: i, result :: Either e o }

exampleForm :: Form FormField
exampleForm = Form
  { name: FormField
      { input: ""
      , result: Left MyCustomError
      }
  }

In Formless, I used to let you provide a single validation function which would be run across your entire form:

validator :: Monad m => Form FormField -> m (Form FormField)

This was nice because you could easily do validation on fields that depend on each other – for example, you could say that the password1 field is only valid if it is equal to the value of the password2 field. But it was NOT nice because validation was always run across all fields every time any individual field changed. When you have expensive validation or are making server calls this is a real problem.

I switched to a different style, where you provide a record of validators, each of which operates just on a single field’s input:

newtype Validation m e i o = Validation (i -> m (Either e o))

validators :: Monad m => Form (Validation m)

With this + variants I can run validation on just one field at a time. But I’ve lost access to the overall state with which to do that dependent validation! I can’t just re-introduce it as an argument, like this:

validators :: Form FormField -> Form (Validation m)

… because the validators will just have access to that state the very first time this function is used, and never afterwards. I briefly considered using a Ref, but didn’t want make the form a mutable variable if I could help it.

To get around that, I opted to have validators actually take the current state as an argument:

newtype Validation st m e i o = Validation (st -> i -> m (Either e o))

validators :: Monad m => Form (Validation (Form FormField) m)

and that actually works quite well…sort of. As you can see in the attached .gif, a field gets the most up-to-date state so long as it’s the one being modified. If it isn’t, then its validation never runs, and so perhaps its dependent condition has been satisfied but the field doesn’t know it yet.

In this example, the two Secret Key fields must be equal. The field you are modifying validates correctly, but the one you are not modifying gets out-of-sync because it hasn’t been refreshed with the latest change from its associated field.


Question

I’m not sure what to do about this. The core of the problem is that I need, somehow, a way to lash two (or more) fields together so that modifying or validating one also triggers validation on the other, and both pull the most recent form state when doing so.

Any ideas?

Some ideas that I have:

  1. Provide the ability to flag a particular field so any events on that field will trigger validation across the entire form. A poor solution because it doesn’t actually solve the “only validate what’s necessary” problem, it just makes it smaller.

  2. Let the user provide symbol proxies for fields they want to tie in to this field. Any action on this field will trigger validation on those fields. Perhaps in some optional, accompanying record that specifies dependencies. Solves the problem, but seems clumsy, heavyweight, and I’m not sure is actually possible. Certainly not without adding more parameter’s to the component’s type, which I want to avoid at all costs.

  3. Forget about monadic validation altogether and force anything with side-effects to be done once the form has left Formless. It’ll eliminate the ability to do things like check the server to see if an email is in use already, but it’ll make the component simpler and I can memoize validation so there’s not too much of a performance hit. It limits what the library can do, but would make it actually usable.

  4. Something else?

Ah! Shower thought!

In rendering, you specify that you would like particular events to trigger modification or validation like this:

renderSecretKey1 :: FormlessState -> FormlessHTML
renderSecretKey1 st =
 UI.input
   [ HP.value $ Formless.getInput secretKey1 st.form
   , HE.onValueInput $ HE.input $ Formless.modifyValidate secretKey1
   ]

In the above snippet, we’re saying that we want the secretKey1 field to be modified to contain the input string AND to be validated on value input.

But there also exists a query, AndThen, which lets you string two (or more) queries in a row. So you could simply trigger validation on another field from the render function:

renderSecretKey1 :: FormlessState -> FormlessHTML
renderSecretKey1 st =
 UI.input
   [ HP.value $ F.getInput secretKey1 st.form
   , HE.onValueInput $ HE.input \str -> Formless.AndThen
       (Formless.modifyValidate_ secretKey1 str)
       (Formless.validate_ secretKey2)
   ]