Sorry, but I'm a newbie with Fable and F#. I started a boilerplate from SAFE project, and I created a SPA with two pages. However, all the logic is inside a single file. My question is. How can I implement a router putting each view in one file?
I would something like that:
...
Client
|_Client.fs
|_Pages
|_ Home.fs
|_ About.fs
Server
|_Server.fs
...
Below is my Client.fs file
src/Client/Client.fs
(**
- title: Navigation demo
- tagline: The router sample ported from Elm
*)
module App
open Fable.Core
open Fable.Import
open Elmish
open Fable.Import.Browser
open Fable.PowerPack
open Elmish.Browser.Navigation
open Elmish.Browser.UrlParser
JsInterop.importAll "whatwg-fetch"
// Types
type Page = Home | Blog of int | Search of string
type Model =
{ page : Page
query : string
cache : Map<string,string list> }
let toHash =
function
| Blog id -> "#blog/" + (string id)
| _ -> "#home"
/// The URL is turned into a Page option.
let pageParser : Parser<Page->_,_> =
oneOf
[ map Home (s "home")
map Blog (s "blog" </> i32) ]
type Msg =
| Query of string
| Enter
| FetchFailure of string*exn
| FetchSuccess of string*(string list)
type Place = { ``place name``: string; state: string; }
(* If the URL is valid, we just update our model or issue a command.
If it is not a valid URL, we modify the URL to whatever makes sense.
*)
let urlUpdate (result:Option<Page>) model =
match result with
| Some page ->
{ model with page = page; query = "" }, []
| None ->
Browser.console.error("Error parsing url")
( model, Navigation.modifyUrl (toHash model.page) )
let init result =
urlUpdate result { page = Home; query = ""; cache = Map.empty }
(* A relatively normal update function. The only notable thing here is that we
are commanding a new URL to be added to the browser history. This changes the
address bar and lets us use the browser’s back button to go back to
previous pages.
*)
let update msg model =
match msg with
| Query query ->
{ model with query = query }, []
| FetchFailure (query,_) ->
{ model with cache = Map.add query [] model.cache }, []
| FetchSuccess (query,locations) ->
{ model with cache = Map.add query locations model.cache }, []
// VIEW
open Fable.Helpers.React
open Fable.Helpers.React.Props
let viewLink page description =
a [ Style [ Padding "0 20px" ]
Href (toHash page) ]
[ str description]
let internal centerStyle direction =
Style [ Display "flex"
FlexDirection direction
AlignItems "center"
unbox("justifyContent", "center")
Padding "20px 0" ]
let words size message =
span [ Style [ unbox("fontSize", size |> sprintf "%dpx") ] ] [ str message ]
let internal onEnter msg dispatch =
function
| (ev:React.KeyboardEvent) when ev.keyCode = 13. ->
ev.preventDefault()
dispatch msg
| _ -> ()
|> OnKeyDown
let viewPage model dispatch =
match model.page with
| Home ->
[ words 60 "Welcome!"
str "Play with the links and search bar above. (Press ENTER to trigger the zip code search.)" ]
| Blog id ->
[ words 20 "This is blog post number"
words 100 (string id) ]
open Fable.Core.JsInterop
let view model dispatch =
div []
[ div [ centerStyle "row" ]
[ viewLink Home "Home"
viewLink (Blog 42) "Cat Facts"
viewLink (Blog 13) "Alligator Jokes"
viewLink (Blog 26) "Workout Plan" ]
hr []
div [ centerStyle "column" ] (viewPage model dispatch)
]
open Elmish.React
open Elmish.Debug
// App
Program.mkProgram init update view
|> Program.toNavigable (parseHash pageParser) urlUpdate
|> Program.withReact "elmish-app"
|> Program.withDebugger
|> Program.run
in general, all the Elmish "components" (you can understand it as "file") have:
Model
representing their stateMsg
representing the possible action supported in the componentupdate
function reacting to a Msg
and generating a new Model
from the previous Model
stateview
function to generate the view from the current Model
stateIn my application, I use the following structure which allows me to scale (indefinitely) the application.
A Router.fs
file responsible to handle to represents the different routes and the parsing
function.
let inline (</>) a b = a + "/" + string b
type Route =
| Home
| Blog of int
let toHash (route : Route) =
match route with
| Home -> "home"
| Blog id -> "blog" </> id
open Elmish.Browser.Navigation
open Elmish.Browser.UrlParser
let routeParser : Parser<Route -> Route, Route> =
oneOf [ // Auth Routes
map (fun domainId -> Route.Blog domainId) (s "blog" </> i32)
map Route.Home (s "home")
// Default Route
map Route.Home top ]
A Main.fs
file responsible to create the Elmish program and handling how to react to the route changes.
open Elmish
open Fable.Helpers.React
open Fable.Import
type Page =
| Home of Home.Model
| Blog of Blog.Model
| NotFound
type Model =
{ ActivePage : Page
CurrentRoute : Router.Route option }
type Msg =
| HomeMsg of Home.Msg
| BlogMsg of Blog.Msg
let private setRoute (optRoute: Router.Route option) model =
let model = { model with CurrentRoute = optRoute }
match optRoute with
| None ->
{ model with ActivePage = Page.NotFound }, Cmd.none
| Some Router.Route.Home ->
let (homeModel, homeCmd) = Home.init ()
{ model with ActivePage = Page.Home homeModel }, Cmd.map HomeMsg homeCmd
| Some (Router.Route.Blog blogId) ->
let (blogModel, blogCmd) = Blog.init blogId
{ model with ActivePage = Page.Blog blogModel }, Cmd.map BlogMsg blogCmd
let init (location : Router.Route option) =
setRoute location
{ ActivePage = Page.NotFound
CurrentRoute = None }
let update (msg : Msg) (model : Model) =
match model.ActivePage, msg with
| Page.NotFound, _ ->
// Nothing to do here
model, Cmd.none
| Page.Home homeModel, HomeMsg homeMsg ->
let (homeModel, homeCmd) = Home.update homeMsg homeModel
{ model with ActivePage = Page.Home homeModel }, Cmd.map HomeMsg homeCmd
| Page.Blog blogModel, BlogMsg blogMsg ->
let (blogModel, blogCmd) = Blog.update blogMsg blogModel
{ model with ActivePage = Page.Blog blogModel }, Cmd.map BlogMsg blogCmd
| _, msg ->
Browser.console.warn("Message discarded:\n", string msg)
model, Cmd.none
let view (model : Model) (dispatch : Dispatch<Msg>) =
match model.ActivePage with
| Page.NotFound ->
str "404 Page not found"
| Page.Home homeModel ->
Home.view homeModel (HomeMsg >> dispatch)
| Page.Blog blogModel ->
Blog.view blogModel (BlogMsg >> dispatch)
open Elmish.Browser.UrlParser
open Elmish.Browser.Navigation
open Elmish.React
// App
Program.mkProgram init update view
|> Program.toNavigable (parseHash Router.routeParser) setRoute
|> Program.withReactUnoptimized "elmish-app"
|> Program.run
So in your case, I would have the following files:
Router.fs
Home.fs
Blog.fs
Main.fs
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With