Fable.Form - created by @MangelMaxime - is a useful library to create forms with validation; but it only comes with bindings for the Bulma UI library. To use a different UI library, such as daisyUI, then we must implement our own bindings. So let's write some bindings linking up Fable.Form with daisyUI!
Set up
In order to keep this blog post a reasonable length, I've linked to some recipes that will prepare us to implement our own custom bindings.
Once we have followed these steps and SAFE is set up with UI libraries, we can use them with Fable.Form!
Create DaisyUI bindings
First of all we need to pull in Fable.Form
and Fable.Form.Simple
from NuGet.
dotnet paket add Fable.Form --keep-major
dotnet paket add Fable.Form.Simple --keep-major
We'll base our implementation on this template: Fable.Form.Simple.MyImplementation
, so we copy that code and add it to a new file in our project. Since we are implementing the bindings using DaisyUI, let's name it Fable.Form.Simple.DaisyUI
. To get started, we should open relevant the namespaces as directed by the comments in the file.
open Feliz
open Feliz.DaisyUI
The minimum implementation for us consists of defining how a text input is rendered and how a form is rendered. The form definition is used as a container for all the input fields and includes the submit button.
Structurally, these definitions are loosely based on the Bulma version of the bindings.
In the form
definition, note the line yield! fields
- that's where the fields will be injected.
let form
({
Dispatch = dispatch
OnSubmit = onSubmit
State = state
Action = action
Fields = fields
}: FormConfig<'Msg>)
=
Html.form [
prop.onSubmit (fun ev ->
ev.stopPropagation ()
ev.preventDefault ()
onSubmit |> Option.map dispatch |> Option.defaultWith ignore)
prop.classes [ "grid"; "grid-cols-1"; "gap-6"; "w-full" ]
prop.children [
yield! fields
match action with
| Action.SubmitOnly submitLabel ->
Daisy.button.button [
button.accent
button.md
prop.type'.submit
prop.text submitLabel
if state = Loading then button.loading
]
| Action.Custom func -> func state dispatch
]
]
let textField
({
Dispatch = dispatch
OnChange = onChange
OnBlur = onBlur
Disabled = disabled
Value = value
Error = error
ShowError = showError
Attributes = attributes
}: TextFieldConfig<'Msg, IReactProperty>)
=
let errorMessage =
function
| Error.RequiredFieldIsEmpty -> $"Required!"
| Error.ValidationFailed "" -> $"{attributes.Label} invalid"
| Error.ValidationFailed msg -> msg
| Error.External msg -> $"Error. {msg}"
let errorElem =
error
|> Option.map (errorMessage >> Html.text)
|> Option.defaultValue Html.none
Daisy.formControl [
prop.children [
Daisy.label [ prop.text attributes.Label ]
Daisy.input [
prop.ariaAutocomplete.none
input.bordered
input.md
prop.placeholder attributes.Placeholder
prop.value value
prop.disabled disabled
prop.onChange (onChange >> dispatch)
match onBlur with
| Some onBlur -> prop.onBlur (fun _ -> dispatch onBlur)
| None -> ()
]
if showError
then Html.p [ prop.classes [ "text-rose-400"; "px-1"; "py-2" ]; prop.children [ errorElem ] ]
else Html.none
]
]
We'll also switch the bindings which are not implemented to use a function so that they only fail if they are used.
let notYetImplemented _ = failwith "Not yet implemented"
Finally we hook up our newly defined functions to the appropriate fields in htmlViewConfig
.
let htmlViewConfig<'Msg> : CustomConfig<'Msg, IReactProperty> =
{
Form = form
TextField = textField
PasswordField = notYetImplemented
EmailField = notYetImplemented
TextAreaField = notYetImplemented
ColorField = notYetImplemented
DateField = notYetImplemented
DateTimeLocalField = notYetImplemented
NumberField = notYetImplemented
SearchField = notYetImplemented
TelField = notYetImplemented
TimeField = notYetImplemented
CheckboxField = notYetImplemented
RadioField = notYetImplemented
SelectField = notYetImplemented
Group = notYetImplemented
Section = notYetImplemented
FormList = notYetImplemented
FormListItem = notYetImplemented
}
Make use of DaisyUI form bindings
Using the documentation as a guide we can create the form's front-end, implementing it to replace the existing form.
A summary of the changes needed in Index.fs
:
- Update the model to include the form values, wrapped in the Fable.Form type
- Update the
Msg
cases to account for form events - Update
init
andupdate
to work with the changed model and message - Define a form, in our case just a field to write a todo and a button to submit
- Display the form in the view
Results
Now we can see the form which has been created using our new bindings.
The form tells us that the textbox is required, if we submit with it blank.
When we do add some text, it is able to submit as normal.
Conclusion
It is true that there is a bit of work to do to get started with custom bindings for Fable.Form
. However - as illustrated by this post - after the initial set up is complete, the bindings are very straightforward to implement.
I'd like to say a huge thank you to @MangelMaxime for providing the FSharp community with this excellent library!