MangelMaxime / Fable.Form

https://mangelmaxime.github.io/Fable.Form/
Other
54 stars 10 forks source link

"Finished" vs "Individual Field" Form Validation #56

Open danne931 opened 4 days ago

danne931 commented 4 days ago

Right now it seems this library uses "individual field" validation. The parser function of each field will be called to recognize the validity of each field on form submit button press (if Form.View.Validation.ValidateOnSubmit configured). If the parser of one of those fields returns Result.Error then the field will be indicated as an error and the Submit message will not be processed. Then you will start detecting for validity on each change.

I am looking to replicate this "individual field" validation functionality for an external error produced by a "Finished form". The onSubmit function we pass to Form.succeed receives the parsed Result.Ok values of each "individual field" validation. I then pass those parsed values to a function, attempting to create an object to send to the server. If that attempt returns Result.Error then instead of returning Msg.Submit in the onSubmit function I return Msg.ExternalError. This allows me to set an "external error" which I can use to display an error message near the submit button.

   | ExternalError errMsg ->
      match state with
      | State.FillingForm(model) ->
         let model = {
            model with
               State = Form.View.State.Error errMsg
         }

         State.FillingForm model, Cmd.none
      | _ -> state, Cmd.none
Form.View.Action.Custom(fun state _ ->
   React.fragment [
      // Render other external error if present
      match state with
      | Form.View.State.Error errMsg ->
         classyNode Html.div ["form-external-error-container"] [ renderError errMsg ]
      | _ -> ()

   let renderCalculation
      (targetAccount: Account)
      (destinations: UnvalidatedDistributionDestinationAccount list)
      (frequency: Frequency)
      =
      let ruleRes =
         PercentDistributionRule.create
            frequency
            {
               Name = targetAccount.Name
               AccountId = targetAccount.AccountId
               OrgId = targetAccount.OrgId
            }
            destinations

      match ruleRes with
      | Ok rule ->
         let hasCycle =
            accounts.Values
            |> Seq.toList
            |> List.choose (_.AutoTransferRule >> Option.map _.Info)
            |> CycleDetection.cycleDetected (
               AutomaticTransferRule.PercentDistribution rule
            )

         if hasCycle then
            Msg.ExternalError
               "You may not add a rule which would create cyclic transfers."
         else
            Msg.DisplayCalculation { Rule = rule; Target = targetAccount }
      | Error e -> Msg.ExternalError(string e)

   Form.succeed renderCalculation

Now if I make a correction to one of the fields in the form it would be ideal if those parsed values were passed to a "finished form" parser would attempt to create an object from the parsed values again & if successful, remove the external error. Instead, the error message remains until I click submit again.

finished-form-parser

You can see in this demonstration how the user might expect the error message to go away once they updated the select field. Instead, because I do not have access to the parsed values of a finished form until I click the submit button, I am unable to detect whether the error should still be displayed on form change.

The most manageable way I can think to fix this from my consumer code is to add Form.meta for each field, attempt to parse all the values and then call the function I use to determine if the composition of those parsed values is valid. Then the validation of this "finished" form would be done on each field change and the error would be indicated next to the field rather than as an "external error" near the submit button.

Do you think supporting this "finished form parser" functionality is desirable from the library implementation perspective or should I go with the consumer code fix I described or something else I might have missed?

Thanks for this excellent library by the way! It has made my forms more declarative and less prone to error.

MangelMaxime commented 3 days ago

Hello,

There is an example of handling external error on Fable.Doc documentation:

However, this is a per field demo but perhaps it can help.

Do you think supporting this "finished form parser" functionality is desirable from the library implementation perspective or should I go with the consumer code fix I described or something else I might have missed?

You can execute form manually, this is for example how tests are done. To execute a form, you will need to use Base.fill from open Fable.Form.

The difficulty I see is that you will get the result of the form but not the Form.View.Model<'Values>. So you will have to take inspiration from this function to map it to a Form.View.Model<'Values>.

https://github.com/MangelMaxime/Fable.Form/blob/703903671909b64b8e68546b6e7d33a80af2309c/packages/Fable.Form.Simple/Form.fs#L1189-L1270

It should be possible to split the linked function to made it usable by the end user.

Thanks for this excellent library by the way! It has made my forms more declarative and less prone to error.

Happy to hear that you like Fable.Form 😊