Как создать SPA с Elm 0.19?

Я пытаюсь построить SPA с Elm и создать три страницы, которые должны показывать содержание, зависит от URL.

Содержание этих трех страниц похожи, например, 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." ]

в Main.elmУ меня есть следующий код:

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
        }

Компилятор жалуется:

-- 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.

Я знаю, что тип неправильный, но не знаю, как это доказать.

Как я могу заставить его работать?

Я также посмотрел пример из https://github.com/rtfeldman/elm-spa-example/blob/master/src/Main.elm но не смог понять, как он работает.

2 ответа

Решение

У вас есть несколько Msg типы, это нормально, но это может привести к путанице. Короче: Main.Msg не тот же тип, что и NotFound.Msg,

Функция matchedRoute возвращает Html Main.Msg в то время как функция NotFound.content возвращает Html NotFound.Msg; совершенно разные типы.

Вы уже 99% пути, потому что у вас есть PageNotFound NotFound.Msg конструктор типа, который производит Main.Msg, Это позволяет вам обернуть NotFound.Msg в Main.Msg, Это должно быть делом PageNotFound NotFound.content в вашем Nothing -> ветка.

Проблема в том, что Msg тип, на который ссылается NotFound.content является NotFound.Msg, Msg тип, на который ссылается Main.matchedRoute является Main.Msgи они не объединяются автоматически. Поэтому, когда вы используете их в разных ветвях case выражении, компилятор скажет вам, что они разные и не могут быть объединены в один тип для case выражение, чтобы вернуться.

Таким образом, вы должны преобразовать одно в другое, и обычный способ сделать это - добавить вариант к "внешнему" типу msg (Main.Msg), который оборачивает "внутренний" тип сообщения (NotFound.Msg). К счастью, вы уже добавили этот вариант как PageNotFound NotFound.Msgтак что мы можем двигаться дальше.

Следующий шаг - сделать обертку NotFound.Msgв PageNotFounds. К сожалению, нам редко удается обработать значения NotFound.Msg в одиночку, обычно в другом Html или же Cmd, что сложнее иметь дело с. К счастью, Эван знал достаточно заранее, чтобы предсказать этот сценарий, и добавил Cmd.map а также Html.map для нас, чтобы использовать. Как List.map а также Maybe.map, Cmd.map а также Html.map берет на себя функцию a -> b и использует его для конвертации Html aс или Cmd aс Html bс или Cmd bс соответственно.

Итак, все, что вам действительно нужно сделать, это использовать Html.map с PageNotFound на NotFound.content:

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

            Nothing ->
                NotFound.content |> Html.map PageNotFound
        ]

Теперь обе ветви вернутся Main.Msg и компилятор должен быть счастлив:)

И, кстати, в elm-spa-example, это делается здесь

Другие вопросы по тегам