16
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Elm2Advent Calendar 2019

Day 3

ElmでHTMLのイベントに詳しくなろう

Posted at

ElmはAltJSの中でもJavaScriptをあまり意識しなくて良い部類の言語ですが、イベントに関してはHTMLとJavaScriptの仕様を意識する必要があります。もしくはElmを学ぶことでイベントについて詳しくなるチャンスとも言えます。この記事ではElmでイベントに関するいくつかのサンプルを見ていくことで、Elmとイベントについて楽しく学べればと思います。

完成品のサンプルソースコードになります。この先の説明では、この完成品のソースコードを抜粋しているので動かして学びたい場合は、cloneをお願いします。

inputイベント

まずは、onInput関数でinput type="text"の入力を扱ってみましょう。inputイベントとはどのようなイベントでしょうか? MDN (input_event)
の説明を見てみましょう。

input イベントは、 <input>, <select>, <textarea> の各要素の value が変更されたときに発生します。

Google検索のインクリメンタルサーチのように、1文字打つたび候補や検索結果が変わる類の体験をさせたい場合は、これを使えば良いでしょう。早速Elmのプログラムを見てみましょう。常に打たれた文字列を取得するため、onInputに渡すMsgの型は、(String -> msg)となります。何故このような型になるかは、onInput関数の仕組みを知ればわかります。

-- (一部抜粋)

-- MODEL
type alias Model =
    { inputedText : String
    }


-- UPDATE
type Msg
    = InputText String


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        InputText inputedText ->
            ( { model | inputedText = inputedText }, Cmd.none )


-- VIEW
div []
    [ input [ type_ "text", value model.inputedText, onInput InputText ] []
      , p [] [ text model.inputedText ]
    ]

Msgの謎を解くために、もう一度、MDNの例を覗いてJavaScriptの気持ちになってみましょう。JavaScriptでは、addEventListnerを使ってinputイベントを監視します。コールバック関数をElm風に考えてみると、(Event -> Void)と言うのが正体になります。

ElmではVoidを許していないので、Msgを発行してupdateでModelを変更する必要があります。このような型(Event -> Msg)になります。しかし、inputイベントのほとんどの場合、value(String)に興味はないでしょう。我々がコールバック関数に求める型は、(String -> Msg)なはずです。

input.addEventListener('input', updateValue);

function updateValue(e) {
  log.textContent = e.target.value;
}

そのためには、EventをStringに変えるつなぎが必要となります。ここで、onInput関数のソースコードを見てみます。むむ、何やら難しそうですが、targetValueがイベントからvalueを取り出す(e.target.value)鍵になっていそうです。

onInput : (String -> msg) -> Attribute msg
onInput tagger =
  stopPropagationOn "input" (Json.map alwaysStop (Json.map tagger targetValue))

targetValue関数を見てみると、全ての真実がわかりました。Json.Decoderによってイベントオブジェクト(e)の中身を取り出していただけに過ぎません。

targetValue : Json.Decoder String
targetValue =
  Json.at ["target", "value"] Json.string

原理さえわかってしまえば、何故このようなシグネチャになっているか理解するのはシンプルですね。

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

changeイベント

changeイベントは、様々な用途に使われるイベントです。MDN(change_event)の説明を見てみましょう。

change イベントは <input>, <select>, <textarea> 要素において、ユーザーによる要素の値の変更が確定したときに発生します。 input イベントとは異なり、 change イベントは要素の値 (value) が変更されるたびに発生するとは限りません。

変更される要素の種類やユーザーが要素を操作する方法によって、 change イベントは異なる時点で発生します。

それでは、type別にElmのコードと学びを深めてみましょう。

input type="text"

まずは、MDNから見てみましょう。これはどのようなときに役に立つのでしょうか? 例えば入力によってHTTP通信が発生させたいが、あまり大きい頻度で発生させたくないときや入力のバリデーションを最終入力時のみに掛けたいときなどの体験をさせたいときに役立ちます。

要素の値が変更されたが、確定しないうちに要素がフォーカスを失ったとき (たとえば、 または の値を編集した後に、要素がフォーカスを失った場合)。

Elmのコードは、inputイベントの時とほとんど変わりはありません。valueが流れてくるタイミングが違うだけで、型は何も変化しないためです。

-- (一部抜粋)

-- MODEL
type alias Model =
    { inputedText : String
    }

-- UPDATE
type Msg
    | ChangeText String

-- VIEW
div []
    [ input [ type_ "text", value model.changedText, onChangeValue ChangeText ] []
    , p [] [ text model.changedText ]
    ]

