31

I built a simple app for learning purposes and want to be able to dispatch an action when the user presses Enter key in input field

view : Model -> Html Action
  view model = 
    let 
      items = List.map (\ item -> li [] [ text item ]) model.items
    in
      div [] [
       input [ onInput Change, value model.content ] [],
       button [ onClick Add ] [ text "Submit" ],
       ul [] items
      ]

Here is the view code. I hope it will be enough to explain my intent for you. What I'd like to have is ability to dispatch some action when user presses the Enter key while he is entering some text to input field.

CHsurfer
  • 1,304
  • 1
  • 15
  • 34
SuperManEver
  • 2,263
  • 6
  • 28
  • 36

6 Answers6

26

You can manually bind to the keydown event with the generic on handler. Elm does currently not support onKeyDown handlers out of the box - but they are planned in the future.

It looks like the spec is moving away from event.keyCode and towards event.key. Once this is supported in more browsers, we may add helpers here for onKeyUp, onKeyDown, onKeyPress, etc. (Source)

Until then you can simply write your own handler and use keycode 13 (enter) to perform your actions. Open the following ellie-app to see how it works. Just enter some text in the input box and press enter to see the current state reflected in the div below the input box.

import Html exposing (text, div, input, Attribute)
import Browser
import Html.Events exposing (on, keyCode, onInput)
import Json.Decode as Json


main =
  Browser.sandbox 
  { init = 
    { savedText = ""
    , currentText = ""
    }
  , view = view
  , update = update
  }


view model =
  div [] 
  [ input [onKeyDown KeyDown, onInput Input] []
  , div [] [ text ("Input: " ++ model.savedText) ]
  ]

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


type Msg 
  = NoOp
  | KeyDown Int
  | Input String


update msg model =
  case msg of

    NoOp ->
      model

    KeyDown key ->
      if key == 13 then
        { model | savedText = model.currentText }
      else
        model

    Input text ->
      { model | currentText = text }
hasanain
  • 764
  • 5
  • 12
dotcs
  • 2,286
  • 1
  • 18
  • 26
  • it looks like you made custom event handler. Can you give some brief explanation on what is going on, please ? – SuperManEver Oct 18 '16 at 18:47
  • @NikitaLuparev keyCode is a JSON decoder documented at the same page that was quoted in the answer above – Greg Nov 06 '16 at 15:50
22

There's a good, straightforward solution to handling onEnter in the Elm version of TodoMVC:

import Html exposing (Attribute)
import Html.Events exposing (keyCode, on)
import Json.Decode as Json

onEnter : Msg -> Attribute Msg
onEnter msg =
    let
        isEnter code =
            if code == 13 then
                Json.succeed msg
            else
                Json.fail "not ENTER"
    in
        on "keydown" (Json.andThen isEnter keyCode)
Quelklef
  • 1,999
  • 2
  • 22
  • 37
