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.csstoIndex.scss -
Update the import in
App.fsApp.fs... importSideEffects "./index.scss" ...App.fs... - importSideEffects "./index.css" + importSideEffects "./index.scss" ... -
Import bulma and fable-form-simple in
Index.scssIndex.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
Valuesto represent each input field on the form (a single textbox), and create a typeFormwhich is an alias forForm.View.Model<Values>:Index.fstype Values = { Todo: string } type Form = Form.View.Model<Values> -
In the
Modeltype definition, replaceInput: stringwithForm: FormIndex.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
initfunction 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
Msgdiscriminated union - replace theSetInputcase withFormChanged of Form, and add string data to theAddTodocase:Index.fstype Msg = | GotTodos of Todo list | FormChanged of Form | AddTodo of string | AddedTodo of TodoIndex.fstype Msg = | GotTodos of Todo list - | SetInput of string - | AddTodo + | FormChanged of Form + | AddTodo of string | AddedTodo of Todo -
Modify the
updatefunction to handle the changedMsgcases: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.noneIndex.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.asHtmlto 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.FormIndex.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.