elmelm-architecture

How to create SPA with Elm 0.19?


I am trying to build a SPA with Elm and create three pages, that should show the content, depends on URL.

enter image description here

The content of these three pages are similar, for example Page.elm:

module Page.NotFound exposing (Msg(..), content)

import Html exposing (..)
import Html.Attributes exposing (..)



---- UPDATE ----


type Msg
    = NotFoundMsg


content : Html Msg
content =
    p [] [ text "Sorry can not find page." ]

In the Main.elm, I have the following code:

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

import API.Keycloak as Keycloak exposing (..)
import Browser
import Browser.Navigation as Nav
import Html exposing (..)
import Html.Attributes exposing (..)
import Json.Decode as Decode
import Page.Account as Account
import Page.Home as Home
import Page.NotFound as NotFound
import Route
import Url
import Url.Parser exposing ((</>), Parser, int, map, oneOf, parse, s, string)



---- MODEL ----


type alias Model =
    { key : Nav.Key
    , url : Url.Url
    , auth : Result String Keycloak.Struct
    }


init : Decode.Value -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
init flags url key =
    ( Model key url (Keycloak.validate flags), Cmd.none )



---- ROUTE ----


type Route
    = Account



---- UPDATE ----


type Msg
    = PageNotFound NotFound.Msg
    | PageAccount Account.Msg
    | PageHome Home.Msg
    | LinkClicked Browser.UrlRequest
    | UrlChanged Url.Url


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        LinkClicked 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 }
            , Cmd.none
            )



-- SUBSCRIPTIONS


subscriptions : Model -> Sub Msg
subscriptions _ =
    Sub.none



---- VIEW ----


info : Html Msg
info =
    header [] [ text "Header" ]


createLink : String -> Html Msg
createLink path =
    a [ href ("/" ++ path) ] [ text path ]


navigation : Html Msg
navigation =
    ul []
        [ li [] [ createLink "home" ]
        , li [] [ createLink "account" ]
        ]


content : Model -> Html Msg
content model =
    main_ []
        [ case parse Route.parser model.url of
            Just path ->
                matchedRoute path

            Nothing ->
                NotFound.content
        ]


matchedRoute : Route.Route -> Html Msg
matchedRoute path =
    case path of
        Route.Home ->
            Home.content

        Route.Account ->
            Account.content


body : Model -> List (Html Msg)
body model =
    [ info
    , navigation
    , content model
    ]


view : Model -> Browser.Document Msg
view model =
    { title = "Cockpit"
    , body = body model
    }



---- PROGRAM ----


main : Program Decode.Value Model Msg
main =
    Browser.application
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        , onUrlChange = UrlChanged
        , onUrlRequest = LinkClicked
        }

The compiler complains:

-- TYPE MISMATCH -------------- /home/developer/Desktop/elm/cockpit/src/Main.elm

The 2nd branch of this `case` does not match all the previous branches:

104|         [ case parse Route.parser model.url of
105|             Just path ->
106|                 matchedRoute path
107|
108|             Nothing ->
109|                 NotFound.content
                     ^^^^^^^^^^^^^^^^
This `content` value is a:

    Html NotFound.Msg

But all the previous branches result in:

    Html Msg

Hint: All branches in a `case` must produce the same type of values. This way,
no matter which branch we take, the result is always a consistent shape. Read
<https://elm-lang.org/0.19.0/union-types> to learn how to “mix” types.

-- TYPE MISMATCH -------------- /home/developer/Desktop/elm/cockpit/src/Main.elm

Something is off with the 2nd branch of this `case` expression:

120|             Account.content
                 ^^^^^^^^^^^^^^^
This `content` value is a:

    Html Account.Msg

But the type annotation on `matchedRoute` says it should be:

    Html Msg

-- TYPE MISMATCH -------------- /home/developer/Desktop/elm/cockpit/src/Main.elm

Something is off with the 1st branch of this `case` expression:

117|             Home.content
                 ^^^^^^^^^^^^
This `content` value is a:

    Html Home.Msg

But the type annotation on `matchedRoute` says it should be:

    Html Msg
Detected errors in 1 module.

I know that the type is wrong, but do not know, how to prove it.

How can I get it to work?

I also looked at the example from https://github.com/rtfeldman/elm-spa-example/blob/master/src/Main.elm but could not figure, how does it work.


Solution

  • You have multiple Msg types, which is OK, but it can lead to confusion. In short: Main.Msg is not the same type as NotFound.Msg.

    The function matchedRoute returns a Html Main.Msg while the function NotFound.content returns a Html NotFound.Msg; completely different types.

    You're already 99% of the way there because you have a PageNotFound NotFound.Msg type constructor which produces a Main.Msg. This allows you to wrap the NotFound.Msg in a Main.Msg. It should be a matter of doing PageNotFound NotFound.content in your Nothing -> branch.