Skip to content

Commit 41acbc7

Browse files
committed
Direct copy
1 parent 6bbdfa5 commit 41acbc7

File tree

2 files changed

+316
-0
lines changed

2 files changed

+316
-0
lines changed
Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
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+
11+
1. Install Fable.Form.Simple.Bulma using Paket:
12+
```sh
13+
dotnet paket add --project src/Client/ Fable.Form.Simple.Bulma --version 3.0.0
14+
```
15+
16+
1. Install bulma and fable-form-simple-bulma npm packages:
17+
```sh
18+
npm add fable-form-simple-bulma
19+
20+
```
21+
22+
## Register styles
23+
24+
1. Create `./src/Client/style.scss` with the following contents:
25+
26+
=== "Code"
27+
``` { .scss title="style.scss" }
28+
@import "~bulma";
29+
@import "~fable-form-simple-bulma";
30+
```
31+
32+
=== "Diff"
33+
``` { .diff title="style.scss" }
34+
+@import "~bulma";
35+
+@import "~fable-form-simple-bulma";
36+
```
37+
38+
1. Update webpack config to include the new stylesheet:
39+
40+
a. Add a `cssEntry` property to the `CONFIG` object:
41+
42+
=== "Code"
43+
```{ .js title="webpack.config.js" }
44+
cssEntry: './src/Client/style.scss',
45+
```
46+
47+
=== "Diff"
48+
```{ .diff title="webpack.config.js" }
49+
+cssEntry: './src/Client/style.scss',
50+
```
51+
52+
b. Modify the `entry` property of the object returned from `module.exports` to include `cssEntry`:
53+
54+
=== "Code"
55+
```{ .js title="webpack.config.js" }
56+
entry: isProduction ? {
57+
app: [resolve(config.fsharpEntry), resolve(config.cssEntry)]
58+
} : {
59+
app: resolve(config.fsharpEntry),
60+
style: resolve(config.cssEntry)
61+
},
62+
```
63+
64+
=== "Diff"
65+
```{ .diff title="webpack.config.js" }
66+
- entry: {
67+
- app: resolve(config.fsharpEntry)
68+
- },
69+
+ entry: isProduction ? {
70+
+ app: [resolve(config.fsharpEntry), resolve(config.cssEntry)]
71+
+ } : {
72+
+ app: resolve(config.fsharpEntry),
73+
+ style: resolve(config.cssEntry)
74+
+ },
75+
```
76+
77+
1. Remove the Bulma stylesheet link from `./src/Client/index.html`, as it is no longer needed:
78+
79+
``` { .diff title="index.html (diff)" }
80+
<link rel="icon" type="image/png" href="/favicon.png"/>
81+
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css">
82+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css">
83+
```
84+
85+
## Replace the existing form with a Fable.Form
86+
87+
With the above preparation done, you can use Fable.Form.Simple.Bulma in your `./src/Client/Index.fs` file.
88+
89+
1. Open the newly added namespaces:
90+
91+
=== "Code"
92+
``` { .fsharp title="Index.fs" }
93+
open Fable.Form.Simple
94+
open Fable.Form.Simple.Bulma
95+
```
96+
97+
=== "Diff"
98+
``` { .diff title="Index.fs" }
99+
+open Fable.Form.Simple
100+
+open Fable.Form.Simple.Bulma
101+
```
102+
103+
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>`:
104+
105+
=== "Code"
106+
``` { .fsharp title="Index.fs" }
107+
type Values = { Todo: string }
108+
type Form = Form.View.Model<Values>
109+
```
110+
111+
=== "Diff"
112+
``` { .diff title="Index.fs" }
113+
+type Values = { Todo: string }
114+
+type Form = Form.View.Model<Values>
115+
```
116+
117+
1. In the `Model` type definition, replace `Input: string` with `Form: Form`
118+
119+
=== "Code"
120+
``` { .fsharp title="Index.fs" }
121+
type Model = { Todos: Todo list; Form: Form }
122+
```
123+
124+
=== "Diff"
125+
``` { .diff title="Index.fs" }
126+
-type Model = { Todos: Todo list; Input: string }
127+
+type Model = { Todos: Todo list; Form: Form }
128+
```
129+
130+
1. Update the `init` function to reflect the change in `Model`:
131+
132+
=== "Code"
133+
``` { .fsharp title="Index.fs" }
134+
let model = { Todos = []; Form = Form.View.idle { Todo = "" } }
135+
```
136+
137+
=== "Diff"
138+
``` { .diff title="Index.fs" }
139+
-let model = { Todos = []; Input = "" }
140+
+let model = { Todos = []; Form = Form.View.idle { Todo = "" } }
141+
```
142+
143+
1. Change `Msg` discriminated union - replace the `SetInput` case with `FormChanged of Form`, and add string data to the `AddTodo` case:
144+
145+
=== "Code"
146+
``` { .fsharp title="Index.fs" }
147+
type Msg =
148+
| GotTodos of Todo list
149+
| FormChanged of Form
150+
| AddTodo of string
151+
| AddedTodo of Todo
152+
```
153+
154+
=== "Diff"
155+
``` { .diff title="Index.fs" }
156+
type Msg =
157+
| GotTodos of Todo list
158+
- | SetInput of string
159+
- | AddTodo
160+
+ | FormChanged of Form
161+
+ | AddTodo of string
162+
| AddedTodo of Todo
163+
```
164+
165+
1. Modify the `update` function to handle the changed `Msg` cases:
166+
167+
=== "Code"
168+
``` { .fsharp title="Index.fs" }
169+
let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
170+
match msg with
171+
| GotTodos todos -> { model with Todos = todos }, Cmd.none
172+
| FormChanged form -> { model with Form = form }, Cmd.none
173+
| AddTodo todo ->
174+
let todo = Todo.create todo
175+
let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo
176+
model, cmd
177+
| AddedTodo todo ->
178+
let newModel =
179+
{ model with
180+
Todos = model.Todos @ [ todo ]
181+
Form =
182+
{ model.Form with
183+
State = Form.View.Success "Todo added"
184+
Values = { model.Form.Values with Todo = "" } } }
185+
newModel, Cmd.none
186+
```
187+
188+
=== "Diff"
189+
``` { .diff title="Index.fs" }
190+
let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
191+
match msg with
192+
| GotTodos todos -> { model with Todos = todos }, Cmd.none
193+
- | SetInput value -> { model with Input = value }, Cmd.none
194+
+ | FormChanged form -> { model with Form = form }, Cmd.none
195+
- | AddTodo ->
196+
- let todo = Todo.create model.Input
197+
- let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo
198+
- { model with Input = "" }, cmd
199+
+ | AddTodo todo ->
200+
+ let todo = Todo.create todo
201+
+ let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo
202+
+ model, cmd
203+
- | AddedTodo todo -> { model with Todos = model.Todos @ [ todo ] }, Cmd.none
204+
+ | AddedTodo todo ->
205+
+ let newModel =
206+
+ { model with
207+
+ Todos = model.Todos @ [ todo ]
208+
+ Form =
209+
+ { model.Form with
210+
+ State = Form.View.Success "Todo added"
211+
+ Values = { model.Form.Values with Todo = "" } } }
212+
+ newModel, Cmd.none
213+
```
214+
215+
216+
1. Create `form`. This defines the logic of the form, and how it responds to interaction:
217+
218+
=== "Code"
219+
``` { .fsharp title="Index.fs" }
220+
let form : Form.Form<Values, Msg, _> =
221+
let todoField =
222+
Form.textField
223+
{
224+
Parser = Ok
225+
Value = fun values -> values.Todo
226+
Update = fun newValue values -> { values with Todo = newValue }
227+
Error = fun _ -> None
228+
Attributes =
229+
{
230+
Label = "New todo"
231+
Placeholder = "What needs to be done?"
232+
HtmlAttributes = []
233+
}
234+
}
235+
236+
Form.succeed AddTodo
237+
|> Form.append todoField
238+
```
239+
240+
=== "Diff"
241+
``` { .diff title="Index.fs" }
242+
+let form : Form.Form<Values, Msg, _> =
243+
+ let todoField =
244+
+ Form.textField
245+
+ {
246+
+ Parser = Ok
247+
+ Value = fun values -> values.Todo
248+
+ Update = fun newValue values -> { values with Todo = newValue }
249+
+ Error = fun _ -> None
250+
+ Attributes =
251+
+ {
252+
+ Label = "New todo"
253+
+ Placeholder = "What needs to be done?"
254+
+ HtmlAttributes = []
255+
+ }
256+
+ }
257+
+
258+
+ Form.succeed AddTodo
259+
+ |> Form.append todoField
260+
```
261+
262+
1. In the function `containerBox`, remove the existing form view. Then replace it using `Form.View.asHtml` to render the view:
263+
264+
=== "Code"
265+
``` { .fsharp title="Index.fs" }
266+
let containerBox (model: Model) (dispatch: Msg -> unit) =
267+
Bulma.box [
268+
Bulma.content [
269+
Html.ol [
270+
for todo in model.Todos do
271+
Html.li [ prop.text todo.Description ]
272+
]
273+
]
274+
Form.View.asHtml
275+
{
276+
Dispatch = dispatch
277+
OnChange = FormChanged
278+
Action = Form.View.Action.SubmitOnly "Add"
279+
Validation = Form.View.Validation.ValidateOnBlur
280+
}
281+
form
282+
model.Form
283+
]
284+
```
285+
286+
=== "Diff"
287+
``` { .diff title="Index.fs" }
288+
let containerBox (model: Model) (dispatch: Msg -> unit) =
289+
Bulma.box [
290+
Bulma.content [
291+
Html.ol [
292+
for todo in model.Todos do
293+
Html.li [ prop.text todo.Description ]
294+
]
295+
]
296+
- Bulma.field.div [
297+
- ... removed for brevity ...
298+
- ]
299+
+ Form.View.asHtml
300+
+ {
301+
+ Dispatch = dispatch
302+
+ OnChange = FormChanged
303+
+ Action = Form.View.Action.SubmitOnly "Add"
304+
+ Validation = Form.View.Validation.ValidateOnBlur
305+
+ }
306+
+ form
307+
+ model.Form
308+
]
309+
```
310+
311+
312+
## Adding new functionality
313+
314+
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
@@ -116,6 +116,8 @@ nav:
116116
- Get data from the server: "recipes/client-server/messaging.md"
117117
- Post data to the server: "recipes/client-server/messaging-post.md"
118118
- Share code between the client and the server: "recipes/client-server/share-code.md"
119+
- Add support for Fable.Forms: "recipes/client-server/fable.forms.md"
120+
119121
- FAQs:
120122
- Moving from dev to prod: "faq/faq-build.md"
121123
- Troubleshooting: "faq/faq-troubleshooting.md"

0 commit comments

Comments
 (0)