how to detect shift-enter in Elm? - elm

I've adapted the onentercode from the todomvc example to create onShiftEnter, but it does not work. Apparently, shiftKey is not passed to Elm. So, how can I detect shift-Enter ?
onShiftEnter : Msg -> Attribute Msg
onShiftEnter msg =
let
tagger (code, shift) =
if code == 13 && shift then msg else NoOp
in
on "keydown"
(Json.Decode.map tagger
( Json.Decode.tuple2 (,)
(Json.Decode.at ["keyCode"] Json.Decode.int)
(Json.Decode.at ["shiftKey"] Json.Decode.bool)
)
)

use Json.Decoder.object2 instead.
Json.Decoder.tuple2 is used for decoding arrays.
import Json.Decode as Json exposing ((:=))
onShiftEnter : Msg -> Attribute Msg
onShiftEnter msg =
let
tagger (code, shift) =
if code == 13 && shift then msg else NoOp
keyExtractor =
Json.object2 (,)
("keyCode" := Json.int)
("shiftKey" := Json.bool)
in
on "keydown" <| Json.map tagger keyExtractor

Judging by https://developer.mozilla.org/en-US/docs/Web/Events/keydown, you need, 'Key', not 'Code', i.e.
(Json.Decode.at ["shiftKey"] Json.Decode.bool)

Elm has changed since #Tosh's answer. Neither tuple2 nor object2 exist as standard decoding functions. Neither := nor (,) is available. It would now look more like:
import Json.Decode as Json
onShiftEnter : Msg -> Attribute Msg
onShiftEnter msg =
let
tagger ( code, shift ) =
if code == 13 && shift then
msg
else
NoOp
keyExtractor =
Json.map2 Tuple.pair
(Json.field "keyCode" Json.int)
(Json.field "shiftKey" Json.bool)
in
on "keydown" <| Json.map tagger keyExtractor
Though I personally prefer import Json.Decode as Decode as a convention.
I would edit Tosh's answer but it is accurate at the time of writing and matches the style & version of the code in the question.

Related

How to create SPA with Elm 0.19?

