noble-ai / rescript-fields

Elaborate form validation for Rescript
https://noble-ai.github.io/rescript-fields
MIT License
1 stars 1 forks source link

When instantiating a Form that uses a `FieldArray` , in both cases `~init=None` && `~init=Some([])`, nested elements inside the `FieldArray` do not update as expected. #3

Open KateChanthakaew opened 2 weeks ago

KateChanthakaew commented 2 weeks ago

Description

Calling Form.use(. ~init=None, ...) or with Form.use(. ~init=Some([]), ...), notice the FieldArray elements are not updating as expected.

Reproduction

We create a FieldArray named LoginArray to contain the field Login as the nested elements. Our example below is driven from the FieldArray example found in Everything.res.

Note: The bug occurs when a FieldArray is nested inside a FieldProduct, and also when a FieldProduct is the element inside a FieldArray. The issue occurs for filterIdentity and filterGrace type FieldArray, however we're just showing the filterIdentity version with a Form.use(. ~init=Some([]), ...) in our example below.

module Status = {
  @react.component
  let make = (~title=?, ~enum: Store.enum, ~error) => {
    <div className={`status ${enum->Store.enumToA}`}>
      {title->Option.map(x => `${x}: `->React.string)->Option.or(React.null)}
      {`${error->Option.or(enum->Store.enumToPretty)}`->React.string}
    </div>
  }
}

module LoginArray = {
  module Field = FieldArray.Make(
    Login.Field,
    {
      type t = Login.Field.t
      let filter = FieldArray.filterIdentity
    },
  )

  let validate = (out: Field.output) => {
    if out->Array.length < 2 {
      Error("Must choose at least two logins")
    } else {
      Ok()
    }
    ->Promise.return
    ->Promise.delay(~ms=1000)
  }

  let contextDefault: Field.context = {
    validate,
    empty: _ => [],
    element: Login.contextValidate,
  }

  module Input = {
    @react.component
    let make = (~form: Fields.Form.t<Field.t, Field.actions<unit>>) => {
      let forms = form->Field.split

      let handleAdd = e => {
        e->ReactEvent.Mouse.preventDefault
        e->ReactEvent.Mouse.stopPropagation
        form.actions.add(None)
      }
      let handleRemove = (~index, e) => {
        e->ReactEvent.Mouse.preventDefault
        e->ReactEvent.Mouse.stopPropagation
        form.actions.remove(index)
      }
      let handleClear = e => {
        e->ReactEvent.Mouse.preventDefault
        e->ReactEvent.Mouse.stopPropagation
        form.actions.clear()
      }

      let handleReset = e => {
        e->ReactEvent.Mouse.preventDefault
        e->ReactEvent.Mouse.stopPropagation
        form.actions.reset()
      }

      <div>
        {forms
        ->Array.mapi((f, i) => {
          <div className="row" key=`${i->Int.toString}`>
            <div className="column">
              <Login.Input form=f />
            </div>
            <button className="button-clear" onClick={handleRemove(~index=i)}>
              {"✕"->React.string}
            </button>
          </div>
        })
        ->React.array}
        <div className="row">
          <Status
            title="Addresses" enum={form.field->Field.enum} error={form.field->Field.printError}
          />
        </div>
        <div className="row">
          <button className="button-clear column column-20" onClick={handleAdd}>
            {"Add One"->React.string}
          </button>
          <button className="button-clear float-left" onClick={handleReset}>
            {"Reset"->React.string}
          </button>
          <button className="button-clear float-left" onClick={handleClear}>
            {"Clear"->React.string}
          </button>
        </div>
      </div>
    }
  }
}

module Field = LoginArray.Field
module Form = UseField.Make(Field)

@react.component
let make = (~onSubmit) => {
  let form = Form.use(.
    ~context=LoginArray.contextDefault,
    ~init=None,
    ~validateInit=false,
  )

  Js.log2("form.field: ", form.field->Field.show)

  let handleSubmit = React.useMemo1( () => {
    (_) => form.field->Field.output->Option.map(onSubmit)->Option.void
  }, [form.field->Field.output])

    <div className="container">
        <form onSubmit={handleSubmit}>
            <h2>{"Addresses"->React.string}</h2>
            <LoginArray.Input form />
            <button type_="submit"> {"Sign In"->React.string} </button>
        </form>
    </div>
}

https://github.com/user-attachments/assets/8562726c-fc62-40d1-879d-3721bd7fbbf1

https://github.com/user-attachments/assets/a7f387c8-6c8c-4dbf-97cc-ad5bd115de76

Expected Behavior

AlexMouton commented 1 week ago

Hi Kate. It looks like that only happens when the child is a Product (Or the other way around like you said?) is that right? If i replace the product with a single string it looks like it works.

AlexMouton commented 1 week ago

I added your repro as an example, and added a first unit test here: https://github.com/AlexMouton/rescript-fields/pull/new/array-product

The unit test doesn't seem to capture the issue, if you want to look at it.

KateChanthakaew commented 5 days ago

@AlexMouton Thanks for your response. I'll take a look and let you know.