Elm SPA always go to Home page when click reload on any pages

Hi,
I hope you can spot the issue in my code below. I just have 3 pages namely Home, About, Accounts. Going to each page by clicking link is working fine. But when I click reload/refresh the page it always go back to Home page.

I start the app using http-server -p 8000 --proxy http://localhost:8000?.

module Main exposing (Model, Msg(..), init, main, subscriptions, update, view, viewLink)

import Browser
import Browser.Navigation as Nav
import Debug exposing (log)
import Html exposing (..)
import Html.Attributes exposing (..)
import Page.About as About
import Page.Home as Home
import Page.ListAccounts as Accounts exposing (..)
import Page.NotFound as NotFound
import Shared exposing (..)
import Url
import Url.Parser as UP exposing ((</>), Parser, int, map, oneOf, parse, s, string)



-- MAIN


main : Program Flags Model Msg
main =
    Browser.application
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        , onUrlChange = UrlChanged
        , onUrlRequest = UrlRequested
        }



-- MODEL


type alias Model =
    { flags : Flags
    , key : Nav.Key
    , url : Url.Url
    , route : Route
    , page : Page
    }


init : Flags -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
init flags url key =
    let
        model =
            { flags = flags
            , key = key
            , url = url
            , route = urlToRoute url
            , page = HomePage
            }

        log1 =
            log "(init) - Home Page: model => " model
    in
    ( model, Cmd.none )


type Route
    = Home
    | About
    | ListAccounts
    | NotFound


type Page
    = HomePage
    | AboutPage
    | ListAccountsPage Accounts.Model


routeParser : UP.Parser (Route -> a) a
routeParser =
    UP.oneOf
        [ UP.map Home UP.top
        , UP.map About (UP.s "about")
        , UP.map ListAccounts (UP.s "accounts")
        ]


urlToRoute : Url.Url -> Route
urlToRoute url =
    url
        |> UP.parse routeParser
        |> Maybe.withDefault NotFound



-- UPDATE


type Msg
    = UrlRequested Browser.UrlRequest
    | UrlChanged Url.Url
    | ListAccountsMsg Accounts.Msg


loadPage : ( Model, Cmd Msg ) -> ( Model, Cmd Msg )
loadPage ( model, cmd ) =
    let
        ( page, newCmd ) =
            case model.route of
                Home ->
                    ( HomePage, Cmd.none )

                About ->
                    ( AboutPage, Cmd.none )

                ListAccounts ->
                    let
                        ( pageModel, pageCmd ) =
                            Accounts.init model.flags
                    in
                    ( ListAccountsPage pageModel, Cmd.map ListAccountsMsg pageCmd )

                NotFound ->
                    ( HomePage, Cmd.none )
    in
    ( { model | page = page }, Cmd.batch [ cmd, newCmd ] )


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case ( msg, model.page ) of
        ( UrlRequested urlRequest, _ ) ->
            case urlRequest of
                Browser.Internal url ->
                    ( model, Nav.pushUrl model.key (Url.toString url) )

                Browser.External href ->
                    ( model, Nav.load href )

        ( UrlChanged url, _ ) ->
            ( { model | url = url, route = urlToRoute url }
            , Cmd.none
            )
                |> loadPage

        ( ListAccountsMsg subMsg, ListAccountsPage pageModel ) ->
            let
                ( newPageModel, newCmd ) =
                    Accounts.update subMsg pageModel

                log1 =
                    log "(update) Main:ListAccountsOnGet result => " subMsg
            in
            ( { model | page = ListAccountsPage newPageModel }
            , Cmd.map ListAccountsMsg newCmd
            )

        ( ListAccountsMsg subMsg, _ ) ->
            ( model, Cmd.none )



-- SUBSCRIPTIONS


subscriptions : Model -> Sub Msg
subscriptions model =
    case model.page of
        HomePage ->
            Sub.none

        AboutPage ->
            Sub.none

        ListAccountsPage pageModel ->
            Sub.map ListAccountsMsg (Accounts.subscriptions pageModel)



-- VIEW


contentPage : Model -> Html Msg
contentPage model =
    let
        page =
            case model.page of
                HomePage ->
                    Home.homePage

                ListAccountsPage pageModel ->
                    let
                        content =
                            Accounts.view pageModel
                                |> Html.map ListAccountsMsg
                    in
                    content

                AboutPage ->
                    About.aboutPage
    in
    page


view : Model -> Browser.Document Msg
view model =
    let
        content =
            contentPage model

        log1 =
            log "Page Content => " content
    in
    { title = "Accounting"
    , body =
        [ ul []
            [ viewLink "/" "home"
            , viewLink "/about" "about"
            , viewLink "/accounts" "accounts"
            ]
        , content
        ]
    }


viewLink : String -> String -> Html msg
viewLink path pathText =
    li [] [ a [ href path ] [ text pathText ] ]

This is expected, routing is handled on the client not the server; what that means is when you refresh or manually navigate to a route by URL the request goes to the server.

Your server is setup to respond to all requests with your elm app, so that’s what happens. You can pass in the URL on init via flags to properly route to the right page on the client.

Hi @pd-andy, apologies! I’m so new in evaluating Elm. Could you provide a small snippet how to do that? I’ve started a basic elm spa here https://github.com/mvperez/elm-spa-basic but it is working fine.
I just trying to expand it more to real use case.
Thanks,
Marvin

Hi, I belive @pd-andy misunderstood.
You are doing it correctly, no need for flags.

The problem seems to be that you set page to Homepage on init, no matter what route is detected…

You actually do not need a .page field in your model.
Better to just derive what page to show in your view function from the model.route field ?

Like:

view model =
  case model.route of 
    HomePage ->
      renderHomepage model
    NotFound ->
      renderNotFound model
    ...

Hi @Atlewee, you’re right. It’s because of the page = HomePage in the init. I tried to create a function that will evaluate the route and return a Page type but I’m having a problem when the match goes to my ListAccountsPage Accounts.Model. So I guess I will try it to work in based on your suggestion.

Oh wait, it becomes more problematic because the model used by ListAccountsPage is from the Model of the sub module. :frowning:

I’m stuck up on this if I remove the page in my model and init. Error on this line:

                        content =
                            Accounts.view pageModel
                                |> Html.map ListAccountsMsg

The function that use this line

subscriptions : Model -> Sub Msg
subscriptions model =
    case model.route of
        Home ->
            Sub.none

        About ->
            Sub.none

        ListAccounts pageModel ->
            Sub.map ListAccountsMsg (Accounts.subscriptions pageModel)



-- VIEW


contentPage : Model -> Html Msg
contentPage model =
    let
        page =
            case model.route of
                Home ->
                    Home.homePage

                ListAccounts ->
                    let
                        content =
                            Accounts.view pageModel
                                |> Html.map ListAccountsMsg
                    in
                    content

                About ->
                    About.aboutPage
    in
    page


view : Model -> Browser.Document Msg
view model =
    let
        content =
            contentPage model

        log1 =
            log "Page Content => " content
    in
    { title = "Accounting"
    , body =
        [ ul []
            [ viewLink "/" "home"
            , viewLink "/about" "about"
            , viewLink "/accounts" "accounts"
            ]
        , content
        ]
    }

Oh, I can actually use the loadPage function in my init. That fixed my problem.

init : Flags -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
init flags url key =
    let
        model =
            { flags = flags
            , key = key
            , url = url
            , route = urlToRoute url
            , page = HomePage
            }

        log1 =
            log "(init) - Home Page: model => " model
    in
    loadPage (model, Cmd.none)