しかし、changeイベントを扱うためのonChangeValueなどと言う関数は、存在しません。これは自作する必要があります。構える必要はありません。先ほどイベントとElmの関係性は理解しているはずです。まず自身で手軽にイベントをフックしたい場合には、on関数を使います。まず第一引数には、イベントを文字列("change")で指定します。次にmsgを解釈するJson.Decoderを作りあげれば良いのですが、これは先ほどと変わりません。イベントからvalue(e.target.value)を取り出せればいいので、targetValue関数を使います。あとは、Stringを返してMsgを返す関数を受け取りmapしてあげます。(この記事ではJson.Decoderの詳しい説明は行いません。一旦感覚的に乗り越えてください。)

onChangeValue : (String -> Msg) -> Attribute Msg
onChangeValue tagger =
    on "change" <| Json.map tagger targetValue

input type="radio"

続いてラジオボタンを扱ってみましょう。ラジオボタンのMDNです。ラジオボタンは、nameでグルーピングされ、valueによってどのラジオボタンかを判別するのが常套手段です。ラジオボタンのchangeイベントは要素が:checkedになったときに流れますが、欲しい値はvalueです。そのため、先ほどのtextと扱いは変わりません。

<input type="radio"> および <input type="checkbox"> の場合は、 (クリックまたはキーボードを使用して) 要素が :checked になったとき。

今回は少し工夫として、ラジオボタンの選択肢をカスタムタイプで扱ってみましょう。これは単体ではあまり意味を為しませんが、ラジオボタンがある状態のときに別の操作ができる場合などのロジックを組むときに有効です。設定画面のようなものを想像すると良いでしょう。

-- MODEL
type ContactType
    = Email
    | Phone
    | Mail


type alias Model =
    { contactTypeMaybe : Maybe ContactType
    }


--UPDATE
type Msg
    = ChangeContactType String


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ChangeContactType contactTypeMaybe ->
            ( { model | contactTypeMaybe = value2ContactType contactTypeMaybe }, Cmd.none )


-- VIEW
div [ class "contact-group" ]
    [ input [ id "contactChoice1", name "contact", type_ "radio", value "email", onChangeValue ChangeContactType ] []
       , label [ for "contactChoice1" ] [ text "電子メール" ]
       , input [ id "contactChoice2", name "contact", type_ "radio", value "phone", onChangeValue ChangeContactType ] []
       , label [ for "contactChoice2" ] [ text "電話" ]
       , input [ id "contactChoice3", name "contact", type_ "radio", value "mail", onChangeValue ChangeContactType ] []
       , label [ for "contactChoice3" ] [ text "郵便" ]
     ]
     , p []
           [ text <|
              case model.contactTypeMaybe of
                  Just contactType ->
                      contactType2Text contactType ++ "が選択されています。"

                  Nothing ->
                      "何も選択されていません。"
           ]
     ]

contactType2Text : ContactType -> String
contactType2Text contactType =
    case contactType of
        Email ->
            "電子メール"

        Phone ->
            "電話"

        Mail ->
            "郵便"


value2ContactType : String -> Maybe ContactType
value2ContactType contact =
    case contact of
        "email" ->
            Just Email

        "phone" ->
            Just Phone

        "mail" ->
            Just Mail

        _ ->
            Nothing

input type="checkbox"

最後にチェックボックスを扱ってみましょう。今回は、checkedになっているかいないか、つまりBool値を取得してみましょう。

<input type="radio"> および <input type="checkbox"> の場合は、 (クリックまたはキーボードを使用して) 要素が :checked になったとき。

今回は、MsgがBoolを受け取るのが変化している点です。

-- MODEL
type alias Model =
    { consentValue : Bool
    }

-- UPDATE
type Msg
    = CheckConsent Bool

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        CheckConsent consentValue ->
            ( { model | consentValue = consentValue }, Cmd.none )

-- VIEW
div []
    [ input [ type_ "checkbox", checked model.consentValue, onCheck CheckConsent ] []
    , p [] [ text <|
                    if model.consentValue then
                        "同意ありがとうございます。"

                    else
                        "同意してください。"
            ]
     ]

Boolを受け取る秘密をonCheck関数から見てみます。もうお分かりですね? targetChecked関数に全ての答えがあります。

onCheck : (Bool -> msg) -> Attribute msg
onCheck tagger =
  on "change" (Json.map tagger targetChecked)

もはやElmからイベントについて詳しくなれそうです。

targetChecked : Json.Decoder Bool
targetChecked =
  Json.at ["target", "checked"] Json.bool

チェックボックスが複数ある場合は、応用編になります。こちらの記事をご覧ください。

まとめ

Elmでイベントを扱う仕組みはどうだったでしょうか。JavaScriptではEventと言う巨大なオブジェクトを扱う必要がありましたが、ElmではJson.Decoderを使い主要な値とイベントの種類のみにフォーカスして扱うことができます。Json.Decoderの扱い方さえ覚えてしまえば、Elmの世界はシンプルに保ちながら、JavaScriptのイベントを自由に扱うことができます。また、Elmのライブラリからイベントの仕組みを逆に簡単に学ぶことも可能です。それでは良いElmライフを!

16
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?