Skip to content

Commit 9da6407

Browse files
committed
update "add routing" recipe for v5
1 parent 6643835 commit 9da6407

File tree

2 files changed

+204
-1
lines changed

2 files changed

+204
-1
lines changed

docs/recipes/ui/add-routing.md

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# How do I add routing to a SAFE app with a shared model for all pages?
2+
3+
4+
When building larger apps, you probably want different pages to be accessible through different URLs. In this recipe, we show you how to add routes to different pages to an application, including adding a "page not found" page that is displayed when an unknown URL is entered.
5+
6+
In this recipe we use the simplest approach to storing states for multiple pages, by creating a single state for the full app. A potential benefit of this approach is that the state of a page is not lost when navigating away from it. You will see how that works at the end of the recipe.
7+
8+
## 1. Adding the Feliz router
9+
10+
Install Feliz.Router in the client project
11+
12+
```bash
13+
dotnet paket add Feliz.Router -p Client
14+
```
15+
16+
17+
To include the router in the Client, open `Feliz.Router` at the top of Index.fs
18+
19+
```fsharp
20+
open Feliz.Router
21+
```
22+
23+
## 2. Adding the URL object
24+
25+
Add the current page to the model of the client, using a new `Page` type
26+
27+
=== "Code"
28+
```fsharp
29+
type Page =
30+
| TodoList
31+
| NotFound
32+
33+
type Model =
34+
{ CurrentPage: Page
35+
Todos: Todo list
36+
Input: string }
37+
```
38+
=== "Diff"
39+
```.diff
40+
+ type Page =
41+
+ | TodoList
42+
+ | NotFound
43+
+
44+
- type Model = { Todos: Todo list; Input: string }
45+
+ type Model =
46+
+ { CurrentPage: Page
47+
+ Todos: Todo list
48+
+ Input: string }
49+
```
50+
51+
## 3. Parsing URLs
52+
53+
Create a function to parse a URL to a page, including a wildcard for unmapped pages
54+
55+
```fsharp
56+
let parseUrl url =
57+
match url with
58+
| ["todo"] -> Page.TodoList
59+
| _ -> Page.NotFound
60+
```
61+
62+
## 4. Initialization when using a URL
63+
64+
On initialization, set the current page
65+
66+
=== "Code"
67+
```fsharp
68+
let init () : Model * Cmd<Msg> =
69+
let page = Router.currentUrl () |> parseUrl
70+
71+
let model =
72+
{ CurrentPage = page
73+
Todos = []
74+
Input = "" }
75+
...
76+
model, cmd
77+
```
78+
=== "Diff"
79+
```diff
80+
let init () : Model * Cmd<Msg> =
81+
+ let page = Router.currentUrl () |> parseUrl
82+
+
83+
- let model = { Todos = []; Input = "" }
84+
+ let model =
85+
+ { CurrentPage = page
86+
+ Todos = []
87+
+ Input = "" }
88+
...
89+
model, cmd
90+
```
91+
## 5. Updating the URL
92+
93+
Add an action to handle navigation.
94+
95+
To the `Msg` type, add a `PageChanged` case of `Page`
96+
97+
=== "Code"
98+
```fsharp
99+
type Msg =
100+
...
101+
| PageChanged of Page
102+
```
103+
=== "Diff"
104+
```.diff
105+
type Msg =
106+
...
107+
+ | PageChanged of Page
108+
```
109+
110+
Add the `PageChanged` update action
111+
112+
=== "Code"
113+
```fsharp
114+
let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
115+
match msg with
116+
...
117+
| PageChanged page -> { model with CurrentPage = page }, Cmd.none
118+
```
119+
=== "Diff"
120+
```.diff
121+
let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
122+
match msg with
123+
...
124+
+ | PageChanged page -> { model with CurrentPage = page }, Cmd.none
125+
```
126+
127+
## 6. Displaying the correct content
128+
129+
Rename the `view` function to `todoView`
130+
131+
=== "Code"
132+
```fsharp
133+
let todoView model dispatch =
134+
Html.section [
135+
...
136+
]
137+
```
138+
=== "Diff"
139+
```.diff
140+
- let view model dispatch =
141+
+ let todoView model dispatch =
142+
Html.section [
143+
...
144+
]
145+
```
146+
147+
Add a new view function, that returns the appropriate page
148+
149+
```fsharp
150+
let view model dispatch =
151+
match model.CurrentPage with
152+
| TodoList -> todoView model dispatch
153+
| NotFound ->
154+
Html.div [
155+
prop.className "flex flex-col items-center justify-center h-full"
156+
prop.text "Page not found"
157+
]
158+
```
159+
160+
!!! info "Adding UI elements to every page of the website"
161+
In this recipe, we moved all the page content to the `todoView`, but you don't have to. You can add UI you want to display on every page of the application to the `view` function.
162+
163+
## 7. Adding the React router to the view
164+
165+
Add the `React.Router` element as the outermost element of the view. Dispatch the PageChanged event on `onUrlChanged`
166+
167+
=== "Code"
168+
```fsharp
169+
let view (model: Model) (dispatch: Msg -> unit) =
170+
React.router [
171+
router.onUrlChanged (parseUrl >> PageChanged >> dispatch)
172+
router.children [
173+
match model.CurrentPage with
174+
...
175+
]
176+
]
177+
```
178+
=== "Diff"
179+
```.diff
180+
let view (model: Model) (dispatch: Msg -> unit) =
181+
+ React.router [
182+
+ router.onUrlChanged (parseUrl >> PageChanged >> dispatch)
183+
router.children [
184+
match model.CurrentPage with
185+
...
186+
]
187+
]
188+
```
189+
190+
## 9. Try it out
191+
192+
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.
193+
194+
To see how the state is maintained even when navigating away from the page, type something in the text box and move away from the page by entering another path in the address bar. Then go back to the todo page. The entered text is still there.
195+
196+
!!! info "# sign"
197+
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.
198+
There are ways to omit this, but getting this to work properly is outside of the scope of this recipe.
199+
200+
## 10. Adding more pages
201+
202+
Now that you have set up the routing, adding more pages is simple: add a new case to the `Page` type; add a route for this page in the `parseUrl` function; add a function that takes a model and dispatcher to generate your new page, and add a new case to the pattern match inside the `view` function to display the new case.

mkdocs.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ nav:
8181
- Add Feliz support: "recipes/ui/add-feliz.md"
8282
- Add FontAwesome support: "recipes/ui/add-fontawesome.md"
8383
- Migrate from a CDN stylesheet to an NPM package: "recipes/ui/cdn-to-npm.md"
84+
- Add routing with state shared between pages: "recipes/ui/add-routing.md"
8485
- Storage:
8586
- Quickly add a database: "recipes/storage/use-litedb.md"
8687
- JavaScript:
@@ -156,4 +157,4 @@ nav:
156157
- Add a NuGet package to the Server: "v4-recipes/package-management/add-nuget-package-to-server.md"
157158
- Migrate to Paket from NuGet: "v4-recipes/package-management/migrate-to-paket.md"
158159
- Migrate to NuGet from Paket: "v4-recipes/package-management/migrate-to-nuget.md"
159-
- Sync NuGet and NPM Packages: "v4-recipes/package-management/sync-nuget-and-npm-packages.md"
160+
- Sync NuGet and NPM Packages: "v4-recipes/package-management/sync-nuget-and-npm-packages.md"

0 commit comments

Comments
 (0)