Skip to content

Commit 97431e6

Browse files
authored
Merge pull request #332 from Larocceau/use-elmish
copy routing with useElmish to V5 recipes
2 parents c3ee6ff + 4eb5407 commit 97431e6

File tree

2 files changed

+290
-0
lines changed

2 files changed

+290
-0
lines changed
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
# How do I create multi-page applications with routing and the useElmish hook?
2+
3+
[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.
4+
5+
In this recipe we add routing to a safe app, and implement the todo list page using the `UseElmish` hook.
6+
7+
## 1. Installing dependencies
8+
9+
Install Feliz.Router in the Client project
10+
11+
```bash
12+
dotnet paket add Feliz.Router -p Client
13+
```
14+
15+
Install Feliz.UseElmish in the Client project
16+
17+
```bash
18+
cd src/Client
19+
dotnet femto install Feliz.UseElmish
20+
```
21+
22+
Open the router in the client project
23+
24+
```fsharp title="Index.fs"
25+
open Feliz.Router
26+
```
27+
28+
## 2. Extracting the todo list module
29+
30+
Create a new Module `TodoList` in the client project. Move the following functions and types to the TodoList Module:
31+
32+
* Model
33+
* Msg
34+
* todosApi
35+
* init
36+
* todoAction
37+
* todoList
38+
39+
Also open `Shared`, `Fable.Remoting.Client`, `Elmish` and `Feliz`.
40+
41+
```fsharp title="TodoList.fs"
42+
module TodoList
43+
44+
open Shared
45+
open Fable.Remoting.Client
46+
open Elmish
47+
48+
open Feliz
49+
50+
type Model = { Todos: Todo list; Input: string }
51+
52+
type Msg =
53+
| GotTodos of Todo list
54+
| SetInput of string
55+
| AddTodo
56+
| AddedTodo of Todo
57+
58+
let todosApi =
59+
Remoting.createApi ()
60+
|> Remoting.withRouteBuilder Route.builder
61+
|> Remoting.buildProxy<ITodosApi>
62+
63+
let init () =
64+
let model = { Todos = []; Input = "" }
65+
let cmd = Cmd.OfAsync.perform todosApi.getTodos () GotTodos
66+
model, cmd
67+
68+
let update msg model =
69+
match msg with
70+
| GotTodos todos -> { model with Todos = todos }, Cmd.none
71+
| SetInput value -> { model with Input = value }, Cmd.none
72+
| AddTodo ->
73+
let todo = Todo.create model.Input
74+
75+
let cmd = Cmd.OfAsync.perform todosApi.addTodo todo AddedTodo
76+
77+
{ model with Input = "" }, cmd
78+
| AddedTodo todo ->
79+
{
80+
model with
81+
Todos = model.Todos @ [ todo ]
82+
},
83+
Cmd.none
84+
85+
let private todoAction model dispatch =
86+
Html.div [
87+
prop.className "flex flex-col sm:flex-row mt-4 gap-4"
88+
prop.children [
89+
Html.input [
90+
prop.className
91+
"shadow appearance-none border rounded w-full py-2 px-3 outline-none focus:ring-2 ring-teal-300 text-grey-darker"
92+
prop.value model.Input
93+
prop.placeholder "What needs to be done?"
94+
prop.autoFocus true
95+
prop.onChange (SetInput >> dispatch)
96+
prop.onKeyPress (fun ev ->
97+
if ev.key = "Enter" then
98+
dispatch AddTodo)
99+
]
100+
Html.button [
101+
prop.className
102+
"flex-no-shrink p-2 px-12 rounded bg-teal-600 outline-none focus:ring-2 ring-teal-300 font-bold text-white hover:bg-teal disabled:opacity-30 disabled:cursor-not-allowed"
103+
prop.disabled (Todo.isValid model.Input |> not)
104+
prop.onClick (fun _ -> dispatch AddTodo)
105+
prop.text "Add"
106+
]
107+
]
108+
]
109+
110+
[<ReactComponent>]
111+
let todoList model dispatch =
112+
Html.div [
113+
prop.className "bg-white/80 rounded-md shadow-md p-4 w-5/6 lg:w-3/4 lg:max-w-2xl"
114+
prop.children [
115+
Html.ol [
116+
prop.className "list-decimal ml-6"
117+
prop.children [
118+
for todo in model.Todos do
119+
Html.li [ prop.className "my-1"; prop.text todo.Description ]
120+
]
121+
]
122+
123+
todoAction model dispatch
124+
]
125+
]
126+
```
127+
128+
## 4. Add the UseElmish hook to the TodoList view function
129+
130+
open Feliz.UseElmish in the TodoList Module
131+
132+
```fsharp title="TodoList.fs"
133+
open Feliz.UseElmish
134+
...
135+
```
136+
137+
In the todoList module, rename the function `todoList` to `view`, and remove the `private` access modifier.
138+
On the first line, call `React.useElmish`, passing it the `init` and `update` functions. Bind the output to `model` and `dispatch`
139+
140+
=== "Code"
141+
```fsharp title="TodoList.fs"
142+
let view model dispatch =
143+
let model, dispatch = React.useElmish (init, update, [||])
144+
...
145+
```
146+
147+
=== "Diff"
148+
```.diff title="TodoList.fs"
149+
-let containerBox model dispatch =
150+
+let view model dispatch =
151+
+ let model, dispatch = React.useElmish (init, update, [||])
152+
...
153+
```
154+
155+
Replace the arguments of the function with unit, and add the `ReactComponent` attribute to it
156+
157+
=== "Code"
158+
```fsharp title="Index.fs"
159+
[<ReactComponent>]
160+
let view () =
161+
...
162+
```
163+
=== "Diff"
164+
```.diff title="Index.fs"
165+
+ [<ReactComponent>]
166+
- let view model dispatch =
167+
+ let view () =
168+
...
169+
```
170+
171+
## 5. Add a new model to the Index module
172+
173+
In the `Index module`, create a model that holds the current page
174+
175+
```fsharp title="Index.fs"
176+
type Page =
177+
| TodoList
178+
| NotFound
179+
180+
type Model =
181+
{ CurrentPage: Page }
182+
```
183+
## 6. Initializing the application
184+
185+
Create a function that initializes the app based on an url
186+
187+
```fsharp title="Index.fs"
188+
let initFromUrl url =
189+
match url with
190+
| [ "todo" ] ->
191+
let model = { CurrentPage = TodoList }
192+
193+
model, Cmd.none
194+
| _ ->
195+
let model = { CurrentPage = NotFound }
196+
197+
model, Cmd.none
198+
```
199+
200+
Create a new `init` function, that fetches the current url, and calls initFromUrl.
201+
202+
```fsharp title="Index.fs"
203+
let init () = Router.currentUrl () |> initFromUrl
204+
```
205+
## 7. Updating the Page
206+
207+
Add a `Msg` type, with an PageChanged case
208+
209+
```fsharp title="Index.fs"
210+
type Msg =
211+
| PageChanged of string list
212+
```
213+
Add an `update` function, that reinitializes the app based on an URL
214+
215+
```fsharp title="Index.fs"
216+
let update msg model =
217+
match msg with
218+
| PageChanged url -> initFromUrl url
219+
```
220+
221+
## 8. Displaying pages
222+
223+
Add a pageContent function to the `Index` module, that returns the appropriate page content
224+
225+
```fsharp title="Index.fs"
226+
let pageContent model =
227+
match model.CurrentPage with
228+
| NotFound -> Html.text "Page not found"
229+
| TodoList -> TodoList.view ()
230+
```
231+
232+
In the `view` function, replace the call to `todoList` with a call to `pageContent`
233+
234+
=== "Code"
235+
```fsharp title="Index.fs"
236+
let view model dispatch =
237+
Html.section [
238+
...
239+
pageContent model
240+
...
241+
]
242+
```
243+
=== "Diff"
244+
```diff title="Index.fs"
245+
let view model dispatch =
246+
Html.section [
247+
...
248+
- todoList view model
249+
+ pageContent model
250+
...
251+
]
252+
```
253+
254+
## 9. Add the router to the view
255+
256+
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
257+
258+
=== "Code"
259+
```fsharp title="Index.fs"
260+
let view model dispatch =
261+
React.router [
262+
router.onUrlChanged ( PageChanged>>dispatch )
263+
router.children [
264+
Html.section [
265+
...
266+
]
267+
]
268+
]
269+
```
270+
=== "Diff"
271+
```diff title="Index.fs"
272+
let view (model: Model) (dispatch: Msg -> unit) =
273+
+ React.router [
274+
+ router.onUrlChanged ( PageChanged>>dispatch )
275+
+ router.children [
276+
Html.section [
277+
...
278+
]
279+
+ ]
280+
+ ]
281+
```
282+
283+
## 10. Try it out
284+
285+
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.
286+
287+
!!! info "# sign"
288+
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.
289+
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)