【Elm】Shift + Enterを認識するイベントリスナを作る

- 記事末尾に追記あります。（2020/02/03）

## はじめに
Elmでイベントを処理するためには基本的に[on](https://package.elm-lang.org/packages/elm/html/latest/Html-Events#on)関数を使ってリスナを定義していくことになるかと思います。

例えば公式のパッケージリファレンスによると、keyupイベントリスナを次のように定義しています。

>```elm
>import Json.Decode as Json
>
>onKeyUp : (Int -> msg) -> Attribute msg
>onKeyUp tagger =
>  on "keyup" (Json.map tagger keyCode)
>```

[引用元](https://package.elm-lang.org/packages/elm/html/latest/Html-Events#keyCode)

そしてElmアーキテクチャ上では次のように利用されます。

```elm
type Msg
    = OnKeyUp Int

update : Msg -> model -> ( model, Cmd Msg )
update msg model =
    case msg of
        OnKeyUp code ->
            case code of
                13 -> -- Enterキーが入力された
                    -- 諸々の更新処理が続く
               
                _ ->
                    ( model, Cmd.none )

view : model -> Html Msg
view model =
    input [ onKeyUp OnKeyUp ] []
```

本稿ではShift + Enterなど、複数のキーが関わる際について考えていきたいと思います。

## 動作環境

Elm 0.19.1

## 方針を考えます。

まずはシンプルなkeydownイベントリスナを書いてみます。

```elm
import Html.Events exposing (keyCode, on)
import Json.Decode as D

onKeyDown : (Int -> msg) -> Attribute msg
onKeyDown msg =
    on "keydown" (D.map msg keyCode)

```

冒頭のkeyupイベントリスナを記述したときとほぼ同じですね。

上記の動作をJSで書いてみるとこんな感じになるかと思います

```js
addEventListener("keydown", event => {
  /* 1.eventオブジェクトからkeyCodeを取り出す処理 */
  /* 2.取得したkeyCodeからmsgを生成する処理 */
  /* 3.生成したmsgをElmアーキテクチャに通知する処理 */
})
```
※あくまでイメージなので厳密な動作ではありません。。

ここでeventオブジェクトが保持するプロパティを確認してみましょう。
[KeyboardEvent](https://developer.mozilla.org/ja/docs/Web/API/KeyboardEvent)

ドキュメントを読み勧めていくと`.shiftKey`なるプロパティを発見。
>Boolean を返し、そのキーイベントが発生した際に Shift キーが押されていれば true を返します。

なるほど。
ということは望ましい動作をするためには
次のような処理をイメージしたら良さそうです。

```js
addEventListener("keydown", event => {
  /* 1-1.eventオブジェクトからkeyCodeを取り出す処理 */
  /* 1-2.eventオブジェクトからshiftKeyを取り出す処理 */
  /* 2.取得したkeyCodeとshiftKeyからmsgを生成する処理 */
  /* 3.生成したmsgをElmアーキテクチャに通知する処理 */
})
```

つまりElm側で「eventオブジェクトからshiftKeyを取り出す」Decoderを用意してあげればなんとかなりそうということが分かりました！

## Json.Decoderを作る。

ここまでさらっと流していましたが、改めて`Html.Events.keyCode`の型を確認しておきましょう。

```elm
keyCode : Json.Decoder Int
```

何らかのオブジェクトから`Int`型の値を取り出すDecoderですね。

次に新たに用意するDecoderは`Bool`型の値を取り出す必要があります。
つまり、

```elm
shiftKey : Json.Decoder Bool
```

が欲しいということが分かります。

前節からプロパティ名は既に明らかなのでさくっと書いてしまいましょう。

```elm
import Json.Decoder as D

shiftKey : D.Decoder Bool
shiftKey =
    D.field "shiftKey" D.bool
```

これで個々のDecoderが揃ったのでmsgを生成するDecoderに変換しましょう！


```elm
import Json.Decoder as D

decoder msg =
    D.map2 msg keyCode shiftKey
```

さて、上記において`msg`の取りうる型はどうなるでしょうか？
答えは、

```elm
Int -> Bool -> a
```

ですね。
Intには`keyCode`,Boolには`shiftKey`から得られた値が渡されます。

## リスナを作る

前節で作成したDecoderからリスナを書いていきましょう！

```elm
import Json.Decoder as D

onKeyDownWithShift : (Int -> Bool -> msg) -> Attribute msg
onKeyDownWithShift msg =
    let
        shiftKey =
            D.field "shiftKey" D.bool

        decoder =
            D.map2 msg keyCode shiftKey
    in
    on "keydown" decoder
```

シンプルにまとまりましたね。
Elmアーキテクチャに組み込むとこんな感じでしょうか。

```elm
type Msg =
    OnKeyDownWithShift Int Bool

update : Msg -> model -> (model, Cmd Msg)
update msg model =
    case msg of
        OnKeyDownWithShift code shiftKey ->
            case (code, shiftKey) of
                (13, True) ->
                    -- Shift + Enterが押されたときの処理
                (_, _) ->
                    -- 上記以外の場合の処理

view : model -> Html Msg
view model =
    input [ onKeyDownWithShift OnKeyDownWithShift ] []
```

お疲れ様でした！
これでElmアーキテクチャで「Shift + Enterが押された状態」を認識することができました。

しかし「Shift + Enterが押された」か「それ以外」かの状態のみを管理することを考えると、
update関数におけるパターンマッチが少々冗長な印象を受けます。
そこで上記のサンプルコードを少し改修してみます。

```elm
type Msg =
    OnKeyDownEnterWithShift Bool

update : Msg -> model -> (model, Cmd Msg)
update msg model =
    case msg of
        OnKeyDownEnterWithShift isPressed ->
            if isPressed then
                -- Shift + Enterが押されたときの処理
            else
                -- 上記以外の場合の処理

view : model -> Html Msg
view model =
    input [ onKeyDownEnterWithShift OnKeyDownEnterWithShift ] []
```

`Bool`のみを取り扱うようMsgを変更しました。
それでは続けてリスナも書き換えていきます。

```elm
import Json.Decode as D

onKeyDownEnterWithShift : (Bool -> msg) -> Attribute msg
onKeyDownEnterWithShift msg =
    let
        shiftKey =
            D.field "shiftKey" D.bool

        f code isShiftKey =
            case ( code, isShiftKey ) of
                ( 13, True ) ->
                    msg True

                ( _, _ ) ->
                    msg False

        decoder =
            D.map2 f keyCode shiftKey
    in
    on "keydown" decoder
```

`D.map2`の第１引数に渡される関数内で
キーコード等のパターンマッチを行うよう修正しました。

## 終わりに

お疲れ様でした！

今回の方法は表題のケースのみに関わらずElmで様々なイベントリスナを書きたいときに有効なケースだと思いますので色々試していければと思っております。

最後に本記事のまとめとしてサンプルコードを載せておきます。


<details><summary>[Elm公式サイトのオンラインエディタ](https://elm-lang.org/try)にコピペで動きます。</summary><div>

```elm
module Main exposing (main)

import Browser
import Html exposing (Attribute, Html, div, input, p, text)
import Html.Attributes exposing (placeholder, style)
import Html.Events exposing (keyCode, on)
import Json.Decode as D



-- Main


main : Program () Model Msg
main =
    Browser.element
        { init = init
        , update = update
        , subscriptions = subscriptions
        , view = view
        }



-- Model


type alias Model =
    Bool


init : flag -> ( Model, Cmd Msg )
init _ =
    ( False, Cmd.none )



-- Update


type Msg
    = OnKeyDownEnterWithShift Bool


update : Msg -> Model -> ( Model, Cmd Msg )
update msg _ =
    case msg of
        OnKeyDownEnterWithShift isPressed ->
            ( isPressed, Cmd.none )



-- Subscriptions


subscriptions : Model -> Sub Msg
subscriptions _ =
    Sub.none



-- View


view : Model -> Html Msg
view model =
    let
        ( color, content ) =
            if model then
                ( "green", "Submit!" )

            else
                ( "red", "Not Submit..." )
    in
    div []
        [ p [ style "color" color ] [ text content ]
        , div []
            [ input
                [ placeholder "message to submit"
                , onKeyDownEnterWithShift OnKeyDownEnterWithShift
                ]
                []
            ]
        ]



-- Custom Events


onKeyDownEnterWithShift : (Bool -> msg) -> Attribute msg
onKeyDownEnterWithShift msg =
    let
        shiftKey =
            D.field "shiftKey" D.bool

        f code isShiftKey =
            case ( code, isShiftKey ) of
                ( 13, True ) ->
                    msg True

                ( _, _ ) ->
                    msg False

        decoder =
            D.map2 f keyCode shiftKey
    in
    on "keydown" decoder
```

</div></details>


## 2020/02/03追記

>https://package.elm-lang.org/packages/Gizra/elm-keyboard-event/latest/Keyboard-Event#considerKeyboardEvent
note : onに与えるDecoderを失敗させるとruntimeにmsgが送られないようになってます

とのアドバイスをいただき早速やってみようと思います！

###リスナをもう一度考える。

前節で実装したリスナはこのような型を持っていました。

```elm
onKeyDownEnterWithShift : (Bool -> msg) -> Attribute msg
```

keydown時に「EnterとShiftキーが同時に押されていたか」をBoolを使って表現しています。
これをブラッシュアップし、「EnterとShiftキーが同時に押された」場合のみmsgを発行するように改修していきます。

出来上がりのイメージはこんな感じです。

```elm
-- Before
onKeyDownEnterWithShift : (Bool -> msg) -> Attribute msg

-- After
onKeyDownEnterWithShift : msg -> Attribute msg
```

###Json.Decoderをもう一度考える。

特定のケースのみ成功するDecoderを書くにあたり、前節で書いたDecoderに`andThen`関数を適用しパターンマッチで成功失敗の場合分けをする方針で実装していきたいと思います。

`andThen`関数の型は次のようになっています。

```elm
andThen : (a -> Decoder b) -> Decoder a -> Decoder b
```

型変数が２つも出てきました。具体的な型を割当ていくと、
最終的に`Decoder msg`を得たいので`b`には`msg`。
キーコードとshiftkeyプロパティを含むDecoderについて処理したいので、
`a`には`(Int, Bool)`を当てはめていけば良さそうです。

```elm
andThen : ((Int, Bool) -> Decoder msg) -> Decoder (Int, Bool) -> Decoder msg
```

ここまでイメージできたところで早速decoderを書いていきます。

```elm
import Json.Decode as D
import Html.Events exposing (keyCode)

shiftKey : D.Decoder Bool
shiftKey =
    D.field "shiftKey" D.bool

decoder : msg -> D.Decoder msg
decoder msg =
    D.map2 Tuple.pair keyCode shiftKey
        |> D.andThen
            (\x ->
                case x of
                    ( 13, True ) ->
                        D.succeed msg

                    ( _, _ ) ->
                        D.fail "failed"
            )

```

`map2`関数で`Decoder (Int, Bool)`に変換、その後`andThen`を適用し
キーコードとshiftkeyプロパティの組み合わせをパターンマッチしています。

これで「EnterとShiftキーが同時に押されていないと失敗する」Decoderが完成しました！

### onKeyDownEnterWithShift関数を修正する。

```elm
import Json.Decode as D
import Html.Events exposing (keyCode, on)

onKeyDownEnterWithShift : msg -> Attribute msg
onKeyDownEnterWithShift msg =
    let
        shiftKey =
            D.field "shiftKey" D.bool

        decoder =
            D.map2 Tuple.pair keyCode shiftKey
                |> D.andThen
                    (\x ->
                        case x of
                            ( 13, True ) ->
                                D.succeed msg

                            ( _, _ ) ->
                                D.fail "failed"
                    )
    in
    on "keydown" decoder

```

お疲れさまでした！
これで「Shift + Enterを認識するイベントリスナ」もとい、
「Shift + Enterで発火するイベントリスナ」を書くことができました！

最後にこちらのリスナを用いたサンプルコードを書いてみます。

<details><summary>[Elm公式サイトのオンラインエディタ](https://elm-lang.org/try)にコピペで動きます。</summary><div>

```elm
module Main exposing (main)

import Browser
import Html exposing (Attribute, Html, div, input, p, text)
import Html.Attributes exposing (placeholder, style)
import Html.Events exposing (keyCode, on)
import Json.Decode as D



-- Main


main : Program () Model Msg
main =
    Browser.element
        { init = init
        , update = update
        , subscriptions = subscriptions
        , view = view
        }



-- Model


type alias Model =
    Bool


init : flag -> ( Model, Cmd Msg )
init _ =
    ( False, Cmd.none )



-- Update


type Msg
    = OnKeyDownEnterWithShift


update : Msg -> Model -> ( Model, Cmd Msg )
update msg _ =
    case msg of
        OnKeyDownEnterWithShift ->
            ( True, Cmd.none )



-- Subscriptions


subscriptions : Model -> Sub Msg
subscriptions _ =
    Sub.none



-- View


view : Model -> Html Msg
view model =
    let
        ( color, content ) =
            if model then
                ( "green", "Submit!" )

            else
                ( "red", "Not Submit..." )
    in
    div []
        [ p [ style "color" color ] [ text content ]
        , div []
            [ input
                [ placeholder "message to submit"
                , onKeyDownEnterWithShift OnKeyDownEnterWithShift
                ]
                []
            ]
        ]



-- Custom Events


onKeyDownEnterWithShift : msg -> Attribute msg
onKeyDownEnterWithShift msg =
    let
        shiftKey =
            D.field "shiftKey" D.bool

        decoder =
            D.map2 Tuple.pair keyCode shiftKey
                |> D.andThen
                    (\x ->
                        case x of
                            ( 13, True ) ->
                                D.succeed msg

                            ( _, _ ) ->
                                D.fail "failed"
                    )
    in
    on "keydown" decoder
```
</div></details>