bowsersenior
  • 12,524
  • 2
  • 46
  • 52
  • 1
    Where is keyCode coming from? Apparently from `Html.Events`. – Natim Jan 18 '19 at 10:53
  • Yes, keyCode comes from [Html.Events](https://package.elm-lang.org/packages/elm/html/latest/Html-Events#keyCode) I updated the post with import statement to clarify. – bowsersenior Jan 22 '19 at 02:34
5

You could use something like this in your input element, this will fire the given message if enter key gets pressed down:

onEnterPressed : msg -> Attribute msg
onEnterPressed msg =
  let
    isEnter code =
      if code == 13 then Ok () else Err ""
    decodeEnterKeyCode = Json.customDecoder keyCode isEnter
  in on "keydown" <| Json.map (\_ -> msg) decodeEnterKeyCode
Tosh
  • 35,955
  • 11
  • 65
  • 55
  • 3
    Already on community package http://package.elm-lang.org/packages/elm-community/html-extra/latest/Html-Events-Extra#onEnter – rofrol Mar 07 '17 at 20:37
5

The above answers were very good - but storing each letter in the Model on every key press - is not always a good idea.

For example in my case i have a fileSystem-like strucutre - and i want to edit any name - no matter how nested it is - on doubbleclick. I can't have the hole fileSystem view reconstructed with each key press. It's laggy.

I found that it's best to receive the input value - only when the user presses Enter..

type Msg =
    | EditingStarted
    | EditingFinished String
    | CancelEdit

input [ whenEnterPressed_ReceiveInputValue EditingFinished, whenEscPressed_CancelOperation CancelEdit, onBlur CancelEdit ] []

update msg model =
    case msg of
        EditingFinished inputValue ->
            { model | name = inputValue }
        CancelEdit -> ...


whenEnterPressed_ReceiveInputValue : (String -> msg) -> H.Attribute msg
whenEnterPressed_ReceiveInputValue tagger =
  let
    isEnter code =
        if code == 13 then
            JD.succeed "Enter pressed"
        else
            JD.fail "is not enter - is this error shown anywhere?!"

    decode_Enter =
        JD.andThen isEnter E.keyCode
  in
    E.on "keydown" (JD.map2 (\key value -> tagger value) decode_Enter E.targetValue)


whenEscPressed_CancelOperation : msg -> H.Attribute msg
whenEscPressed_CancelOperation tagger =
  let
    isESC code =
        if code == 27 then
            JD.succeed "ESC pressed"
        else
            JD.fail "it's not ESC"

    decodeESC =
        JD.andThen isESC E.keyCode
  in
    E.on "keydown" (JD.map (\key -> tagger) decodeESC)

Note: If you are doing time-traveling debugging - you will not see each letter appearing as it was typed. But all text at once - because there was only one msg.. Depending on what you do - this can be a problem. If not, Enjoy :)

AIon
  • 12,521
  • 10
  • 47
  • 73
  • Unfortunately, this doesn't work because multiple handlers for the same event, `keydown` in this case, aren't supported: https://github.com/elm-lang/virtual-dom/issues/17. See this answer for a working solution https://stackoverflow.com/a/39726025/4618482 – joki May 12 '18 at 11:51
5

If you're willing to use the community package Html.Events.Extra http://package.elm-lang.org/packages/elm-community/html-extra/latest/Html-Events-Extra#onEnter it is very easy.

(Assuming that you want to send the Add message when the enter key is pressed.)

import Html.Events.Extra exposing (onEnter)

view : Model -> Html Action
  view model = 
    let 
      items = List.map (\ item -> li [] [ text item ]) model.items
    in
      div [] [
       input [ onInput Change, onEnter Add, value model.content ] [],
       button [ onClick Add ] [ text "Submit" ],
       ul [] items
      ]
NateW
  • 2,101
  • 3
  • 28
  • 37
2

I liked Alon's answer and iterated on it a bit to create an attribute that responds to <enter> and <esc>

onEscEnter : String -> (String -> msg) -> Attribute msg
onEscEnter originalValue tagger =
    let
        handleKey : Int -> Jdec.Decoder Int
        handleKey code =
            if L.member code [ 13, 27 ] then
                -- Enter (13) or ESC (27)
                Jdec.succeed code
            else
                Jdec.fail "something to ignore"

        combiner : Int -> String -> msg
        combiner keyCode tgtVal =
            if keyCode == 13 then
                tagger tgtVal
            else if keyCode == 27 then
                tagger originalValue
            else
                Debug.crash "onEscEnter"

        keyCodeDecoder : Jdec.Decoder Int
        keyCodeDecoder =
            Jdec.andThen handleKey keyCode
    in
        on "keydown" (Jdec.map2 combiner keyCodeDecoder targetValue)
Natim
  • 17,274
  • 23
  • 92
  • 150
Simon H
  • 20,332
  • 14
  • 71
  • 128