Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I do a simple elmish router?

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&rsquo;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
like image 689
Pablo Darde Avatar asked Mar 03 '19 15:03

Pablo Darde


Video Answer


1 Answers

in general, all the Elmish "components" (you can understand it as "file") have:

  • a Model representing their state
  • a Msg representing the possible action supported in the component
  • an update function reacting to a Msg and generating a new Model from the previous Model state
  • a view function to generate the view from the current Model state

In 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
like image 184
Maxime Mangel Avatar answered Oct 13 '22 16:10

Maxime Mangel