Skip to content

Commit

Permalink
Merge pull request #345 from Larocceau/fable-forms-safeV5
Browse files Browse the repository at this point in the history
Fable forms safe v5
  • Loading branch information
theprash authored Jan 3, 2024
2 parents 1af90d4 + e4cb38a commit 1dbc681
Show file tree
Hide file tree
Showing 2 changed files with 239 additions and 0 deletions.
237 changes: 237 additions & 0 deletions docs/recipes/client-server/fable.forms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
## Install dependencies

First off, you need to create a SAFE app, [install the relevant dependencies](https://mangelmaxime.github.io/Fable.Form/Fable.Form.Simple.Bulma/installation.html), and wire them up to be available for use in your F# Fable code.

1. Create a new SAFE app and restore local tools:
```sh
dotnet new SAFE
dotnet tool restore
```
1. Add bulma to your project:
follow [this recipe](../ui/add-bulma.md)

1. Install Fable.Form.Simple.Bulma using Paket:
```sh
dotnet paket add Fable.Form.Simple.Bulma -p Client
```

1. Install bulma and fable-form-simple-bulma npm packages:
```sh
npm add fable-form-simple-bulma
npm add bulma
```

## Register styles

1. Rename `src/Client/Index.css` to `Index.scss`

2. Update the import in `App.fs`

=== "Code"
```.fsharp title="App.fs"
...
importSideEffects "./index.scss"
...
```
=== "Diff"
```.diff title="App.fs"
...
- importSideEffects "./index.css"
+ importSideEffects "./index.scss"
...
```

3. Import bulma and fable-form-simple in `Index.scss`

``` .scss title="Index.scss"
@import "bulma/bulma.sass";
@import "fable-form-simple-bulma/index.scss";
...
```

2. Remove the Bulma stylesheet link from `./src/Client/index.html`, as it is no longer needed:

``` { .diff title="index.html" }
<link rel="icon" type="image/png" href="/favicon.png"/>
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/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.

1. Open the newly added namespaces:

``` { .fsharp title="Index.fs" }
open Fable.Form.Simple
open Fable.Form.Simple.Bulma
```


1. Create type `Values` to represent each input field on the form (a single textbox), and create a type `Form` which is an alias for `Form.View.Model<Values>`:


``` { .fsharp title="Index.fs" }
type Values = { Todo: string }
type Form = Form.View.Model<Values>
```

1. In the `Model` type definition, replace `Input: string` with `Form: Form`

=== "Code"
``` { .fsharp title="Index.fs" }
type Model = { Todos: Todo list; Form: Form }
```

=== "Diff"
``` { .diff title="Index.fs" }
-type Model = { Todos: Todo list; Input: string }
+type Model = { Todos: Todo list; Form: Form }
```

1. Update the `init` function to reflect the change in `Model`:

=== "Code"
``` { .fsharp title="Index.fs" }
let model = { Todos = []; Form = Form.View.idle { Todo = "" } }
```

=== "Diff"
``` { .diff title="Index.fs" }
-let model = { Todos = []; Input = "" }
+let model = { Todos = []; Form = Form.View.idle { Todo = "" } }
```

1. Change `Msg` discriminated union - replace the `SetInput` case with `FormChanged of Form`, and add string data to the `AddTodo` case:

=== "Code"
``` { .fsharp title="Index.fs" }
type Msg =
| GotTodos of Todo list
| FormChanged of Form
| AddTodo of string
| AddedTodo of Todo
```

=== "Diff"
``` { .diff title="Index.fs" }
type Msg =
| GotTodos of Todo list
- | SetInput of string
- | AddTodo
+ | FormChanged of Form
+ | AddTodo of string
| AddedTodo of Todo
```

1. Modify the `update` function to handle the changed `Msg` cases:

=== "Code"
``` { .fsharp title="Index.fs" }
let 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
```

=== "Diff"
``` { .diff title="Index.fs" }
let 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
```


1. Create `form`. This defines the logic of the form, and how it responds to interaction:

``` { .fsharp title="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
```

1. In the function `todoAction`, remove the existing form view. Then replace it using `Form.View.asHtml` to render the view:

=== "Code"
``` { .fsharp title="Index.fs" }
let private todoAction model dispatch =
Form.View.asHtml
{
Dispatch = dispatch
OnChange = FormChanged
Action = Action.SubmitOnly "Add"
Validation = Validation.ValidateOnBlur
}
form
model.Form
```
=== "Diff"
``` { .diff title="Index.fs" }
let 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](https://github.com/CompositionalIT/safe-fable-form/commit/6342ee8f4abcfeed6dd5066718e6845e6e2174d0) necessary to add a high priority checkbox are pretty small.
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ nav:
- Get data from the server: "recipes/client-server/messaging.md"
- Post data to the server: "recipes/client-server/messaging-post.md"
- Share code between the client and the server: "recipes/client-server/share-code.md"
- Add support for Fable.Forms: "recipes/client-server/fable.forms.md"

- FAQs:
- Moving from dev to prod: "faq/faq-build.md"
- Troubleshooting: "faq/faq-troubleshooting.md"
Expand Down

0 comments on commit 1dbc681

Please sign in to comment.