Elm SPA テンプレートを作った 1

**WARNING: This article is outdated and not maintained anymore.**

作った
https://github.com/kawausokun/elm-my-spa

## 概要

[elm-spa-example](https://github.com/rtfeldman/elm-spa-example) というリポジトリがありまして、Elm で大きめの SPA(Single Page Application) を構築する際に非常に参考になるものです。ただ自分が Elm 初心者ゆえ仕組みを理解するのに少し時間がかかったため、後続向けに理解の手助けになる記事を書きたいなと思った次第。
...が、最初は elm-spa-example のコードを解説していくつもりだったんですけど、まともにやろうとすると結構時間かかりそう。ちょっと方針を変えて、よりシンプルな Elm SPA テンプレート elm-my-spa を作ったので、それをベースにざっくり解説しようかと思います。
対象は [Elm guide](https://guide.elm-lang.org/) を読み終わったくらいの人です。

elm-my-spa は;

* 複数ページを持つ SPA を作るとっかかりに使えるテンプレートです。
* 1ページ 1Elm ファイルというちょうどよい単位でコード分割できます
* ほとんどの仕組みは elm-spa-example のパクりですが、自分にとって不要だったロジックを省略することでシンプルになっています
* このテンプレートを基本にして自社製品を作り完成させたので、実績あり :)

解説を始めます。コードの細部は説明せず、重要な箇所だけ言及していきます。

## どんな画面

https://kawausokun.github.io/elm-my-spa/
2ページ構成で、リンクをクリックすることで各ページを行き来できます。

## プロジェクト構成

<img width="285" alt="Screenshot 2018-12-05 09.31.05.png" src="https://qiita-image-store.s3.amazonaws.com/0/15528/cd97144d-4417-8438-38ef-dd99d3c66f39.png">

`src/Main.elm` が Elm のエントリーポイントです。 `src/Page/*.elm` が各ページのコードです。

## ページを切り替える仕組み

`src/Main.elm` でページ切り替えを行っています。
view 関数を見てみましょう。

```elm:Main.elm
view : Model -> Browser.Document Msg
view model =
    let
        viewPage toMsg { title, body } =
            { title = title, body = List.map (Html.map toMsg) body }
    in
    case model of
        NotFound _ ->
            { title = "Not Found", body = [ Html.text "Not Found" ] }

        Index _ subModel ->
            viewPage GotIndexMsg (IndexPage.view subModel)

        View _ _ subModel ->
            viewPage GotViewMsg (ViewPage.view subModel)
```

Model が Custom Types で、 NotFound, Index, View と3パターン定義されていて、これでどのページを表示するか判断しています。
`IndexPage.view` または `ViewPage.view` で、 `src/Page/*.elm` で定義されている各ページの view 関数を呼び出しています。
では各ページを表現している Model をどう変化させているのか。[Elm guide > Navigation](https://guide.elm-lang.org/webapps/navigation.html) で説明があったように、URLが変化したタイミングで Msg が受け取れるので、そこで Model を切り替えます。

update 関数を見てみます。

```elm:Main.elm
update : Msg -> Model -> ( Model, Cmd Msg )
update message model =
    let
        env =
            toEnv model
    in
    case ( message, model ) of
        ( LinkClicked urlRequest, _ ) ->
            case urlRequest of
                Browser.Internal url ->
                    case Route.fromUrl url of
                        Just _ ->
                            ( model, Nav.pushUrl (Env.navKey env) (Url.toString url) )

                        Nothing ->
                            ( model, Nav.load <| Url.toString url )

                Browser.External href ->
                    if String.length href == 0 then
                        ( model, Cmd.none )

                    else
                        ( model, Nav.load href )

        ( UrlChanged url, _ ) ->
            changeRouteTo (Route.fromUrl url) model
```

LinkClicked が画面でリンクをクリックした際に呼び出される Msg です。これは Elm guide 同様 `Browser.Navigation.pushUrl` または `Browser.Navigation.load` しているだけです。
UrlChanged が実際に URL が変化した際に呼び出される Msg です。 Route は URL 文字列と各ページの対応表のような役割を持つモデルです。 `Route.fromUrl url` で URL 文字列から現在のページに対応する Route に変換します。  changeRouteTo 関数の定義は以下。

```elm:Main.elm
changeRouteTo : Maybe Route -> Model -> ( Model, Cmd Msg )
changeRouteTo maybeRoute model =
    let
        env =
            toEnv model
    in
    case maybeRoute of
        Nothing ->
            ( NotFound env, Cmd.none )

        Just Route.Index ->
            IndexPage.init env
                |> updateWith (Index env) GotIndexMsg

        Just (Route.View id) ->
            ViewPage.init env id
                |> updateWith (View env id) GotViewMsg
```

この関数で Route を受け取って 対応する Model に変換しています。
`IndexPage.init` または `ViewPage.init` で、 `src/Page/*.elm` で定義されている各ページの init 関数を呼び出しています。各ページの init 関数は、そのページごとに定義されている `( Model, Cmd Msg)` を返します。 `changeRouteTo` 関数は最終的に Main で定義している `( Model, Cmd Msg )` を返さないといけないわけですが... updateWith 関数を見てみましょう。

```elm:Main.elm
updateWith : (subModel -> Model) -> (subMsg -> Msg) -> ( subModel, Cmd subMsg ) -> ( Model, Cmd Msg )
updateWith toModel toMsg ( subModel, subCmd ) =
    ( toModel subModel
    , Cmd.map toMsg subCmd
    )
```

...簡単に言うと、 各ページの Model (コード中の subModel) と Msg (コード中の subMsg) を、 Main の Model と Msg に変換しています。

```elm:Main.elm
type Model =
    ...
    | Index Env IndexPage.Model
    | View Env Id ViewPage.Model

type Msg =
    ...
    | GotIndexMsg IndexPage.Msg
    | GotViewMsg ViewPage.Msg
```

Model と Msg が各ページの Model と Msg をラップして保持してくれるわけです。


ちょっと長くなってきたので記事を分けます。
[Elm SPA テンプレートを作った 2](https://qiita.com/kawauso7/items/5f96778456ed78362dd4)