Fable.FormValidation
Fable.FormValidation copied to clipboard
A Fable React hook library for validating UI inputs and displaying error messages
Fable.FormValidation
A Fable React hook library for validating UI inputs and displaying error messages
Installation
Get it from NuGet!
Sample Form
Call the useValidation() hook
open Fable.FormValidation
[<ReactComponent>]
let Page() =
let model, setModel = React.useState init
let rulesFor, validate, resetValidation, errors = useValidation()
let save() =
if validate() then
resetValidation()
Toastify.success "Form is valid!"
let cancel() =
resetValidation()
setModel init
Common Validation Rules
-
Rule.Required
-> Validates a textbox text value attribute at validate time. -
Rule.MinLen n
-> Validates a textbox text value minimum length at validate time. -
Rule.MaxLen n
-> Validates a textbox text value maximum length at validate time. -
Rule.Regex (pattern, desc)
-> Validates a textbox text value with a regex pattern at validate time.
Example:
input [
Ref (rulesFor "First Name" [Required; MaxLen 50])
Class B.``form-control``
Value model.FName
OnChange (fun e -> setModel { model with FName = e.Value })
]
input [
Ref (rulesFor "User Email" [
Required
Regex(@"^\S+@\S+$", "Email")
])
Class B.``form-control``
Value model.Email
OnChange (fun e -> setModel { model with Email = e.Value })
]
Custom Validation Rules
-
Rule.CustomRule (fn)
-> Takes any function that returns aResult<unit,string>
. These rules will directly validate against the current model values and will be calculated during render.
Example:
This example features the Feliz date input with a custom rule:
Html.input [
prop.ref (rulesFor "Birth Date" [
Required
CustomRule (
match model.BirthDate with
| Some bd ->
if bd <= DateTime.Now
then Ok()
else (Error "{0} cannot be a future date")
| None -> Ok()
)
])
prop.className "date"
prop.type'.date
if model.BirthDate.IsSome
then prop.value model.BirthDate.Value
prop.onChange (fun value ->
let success, bd = DateTime.TryParse value
if success then setModel { model with BirthDate = Some bd }
)
]
Validating Radio and Checkbox Groups
Validation rules can also be applied to non-input elements! To validate a radio button group, you can apply validation to the parent container div:
let fieldName = "Favorite .NET Language"
let rdoGroup = "FavLangGrp"
requiredField fieldName (
div [
Class $"{B.``p-2``} {B.``form-control``}"
Style [Width 200]
Ref (rulesFor fieldName [
CustomRule (
match model.FavoriteLang with
| None -> Error "{0} is required"
| Some lang when lang <> FSharp -> Error "{0} is invalid"
| Some lang -> Ok()
)
])
] [
label [Class B.``mr-4``] [
input [
Type "radio"
Checked (model.FavoriteLang = Some FSharp)
Class B.``mr-1``
RadioGroup rdoGroup
Value "Yes"
OnChange (fun e -> setModel { model with FavoriteLang = Some FSharp })
]
str "F#"
]
label [Class B.``mr-4``] [
input [
Type "radio"
Checked (model.FavoriteLang = Some CSharp)
Class B.``mr-1``
RadioGroup rdoGroup
Value "No"
OnChange (fun e -> setModel { model with FavoriteLang = Some CSharp })
]
str "C#"
]
label [Class B.``mr-4``] [
input [
Type "radio"
Checked (model.FavoriteLang = Some VB)
Class B.``mr-1``
RadioGroup rdoGroup
Value "No"
OnChange (fun e -> setModel { model with FavoriteLang = Some VB })
]
str "VB"
]
]
)
Creating Custom Rule Libraries
It is very easy to extract your custom rules into a reusable library.
When creating your custom rules, you can templatize the field name with {0}
:
module CustomRules =
let mustBeTrue b = CustomRule (if b then Ok() else Error "{0} must be true")
Built-in Rule Functions
You can also use the existing rule functions in the RuleFn
module in your custom rules.
In fact, some rules, like gt
, gte
, lt
and lte
exist only as as functions.
This is because the common rules like Required
and MinLen
all expect a textbox text value, so we would lose out of F# type safety if we tried coerce those text values into numeric values at validate time.
Fortunately, CustomRule
allows to use these in a type-safe manner:
input [
Ref (rulesFor "Amount" [
CustomRule (model.Amount |> RuleFn.gte 0)
])
Class B.``form-control``
Value model.Amount
OnChange (fun e -> setModel { model with Amount = e.target?value })
]
Add an Error Summary
Fable.FormValidation.errorSummary errors
Create an "error" style
When a form input is invalid, the "error" class will be appended. You must add styling for invalid inputs in your .css file:
.error {
border: 1px solid red;
background: rgb(255, 232, 235);
}
Edge Cases
You may encounter a situation where your validated input field is sometimes hidden and then redisplayed (as in the case of a collapsible panel).
This can cause an issue where React regenerates a different hashcode for the input each time it is made visible.
To resolve this problem, you can add a override "vkey" (validation key) that will be used instead, which will allow Fable.FormValidation to consistently track the input.
input [
Ref (rulesFor "First Name" [Required; MinLen 2; MaxLen 50])
Data ("vkey", $"username-{model.Id}") // This value must uniquely identify this field
Class B.``form-control``
Value model.FName
OnChange (fun e -> setModel { model with FName = e.Value })
]
Overriding the getValue
and setStyle
functions
You may now, optionally, pass in custom getValue
and setStyle
implementations. Use these if you require custom logic to pull your value from a control or want to apply a different error style.
let rulesFor, validate, resetValidation, errors =
FormValidation.useValidation(
setStyle = setStyleCustom,
getValue = getValueCustom
)
Please note that FormValidation.useValidation
is a new static method with optional parameters. The original useValidation
function still exists for backwards compatibility.
Here are the default implementations:
let setStyleDefault (el: Element) (fieldErrors: ValidationErrors) =
// Apply or remove error highlighting to fields
if fieldErrors.Length > 0 then
el.classList.add("error")
el.setAttribute("title", fieldErrors.[0])
else
el.classList.remove("error")
el.removeAttribute("title")
let getValueDefault (el: Element) : string =
// NOTE: assumes you have opened "Fable.Core.JsInterop"
el?value
setStyleDefault
adds or removes the "error" css class and a title/tooltip of the first error.
getValueDefault
pulls the element's value
property for validation.
Targetting specific controls
You can target specific input controls in your custom handlers by checking for a special class name. For example, if you wanted to override the error highlighting to use a different class for a subset of controls, you could add mark them with a custom class name, "custom-validation":
input [
Ref (rulesFor "Project Name" [ Required ])
Class "form-control custom-error" projName"
Value model.Project.Name
OnChange (fun e -> dispatch (SetProject { model.Project with Name = e.Value }))
]
Then, you can check for that class in your setStyle
handler:
let setStyle (el: Element) (fieldErrors: ValidationErrors) =
let errorClass =
if el.className.Contains "custom-validation"
then "custom-error"
else "error"
// Apply or remove error highlighting to fields
if fieldErrors.Length > 0 then
el.classList.add(errorClass)
el.setAttribute("title", fieldErrors.[0])
else
el.classList.remove(errorClass)
el.removeAttribute("title")
Sample App
Click here to see the full sample app using the Fable 3 template, HookRouter and Toastify.