- 記事末尾に追記あります。(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