Skip to content

Custom field

Fable.Ripple.Form allows you to create your own custom fields.

A field concist in three things: attributes, a render function, and a constructor.

This page adds an autocomplete field: a text input whose suggestions come from a Var, drawn with a datalist.

Step by Step Guide

  1. Describe the field's attributes

    Any record works, as long as it gives the form the field's id.

    Suggestions is a function so that it can read a Var: whatever it reads becomes a dependency of the field. The create and with* builders keep the field consistent with the built-in ones.

    module AutoCompleteField =
    
        type Attributes =
            {
                FieldId: string
                Label: string
                Suggestions: unit -> string list
            }
    
            interface IAttributes with
                member this.GetFieldId() = this.FieldId
    
    type AutoCompleteField =
    
        static member create
            (fieldId: string)
            (suggestions: unit -> string list)
            : AutoCompleteField.Attributes
            =
            {
                FieldId = fieldId
                Label = ""
                Suggestions = suggestions
            }
    
        static member withLabel (label: string) (attributes: AutoCompleteField.Attributes) =
            { attributes with
                Label = label
            }
    
  2. Draw it

    The render function receives the field's Var, its attributes, and tracked reads of its state.

    Three things wire a control into the form:

    • View.controlAttributes for the id and the ARIA attributes.
    • config.OnBlur on the control's blur event.
    • PlainView.withLabelAndError for the label and the help text around it.
    module Fields =
    
        let autoComplete (config: FieldRenderConfig<string, AutoCompleteField.Attributes>) : DomItem =
            let listId = config.Id + "-list"
    
            Html.fragment
                [
                    Html.input
                        [
                            View.controlAttributes config
                            attr.className "rf-input"
                            attr.type' "text"
                            attr.list listId
                            attr.bindValue config.Value
                            on.blur (fun _ -> config.OnBlur())
                            attr.disabled config.Disabled
                            attr.readOnly config.ReadOnly
                        ]
                    Html.datalist
                        [
                            attr.id listId
                            Html.each
                                (fun () ->
                                    config.Attributes.Suggestions()
                                    |> List.filter (fun s -> s.Contains config.Value.Value)
                                    |> List.toArray
                                )
                                id
                                (fun s -> Html.option [ attr.value s ])
                        ]
                ]
            |> PlainView.withLabelAndError config.Attributes.Label config
    
  3. Give it a constructor

    Base.field takes what counts as empty, the render function and the config.

    The empty check is what makes a blank field "required".

    [<RequireQualifiedAccess>]
    module Form =
    
        let autoCompleteField
            (config: FieldConfig<AutoCompleteField.Attributes, string, 'Output>)
            : Form<'Output>
            =
            Base.field System.String.IsNullOrEmpty Fields.autoComplete config
    

Using it

The three pieces together, in a form:

open Fable.Ripple
open Fable.Ripple.Dom
open Fable.Ripple.Form
open Fable.Ripple.Form.Plain

module AutoCompleteField =

    type Attributes =
        {
            FieldId: string
            Label: string
            Suggestions: unit -> string list
        }

        interface IAttributes with
            member this.GetFieldId() = this.FieldId

type AutoCompleteField =

    static member create
        (fieldId: string)
        (suggestions: unit -> string list)
        : AutoCompleteField.Attributes
        =
        {
            FieldId = fieldId
            Label = ""
            Suggestions = suggestions
        }

    static member withLabel (label: string) (attributes: AutoCompleteField.Attributes) =
        { attributes with
            Label = label
        }

module Fields =

    let autoComplete (config: FieldRenderConfig<string, AutoCompleteField.Attributes>) : DomItem =
        let listId = config.Id + "-list"

        Html.fragment
            [
                Html.input
                    [
                        View.controlAttributes config
                        attr.className "rf-input"
                        attr.type' "text"
                        attr.list listId
                        attr.bindValue config.Value
                        on.blur (fun _ -> config.OnBlur())
                        attr.disabled config.Disabled
                        attr.readOnly config.ReadOnly
                    ]
                Html.datalist
                    [
                        attr.id listId
                        Html.each
                            (fun () ->
                                config.Attributes.Suggestions()
                                |> List.filter (fun s -> s.Contains config.Value.Value)
                                |> List.toArray
                            )
                            id
                            (fun s -> Html.option [ attr.value s ])
                    ]
            ]
        |> PlainView.withLabelAndError config.Attributes.Label config

[<RequireQualifiedAccess>]
module Form =

    let autoCompleteField
        (config: FieldConfig<AutoCompleteField.Attributes, string, 'Output>)
        : Form<'Output>
        =
        Base.field System.String.IsNullOrEmpty Fields.autoComplete config

let city = Var.create ""

let cities =
    Var.create
        [
            "Paris"
            "Prague"
            "Porto"
            "Lyon"
            "Lisbon"
        ]

let form =
    AutoCompleteField.create "city" (fun () -> cities.Value)
    |> AutoCompleteField.withLabel "City"
    |> Field.create
        city
        (fun value ->
            if value.Length < 2 then
                Error "At least 2 characters"
            else
                Ok value
        )
    |> Form.autoCompleteField

let state = Var.create View.Idle

Html.mount
    "app"
    (Form.View.asHtml
        {
            OnSubmit = fun city -> state.Value <- View.Success $"Going to %s{city}"
            State = state
            ErrorVisibility = View.errorVisibility ()
            Action = View.Action.SubmitOnly "Go"
            Validation = ValidateOnBlur
        }
        form)

Type P: the suggestions narrow as you type.

The field validates on blur, can be made optional, disabled or part of a list, like any other.

Beyond text

The Var holds whatever the control edits. A tag picker holds a string list and is empty when the list is; a date picker holds a DateTime option and is empty on None. The parser then receives that type instead of a string.

Edit this page