Form Library Bindings

First I would like to say thank you to anyone who has put in time on this language.
I love the website and the documentation is great.

I am struggling with handling forms nicely. I found this library https://github.com/rescriptbr/reform
however, I am a bit afraid of the ppx. Should I be?

I tried binding to react-hook-forms but spun my wheels for an hour and then gave up.

I thought I would take a look at how elm does it for inspiration. I am not very good with elm but it looks like the events are messages and that all plays into elms runtime somehow which is out of my reach for now.

I am a beginner when it comes to this typed functional language stuff and I still have little to no intuition when it comes to structuring my programs.

Thoughts on dealing with forms in rescript?

One very simple way is to use HTML 5 forms validation: Client-side form validation - Learn web development | MDN

Modern HTML has pretty good form validation capabilities. You don’t actually need to do it in React at all. You can enforce constraints like ‘this field is required’, ‘this field must be a whole number’, ‘this field must be an email address’, and so on.

If you need more programmatic access, you can use the FormData API to access the contents of the form for more fine-grained tuning before submission: https://github.com/tinymce/rescript-webapi/blob/main/src/Webapi/Webapi__FormData.resi

HTML5 Form validation is not satisfy UX requirements in most our case. like:

  • validate on blur
  • validate based on another field
  • custom validate rule
  • variadic form field

The PPX is definitely additional complexity but I wouldn’t say it’s anything to be scared of, the transform is actually quite simple. You can also skip the ppx and write the lenses by hand if you want

See an example of the transform here: https://github.com/Astrocoders/lenses-ppx#watch

It sounds like you’re using React? I’d always start by using standard React stuff using a reducer (which are really nice to work with in ReScript). You can wire this up manually with regular onChange & onSubmit handlers, your reducer can handle setting errors and values.

Here’s a quick example I came up with:

let isPasswordValid = _ => true
let isEmailValid = _ => true

module LoginForm = {
  // A state to track the the network request that happens when we submit
  // We would usually track submission errors (from the server) here too
  type submittingState = Idle | Submitting | Submitted

  // The state of our form
  type state = {
    email: string,
    emailError: option<string>,
    password: string,
    passwordError: option<string>,
    submitting: submittingState,
  }
  // The actions that the user can perform
  type action =
    | ChangeEmail(string)
    | ChangePassword(string)
    | Submit
    | DoneSubmitting
    | Blur

  let initialState = {
    email: "",
    password: "",
    passwordError: None,
    emailError: None,
    submitting: Idle,
  }

  let reducer = (state, action) =>
    switch action {
    | ChangeEmail(newEmail) => {...state, email: newEmail}
    | ChangePassword(newPassword) => {...state, password: newPassword}
    | Submit
      if state.emailError->Belt.Option.isNone &&
        state.passwordError->Belt.Option.isNone => {
        ...state,
        submitting: Submitting,
      }
    | Submit => state
    | DoneSubmitting => {...state, submitting: Submitted}
    | Blur => {
        ...state,
        passwordError: if isPasswordValid(state.password) {
          Some("Invalid password")
        } else {
          None
        },
        emailError: if isEmailValid(state.email) {
          Some("Invalid email")
        } else {
          None
        },
      }
    }

  // We would usually call our API here
  let submitForm = _ => Js.Promise.resolve()

  @react.component
  let make = () => {
    let (state, dispatch) = React.useReducer(reducer, initialState)

    React.useEffect(() => {
      if state.submitting === Submitting {
        submitForm(state)
        |> Js.Promise.then_(() => {
          dispatch(DoneSubmitting)
          Js.Promise.resolve()
        })
        |> ignore
      }

      None
    })

    // Now we can render our login form
    <form
      onSubmit={event => {
        event->ReactEvent.Form.preventDefault
        dispatch(Submit)
      }}>
      {switch state.emailError {
      | Some(errorMessage) => <p> {React.string(errorMessage)} </p>
      | None => React.null
      }}
      <input
        value={state.email}
        onChange={event =>
          dispatch(ChangeEmail((event->ReactEvent.Form.target)["value"]))}
      />
      {switch state.passwordError {
      | Some(errorMessage) => <p> {React.string(errorMessage)} </p>
      | None => React.null
      }}
      <input
        value={state.password}
        onChange={event =>
          dispatch(ChangePassword((event->ReactEvent.Form.target)["value"]))}
      />
      <button disabled={state.submitting === Submitting}>
        {React.string("Login")}
      </button>
    </form>
  }
}
2 Likes

On the other hand, it’s super simple and requires zero custom code. In my opinion it’s worth thinking about the tradeoff–do we really need complex validations or can we make them a bit simpler in exchange for much less code and easier maintainability. I feel like too often UI/UX completely dictates all the requirements and devs just go along with super complex requirements instead of pushing back, because they just want to code.

1 Like

Thanks everyone for the responses!

@yawaramin
I did think about just using html5 forms. If I was trying to build something within a deadline I might go with that for the time being but my main goal here is to learn typed functional programming. I agree that UI/UX does get to dictate too much sometimes and I have pushed back with certain amounts of success: ie not much.

@tom-sherman
Thanks for the detailed code sample and the link to the lenses-ppx. They had a linked article about GADT’s which I found interesting, though I am not sure I understand what is going on with Peano Numbers

My suggestion: try using a normal HTML form and then override the onsubmit event with a handler: GlobalEventHandlers.onsubmit - Web APIs | MDN

ReScript-React has bindings to form events and you can grab the event target i.e. the form:
https://github.com/rescript-lang/rescript-react/blob/c3017ec4bbce5847c3b2da8d2d536450a3e2fd6d/src/ReactEvent.resi#L168 (or actually you can even use document.forms['name']).

Then you can get the form data by binding to a FormData constructor which takes the form object as its argument. You can use the rescript-webapi package as a base to get started from.

Now here is where the really interesting part, i.e. typed FP, comes in. The FormData object is basically a dictionary of string keys to string (or file) values. The job is to parse this stringly-typed dictionary into a custom type and validate it on the client side. Here you can use techniques like: defining a custom record type and even variant types, using polymorphic variant types to restrict strings, using pattern matching to check for valid values, and so on. And there’s no need for GADTs and lenses, which are imho overkill at this point.

Just my 2c.

1 Like