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
Describe the field's attributes
Any record works, as long as it gives the form the field's id.
Suggestionsis a function so that it can read aVar: whatever it reads becomes a dependency of the field. Thecreateandwith*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 }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.controlAttributesfor the id and the ARIA attributes.config.OnBluron the control'sblurevent.PlainView.withLabelAndErrorfor 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 configGive it a constructor
Base.fieldtakes 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.