Getting started
After setting up reform
, reschema
, and lenses-ppx
in the previous section, you're ready to create your first form. You can understand every
part of this tutorial by reading the API reference, but we know that you want a sneak peek before getting deeper into the API.
Quick start
Creating our form
First off, we need to create our "lenses module" using lenses-ppx. Don't worry if you don't know what a ppx is, you can keep going just following our instructions.
We need to create a record that represents the state of the form. In this tutorial, the form has three fields: name
, email
, and password
.
module FormFields = %lenses(
type state = {
name: string,
email: string,
password: string
}
)
caution
The name of the record passed to lenses-ppx
must be named as state
.
You might be asking yourself: "what this lenses-ppx is doing?" and it's kind of magic, but it's a way to create "getters" and "setters" for the state
record.
After that, we have to create a new form using the ReForm.Make
module functor.
The module functor expects a lenses module that was created by
lenses-ppx
and returns a new form module. You can see the API reference of this module here.
module FormFields = %lenses(
type state = {
name: string,
email: string,
password: string,
}
)
module Form = ReForm.Make(FormFields)
tip
You can read more about module functors here.
The Form.use
hook
ReForm provides a form hook and we're going to use it by passing some parameters like schema
, an onSubmit
function, initialState
, etc.
module FormFields = %lenses(
type state = {
name: string,
email: string,
password: string,
}
)
module Form = ReForm.Make(FormFields)
@react.component
let make = () => {
let handleSubmit = ({state}: Form.onSubmitAPI) => {
Js.log(state.values)
None
}
let _form = Form.use(
~initialState={name: "", email: "", password: ""},
~onSubmit=handleSubmit,
~validationStrategy=OnDemand,
~schema={
open Form.Validation
schema([
string(~min=3, Name),
string(~min=8, Password),
email(~error="Invalid email", Email),
])
},
(),
)
React.null
}
We can split this snippet into four parts:
- The
Form.use
hook calling: This is the hook provided by reform. It returns aform
record that is typed asForm.api
and you can read more about its api here.
- The
- The
onSubmit
parameter: Just a function that will be called when you trigger theform.submit
function.
- The
- The
validationStrategy
: We're telling to reform which strategy of validation we want to use, in this case, we're usingOnDemand
which means that we'll trigger the validation manually using theform
record.
- The
Creating our form component
We need a form to make everything work, so we're going to use a combination of inputs and buttons to create a simple sign up form:
important
For this tutorial, we created some local components (like Input
, Button
, Input.Error
) just to make the markup more readable, but with the same API (onChange
, value
, onClick
, etc). Another components like Box
or Typography
are from Ancestor which is
an ui library and is totally optional for this tutorial. Feel free to use pure html with or without css to create your form.
open Ancestor.Default
module FormFields = %lenses(
type state = {
name: string,
email: string,
password: string,
}
)
module Form = ReForm.Make(FormFields)
@react.component
let make = () => {
let handleSubmit = ({state}: Form.onSubmitAPI) => {
Js.log(state.values)
None
}
let _form = Form.use(
~initialState={name: "", email: "", password: ""},
~onSubmit=handleSubmit,
~validationStrategy=OnDemand,
~schema={
open Form.Validation
schema([
string(~min=3, Name),
string(~min=8, Password),
email(~error="Invalid email", Email),
])
},
(),
)
<Box display=[xs(#flex)] flexDirection=[xs(#column)] alignItems=[xs(#center)]>
<Box tag=#form maxW=[xs(320->#px)] width=[xs(100.0->#pct)]>
<Typography tag=#h1 fontSize=[xs(24->#px)] fontWeight=[xs(#700)] mb=[xs(1)]>
{"Sign up"->React.string}
</Typography>
<Box> <Input placeholder="Your name" /> </Box>
<Box mt=[xs(1)]> <Input placeholder="Your email" /> </Box>
<Box mt=[xs(1)]> <Input type_="password" placeholder="Password" /> </Box>
<Box mt=[xs(1)]> <Button> {"Submit"->React.string} </Button> </Box>
</Box>
</Box>
}
Integrating the form
We created the Form
module by combining lenses-ppx
, reform
, and reschema
and we also have a simple form component. Now, it's time to make everything work together.
Different from libraries like react-hook-form, ReForm doesn't use any kind of magic with refs. ReForm was created to be both deadly simple and to make forms sound good, leveraging ReScript's powerful typesytem. Even the schemas we use are nothing more than constructors built-in in the language itself.
We encourage you to handle every change in your inputs manually. Not just the changes, but also the conversion of values, like string to int or string to float. Might be more verbose to do everything manually, but it's intentional and keep you in control of everything that happens with your forms.
The form
record returned by reform has some fields like handleChange
and values
that we're going to use to integrate the form module with our form component.
We're going to start by handling the changes on the inputs:
open Ancestor.Default
module FormFields = %lenses(
type state = {
name: string,
email: string,
password: string,
}
)
module Form = ReForm.Make(FormFields)
@react.component
let make = () => {
let handleSubmit = ({state}: Form.onSubmitAPI) => {
Js.log(state.values)
None
}
let form = Form.use(
~initialState={name: "", email: "", password: ""},
~onSubmit=handleSubmit,
~validationStrategy=OnDemand,
~schema={
open Form.Validation
schema([
string(~min=3, Name),
string(~min=8, Password),
email(~error=`Invalid email`, Email),
])
},
(),
)
<Box display=[xs(#flex)] flexDirection=[xs(#column)] alignItems=[xs(#center)]>
<Box tag=#form maxW=[xs(320->#px)] width=[xs(100.0->#pct)]>
<Typography tag=#h1 fontSize=[xs(24->#px)] fontWeight=[xs(#700)] mb=[xs(1)]>
{"Sign up"->React.string}
</Typography>
<Box>
<Input
placeholder="Your name"
value={form.values.name}
onChange={ReForm.Helpers.handleChange(form.handleChange(Name))}
/>
</Box>
<Box mt=[xs(1)]>
<Input
placeholder="Your email"
value={form.values.email}
onChange={ReForm.Helpers.handleChange(form.handleChange(Email))}
/>
</Box>
<Box mt=[xs(1)]>
<Input
type_="password"
placeholder="Password"
value={form.values.password}
onChange={ReForm.Helpers.handleChange(form.handleChange(Password))}
/>
</Box>
<Box mt=[xs(1)]> <Button> {"Submit"->React.string} </Button> </Box>
<Box mt=[xs(1)]>
<Typography tag=#strong> {"Form Values"->React.string} </Typography>
<Box display=[xs(#flex)] gap=[xs(8->#px->#one)]>
<span> {form.values.name->React.string} </span>
<span> {form.values.email->React.string} </span>
<span> {form.values.password->React.string} </span>
</Box>
</Box>
</Box>
</Box>
}
Also, we've added a simple block to display the form values. If you type something in any input, you can see the values changing:
Now, we can just type and see the values. If you click on the submit button, nothing happens, no error messages, no console.log, etc.
To make everything work, we still have two things to do: trigger the form.submit
function and render the validation errors (when we got an error) using the form.getFieldError
function:
open Ancestor.Default
module FormFields = %lenses(
type state = {
name: string,
email: string,
password: string,
}
)
module Form = ReForm.Make(FormFields)
@react.component
let make = () => {
let handleSubmit = ({state}: Form.onSubmitAPI) => {
Js.log(state.values)
None
}
let form = Form.use(
~initialState={name: "", email: "", password: ""},
~onSubmit=handleSubmit,
~validationStrategy=OnDemand,
~schema={
open Form.Validation
schema([
string(~min=3, Name),
string(~min=8, Password),
email(~error="Invalid email", Email),
])
},
(),
)
<Box display=[xs(#flex)] flexDirection=[xs(#column)] alignItems=[xs(#center)]>
<Box tag=#form maxW=[xs(320->#px)] width=[xs(100.0->#pct)]>
<Typography tag=#h1 fontSize=[xs(24->#px)] fontWeight=[xs(#700)] mb=[xs(1)]>
{"Sign up"->React.string}
</Typography>
<Box>
<Input
placeholder="Your name"
value={form.values.name}
onChange={ReForm.Helpers.handleChange(form.handleChange(Name))}
/>
{switch Field(Name)->form.getFieldError {
| None => React.null
| Some(message) => <Input.Error> message </Input.Error>
}}
</Box>
<Box mt=[xs(1)]>
<Input
placeholder="Your email"
value={form.values.email}
onChange={ReForm.Helpers.handleChange(form.handleChange(Email))}
/>
{switch Field(Email)->form.getFieldError {
| None => React.null
| Some(message) => <Input.Error> message </Input.Error>
}}
</Box>
<Box mt=[xs(1)]>
<Input
type_="password"
placeholder="Password"
value={form.values.password}
onChange={ReForm.Helpers.handleChange(form.handleChange(Password))}
/>
{switch Field(Password)->form.getFieldError {
| None => React.null
| Some(message) => <Input.Error> message </Input.Error>
}}
</Box>
<Box mt=[xs(1)]>
<Button
onClick={e => {
e->ReactEvent.Mouse.preventDefault
form.submit()
}}>
{"Submit"->React.string}
</Button>
</Box>
</Box>
</Box>
}
Now, when we click on the submit button without filling the form (or filling with invalid values), we can see the error message for each field. Also, if we open the browser console and fill all fields correctly, we can see the result of the form submission on the console.
Disclaimers
There are some disclaimers about this part of the tutorial:
- The first one is about the handling of our
handleSubmit
function. If you click on the submit button without filling the fields, the function will not be triggered. That's the expected behavior. TheonSubmit
function will be triggered when the form is valid. If you need to handle a function on the submit and there are invalid fields, you can use theonSubmitFail
parameter. You can read more about it here. - Because we're passing
OnDemand
to thevalidationStrategy
parameter, we have to call the form validation manually using a function likeform.validateForm
or just call theform.submit
function (like we did) that triggers the form validation automatically. If you need to trigger the validation on every change, you can useOnChange
as a validation strategy. You can see the validation strategies here.