Skip to content

Commit 1dbc681

Browse files
authored
Merge pull request #345 from Larocceau/fable-forms-safeV5
Fable forms safe v5
2 parents 1af90d4 + e4cb38a commit 1dbc681

File tree

2 files changed

+239
-0
lines changed

2 files changed

+239
-0
lines changed
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
## Install dependencies
2+
3+
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.
4+
5+
1. Create a new SAFE app and restore local tools:
6+
```sh
7+
dotnet new SAFE
8+
dotnet tool restore
9+
```
10+
1. Add bulma to your project:
11+
follow [this recipe](../ui/add-bulma.md)
12+
13+
1. Install Fable.Form.Simple.Bulma using Paket:
14+
```sh
15+
dotnet paket add Fable.Form.Simple.Bulma -p Client
16+
```
17+
18+
1. Install bulma and fable-form-simple-bulma npm packages:
19+
```sh
20+
npm add fable-form-simple-bulma
21+
npm add bulma
22+
```
23+
24+
## Register styles
25+
26+
1. Rename `src/Client/Index.css` to `Index.scss`
27+
28+
2. Update the import in `App.fs`
29+
30+
=== "Code"
31+
```.fsharp title="App.fs"
32+
...
33+
importSideEffects "./index.scss"
34+
...
35+
```
36+
=== "Diff"
37+
```.diff title="App.fs"
38+
...
39+
- importSideEffects "./index.css"
40+
+ importSideEffects "./index.scss"
41+
...
42+
```
43+
44+
3. Import bulma and fable-form-simple in `Index.scss`
45+
46+
``` .scss title="Index.scss"
47+
@import "bulma/bulma.sass";
48+
@import "fable-form-simple-bulma/index.scss";
49+
...
50+
```
51+
52+
2. Remove the Bulma stylesheet link from `./src/Client/index.html`, as it is no longer needed:
53+
54+
``` { .diff title="index.html" }
55+
<link rel="icon" type="image/png" href="/favicon.png"/>
56+
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css">
57+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css">
58+
```
59+
60+
## Replace the existing form with a Fable.Form
61+
62+
With the above preparation done, you can use Fable.Form.Simple.Bulma in your `./src/Client/Index.fs` file.
63+
64+
1. Open the newly added namespaces:
65+
66+
``` { .fsharp title="Index.fs" }
67+
open Fable.Form.Simple
68+
open Fable.Form.Simple.Bulma
69+
```
70+
71+
72+
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>`:
73+
74+
75+
``` { .fsharp title="Index.fs" }
76+
type Values = { Todo: string }
77+
type Form = Form.View.Model<Values>
78+
```
79+
80+
1. In the `Model` type definition, replace `Input: string` with `Form: Form`
81+
82+
=== "Code"
83+
``` { .fsharp title="Index.fs" }
84+
type Model = { Todos: Todo list; Form: Form }
85+
```
86+
87+
=== "Diff"
88+
``` { .diff title="Index.fs" }
89+
-type Model = { Todos: Todo list; Input: string }
90+
+type Model = { Todos: Todo list; Form: Form }
91+
```
92+
93+
1. Update the `init` function to reflect the change in `Model`:
94+
95+
=== "Code"
96+
``` { .fsharp title="Index.fs" }
97+
let model = { Todos = []; Form = Form.View.idle { Todo = "" } }
98+
```
99+
100+
=== "Diff"
101+
``` { .diff title="Index.fs" }
102+
-let model = { Todos = []; Input = "" }
103+
+let model = { Todos = []; Form = Form.View.idle { Todo = "" } }
104+
```
105+
106+
1. Change `Msg` discriminated union - replace the `SetInput` case with `FormChanged of Form`, and add string data to the `AddTodo` case:
107+
108+
=== "Code"
109+
``` { .fsharp title="Index.fs" }
110+
type Msg =
111+
| GotTodos of Todo list
112+
| FormChanged of Form
113+
| AddTodo of string
114+
| AddedTodo of Todo
115+
```
116+
117+
=== "Diff"
118+
``` { .diff title="Index.fs" }
119+
type Msg =
120+
| GotTodos of Todo list
121+
- | SetInput of string
122+
- | AddTodo
123+
+ | FormChanged of Form
124+
+ | AddTodo of string
125+
| AddedTodo of Todo
126+
```
127+
128+
1. Modify the `update` function to handle the changed `Msg` cases:
129+
130+
=== "Code"
131+
``` { .fsharp title="Index.fs" }
132+
let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
133+
match msg with
134+
| GotTodos todos -> { model with Todos = todos }, Cmd.none
135+
| FormChanged form -> { model with Form = form }, Cmd.none
136+
| AddTodo todo ->
137+
let todo = Todo.create todo
138+
let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo
139+
model, cmd
140+
| AddedTodo todo ->
141+
let newModel =
142+
{ model with
143+
Todos = model.Todos @ [ todo ]
144+
Form =
145+
{ model.Form with
146+
State = Form.View.Success "Todo added"
147+
Values = { model.Form.Values with Todo = "" } } }
148+
newModel, Cmd.none
149+
```
150+
151+
=== "Diff"
152+
``` { .diff title="Index.fs" }
153+
let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
154+
match msg with
155+
| GotTodos todos -> { model with Todos = todos }, Cmd.none
156+
- | SetInput value -> { model with Input = value }, Cmd.none
157+
+ | FormChanged form -> { model with Form = form }, Cmd.none
158+
- | AddTodo ->
159+
- let todo = Todo.create model.Input
160+
- let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo
161+
- { model with Input = "" }, cmd
162+
+ | AddTodo todo ->
163+
+ let todo = Todo.create todo
164+
+ let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo
165+
+ model, cmd
166+
- | AddedTodo todo -> { model with Todos = model.Todos @ [ todo ] }, Cmd.none
167+
+ | AddedTodo todo ->
168+
+ let newModel =
169+
+ { model with
170+
+ Todos = model.Todos @ [ todo ]
171+
+ Form =
172+
+ { model.Form with
173+
+ State = Form.View.Success "Todo added"
174+
+ Values = { model.Form.Values with Todo = "" } } }
175+
+ newModel, Cmd.none
176+
```
177+
178+
179+
1. Create `form`. This defines the logic of the form, and how it responds to interaction:
180+
181+
``` { .fsharp title="Index.fs" }
182+
let form : Form.Form<Values, Msg, _> =
183+
let todoField =
184+
Form.textField
185+
{
186+
Parser = Ok
187+
Value = fun values -> values.Todo
188+
Update = fun newValue values -> { values with Todo = newValue }
189+
Error = fun _ -> None
190+
Attributes =
191+
{
192+
Label = "New todo"
193+
Placeholder = "What needs to be done?"
194+
HtmlAttributes = []
195+
}
196+
}
197+
198+
Form.succeed AddTodo
199+
|> Form.append todoField
200+
```
201+
202+
1. In the function `todoAction`, remove the existing form view. Then replace it using `Form.View.asHtml` to render the view:
203+
204+
=== "Code"
205+
``` { .fsharp title="Index.fs" }
206+
let private todoAction model dispatch =
207+
Form.View.asHtml
208+
{
209+
Dispatch = dispatch
210+
OnChange = FormChanged
211+
Action = Action.SubmitOnly "Add"
212+
Validation = Validation.ValidateOnBlur
213+
}
214+
form
215+
model.Form
216+
```
217+
=== "Diff"
218+
``` { .diff title="Index.fs" }
219+
let private todoAction model dispatch =
220+
- Html.div [
221+
- ...
222+
- ]
223+
+ Form.View.asHtml
224+
+ {
225+
+ Dispatch = dispatch
226+
+ OnChange = FormChanged
227+
+ Action = Action.SubmitOnly "Add"
228+
+ Validation = Validation.ValidateOnBlur
229+
+ }
230+
+ form
231+
+ model.Form
232+
```
233+
234+
235+
## Adding new functionality
236+
237+
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.

mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ nav:
117117
- Get data from the server: "recipes/client-server/messaging.md"
118118
- Post data to the server: "recipes/client-server/messaging-post.md"
119119
- Share code between the client and the server: "recipes/client-server/share-code.md"
120+
- Add support for Fable.Forms: "recipes/client-server/fable.forms.md"
121+
120122
- FAQs:
121123
- Moving from dev to prod: "faq/faq-build.md"
122124
- Troubleshooting: "faq/faq-troubleshooting.md"

0 commit comments

Comments
 (0)