Elm 0.18 入門 (2)パラメータ付きのアクションと入力フォーム

前回に引き続き、サンプルアプリケーションを構築しながらElmを学んでいきましょう。

今回はカウントアップ機能を拡張しながら、入力フォームの使い方やエラーハンドリングの処理を学んでいきましょう。

  • 増加幅のパラメータ化
  • フォーム入力によるパラメータの更新
  • 文字列の数値変換とエラーハンドリング

増加幅をパラメータ化する

前回までの内容では Increase+1 するのみでした。
まずはこの +1 をパラメータにしてみましょう。

 type Msg
     = NoOp
-    | Increase
+    | Increase Int

引数を設定したので、 Increase 1 などのようにアクションに対応できるように update を変更します。

 update : Msg -> Model -> Model
 update msg model =
     case msg of
         NoOp ->
             model

-        Increase ->
-            { model | count = model.count + 1 }
+        Increase num ->
+            { model | count = model.count + num }

この Increase はViewの onClick で呼び出されているのでした。こちらも呼び出し方を変更します。

 increaseButton : Html Msg
 increaseButton =
     div []
-        [ button [ onClick Increase ]
+        [ button [ onClick (Increase 1) ]
             [ text "+1" ]
         ]

ここで気をつけたいのが、 onClick Increase 1 と書くとエラーになる点です。
括弧がない場合は 1onClick の第2引数とみなされてしまうので、必ず (Increase 1) と括弧で囲います。

これでコンパイル可能です。ただ、増加幅をパラメータ化しただけで値は同じなので、前回までの動きと一切替わりません。
試しに Increase 5 のように呼び出し方を変えたものを追加してみましょう。

increaseButton : Html Msg
increaseButton =
    div []
        [ button [ onClick (Increase 1) ] [ text "+1" ]
        , button [ onClick (Increase 5) ] [ text "+5" ]
        , button [ onClick (Increase -1) ] [ text "-1" ]
        ]

コンパイルしてみて動作確認できるでしょうか?

ココまでのコード -> https://ellie-app.com/46Wjz8WRPzda1/1

増加幅のフォーム入力

増加幅の数値自体をアプリケーションの入力項目にしてみましょう。
まず、入力フォームの文字列(入力時点では数値ではない)をアプリケーション内部の状態として受け取れるようにします。

 type alias Model =
     { count : Int
+    , countStepInput : String   
     }

初期値は空文字にしておきましょう。

 initModel : Model
 initModel =
     { count = 0
+    , countStepInput = ""
     }

続いて、この値を更新するためのアクションを追加します。

 type Msg
     = NoOp
     | Increase Int
+    | UpdateCountStepInput String
 update : Msg -> Model -> Model
 update msg model =
     case msg of
         NoOp ->
             model

         Increase num ->
             { model | count = model.count + num }

+        UpdateCountStepInput s ->
+            { model | countStepInput = s }

最後に、Viewからこのアクションを呼び出すフォームを作ります。

stepInput : Model -> Html Msg
stepInput model =
    form []
        [ input [ onInput UpdateCountStepInput, value model.countStepInput ] []
        ]

フォームを全体のViewに組み込みます。(ついでに内部状態が見えるように出力を追加してます)

 view : Model -> Html Msg
 view model =
     div []
         [ counter model
         , increaseButton
+        , stepInput model
+        , text ("countStepInput = " ++ model.countStepInput)
         ]

追加したタグやイベント、更にHTMLの属性値 (value) が増えました。 import を変更しておきましょう。

-import Html exposing (Html, text, p, div, button)
-import Html.Events exposing (onClick)
+import Html exposing (Html, text, p, div, button, form, input)
+import Html.Events exposing (onClick, onInput)
+import Html.Attributes exposing (value)

コンパイルして動作確認してみましょう。入力した内容がちゃんと出力されていますか?

onInput について

onClick (Increase 1) のときと異なり、 onInput UpdateCountStepInput ではアクションに渡す引数を指定しませんでした。公式ドキュメントを確認してシグネチャの違いを確認しましょう。

http://package.elm-lang.org/packages/elm-lang/html/2.0.0/Html-Events

onClick : msg -> Attribute msg
onInput : (String -> msg) -> Attribute msg

ここで、 msg は型引数(各アプリケーションで実際の方が決まる)で、今回のアプリケションでは Msg になります。
つまり、 onClick では Msg そのものを引数にするのに対し、 onInput では String -> Msg という関数を引数にしています。
UpdateCountStepInputString を引数にして Msg を返す関数そのものであるため、 onInput の引数にすることができました。

ココまでのコード -> https://ellie-app.com/46Wjz8WRPzda1/2

入力の数値変換とエラーハンドリング

さて、入力を取得することができたので入力された値を増加幅にしましょう。
増加幅を定義します。

 type alias Model =
     { count : Int
     , countStepInput : String
+    , countStepNum : Int
     }


 initModel : Model
 initModel =
     { count = 0
     , countStepInput = ""
+    , countStepNum = 0
     }

アクションも追加しましょう。このルーチンにだんだん慣れてきましたか?

 type Msg
     = NoOp
     | Increase Int
     | UpdateCountStepInput String
+    | UpdateCountStepNum Int


 update : Msg -> Model -> Model
 update msg model =
     case msg of
         NoOp ->
             model

         Increase num ->
             { model | count = model.count + num }

         UpdateCountStepInput s ->
             { model | countStepInput = s }
+            
+        UpdateCountStepNum num ->
+            { model | countStepNum = num }

Viewを作成する前にちょっと考えておくべきことがあります。
countStepInput はどんな文字列でも入力してしまえるため、数値に変換しようとするときに数値以外のケースで問題が起こることは容易に想像できます。
その問題は文字列を数値に変換する関数 toInt のシグネチャに具体的に現れています。

http://package.elm-lang.org/packages/elm-lang/core/5.1.1/String#toInt

toInt : String -> Result String Int

さらに Result 型の定義はこちらです。

http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Result

type Result error value
    = Ok value
    | Err error

Ok は成功したときの値を返し、 Err でエラーが発生していることを表現しています。つまり、 toInt
の実行にエラーが起こりうることを Result String Int 型として表現してます。
今回はエラーが発生した場合はなにもしないことにしましょう。

convertInputToMsg : String -> Msg
convertInputToMsg s =
    case (toInt s) of
        Ok num ->
            UpdateCountStepNum num

        Err msg ->
            NoOp

ここで初めて NoOp を実際に使いました。「何もしない」というアクションが定義されていることで利点のある例です。

さて、フォームに入力された値を変換してモデルを更新できるようにしましょう。

 stepInput : Model -> Html Msg
 stepInput model =
     form []
         [ input [ onInput UpdateCountStepInput, value model.countStepInput ] []
+        , input [ type_ "button", onClick (convertInputToMsg model.countStepInput), value "Update"] []
         ]

type_ というボタン用の属性値が増えました。これを import しておきましょう。
なお、 type は予約語であるためHTMLの属性を表す関数は type_ となっています。

-import Html.Attributes exposing (value)
+import Html.Attributes exposing (value, type_)
+import String exposing (toInt)

ただ、このままでは実際に増加させるボタンがありません。ボタンを変更しておきましょう。

-increaseButton : Html Msg
-increaseButton =
+increaseButton : Model -> Html Msg
+increaseButton model =
     div []
         [ button [ onClick (Increase 1) ] [ text "+1" ]
-        , button [ onClick (Increase 5) ] [ text "+5" ]
-        , button [ onClick (Increase -1) ] [ text "-1" ]
+        , button [ onClick (Increase model.countStepNum) ] [ text ("Add " ++ (toString model.countStepNum)) ]
         ]

モデルの状態を入力にしたので、呼び出し元も変更しましょう。

 view : Model -> Html Msg
 view model =
     div []
         [ counter model
-        , increaseButton
+        , increaseButton model
         , stepInput model
         , text ("countStepInput = " ++ model.countStepInput)
         ]

さて、上手く行ったでしょうか?

https://gyazo.com/2cf7ea86e260d88359bf3bad8e4bbf07

他に数値でない文字列や、ゼロやマイナスの値などを入力して動作確認してみましょう。
また、DEBUGを押して実際に発行されているアクションに関してもチェックしてみましょう。

ココまでのコード -> https://ellie-app.com/46Wjz8WRPzda1/3

まとめ

パラメータ付きのアクションの定義と、数値変換のエラーハンドリングについて見てきました。
いかがだったでしょうか?

次回は beginnerProgram から飛び出してアクションの連鎖などをやってみましょう。
http://qiita.com/mather314/items/01acf5d72b36dc55bbe1

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.