Skip to content

Commit 5d32c7b

Browse files
committed
Copy v4 of routing with UseElmish
1 parent a690852 commit 5d32c7b

2 files changed

Lines changed: 283 additions & 0 deletions

File tree

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
# How do I create multi-page applications with routing and the useElmish hook?
2+
3+
*Written for SAFE template version 4.2.0*
4+
5+
[UseElmish](https://zaid-ajaj.github.io/Feliz/#/Hooks/UseElmish) is a powerful package that allows you to write standalone components using Elmish. A component built around the `UseElmish` hook has its own view, state and update function.
6+
7+
In this recipe we add routing to a safe app, and implement the todo list page using the `UseElmish` hook.
8+
9+
## 1. Installing dependencies
10+
11+
!!! warning "Pin Fable.Core to V3"
12+
At the time of writing, the published version of the SAFE template does not have the version of `Fable.Core` pinned; this can create problems when installing dependencies.
13+
14+
If you are using version v.4.2.0 of the template, pin `Fable.Core` to version 3 in `paket.depedencies` at the root of the project
15+
16+
```.diff title="paket.dependencies"
17+
...
18+
-nuget Fable.Core
19+
+nuget Fable.Core ~> 3
20+
...
21+
```
22+
23+
24+
Install Feliz.Router in the Client project
25+
26+
```bash
27+
dotnet paket add Feliz.Router -p Client -V 3.8
28+
```
29+
30+
!!! Warning "Feliz.Router versions"
31+
At the time of writing, the current version of the SAFE template (4.2.0) does not work well with the latest version of Feliz.Router (4.0).
32+
To work around this, we install Feliz.Router 3.8, the latest version that works with SAFE template version 4.2.0.
33+
34+
If you are working with a newer version of the SAFE template, it might be worth trying to install the newest version of Feliz.Router.
35+
To see the installed version of the SAFE template, run in the command line:
36+
37+
```bash
38+
dotnet new --list
39+
```
40+
41+
Install Feliz.UseElmish in the Client project
42+
43+
```bash
44+
dotnet paket add Feliz.UseElmish -p client
45+
```
46+
47+
Open the router in the client project
48+
49+
```fsharp title="Index.fs"
50+
open Feliz.Router
51+
```
52+
53+
## 2. Extracting the todo list module
54+
55+
Create a new Module `TodoList` in the client project. Move the following functions and types to the TodoList Module:
56+
57+
* Model
58+
* Msg
59+
* todosApi
60+
* init
61+
* update
62+
* containerBox
63+
64+
Also open `Shared`, `Fable.Remoting.Client`, `Elmish`, `Feliz.Bulma` and `Feliz`.
65+
66+
```fsharp title="TodoList.fs"
67+
module TodoList
68+
69+
open Shared
70+
open Fable.Remoting.Client
71+
open Elmish
72+
73+
open Feliz.Bulma
74+
open Feliz
75+
76+
type Model = { Todos: Todo list; Input: string }
77+
78+
type Msg =
79+
| GotTodos of Todo list
80+
| SetInput of string
81+
| AddTodo
82+
| AddedTodo of Todo
83+
84+
let todosApi =
85+
Remoting.createApi ()
86+
|> Remoting.withRouteBuilder Route.builder
87+
|> Remoting.buildProxy<ITodosApi>
88+
89+
let init () : Model * Cmd<Msg> =
90+
let model = { Todos = []; Input = "" }
91+
let cmd = Cmd.OfAsync.perform todosApi.getTodos () GotTodos
92+
93+
model, cmd
94+
95+
let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
96+
match msg with
97+
| GotTodos todos -> { model with Todos = todos }, Cmd.none
98+
| SetInput value -> { model with Input = value }, Cmd.none
99+
| AddTodo ->
100+
let todo = Todo.create model.Input
101+
102+
let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo
103+
104+
{ model with Input = "" }, cmd
105+
| AddedTodo todo -> { model with Todos = model.Todos @ [ todo ] }, Cmd.none
106+
107+
let containerBox (model: Model) (dispatch: Msg -> unit) =
108+
Bulma.box [
109+
Bulma.content [
110+
Html.ol [
111+
for todo in model.Todos do
112+
Html.li [ prop.text todo.Description ]
113+
]
114+
]
115+
Bulma.field.div [
116+
field.isGrouped
117+
prop.children [
118+
Bulma.control.p [
119+
control.isExpanded
120+
prop.children [
121+
Bulma.input.text [
122+
prop.value model.Input
123+
prop.placeholder "What needs to be done?"
124+
prop.onChange (fun x -> SetInput x |> dispatch)
125+
]
126+
]
127+
]
128+
Bulma.control.p [
129+
Bulma.button.a [
130+
color.isPrimary
131+
prop.disabled (Todo.isValid model.Input |> not)
132+
prop.onClick (fun _ -> dispatch AddTodo)
133+
prop.text "Add"
134+
]
135+
]
136+
]
137+
]
138+
]
139+
```
140+
141+
## 4. Add the UseElmish hook to the TodoList Module
142+
143+
open Feliz.UseElmish in the TodoList Module
144+
145+
```fsharp title="TodoList.fs"
146+
open Feliz.UseElmish
147+
...
148+
```
149+
150+
In the todoList module, rename `containerBox` to `view`.
151+
On the first line, call `React.useElmish` passing it the `init` and `update` functions. Bind the output to `model` and `dispatch`
152+
153+
=== "Code"
154+
```fsharp title="TodoList.fs"
155+
let view (model: Model) (dispatch: Msg -> unit) =
156+
let model, dispatch = React.useElmish(init, update, [||])
157+
...
158+
```
159+
160+
=== "Diff"
161+
```.diff title="TodoList.fs"
162+
-let containerBox (model: Model) (dispatch: Msg -> unit) =
163+
+let view (model: Model) (dispatch: Msg -> unit) =
164+
+ let model, dispatch = React.useElmish(init, update, [||])
165+
...
166+
```
167+
168+
Replace the arguments of the function with unit, and add the `ReactComponent` attribute to it
169+
170+
=== "Code"
171+
```fsharp title="Index.fs"
172+
[<ReactComponent>]
173+
let view () =
174+
...
175+
```
176+
=== "Diff"
177+
```.diff title="Index.fs"
178+
+ [<ReactComponent>]
179+
- let view (model: Model) (dispatch: Msg -> unit) =
180+
+ let view () =
181+
...
182+
```
183+
184+
## 5. Add a new model to the Index module
185+
186+
In the `Index module`, create a model that holds the current page
187+
188+
```fsharp title="Index.fs"
189+
type Page =
190+
| TodoList
191+
| NotFound
192+
193+
type Model =
194+
{ CurrentPage: Page }
195+
```
196+
## 6. Initializing the application
197+
198+
Create a function that initializes the app based on an url
199+
200+
```fsharp title="Index.fs"
201+
let initFromUrl url =
202+
match url with
203+
| [ "todo" ] ->
204+
let model = { CurrentPage = TodoList }
205+
206+
model, Cmd.none
207+
| _ ->
208+
let model = { CurrentPage = NotFound }
209+
210+
model, Cmd.none
211+
```
212+
213+
Create a new `init` function, that fetches the current url, and calls initFromUrl.
214+
215+
```fsharp title="Index.fs"
216+
let init () =
217+
Router.currentUrl ()
218+
|> initFromUrl
219+
```
220+
## 7. Updating the Page
221+
222+
Add a `Msg` type, with an PageChanged case
223+
224+
```fsharp title="Index.fs"
225+
type Msg =
226+
| PageChanged of string list
227+
```
228+
Add an `update` function, that reinitializes the app based on an URL
229+
230+
```fsharp title="Index.fs"
231+
let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
232+
match msg with
233+
| PageChanged url ->
234+
initFromUrl url
235+
```
236+
237+
## 8. Displaying pages
238+
239+
Add a containerBox function to the `Index` module, that returns the appropriate page content
240+
241+
```fsharp title="Index.fs"
242+
let containerBox (model: Model) (dispatch: Msg -> unit) =
243+
match model.CurrentPage with
244+
| NotFound -> Bulma.box "Page not found"
245+
| TodoList -> TodoList.view ()
246+
```
247+
## 9. Add the router to the view
248+
249+
Wrap the content of the view method in a `React.Router` element's router.children property, and add a `router.onUrlChanged` property to dispatch the urlChanged message
250+
251+
=== "Code"
252+
```fsharp title="Index.fs"
253+
let view (model: Model) (dispatch: Msg -> unit) =
254+
React.router [
255+
router.onUrlChanged ( PageChanged>>dispatch )
256+
router.children [
257+
Bulma.hero [
258+
...
259+
]
260+
]
261+
]
262+
```
263+
=== "Diff"
264+
```diff title="Index.fs"
265+
let view (model: Model) (dispatch: Msg -> unit) =
266+
+ React.router [
267+
+ router.onUrlChanged ( PageChanged>>dispatch )
268+
+ router.children [
269+
Bulma.hero [
270+
...
271+
]
272+
+ ]
273+
+ ]
274+
```
275+
276+
## 10. Try it out
277+
278+
The routing should work now. Try navigating to [localhost:8080](http://localhost:8080/); you should see a page with "Page not Found". If you go to [localhost:8080/#/todo](http://localhost:8080/#/todo), you should see the todo app.
279+
280+
!!! info "# sign"
281+
You might be surprised to see the hash sign as part of the URL. It enables React to react to URL changes without a full page refresh.
282+
There are ways to omit this, but getting this to work properly is outside of the scope of this recipe.

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ nav:
8686
- Migrate from a CDN stylesheet to an NPM package: "recipes/ui/cdn-to-npm.md"
8787
- Add routing with state shared between pages: "recipes/ui/add-routing.md"
8888
- Add routing with separate models per page: "recipes/ui/add-routing-with-separate-models.md"
89+
- Add Routing with UseElmish: "recipes/ui/routing-with-elmish.md"
8990
- Storage:
9091
- Quickly add a database: "recipes/storage/use-litedb.md"
9192
- JavaScript:

0 commit comments

Comments
 (0)