I am trying to build a SPA with Elm and create three pages, that should show the content, depends on URL.
The content of these three pages are similar, for example Page.elm:
module Page.NotFound exposing (Msg(..), content)
import Html exposing (..)
import Html.Attributes exposing (..)
---- UPDATE ----
type Msg
= NotFoundMsg
content : Html Msg
content =
p [] [ text "Sorry can not find page." ]
In the Main.elm, I have the following code:
module Main exposing (Model, Msg(..), init, main, update, view)
import API.Keycloak as Keycloak exposing (..)
import Browser
import Browser.Navigation as Nav
import Html exposing (..)
import Html.Attributes exposing (..)
import Json.Decode as Decode
import Page.Account as Account
import Page.Home as Home
import Page.NotFound as NotFound
import Route
import Url
import Url.Parser exposing ((</>), Parser, int, map, oneOf, parse, s, string)
---- MODEL ----
type alias Model =
{ key : Nav.Key
, url : Url.Url
, auth : Result String Keycloak.Struct
}
init : Decode.Value -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
init flags url key =
( Model key url (Keycloak.validate flags), Cmd.none )
---- ROUTE ----
type Route
= Account
---- UPDATE ----
type Msg
= PageNotFound NotFound.Msg
| PageAccount Account.Msg
| PageHome Home.Msg
| LinkClicked Browser.UrlRequest
| UrlChanged Url.Url
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
LinkClicked urlRequest ->
case urlRequest of
Browser.Internal url ->
( model, Nav.pushUrl model.key (Url.toString url) )
Browser.External href ->
( model, Nav.load href )
UrlChanged url ->
( { model | url = url }
, Cmd.none
)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none
---- VIEW ----
info : Html Msg
info =
header [] [ text "Header" ]
createLink : String -> Html Msg
createLink path =
a [ href ("/" ++ path) ] [ text path ]
navigation : Html Msg
navigation =
ul []
[ li [] [ createLink "home" ]
, li [] [ createLink "account" ]
]
content : Model -> Html Msg
content model =
main_ []
[ case parse Route.parser model.url of
Just path ->
matchedRoute path
Nothing ->
NotFound.content
]
matchedRoute : Route.Route -> Html Msg
matchedRoute path =
case path of
Route.Home ->
Home.content
Route.Account ->
Account.content
body : Model -> List (Html Msg)
body model =
[ info
, navigation
, content model
]
view : Model -> Browser.Document Msg
view model =
{ title = "Cockpit"
, body = body model
}
---- PROGRAM ----
main : Program Decode.Value Model Msg
main =
Browser.application
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
, onUrlChange = UrlChanged
, onUrlRequest = LinkClicked
}
The compiler complains:
-- TYPE MISMATCH -------------- /home/developer/Desktop/elm/cockpit/src/Main.elm
The 2nd branch of this `case` does not match all the previous branches:
104| [ case parse Route.parser model.url of
105| Just path ->
106| matchedRoute path
107|
108| Nothing ->
109| NotFound.content
^^^^^^^^^^^^^^^^
This `content` value is a:
Html NotFound.Msg
But all the previous branches result in:
Html Msg
Hint: All branches in a `case` must produce the same type of values. This way,
no matter which branch we take, the result is always a consistent shape. Read
<https://elm-lang.org/0.19.0/union-types> to learn how to “mix” types.
-- TYPE MISMATCH -------------- /home/developer/Desktop/elm/cockpit/src/Main.elm
Something is off with the 2nd branch of this `case` expression:
120| Account.content
^^^^^^^^^^^^^^^
This `content` value is a:
Html Account.Msg
But the type annotation on `matchedRoute` says it should be:
Html Msg
-- TYPE MISMATCH -------------- /home/developer/Desktop/elm/cockpit/src/Main.elm
Something is off with the 1st branch of this `case` expression:
117| Home.content
^^^^^^^^^^^^
This `content` value is a:
Html Home.Msg
But the type annotation on `matchedRoute` says it should be:
Html Msg
Detected errors in 1 module.
I know that the type is wrong, but do not know, how to prove it.
How can I get it to work?
I also looked at the example from https://github.com/rtfeldman/elm-spa-example/blob/master/src/Main.elm but could not figure, how does it work.
You have multiple Msg types, which is OK, but it can lead to confusion. In short: Main.Msg is not the same type as NotFound.Msg.
The function matchedRoute returns a Html Main.Msg while the function NotFound.content returns a Html NotFound.Msg; completely different types.
You're already 99% of the way there because you have a PageNotFound NotFound.Msg type constructor which produces a Main.Msg. This allows you to wrap the NotFound.Msg in a Main.Msg. It should be a matter of doing PageNotFound NotFound.content in your Nothing -> branch.
The problem is that the Msg type referred to by NotFound.content is NotFound.Msg, the Msg type referred to by Main.matchedRoute is Main.Msg, and these do not unify automatically. So when you use these in different branches of a case expression, the compiler will tell you they are different and can't be unified into a single type for the case expression to return.
So you have to convert one to the other, and the usual way to do that is to add a variant to the "outer" msg type (Main.Msg) that wraps the "inner" msg type (NotFound.Msg). Fortunately you've already added that variant as PageNotFound NotFound.Msg, so we can move on.
The next step is to do the wrapping of NotFound.Msgs in PageNotFounds. Unfortunately, we rarely get to handle values of NotFound.Msg alone, it's usually wrapped in some other type like Html or Cmd, which is trickier to deal with. Fortunately, Evan was foreknowing enough to predict this scenario and added Cmd.map and Html.map for us to use. Just like List.map and Maybe.map, Cmd.map and Html.map takes a function a -> b and uses it to convert Html as or Cmd as to Html bs or Cmd bs respectively.
So, all you really need to do here is use Html.map with PageNotFound on NotFound.content:
content : Model -> Html Msg
content model =
main_ []
[ case parse Route.parser model.url of
Just path ->
matchedRoute path
Nothing ->
NotFound.content |> Html.map PageNotFound
]
Both branches will now return Main.Msg and the compiler should be happy :)
And btw, in elm-spa-example, this is being done here

