rescriptbr / reform

📋 Reasonably making forms sound good
https://rescript-reform.netlify.app/
MIT License
353 stars 41 forks source link

On submit validation failure, focus on first invalid input #243

Closed nireno closed 1 year ago

nireno commented 1 year ago

On submit validation failure, is there any feature that would help to focus on the first invalid input element?

vmarcosp commented 1 year ago

You can create a hook that stores an array of tuple (or map) of (Field, React.domRef) and check for fields with errors to focus/scroll to the input. Here it is an example of how to do something similar:

// FormWrapper.res
module Make = (Config: ReForm.Config) => {
  include ReForm.Make(Config)
  module ComparableField = Belt.Id.MakeComparable({
    type t = field
    let cmp = Pervasives.compare
  })

  type fields = Belt.MutableMap.t<ComparableField.t, Dom.element, ComparableField.identity>

  type registerFieldFn = (field, Js.Nullable.t<Dom.element>) => unit

  let useScrollToField = (~offsetTop=0, ()) => {
    let fieldsRef: React.ref<fields> = React.useRef(
      Belt.MutableMap.make(~id=module(ComparableField)),
    )

    let register = (fieldName, fieldRef) => {
      switch fieldRef->Js.Nullable.toOption {
      | Some(el) => fieldsRef.current->Belt.MutableMap.set(fieldName, el)
      | None => ()
      }
    }

    let scrollToFieldWithError = ({state}: onSubmitAPI) => {
      state.fieldsState
      ->Belt.Array.get(0)
      ->Belt.Option.map(((field, _)) => field)
      ->Belt.Option.flatMap(field => fieldsRef.current->Belt.MutableMap.get(field))
      ->Belt.Option.map(el => DomApiExtra.Window.scrollTo(0, el->DomApiExtra.offsetTop - offsetTop))
      ->ignore
    }

    (register, scrollToFieldWithError)
  }

}

// MyApp.res
module Lenses = %lenses(
 type state = {
  name: string,
 } 
)
module MyForm = FormWrapper.Make(Lenses)
@react.component
let make = () => {
   // reform stuff here (useForm, etc)
  let (registerField, scrollWhenFail) = MyForm.useScrollToField()

  <input ref={Field(Name)->registerField->ReactDOM.Ref.callbackDomRef}  />
}
nireno commented 1 year ago

Thanks I'll give your example a shot. I thought that's what it would boil down to but was hoping there might be some magic to help out.

vmarcosp commented 1 year ago

Let me know if you need help to change the example from scrolling to a focusing behaviour.

vmarcosp commented 1 year ago

Any progress here? Can I close this issue? Let me know if you need help.

nireno commented 1 year ago

Had some friction getting things wired up but finally got it:

  1. Wired scrollWhenFail up to onSubmitFail when configuring ReForm use.
  2. Realized order of the Validation.Schema array determines order that the fields will appear in the fieldsState array.
  3. Realized that, for onSubmitFail, the fieldsState array only contains the fields that have an error which is why it was safe to state.fieldsState->Belt.Array.get(0).
  4. Added external to allow el.focus({preventScroll: true}). Without preventScroll the focused element would just pop into view. Having it lets me follow up with el.scrollIntoView({"behavior": smooth, "block": center}) to smoothly scroll the invalid input into view.

Thanks!