この記事はElm+Firebaseでチャットアプリを作るのエントリーです。
前記事 : Elmでビュー(チャット画面)を作る
次記事 : Elm TDDしながらチャットコメントの投稿を実装する
この記事で行うこと
本稿では、TEA (The Elm Architecture)とスナップショットテストをしながら実装を進めていく方法をお伝えします。
実装内容
TEAを利用したコメント反映
以下のような流れで実装をしていきます。TDDを交えながらステップバイステップで見ていきましょう。
フォームのInputイベント -> Msg発行 -> updateによるMsgのハンドリング, Modelの更新 -> Modelをviewに反映
コメントフォームにおけるInputイベントの実装
コメントフォームにおいて、Inputイベントが起きていることをテストします。まずは前回のviewからchatForm
と言う名前でイベントを起こしたい箇所を抽出します。Elmでは、viewは関数と配列によって構成されているに過ぎないので関数として切り出すのはとっても簡単です。
-- テスト対象の関数をexportする
module Main exposing (Msg(..), chatForm)
...
type Msg
= UpdateContent String
view : Model -> Html Msg
view model =
div [ class "page" ]
[ section [ class "card" ]
[ div [ class "card-header" ]
[ text "Elm Chat"
]
, div [ class "card-body" ]
[ div [ class "media" ]
[ div [ class "media-left" ]
[ a [ href "#", class "icon-rounded" ] [ text "S" ]
]
, div [ class "media-body" ]
[ h4 [ class "media-heading" ] [ text "Suzuki Taro Date:2016/09/01" ]
, div [] [ text "この文章はダミーです。文字の大きさ、量、字間、行間等を確認するために入れています。この文章はダミーです。文字の大きさ、量、字間、行間等を確認するために入れています。" ]
]
]
, hr [] []
, div
[ class "media" ]
[ div [ class "media-body" ]
[ h4 [ class "media-heading" ] [ text "Tanaka Jiro Date:2016/09/01" ]
, div [] [ text "この文章はダミーです。文字の大きさ、量、字間、行間等を確認するために入れています。" ]
]
, div
[ class "media-right" ]
[ a [ href "#", class "icon-rounded" ] [ text "T" ]
]
]
]
]
, section []
-- ここを関数化
[ chatForm
]
]
-- Form部分を切り出し
chatForm : Html Msg
chatForm =
form [ class "chart-form pure-form" ]
[ div [ class "input-group" ]
[ input [ type_ "text", class "", placeholder "Comment" ] []
, button [ class "pure-button button-secondary" ] [ text "SNED" ]
]
]
期待をテストで表現します。イベントが起きたときには、後述するupdate
関数でハンドリングするためにMsg
を発行します。今回の場合、chatFormのviewから対象となるタグを辿っていってイベントをシミュレートし、UpdateContent "abc"
というMsgが発行されることを期待します。
module Tests exposing (suite)
import Expect exposing (Expectation)
import Main exposing (..)
import Test exposing (..)
import Test.Html.Event as Event
import Test.Html.Query as Query
import Test.Html.Selector as Selector
suite : Test
suite =
describe "The Main module"
[ describe "chatForm"
[ test "フォームに 'abc' と入力したら UpdateContent 'abc' の Msgが発行される" <|
\_ ->
chatForm
|> Query.fromHtml
|> Query.find [ Selector.tag "input" ]
|> Event.simulate (Event.input "abc")
|> Event.expect (UpdateContent "abc")
]
]
良い感じでREDとなりました。
GREENになるようにonInput
イベントを追記しましょう。
import Html.Events exposing (onInput)
...
chatForm : Html Msg
chatForm =
form [ class "chart-form pure-form" ]
[ div [ class "input-group" ]
[ input [ type_ "text", class "", placeholder "Comment", onInput UpdateContent ] []
, button [ class "pure-button button-secondary" ] [ text "SNED" ]
]
]
GREENですね。
Modelの反映
Inputイベントがテストできたので、Modelの定義とupdate関数の定義、viewの更新をしていきましょう。
type alias Model =
{ content : String }
init : () -> ( Model, Cmd Msg )
init _ =
( { content = "" }, Cmd.none )
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
-- イベントで受け取った値でモデルを更新
UpdateContent c ->
( { model | content = c }, Cmd.none )
view : Model -> Html Msg
view model =
div [ class "page" ]
[ section [ class "card" ]
[ div [ class "card-header" ]
[ text "Elm Chat"
]
, div [ class "card-body" ]
[ div [ class "media" ]
[ div [ class "media-left" ]
[ a [ href "#", class "icon-rounded" ] [ text "S" ]
]
, div [ class "media-body" ]
[ h4 [ class "media-heading" ] [ text "Suzuki Taro Date:2016/09/01" ]
, div [] [ text "この文章はダミーです。文字の大きさ、量、字間、行間等を確認するために入れています。この文章はダミーです。文字の大きさ、量、字間、行間等を確認するために入れています。" ]
]
]
, hr [] []
, div
[ class "media" ]
[ div [ class "media-body" ]
[ h4 [ class "media-heading" ] [ text "Tanaka Jiro Date:2016/09/01" ]
-- ここが変更点
, div [] [ text model.content ]
]
, div
[ class "media-right" ]
[ a [ href "#", class "icon-rounded" ] [ text "T" ]
]
]
]
]
, section []
[ chatForm
]
]
毎回model.content
とするのは面倒な為、パターンマッチで中身だけを取り出しましょう。
view { content } =
...
, div [] [ text content ]
実行結果
これがTEAを利用したWEBアプリケーションの構築になります。最初は慣れるまで時間が掛かりますが、すべての工程がTEAに乗っかっていくので無駄な思考が要らなくなります。また、テストとコンパイラの強力さがあるため、その2つが通ってさえいれば間違いなく動き、実行時エラーが起きないことが保証されます。またElmのデバッガはとても便利で発行されたMsgとそのときのModelをトレースすることができます。
List.map によるレコードとHtmlの変換
CommentレコードとHtmlの変換
Modelのときと同様にComment
というレコードを定義します。mediaView
関数は、Commentを受け取りチャットのコメントを表すviewを表現します。今回もTDDで実装するため中身は空にします。
module Main exposing (Comment, Msg(..), chatForm, mediaView)
type alias Comment =
{ name : String, content : String }
mediaView : Comment -> Html Msg
mediaView comment =
div [ class "media" ]
[ div [ class "media-left" ]
[ a [ href "#", class "icon-rounded" ] [ text "S" ]
]
, div [ class "media-body" ]
[ h4 [ class "media-heading" ] [ text " Date:2018/12/29" ]
, div [] [ text "" ]
]
]
テストを書きます。suzukiComment = mediaView (Comment "Suzuki Taro" "コメントです。")
に対して、名前とコメントがHtml上に表示されていることを期待するテストを書きます。
module Tests exposing (suite)
import Expect exposing (Expectation)
import Main exposing (..)
import Test exposing (..)
import Test.Html.Event as Event
import Test.Html.Query as Query
import Test.Html.Selector as Selector
suite : Test
suite =
describe "The Main module"
[ describe "chatForm"
[ test "フォームに 'abc' と入力したら UpdateContent 'abc' の Msgが発行される" <|
\_ ->
chatForm
|> Query.fromHtml
|> Query.find [ Selector.tag "input" ]
|> Event.simulate (Event.input "abc")
|> Event.expect (UpdateContent "abc")
]
, describe "mediaView" <|
let
suzukiComment =
mediaView (Comment "Suzuki Taro" "コメントです。")
in
[ test "コメントしたのは、「Suzuki Taro」だ。" <|
\_ ->
suzukiComment
|> Query.fromHtml
|> Query.find [ Selector.class "media-body" ]
|> Query.find [ Selector.tag "h4" ]
|> Query.has [ Selector.text "Suzuki Taro Date:2018/12/29" ]
, test "コメント内容は、「コメントです。」だ。" <|
\_ ->
suzukiComment
|> Query.fromHtml
|> Query.find [ Selector.class "media-body" ]
|> Query.find [ Selector.tag "div" ]
|> Query.has [ Selector.text "コメントです。" ]
]
]
期待通りテストが失敗しました。
テストが通った為、TEAに組み込んでいきます。Modelにcommentsを初期値として追加しましょう。viewではテストしたmediaViewに対して複数のCommentを渡していくので、List.mapを利用しましょう。型として見ると以下のように変換されているわけですね。List同士の連結には、(++)を利用します。
List Comment -> List (Html Msg)
type alias Model =
{ content : String, comments : List Comment }
init : () -> ( Model, Cmd Msg )
init _ =
( { content = ""
, comments =
[ Comment "Suzuki Taro" "1つ目のコメントです。"
, Comment "Suzuki Taro" "2つ目のコメントです。"
, Comment "Suzuki Taro" "3つ目のコメントです。"
]
}
, Cmd.none
)
view : Model -> Html Msg
view { content, comments } =
div [ class "page" ]
[ section [ class "card" ]
[ div [ class "card-header" ]
[ text "Elm Chat"
]
-- ここが変更点。
, div [ class "card-body" ] <|
List.map mediaView comments
++ [ hr [] []
, div
[ class "media" ]
[ div [ class "media-body" ]
[ h4 [ class "media-heading" ] [ text "Tanaka Jiro Date:2016/09/01" ]
, div [] [ text content ]
]
, div
[ class "media-right" ]
[ a [ href "#", class "icon-rounded" ] [ text "T" ]
]
]
]
]
, section []
[ chatForm
]
]
アプリケーションを起動すると、無事反映されていることがわかりました。
ソースコード
この時点でのソースコード