Can't decode session from elm port

Trying to get elm ports working to maintain the session.
In index.html, the script includes the following listener:
window.addEventListener("load", function(event) {
app.ports.onSessionChange.send(localStorage.session);
}, false);
localStorage.session looks like this (and it stays there until I've logged out):
{"email":"user#fake.com","token":"eyJhbG...","user_id":1,"handle":"me"}
The definition in Ports.elm is:
port onSessionChange : (Value -> msg) -> Sub msg
This port is connected to Main.elm here (let me know if I've forgotten to include some of the definitions below):
subscriptions : Model -> Sub Msg
subscriptions model =
Ports.onSessionChange sessionChange
sessionChange : Json.Decode.Value -> Msg
sessionChange value =
let
result =
Json.Decode.decodeValue sessionDecoder value
in
case result of
Ok sess ->
SetSession (Just sess)
Err err ->
SetSession Nothing
...
type alias Session =
{ email : String
, token : String
, user_id : Int
, handle : String
}
...
import Json.Decode as Decode exposing (..)
import Json.Decode.Pipeline as Pipeline exposing (decode, required)
sessionDecoder : Decode.Decoder Session
sessionDecoder =
Pipeline.decode Session
|> Pipeline.required "email" Decode.string
|> Pipeline.required "token" Decode.string
|> Pipeline.required "user_id" Decode.int
|> Pipeline.required "handle" Decode.string
...
type Msg
= NoOp
| SetSession (Maybe Session)
...
update msg model =
case msg of
SetSession session ->
case Debug.log "session = " session of
Just sess ->
({ model | session = sess } , Cmd.none)
Nothing ->
(model, Cmd.none)
Debug.log "session" displays Nothing in the console when the page loads, so JS is talking to elm, but the decoder seems to be failing. Any ideas?
I've plugged your code into a minimal working example and everything works fine. You might want to log the value of localStorage.session from inside the javascript portion to make sure it's a valid JSON value.

How do I implement and return a Cmd Msg?

How do I implement and return a Cmd Msg?
For example, the following line generates a Cmd Msg:
Http.send msg request
It's used in the following function:
tryRegister : Form -> (Result Http.Error JsonProfile -> msg) -> Cmd msg
tryRegister form msg =
let
registerUrl =
"http://localhost:5000/register"
body =
encode form |> Http.jsonBody
request =
Http.post registerUrl body decoder
in
Http.send msg request
I'm trying to hand code a similar function within my TestAPI:
tryRegister : Form -> (Result Http.Error JsonProfile -> msg) -> Cmd msg
tryRegister form msg =
Cmd.none
The above code compiles. However, it's not clear to me how to implement a function that returns a Cmd Msg other than Cmd.none.
Appendix:
type Msg
=
...
| Submit
| Response (Result Http.Error JsonProfile)
update : Msg -> Form -> ( Form, Cmd Msg )
update msg model =
case msg of
...
Submit ->
( model, runtime.tryRegister model Response )
Source code on GitHub.
Edit
The original answer suggested mapping over Cmd.none, which compiles and may potentially be useful when mocking out functions for testing, but if you are actually trying to force another update cycle in The Elm Architecture, you will need to convert to a Task, as outlined in the send function described here.
send : msg -> Cmd msg
send msg =
Task.succeed msg
|> Task.perform identity
As #SwiftsNamesake mentioned above, in most cases this is not necessary, and the entire blog post on the subject is worth a read.
Original Answer
You can use Cmd.map over Cmd.none to change it to any Cmd:
Cmd.map (always Submit) Cmd.none

How to listen for both keypress and keydown events in Elm?

I want to listen to both keypress and keydown events in Elm. But if I have the following, only the keydown events will be listened to:
textarea
[ onWithOptions "keypress" (Options False True) <| Json.Decode.map KeyPress keyCode
, onWithOptions "keydown" (Options False True) <| Json.Decode.map KeyDown keyCode
] []
If I change the Options to not preventDefault, then both events will be listened to. But I need to preventDefault in order to not let tab keys from changing focus.
Any way to do this in Elm?
In Elm 19, to access keys pressed, you can use Browser.Events which is a part of the Elm Browser library. If you want to capture what key is down, you could use something like this:
import Json.Decode as Decode
type Key
= Character Char
| Control String
subscriptions : Model -> Sub Msg
subscriptions model =
Browser.Events.onKeyDown keyDecoder
keyDecoder : Decode.Decoder Msg
keyDecoder =
Decode.map toKey (Decode.field "key" Decode.string)
toKey : String -> Msg
toKey string =
case String.uncons string of
Just ( char, "" ) ->
PressedLetter char
_ ->
Control string
Adapted from here.
Pre Elm 0.19, I recommend you to use elm-lang/keyboard. This package uses subscriptions and it's very easy to use. You can subscribe to keydown and keyup at the same time.
For your specific case:
The default action for a keydown event is to trigger a keypress event. If you prevent that default behaviour, you will not get the keypress events.
You can read more about keydown events here
Maybe you just need to use keyup and keydown.
I hope it helps you.
Inspired by the docs and HParker's answer, I wrote up a more complete solution using Elm 0.19 and subscriptions.
It adds a Subscription for Browser.Events.onKeyUp/Down, and handles them in a top-level Msg with a subMsg for easier processing. It includes Alt, Ctrl, Shift, and Meta (Windows Key/Option).
Remember that onKeyUp/Down and onKeyPressed handle characters differently (where up/down use the appropriate case, where pressed does not).
import Browser.Events
import Json.Decode as Decode
type KeyEventMsg
= KeyEventControl
| KeyEventAlt
| KeyEventShift
| KeyEventMeta
| KeyEventLetter Char
| KeyEventUnknown String
type Msg
= KeyPressedMsg KeyEventMsg
| KeyReleasedMsg KeyEventMsg
update : Msg -> ( Model, Cmd Msg )
update msg =
case msg of
KeyPressedMsg keyEventMsg ->
case keyEventMsg of
KeyEventShift ->
( { model | shiftIsPressed = True }, Cmd.none )
_ ->
( model, Cmd.none )
KeyReleasedMsg keyEventMsg ->
case keyEventMsg of
KeyEventShift ->
( { model | shiftIsPressed = False }, Cmd.none )
_ ->
( model, Cmd.none )
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ Browser.Events.onKeyDown keyPressedDecoder
, Browser.Events.onKeyUp keyReleasedDecoder
]
keyPressedDecoder : Decode.Decoder Msg
keyPressedDecoder =
Decode.map (toKeyEventMsg >> KeyPressedMsg) (Decode.field "key" Decode.string)
keyReleasedDecoder : Decode.Decoder Msg
keyReleasedDecoder =
Decode.map (toKeyEventMsg >> KeyReleasedMsg) (Decode.field "key" Decode.string)
toKeyEventMsg : String -> KeyEventMsg
toKeyEventMsg eventKeyString =
case eventKeyString of
"Control" ->
KeyEventControl
"Shift" ->
KeyEventShift
"Alt" ->
KeyEventAlt
"Meta" ->
KeyEventMeta
string_ ->
case String.uncons string_ of
Just ( char, "" ) ->
KeyEventLetter char
_ ->
KeyEventUnknown eventKeyString

Elm Html.Events - Pass input value to onBlur message

I have this kind of input:
inputName player =
input
[ type_ "text"
, onInput (Msgs.ChangeName player)
, value player
]
which creates Msgs.ChangeName for every single character added to input.
I would prefer to update model after user leaves input but onBlur doesn't have any payload about input:
inputName player =
input
[ type_ "text"
, onBlur (Msgs.ChangeName player)
, value player
]
Above code doesn't compile ending with error:
The 1st entry has this type:
Html (String -> Msg)
But the 2nd is:
Html (Msg)
Hint: It looks like a function needs 1 more argument.
You can create a variation on the "blur" handler which pulls out target.value like this:
import Html.Events exposing (on, targetValue)
import Json.Decode as Json
onBlurWithTargetValue : (String -> msg) -> Attribute msg
onBlurWithTargetValue tagger =
on "blur" (Json.map tagger targetValue)