/reform

Reasonably making forms sound good :hammer_and_wrench:

Primary LanguageOCamlMIT LicenseMIT

ReForm.re

Greenkeeper badge Build Status

ReForm demo

Reasonably making forms sound good again (pun 100% intended)

Installation

yarn add bs-reform

Then add it to bsconfig.json

"bs-dependencies": [
 "bs-reform"
]

What this is and why

Code that deals with strongly typed forms can quickly become walls of repeated text. We created ReForm to be both deadly simple and to make forms sound good leveraging ReasonML's powerful typesytem. Even the schemas we use are nothing more than constructors built-in in the language itself with a small size footprint.

Usage

Checkout demo/src/app.re also

module SignUpParams = {
  type state = {email: string};
  type fields = [ | `email];
  /* (fieldName, getter, setter) */
  let lens = [(`email, s => s.email, (_s, email) => {email: email})];
};

module SignUpForm = ReForm.Create(SignUpParams);

let component = ReasonReact.statelessComponent("Form");

let make = _children => {
  ...component,
  render: _self =>
    <SignUpForm
      onSubmit=(({values}) => Js.log(values))
      initialState={email: ""}
      schema=[(`email, Email)]>
      ...(
           ({handleSubmit, handleChange, form, getErrorForField}) =>
             <form
               onSubmit=(ReForm.Helpers.handleDomFormSubmit(handleSubmit))>
               <label>
                 <input
                   value=form.values.email
                   onChange=(
                     ReForm.Helpers.handleDomFormChange(handleChange(`email))
                   )
                 />
               </label>
               <p>
                 (
                   getErrorForField(`email)
                   |> Belt.Option.getWithDefault(_, "")
                   |> ReasonReact.stringToElement
                 )
               </p>
               <button _type="submit">
                 ("Submit" |> ReasonReact.stringToElement)
               </button>
             </form>
         )
    </SignUpForm>,
};

API

We made the API simple yet powerful and thus avoided a lot of quirks

Component params

Each ReForm module is a ReasonReact component

/* Just regular ReasonReact */
module Form = ReForm.Create(SignUpFormParams);

These are the props/params it accepts:

schema param

ReForm uses a Schema using idiomatic ReasonML to validate your data.

validate param

ReForm includes a number of validators, however they can't cover every case. Writing your own validator is trivial:

let validate: SignUpForm.values => option(string) = (values) => {
  switch (values) {
    | { email: "unsafeTypeGuy@ohno.com" } when values.password === "secretThing" => Some("Can't do.")
    | _ => None
  }
}

<Form
  validate
  /* Yes! You can still use schema with it */
  schema=[(`email, Email)]
>

The returned valued of validate will set reform.form.error

onSubmit param

If your data is validated then onSubmit will be called. This should contain your POST/mutation/whatever logic into and is triggered after handleSubmit is called.

let onSubmit = ({values, setError, setSubmitting, resetFormState}) => {
  Js.Promise.(
    values 
    |> convertToJS
    |> mutate
    |> then_(response => {
        switch(response##error |> Js.Null_undefined.to_opt) {
          | None =>
            setSubmitting(false);
            /* if you need to reset the form state to the initialState */
            resetFormState();
            doSomeOtherThing();
          | Some(error) =>
            setSubmitting(false);
            setError(Some("Something went wrong, try again"))
        }
      })
  )
  |> ignore
}


<Form schema onSubmit>

onFormStateChange param

This optional param will be called every time the form state changes. You might use this to lift the form state to its parent for example.

Its type is the same as the reform.state passed to the children.

<Form
  onFormStateChange=formState => Js.log(formState)
>

i18n param

ReForm supports internationalization. If you use this then your error messages should be message keys.

children: (YourForm.reform => ReasonReact.reactElement)

The param passed to the children has the following type:

type reform = {
  form: state,
  handleChange: (Config.fields, value) => unit,
  handleGlobalValidation: option(string) => unit,
  handleSubmit: unit => unit,
  getErrorForField: Config.fields => option(string)
};

form: Params.state

reform.form contains the following

{
  /* The record containing the actual form state */
  values: Params.state,
  /* The submitting state */
  isSubmitting: bool,
  /* This is intended to store global validation error, like a submitting failure */
  error: option(string)
}

handleChange: (Config.fields, string) => unit

handleChange takes the field and (string) value. This is an extension point that could be used in both Web and React Native

handleSubmit: unit => unit

Triggers the submitting and makes ReForm set reform.form.isSubmitting to true

getErrorForField: Config.fields => options(string)

Returns the (optional) validation error for the field in question

handleGlobalValidation: option(string) => unit

Handles the global error value at reform.form.error

Schema

ReForm's schema consists simply of a (fieldName: string, validator: constructor) tuple. The first item is the name of the field and the second property is a constructor.

For example:

(fieldName, validator)

or

(`email, Email)

It is passed as the first param for a Form: <SignInForm schema>

For more details, look at the demo to see it in action.

Available validators

Custom(state => option(string))

(`password, Custom(values => values.password == "123" ? Some("Really?") : None))

Required

(`fullName, Required)

Email

(`email, Email)

Support

The authors regularly hand out at the wonderful https://discord.gg/reasonml or https://reasonml.chat so feel free to visit us there.