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
-
Add bulma to your project: follow this recipe
-
Install Fable.Form.Simple.Bulma using Paket:
dotnet paket add Fable.Form.Simple.Bulma -p Client
-
Install bulma and fable-form-simple-bulma npm packages:
npm add fable-form-simple-bulma npm add bulma
Register styles
-
Rename
src/Client/Index.css
toIndex.scss
-
Update the import in
App.fs
App.fs... importSideEffects "./index.scss" ...
App.fs... - importSideEffects "./index.css" + importSideEffects "./index.scss" ...
-
Import bulma and fable-form-simple in
Index.scss
Index.scss@import "bulma/bulma.sass"; @import "fable-form-simple-bulma/index.scss"; ...
-
Remove the Bulma stylesheet link from
./src/Client/index.html
, as it is no longer needed:index.html<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
-
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>
-
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
-
In the function
todoAction
, remove the existing form view. Then replace it usingForm.View.asHtml
to render the view:Index.fslet private todoAction model dispatch = Form.View.asHtml { Dispatch = dispatch OnChange = FormChanged Action = Action.SubmitOnly "Add" Validation = Validation.ValidateOnBlur } form model.Form
Index.fslet private todoAction model dispatch = - Html.div [ - ... - ] + Form.View.asHtml + { + Dispatch = dispatch + OnChange = FormChanged + Action = Action.SubmitOnly "Add" + Validation = 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.