Elmでviewを分割

アーキテクチャを複数のファイルに切り分けて入れ子にするのにちょっとひと工夫必要だったのだけど、これに関して日本語での情報があまり見当たらなかったので残しておく。 先に結論を言っておくとHtml.mapCmd.mapが肝。 なおElmのバージョンは0.18。

文字列の表示をオンオフするだけのアプリで考えてみよう。 プログラムの全体はこう。

module Main exposing (..)

import Html exposing (..)
import Html.Attributes exposing (style)
import Html.Events exposing (onClick)


main : Program Never Bool Msg
main =
    program
        { init = init
        , view = view
        , update = update
        , subscriptions = \_ -> Sub.none
        }


type alias Model =
    Bool


type Msg
    = Toggle


init : ( Bool, Cmd msg )
init =
    ( False, Cmd.none )


view : Model -> Html Msg
view model =
    let
        viewText =
            (if model then
                "block"
             else
                "none"
            )
                |> (\d -> p [ style [ ( "display", d ) ] ] [ text "text" ])
    in
    div []
        [ h1 [] [text "demo"]
        , button [ onClick Toggle ] [ text "toggle" ]
        , viewText
        ]


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    ( not model, Cmd.none )

ここからh1以外のviewの要素と状態更新に関わる部分を別ファイルに切り出す。 言うまでもなくこの規模のアプリではそんなことをする必要はないけど、規模が大きくなると実質必須となる。

module Toggle exposing (..)

import Html exposing (..)
import Html.Attributes exposing (style)
import Html.Events exposing (onClick)


type alias Model =
    Bool


type Msg
    = Toggle


init : ( Bool, Cmd msg )
init =
    ( False, Cmd.none )


view : Model -> Html Msg
view model =
    let
        viewText =
            (if model then
                "block"
             else
                "none"
            )
                |> (\d -> p [ style [ ( "display", d ) ] ] [ text "text" ])
    in
    div []
        [ button [ onClick Toggle ] [ text "toggle" ]
        , viewText
        ]


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    ( not model, Cmd.none )
module Main exposing (..)

import Html exposing (..)
import Toggle


main : Program Never Model Msg
main =
    program
        { init = init
        , view = view
        , update = update
        , subscriptions = \_ -> Sub.none
        }


type alias Model =
    { toggle : Toggle.Model }


type Msg
    = ToggleMsg Toggle.Msg


init : ( Model, Cmd msg )
init =
    ( { toggle = False }, Cmd.none )


view : Model -> Html Msg
view model =
    div []
        [ h1 [] [ text "demo" ]
        , Toggle.view model.toggle |> Html.map ToggleMsg
        ]


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ToggleMsg msg_ ->
            let
                ( m_, cmd ) =
                    Toggle.update msg_ model.toggle
            in
            ( { model | toggle = m_ }, Cmd.map ToggleMsg cmd )

Toggle.elmを作って、Model, Msgとviewの一部を別モジュールとしMain.elmから呼び出すようにした。 今回は外部モジュールも状態も1つだけだが、複数になることを想定してModelやMsgを定義しなおした。 Msgを上記のように定義しておくと、呼び出すビューが増えた場合も

type Msg =
    ToggleMsg Toggle.Msg
    | HogeMsg Hoge.Msg

というかたちで拡張しやすく、updateも書きやすい。 ただ、Toggle.updateToggle.Msg -> Toggle.Model -> (Toggle.Model, Cmd Toggle.Msg)という定義になっているので、Mainモジュールのupdateとは型が合わない。

そこでCmd.mapの出番となる。 mapという名前から大方予想はつくと思うけど、型はこうなっている(elm-packageのドキュメント)。

(a -> msg) -> Cmd a -> Cmd msg

ToggleMsgはToggle.Msg -> Msgと定義されているので、Main.update関数のCmd.map ToggleMsg cmdという式はCmd Main.Msgを返す。 viewについても同様で、Toggle.viewの戻り値がHtml Toggle.MsgなのでHtml.mapで置換してあげる。

Html.mapについてはドキュメントで少し説明があるけど、Cmd.mapは公式情報がないような。 若い言語なので情報不足は否めない。

なお、モジュールの切り分け方はelm-spa-sampleを参考にした。 しかしいきなりこれに当たるのはヘビーなんだよな……。

Elm 
comments powered by Disqus