Preferable form validators API


#9

I support this change wholeheartedly, more so that’s exactly what I had in mind.
I believe what @chenglou mentioned about combinations resonates well with ocaml’s practice of making illegal states unrepresentable.

That being said, I still think having built-in validators gives more value than it brings complexity. People will likely create their shortcuts, anyway. I can’t see myself copy pasting validators for every form I have, especially the basic, tedious, but absolutely necessary ones.

I realise that current explicit validators are self-explaining, but they make forms so painfully verbose. From the UX/DX standpoint, making people write functions for number range or string length and regexes for emails and dates is a big turn off.

What I see less needed, however, is the meta field. I can’t think of any essential use case for it other than password strength.

As for Valid message, I’d consider adding it to the form’s state instead (to possibly consume it from the response).


#10

If you wanna go down this road, then I believe what you want is a monadic Result pipeline, right? (Though please don’t throw monadic infix operators in newcomers face!)


#11

That’s what I’m vaguely imagining. (minus the infix part :smile:)

I maybe am short-sighted, but I see value in polishing the form state to make it seamless to work with fields, their states and changes.

It’s probably a topic for a different thread, right now I’m lacking the experience to understand what might actually work.


#12

Credit card type, phone number’s country etc etc

I’m not sure I fully understand your idea, but I have this TODO to add ability to pass error messages from response.


#13

I was wondering how could we make validation more composable, chainable.
Instead of a list of strategies and validators, monadic Result pipeline would be a great experiment (also, idiomatic). Cc: @alexfedoseev


#14
type fieldStatus =
  | Valid
  | Invalid(string) // Invalid(list(string)) ?

type formStatus = 
  | Submitting
  | Ready(`data, option(string))
  | Error(list(`field, fieldStatus), option(string))

I had something like this in mind. ^

Oh I see. Imo, that’s completely out of the validation scope. (it’s a value derived from the current state, not the validation status)


#15

Now we’re talking :smile:

This is pretty much how it’s gonna be except Invalid(option('message)) b/c there can be no massage at all + message might be i18n object instead of string. And this data is stored inside formality container as a Map w/ field as a key and option(fieldStatus) as a value.

I think we can leave list out of our scope as message type is defined by user, for us it’s abstraction: what we receive is what we store and return. We just pipe it to UI in the right time. So user might define type message = list(string);. Does it makes sense?

That prolly also ok. Need to play around w/ it a bit but after Valid/Invalid change.

BTW what is option(string)?

Makes sense! Moving meta to user land also makes it sound as it’s possible to use variants instead of strings.


#16

I should have wrote

type fieldStatus =
  | Valid
  | Invalid(list(`message))

I was thinking about the case where we could pass our field through all the validators without ‘failing fast’. (just a thought, not sure this will fly with Result monad)

Invalid(["Min length should be 6", "Incorrect format"])

It’s a message.

Ready(data, Some("Document has been successfully updated"))
Error(error_map, Some("We couldn't save your blogpost, please try again later"))

#17

I mean you already can do this if you define your type as list.

type message = list(string);
...
valudate: (value, _state) => {
  /* fold results in a list... */
  Invalid(Some(["Min length should be 6", "Incorrect format"]))
}

Otherwise you force all users handle list in the render which might be overhead (perf & code) if user is fine w/ just a single message.

just a thought, not sure this will fly with Result monad

Me either b/c I have zero experience using monads & I’m in the bubble of current API, so show me the code :sweat_smile:


#18

This was awesome change! Thanks @chenglou @rauan

Feel free to review: https://github.com/alexfedoseev/re-formality/pull/8


#19

Just wanted to say: this thread was really interesting to read through! Great thoughts on all sides. I like the change to the validation result type and agree it feels better as a variant than an object.

Regarding the original question - I tend to agree that “default” validations often don’t actually meet my specific needs for whatever reason. Aside from “required” they tend to have kind of specific requirements depending on the exact use case. That said, I love the idea of the validate function taking a list of validators, or somehow making validations able to be composed. That way I can define my own “defaults” for things like required, min, max, whatever and reuse them throughout my app by combining them as needed. You could even provide some example code for common validations - that way beginners could borrow those implementations without too much effort, but you’re not stuck maintaining them as a part of the package itself, responding to requests for different use cases, etc.

Thanks for putting the thought into all this. I’m really enjoying working with re-formality!


#20

@rauan I’m going to work on form status next. I want to discuss if we really need these messages (option(string)):

| Ready(`data, option(string))
| Error(list(`field, fieldStatus), option(string))

When server responds w/ success client knows that result is achieved (e.g. Document has been successfully updated) and Ready('data) status should be enough to derive this message inside render w/o additional argument in constructor. Same about Error: all details are stored in list('field, fieldStatus) and the fact that We couldn't save your blogpost can be easily derived from Error(list('field, fieldStatus)) itself in render. Or am I missing some use cases?


#21

Form-level status messages make sense when backend is the one doing all the heavy lifting (not just doing puny CRUD).

So if we assume that an api call response has data and message (any meta, really), it’s a matter of choice where we want to have it. (same way, it’s a matter of choice how we want to handle submission: have an url in the form’s type and make assumptions, or completely leave it out)

Personally, I found general status messages to be better placed in the form scope.
It could easily be moved into user land, I don’t have a clear opinion on that yet.


#22

@alexfedoseev I’ve look at the new API in 0.4.0 and I think the API could be cleaner.

Instead of a switch like this

| Some(Invalid('a))
| Some(Valid)
| None

We can change it to

| Valid
| Invalid('a)
| Pristine

My proposed API would also my current usecase, where I want some field to be optional, I’m currently return Valid for empty string but it feels wrong. I need to return Pristine


#23

@thangngoc89 TBH I’m not sure about it. In my mental model there’s clear separation of 2 concerns:

— Validators are responsible for validation of value. Basically, validator answers the question “Is this value valid?” but not “Should I show result in UI?” or something else. Thus this abstraction is not coupled to UI and can be reused whenever you want to check validity of attribute.
— Formality is responsible for managing when UI receives feedback. On each update it answers the question “Should I show validation result right now?”.

With addition of Pristine tag I feel like we introduce the responsibility that validator shouldn’t care about. Also, I’m concerned that it might be confusing for users who not aware of the optional field use case and it’s not clear when Pristine should be returned.

But the optional field case you mentioned is absolutely valid and I completely forgot to handle it. AFAICT empty string is the only case when success result shouldn’t be ever emitted. How I solve it in JS implementation: internally I just special case it and don’t emit results unless value exists. So in Formality I can special case it as well: e.g. you can still return Valid on empty string but formality will still emit None.

Does it make sense?


#24

This is totally make senses. But your proposed approach doesn’t feel right. Currently, required fields will show an error after blur. If you return None for empty string then there won’t be any errors.


#25

Yes, of course, Invalid result will be emitted, I mean something like

| Valid when value === "" => None
| Valid => Some(Valid)
| Invalid => Some(Invalid(message))

#26

I see. This is an acceptable solution for me. But I still feel like I have to keep a mental model about this in mind. :confused: not so obvious


#27

Just to make clear :slight_smile: This snippet is internal implementation, there won’t be any changes in your app:

validate: (value, _state) =>
  switch (value) {
  | "" => Valid
  | ...
  }

...

<div>
  (
    switch(MyField |> form.result) {
    | None => /* this is the case if value is "" */
    | Some(Valid) => /* it won't get here if value is "", only if value is not empty */
    }
  )
</div>

#28

Ah. Thank you. This makes sense now