United Kingdom: +44 (0)208 088 8978

DaisyUI bindings for Fable.Form.Simple

Getting started with custom Fable.Form bindings

We're hiring Software Developers

Click here to find out more

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.

  1. Set up SAFE
  2. Add Tailwind
  3. Add DaisyUI
  4. Remove Bulma

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:

  1. Update the model to include the form values, wrapped in the Fable.Form type
  2. Update the Msg cases to account for form events
  3. Update init and update to work with the changed model and message
  4. Define a form, in our case just a field to write a todo and a button to submit
  5. Display the form in the view

Results

Now we can see the form which has been created using our new bindings.

Screenshot of SAFE template default todo site reimplemented using Fable.Form with daisyUI bindings

The form tells us that the textbox is required, if we submit with it blank.

Screenshot of SAFE template default todo site reimplemented using Fable.Form with daisyUI bindings, displaying a validation error because a required field is empty

When we do add some text, it is able to submit as normal.

Screenshot of SAFE template default todo site reimplemented using Fable.Form with daisyUI bindings, displaying an updated todo list, post-submit

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!