This makes the code simpler because you don't need to worry about how to forward OnChange
, Value
, etc.
Create a Field
Fable.Form.Simple.Bulma
allows you to create your own custom fields.
For example, you can create a SignatureField
, AutoCompleteField
, ColorField
, etc.
Click here to see a SignatureField
in action.
Different types of fields
2 types of fields are available:
IStandardField
- most of the fields will implement this interface which provides a ready to use configuration for the renderingExamples:
- TextField
- CheckboxField
- SelectField
IGenericField
- low level API for when you don't need the standard configuration or want more control over the logicExamples:
- List
- Section
- Group
We are going to focus on IStandardField
in this example, but you can look at the source code to learn more about IGenericField
.
Step by step guide
For this guide, we are going to create a standard text input field.
Create a new file named
MyTextField.fs
.Create the namespace for hosting your custom field and open the required modules
I am using a namespace because it allows it to be used in multiple files and I like to use
[<RequireQualifiedAccess>]
on some modules to control/improve the user experience at some places.Feel free to experiment with different approaches and see what works best for you.
namespace MyForm.Fields open System open Fable.Form open Feliz open Feliz.Bulma open Fable.Form.Simple.Bulma module MyTextField =
Create an
Attributes
type to hold the configuration of the field, it needs to implementsField.IAttributes
.Here I am using a record, but you can use a class or interface if you prefer.
type Attributes = { FieldId: string Label: string Placeholder: string option } interface Field.IAttributes with member this.GetFieldId() = this.FieldId
Define the inner field type which will be used to create the field.
Here we say that the field will use our
Attributes
type to pass additional configuration and will use astring
as the value type to represent the field value in the view.type InnerField<'Values> = Field.Field<Attributes, string, 'Values>
Create the
form
function which will be used to create the field.What is important here is that we pass the
isEmpty
function to theBase.field
function so this where we determine how to detect if the field is empty.let form<'Values, 'Field, 'Output> : ((InnerField<'Values> -> 'Field) -> Base.FieldConfig<Attributes, string, 'Values, 'Output> -> Base.Form<'Values, 'Output, 'Field>) = Base.field String.IsNullOrEmpty
Actual implementation of the field, we inherit from
IStandardField
because we want to have access to theStandardRenderFieldConfig
which provides a ready to use configuration for the rendering.type Field<'Values>(innerField: InnerField<'Values>) = inherit IStandardField<'Values, string, Attributes>(innerField)
Interface with
IField
to be able to map the field values most of the time you will just copy this codeinterface IField<'Values> with member _.MapFieldValues(update: 'Values -> 'NewValues) : IField<'NewValues> = Field(Field.mapValues update innerField)
Override the
RenderField
method to provide the rendering logic for the field.For a standard field, you need to handle all the following:
Attributes: 'Attributes
- Any view related configuration you have in your attributesDisabled: bool
- If the field is disabledError: option<Error.Error>
- If the field has an errorIsReadOnly: bool
- If the field is read-onlyOnBlur: option<(unit -> unit)>
- Event to call when the field loses focusOnChange: 'Value -> unit
- Event to call when the field value changesShowError: bool
- If the field should show an errorValue: 'Value
- The value of the field
override _.RenderField(config: StandardRenderFieldConfig<string, Attributes>) = Bulma.input.text [ prop.onChange config.OnChange match config.OnBlur with | Some onBlur -> prop.onBlur (fun _ -> onBlur ()) | None -> () prop.disabled config.Disabled prop.readOnly config.IsReadOnly prop.value config.Value if config.ShowError && config.Error.IsSome then color.isDanger match config.Attributes.Placeholder with | Some placeholder -> prop.placeholder placeholder | None -> () ] |> Html.View.withLabelAndError config.Attributes.Label config.ShowError config.Error
We could stop here, but let's add some helper functions to make it easier to create the field.
Indeed, if your
Attributes
add 10 properties with a majority of them being optional, it can be a bit cumbersome to create the field.For Fable.Form, I decided to go with a pipeline builder style because it is easy to read and intellisense is well supported on all IDE.
type MyTextField = static member create(fieldId: string) : MyTextField.Attributes = { FieldId = fieldId Label = "" Placeholder = None } static member withLabel (label: string) (attributes: MyTextField.Attributes) = { attributes with Label = label } static member withPlaceholder (placeholder: string) (attributes: MyTextField.Attributes) = { attributes with Placeholder = Some placeholder }
In another field named
Form.fs
, create the field which will be used by the user to create an instance of the custom field.namespace MyForm open Fable.Form open MyForm.Fields open Fable.Form.Simple.Bulma [<RequireQualifiedAccess>] module Form = let myTextField (config: Base.FieldConfig<MyTextField.Attributes, string, 'Values, 'Output>) : Form<'Values, 'Output> = MyTextField.form (fun field -> MyTextField.Field field) config
Like with any other field, the user will use
Form.myTextField
to create an instance of the custom field.open Fable.Form.Simple open Fable.Form.Simple.Bulma open Fable.Form.Simple.Fields.Html open MyForm Form.myTextField { Parser = Ok Value = fun values -> values.Email Update = fun newValue values -> { values with Email = newValue } Error = fun _ -> None Attributes = MyTextField.create "email" |> MyTextField.withLabel "Email" |> MyTextField.withPlaceholder "some@email.com" }
You now know how to create a custom field for Fable.Form.Simple.Bulma
.
Like mentionned earlier, a field can be anything you want, so feel free to implement any field you need and share it with the community.