LoginSignup
7
4

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-01-29
  • 記事末尾に追記あります。(2020/02/03)

はじめに

Elmでイベントを処理するためには基本的にon関数を使ってリスナを定義していくことになるかと思います。

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

import Json.Decode as Json

onKeyUp : (Int -> msg) -> Attribute msg
onKeyUp tagger =
 on "keyup" (Json.map tagger keyCode)

引用元

そして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イベントリスナを書いてみます。

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で書いてみるとこんな感じになるかと思います

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

※あくまでイメージなので厳密な動作ではありません。。

ここでeventオブジェクトが保持するプロパティを確認してみましょう。
KeyboardEvent

ドキュメントを読み勧めていくと.shiftKeyなるプロパティを発見。

Boolean を返し、そのキーイベントが発生した際に Shift キーが押されていれば true を返します。

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

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の型を確認しておきましょう。

keyCode : Json.Decoder Int

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

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

shiftKey : Json.Decoder Bool

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

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

import Json.Decoder as D

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

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

import Json.Decoder as D

decoder msg =
    D.map2 msg keyCode shiftKey

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

Int -> Bool -> a

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

リスナを作る

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

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アーキテクチャに組み込むとこんな感じでしょうか。

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関数におけるパターンマッチが少々冗長な印象を受けます。
そこで上記のサンプルコードを少し改修してみます。

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を変更しました。
それでは続けてリスナも書き換えていきます。

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の第1引数に渡される関数内で
キーコード等のパターンマッチを行うよう修正しました。

終わりに

お疲れ様でした!

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

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

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

2020/02/03追記

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

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

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

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

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

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

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

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

-- After
onKeyDownEnterWithShift : msg -> Attribute msg

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

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

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

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

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

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

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

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関数を修正する。

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で発火するイベントリスナ」を書くことができました!

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

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

7
4
3

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
7
4