Elmでフォームを組むときの素晴らしいライブラリとして、@arowMさんのelm-form-decoderがあります(このライブラリの考えは次の記事をご参照ください。フォームバリデーションからフォームデコーディングの時代へ)。このライブラリはとても単純な例でも大きな効果を発揮するため、紹介していきたいと思います。例では数字を2つ入力し、足し算をするという簡単なアプリです。
elm-form-decoderを使わずに考えてみる
まずは、form-decoderを使わずにやってみましょう。Model、update、viewの入力部分は使う場合でも共通です。
Formのinputは常に文字列を返します。そのためModelの定義もStringとして左と右の入力を定義します。
type alias Model =
{ left : String
, right : String
}
init : () -> ( Model, Cmd Msg )
init _ =
( { left = "1", right = "1" }, Cmd.none )
update関数では、入力された文字列を単にModelにセットするだけです。
type Msg
= UpdateLeft String
| UpdateRight String
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
UpdateLeft left ->
( { model | left = left }, Cmd.none )
UpdateRight right ->
( { model | right = right }, Cmd.none )
view関数は以下のようになります。
view : Model -> Browser.Document Msg
view model =
{ title = "足し算くん"
, body =
[ main_ []
[ div []
[ input [ onInput UpdateLeft, placeholder "1", value model.left ] []
, span [] [ text "+" ]
, input [ onInput UpdateRight, placeholder "1", value model.right ] []
, span [] [ text "=" ]
]
, p [] [ text <| result model.left model.right ]
]
]
}
そして問題の計算部分です。文字列のままでは解答が得られないため、String.toInt関数で数値に変換します。変換に成功したときは、足し算をしてHtmlに表示するために文字列に変換し、変換に失敗した場合には、「エラーがあります」という文字列を返します。このアプローチの問題は、数値が入力された場合の処理とエラーの種類を特定するための分岐の処理が膨大になる上に分離できなくなってしまう点です。また、正常な場合も異常な場合もエラーの型が同一であることも問題として挙げられます。
-- Stringでは正常な答えか、エラーかわからない
result : String -> String -> String
result left right =
case ( String.toInt left, String.toInt right ) of
( Just leftV, Just rightV ) ->
String.fromInt <| leftV + rightV
-- 本格的な分岐をしようとすると、とてつもない分岐の量になる。
_ ->
"エラーがあります"
elm-form-decoderを使って考えてみる
それではform-decoderを使う部分について考えてみましょう。DecoderはErrorを明示的に定義する必要があります。今回はそのレールに乗りましょう。今回は数字以外の文字などを入力されたときの不正と数字の最大値のエラーを左と右の入力に対して定義します。また、エラーをユーザに伝える文章を返す関数を定義しましょう。先ほどと比べるとエラーだけの分岐のため分岐が楽ですね。
type Error
= LeftInvalid
| RightInvalid
| LeftTooBig
| RightTooBig
errorToText : Error -> String
errorToText error =
case error of
LeftInvalid ->
"左に数字以外が入力されています"
RightInvalid ->
"右に数字以外が入力されています"
LeftTooBig ->
"左に6桁以上の数字が入力されています"
RightTooBig ->
"右に6桁以上の数字が入力されています"
Decoderを定義していきます。詳しい使い方の説明はドキュメントに任せます。
-- 入力がString, Decode後は数値となる
left : Decoder String Error Int
left =
-- 数値の変換とエラー
Decoder.int LeftInvalid
-- 指定した数値を超えた場合のエラー
|> Decoder.assert (Decoder.maxBound LeftTooBig 99999)
-- Decodeの入力(String)をModel(Record)のフィールド(left)に変換する
left_ : Decoder Model Error Int
left_ =
Decoder.lift .left left
right : Decoder String Error Int
right =
Decoder.int RightInvalid
|> Decoder.assert (Decoder.maxBound RightTooBig 99999)
right_ : Decoder Model Error Int
right_ =
Decoder.lift .right right
form : Decoder Model Error String
form =
-- Decodeした結果の2つの数値を足し 文字列とする
Decoder.top (\leftV rightV -> String.fromInt <| leftV + rightV)
|> Decoder.field left_
|> Decoder.field right_
Decoder.run関数を用いて入力をDecoderに渡し、Decodeする。正常な場合はそのまま出力し、エラーがある場合はエラーをリストで表示する。
case Decoder.run form model of
Ok result ->
p [] [ text result ]
Err errors ->
ul [] <|
List.map
(\error ->
li [ style "color" "red" ] [ text <| errorToText error ]
)
errors
すると以下のように左と右のエラーを扱えるようになります!
まとめ
ユーザの入力は異常系の処理を考えることが難しい上にコードが煩雑になりやすい部分になります。異常系を握りつぶすことは簡単ではありますが、その分UXは不親切なものになります。elm-form-decoderは異常系と正常系へのデータ変換の構築をおこないやすいような設計を提供するツールになります。これでフォームプログラミングはもう怖くありません!それでは素敵なElmライフを!