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.
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.
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
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>
}
}
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.
@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
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.