Add support for Fable.Forms
Install dependencies
First off, you need to create a SAFE app, install the relevant dependencies, and wire them up to be available for use in your F# Fable code.
-
Create a new SAFE app and restore local tools:
dotnet new SAFE dotnet tool restore
-
Install Fable.Form.Simple.Bulma using Paket:
dotnet paket add --project src/Client/ Fable.Form.Simple.Bulma --version 3.0.0
-
Install bulma and fable-form-simple-bulma npm packages:
npm add fable-form-simple-bulma npm add bulma@0.9.0
Register styles
-
Create
./src/Client/style.scss
with the following contents:style.scss@import "~bulma"; @import "~fable-form-simple-bulma";
style.scss+@import "~bulma"; +@import "~fable-form-simple-bulma";
-
Update webpack config to include the new stylesheet:
a. Add a
cssEntry
property to theCONFIG
object:webpack.config.jscssEntry: './src/Client/style.scss',
webpack.config.js+cssEntry: './src/Client/style.scss',
b. Modify the
entry
property of the object returned frommodule.exports
to includecssEntry
:webpack.config.jsentry: isProduction ? { app: [resolve(config.fsharpEntry), resolve(config.cssEntry)] } : { app: resolve(config.fsharpEntry), style: resolve(config.cssEntry) },
webpack.config.js- entry: { - app: resolve(config.fsharpEntry) - }, + entry: isProduction ? { + app: [resolve(config.fsharpEntry), resolve(config.cssEntry)] + } : { + app: resolve(config.fsharpEntry), + style: resolve(config.cssEntry) + },
-
Remove the Bulma stylesheet link from
./src/Client/index.html
, as it is no longer needed:index.html (diff)<link rel="icon" type="image/png" href="/favicon.png"/> - <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css">
Replace the existing form with a Fable.Form
With the above preparation done, you can use Fable.Form.Simple.Bulma in your ./src/Client/Index.fs
file.
-
Open the newly added namespaces:
Index.fsopen Fable.Form.Simple open Fable.Form.Simple.Bulma
Index.fs+open Fable.Form.Simple +open Fable.Form.Simple.Bulma
-
Create type
Values
to represent each input field on the form (a single textbox), and create a typeForm
which is an alias forForm.View.Model<Values>
:Index.fstype Values = { Todo: string } type Form = Form.View.Model<Values>
Index.fs+type Values = { Todo: string } +type Form = Form.View.Model<Values>
-
In the
Model
type definition, replaceInput: string
withForm: Form
Index.fstype Model = { Todos: Todo list; Form: Form }
Index.fs-type Model = { Todos: Todo list; Input: string } +type Model = { Todos: Todo list; Form: Form }
-
Update the
init
function to reflect the change inModel
:Index.fslet model = { Todos = []; Form = Form.View.idle { Todo = "" } }
Index.fs-let model = { Todos = []; Input = "" } +let model = { Todos = []; Form = Form.View.idle { Todo = "" } }
-
Change
Msg
discriminated union - replace theSetInput
case withFormChanged of Form
, and add string data to theAddTodo
case:Index.fstype Msg = | GotTodos of Todo list | FormChanged of Form | AddTodo of string | AddedTodo of Todo
Index.fstype Msg = | GotTodos of Todo list - | SetInput of string - | AddTodo + | FormChanged of Form + | AddTodo of string | AddedTodo of Todo
-
Modify the
update
function to handle the changedMsg
cases:Index.fslet update (msg: Msg) (model: Model) : Model * Cmd<Msg> = match msg with | GotTodos todos -> { model with Todos = todos }, Cmd.none | FormChanged form -> { model with Form = form }, Cmd.none | AddTodo todo -> let todo = Todo.create todo let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo model, cmd | AddedTodo todo -> let newModel = { model with Todos = model.Todos @ [ todo ] Form = { model.Form with State = Form.View.Success "Todo added" Values = { model.Form.Values with Todo = "" } } } newModel, Cmd.none
Index.fslet update (msg: Msg) (model: Model) : Model * Cmd<Msg> = match msg with | GotTodos todos -> { model with Todos = todos }, Cmd.none - | SetInput value -> { model with Input = value }, Cmd.none + | FormChanged form -> { model with Form = form }, Cmd.none - | AddTodo -> - let todo = Todo.create model.Input - let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo - { model with Input = "" }, cmd + | AddTodo todo -> + let todo = Todo.create todo + let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo + model, cmd - | AddedTodo todo -> { model with Todos = model.Todos @ [ todo ] }, Cmd.none + | AddedTodo todo -> + let newModel = + { model with + Todos = model.Todos @ [ todo ] + Form = + { model.Form with + State = Form.View.Success "Todo added" + Values = { model.Form.Values with Todo = "" } } } + newModel, Cmd.none
-
Create
form
. This defines the logic of the form, and how it responds to interaction:Index.fslet form : Form.Form<Values, Msg, _> = let todoField = Form.textField { Parser = Ok Value = fun values -> values.Todo Update = fun newValue values -> { values with Todo = newValue } Error = fun _ -> None Attributes = { Label = "New todo" Placeholder = "What needs to be done?" HtmlAttributes = [] } } Form.succeed AddTodo |> Form.append todoField
Index.fs+let form : Form.Form<Values, Msg, _> = + let todoField = + Form.textField + { + Parser = Ok + Value = fun values -> values.Todo + Update = fun newValue values -> { values with Todo = newValue } + Error = fun _ -> None + Attributes = + { + Label = "New todo" + Placeholder = "What needs to be done?" + HtmlAttributes = [] + } + } + + Form.succeed AddTodo + |> Form.append todoField
-
In the function
containerBox
, remove the existing form view. Then replace it usingForm.View.asHtml
to render the view:Index.fslet containerBox (model: Model) (dispatch: Msg -> unit) = Bulma.box [ Bulma.content [ Html.ol [ for todo in model.Todos do Html.li [ prop.text todo.Description ] ] ] Form.View.asHtml { Dispatch = dispatch OnChange = FormChanged Action = Form.View.Action.SubmitOnly "Add" Validation = Form.View.Validation.ValidateOnBlur } form model.Form ]
Index.fslet containerBox (model: Model) (dispatch: Msg -> unit) = Bulma.box [ Bulma.content [ Html.ol [ for todo in model.Todos do Html.li [ prop.text todo.Description ] ] ] - Bulma.field.div [ - ... removed for brevity ... - ] + Form.View.asHtml + { + Dispatch = dispatch + OnChange = FormChanged + Action = Form.View.Action.SubmitOnly "Add" + Validation = Form.View.Validation.ValidateOnBlur + } + form + model.Form ]
Adding new functionality
With the basic structure in place, it's easy to add functionality to the form. For example, the changes necessary to add a high priority checkbox are pretty small.