Skip to content

Commit 45189b4

Browse files
authored
Merge pull request #327 from Larocceau/routing-with-single-model
update "add routing" recipe for v5
2 parents 0baae7e + 98034ca commit 45189b4

File tree

2 files changed

+203
-1
lines changed

2 files changed

+203
-1
lines changed

docs/recipes/ui/add-routing.md

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
# How do I add routing to a SAFE app with a shared model for all pages?
2+
3+
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.
4+
5+
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.
6+
7+
## 1. Adding the Feliz router
8+
9+
Install Feliz.Router in the client project
10+
11+
```bash
12+
dotnet paket add Feliz.Router -p Client
13+
```
14+
15+
16+
To include the router in the Client, open `Feliz.Router` at the top of Index.fs
17+
18+
```fsharp
19+
open Feliz.Router
20+
```
21+
22+
## 2. Adding the URL object
23+
24+
Add the current page to the model of the client, using a new `Page` type
25+
26+
=== "Code"
27+
```fsharp
28+
type Page =
29+
| TodoList
30+
| NotFound
31+
32+
type Model =
33+
{ CurrentPage: Page
34+
Todos: Todo list
35+
Input: string }
36+
```
37+
=== "Diff"
38+
```.diff
39+
+ type Page =
40+
+ | TodoList
41+
+ | NotFound
42+
+
43+
- type Model = { Todos: Todo list; Input: string }
44+
+ type Model =
45+
+ { CurrentPage: Page
46+
+ Todos: Todo list
47+
+ Input: string }
48+
```
49+
50+
## 3. Parsing URLs
51+
52+
Create a function to parse a URL to a page, including a wildcard for unmapped pages
53+
54+
```fsharp
55+
let parseUrl url =
56+
match url with
57+
| ["todo"] -> Page.TodoList
58+
| _ -> Page.NotFound
59+
```
60+
61+
## 4. Initialization when using a URL
62+
63+
On initialization, set the current page
64+
65+
=== "Code"
66+
```fsharp
67+
let init () : Model * Cmd<Msg> =
68+
let page = Router.currentUrl () |> parseUrl
69+
70+
let model =
71+
{ CurrentPage = page
72+
Todos = []
73+
Input = "" }
74+
...
75+
model, cmd
76+
```
77+
=== "Diff"
78+
```diff
79+
let init () : Model * Cmd<Msg> =
80+
+ let page = Router.currentUrl () |> parseUrl
81+
+
82+
- let model = { Todos = []; Input = "" }
83+
+ let model =
84+
+ { CurrentPage = page
85+
+ Todos = []
86+
+ Input = "" }
87+
...
88+
model, cmd
89+
```
90+
## 5. Updating the URL
91+
92+
Add an action to handle navigation.
93+
94+
To the `Msg` type, add a `PageChanged` case of `Page`
95+
96+
=== "Code"
97+
```fsharp
98+
type Msg =
99+
...
100+
| PageChanged of Page
101+
```
102+
=== "Diff"
103+
```.diff
104+
type Msg =
105+
...
106+
+ | PageChanged of Page
107+
```
108+
109+
Add the `PageChanged` update action
110+
111+
=== "Code"
112+
```fsharp
113+
let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
114+
match msg with
115+
...
116+
| PageChanged page -> { model with CurrentPage = page }, Cmd.none
117+
```
118+
=== "Diff"
119+
```.diff
120+
let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
121+
match msg with
122+
...
123+
+ | PageChanged page -> { model with CurrentPage = page }, Cmd.none
124+
```
125+
126+
## 6. Displaying the correct content
127+
128+
Rename the `view` function to `todoView`
129+
130+
=== "Code"
131+
```fsharp
132+
let todoView model dispatch =
133+
Html.section [
134+
...
135+
]
136+
```
137+
=== "Diff"
138+
```.diff
139+
- let view model dispatch =
140+
+ let todoView model dispatch =
141+
Html.section [
142+
...
143+
]
144+
```
145+
146+
Add a new view function, that returns the appropriate page
147+
148+
```fsharp
149+
let view model dispatch =
150+
match model.CurrentPage with
151+
| TodoList -> todoView model dispatch
152+
| NotFound ->
153+
Html.div [
154+
prop.className "flex flex-col items-center justify-center h-full"
155+
prop.text "Page not found"
156+
]
157+
```
158+
159+
!!! info "Adding UI elements to every page of the website"
160+
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.
161+
162+
## 7. Adding the React router to the view
163+
164+
Add the `React.Router` element as the outermost element of the view. Dispatch the PageChanged event on `onUrlChanged`
165+
166+
=== "Code"
167+
```fsharp
168+
let view (model: Model) (dispatch: Msg -> unit) =
169+
React.router [
170+
router.onUrlChanged (parseUrl >> PageChanged >> dispatch)
171+
router.children [
172+
match model.CurrentPage with
173+
...
174+
]
175+
]
176+
```
177+
=== "Diff"
178+
```.diff
179+
let view (model: Model) (dispatch: Msg -> unit) =
180+
+ React.router [
181+
+ router.onUrlChanged (parseUrl >> PageChanged >> dispatch)
182+
router.children [
183+
match model.CurrentPage with
184+
...
185+
]
186+
]
187+
```
188+
189+
## 9. Try it out
190+
191+
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.
192+
193+
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.
194+
195+
!!! info "# sign"
196+
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.
197+
There are ways to omit this, but getting this to work properly is outside of the scope of this recipe.
198+
199+
## 10. Adding more pages
200+
201+
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
@@ -82,6 +82,7 @@ nav:
8282
- Add Feliz support: "recipes/ui/add-feliz.md"
8383
- Add FontAwesome support: "recipes/ui/add-fontawesome.md"
8484
- Migrate from a CDN stylesheet to an NPM package: "recipes/ui/cdn-to-npm.md"
85+
- Add routing with state shared between pages: "recipes/ui/add-routing.md"
8586
- Storage:
8687
- Quickly add a database: "recipes/storage/use-litedb.md"
8788
- JavaScript:
@@ -157,4 +158,4 @@ nav:
157158
- Add a NuGet package to the Server: "v4-recipes/package-management/add-nuget-package-to-server.md"
158159
- Migrate to Paket from NuGet: "v4-recipes/package-management/migrate-to-paket.md"
159160
- Migrate to NuGet from Paket: "v4-recipes/package-management/migrate-to-nuget.md"
160-
- Sync NuGet and NPM Packages: "v4-recipes/package-management/sync-nuget-and-npm-packages.md"
161+
- Sync NuGet and NPM Packages: "v4-recipes/package-management/sync-nuget-and-npm-packages.md"

0 commit comments

Comments
 (0)