How do I build keyboard combinations into my Elm app, eg. "shift + alt + enter"? You would do something like this to react to a single key pressed (for example the enter key):
import Keyboard
type Msg
= KeyDown Keyboard.KeyCode
type alias Model =
...
update msg model =
case msg of
KeyDown key ->
handleKeyDown key model
subscriptions model =
Sub.batch
[
Keyboard.downs KeyDown
]
handleKeyDown key model =
case key of
13 -> -- Enter key
Debug.log "Other key"
model
_ -> -- Any other key
Debug.log "Other key"
model
view model =
...
But how can you do the same for multiple keys pressed?
You could use Keyboard.downs as pdoherty926 mentioned as well as a Set to keep track of which keys are pressed. You'll need to look at Keyboard.ups as well to know when a key is released.
Here is a working example:
import Html exposing (..)
import Html.App exposing (program)
import Keyboard exposing (..)
import Set exposing (Set)
import Char
main =
program { init = (initialModel, Cmd.none), view = view, update = update, subscriptions = subscriptions }
initialModel =
{ keysDown = Set.empty
}
view : Model -> Html Msg
view model =
text <| toString <| Set.map Char.fromCode model.keysDown
type Msg
= KeyDown KeyCode
| KeyUp KeyCode
type alias Model =
{ keysDown : Set KeyCode
}
update msg model =
case msg of
KeyDown key ->
({ model | keysDown = Set.insert key model.keysDown }, Cmd.none)
KeyUp key ->
({ model | keysDown = Set.remove key model.keysDown }, Cmd.none)
subscriptions _ =
Sub.batch
[ Keyboard.downs KeyDown
, Keyboard.ups KeyUp
]
From the documentation:
Longer-term, it makes sense to help track key combinations and a couple other common scenarios. If you have a particular problem you think this library could address more directly, please describe it as a SSCCE in an issue. Please do not suggest the solution. Just describe the scenario. Once it becomes clearer what folks are up to, it will be time to add support for those cases in a coherent way.
So, at least it means that it is not supported right now. I'm not fully sure whether it also means that you should open an issue to encourage this later addition or not.
Now, Chad's answer looks like a good temporary solution to me. It looks even better with swelet's suggestion of tracking only a few modifier keys. Note how if you take this approach you don't need to rely on any List or Set but you can instead have a little record with 4 booleans.
Finally, if you want for some reasons to track all the keys (if for instance you would like the user being able to link [F1] to some action, then performance-wise it would make sense to use a more efficient representation of sets. Any subset of a finite set for instance can be defined as a single number, each bit being a boolean meaning absence or presence of one particular element. You can do such things with the Bitwise library.
Related
I'm trying to build a website in Elm and it includes a couple links to change the language:
div [ class "language" ][ a [ class englishClass, onClick (ChangeLanguage English) ] [ text "EN" ]
, a [ class germanClass, onClick (ChangeLanguage German) ] [ text "DE" ]
, a [ class italianClass, onClick (ChangeLanguage Italian) ] [ text "IT" ]
]
My Update looks like this:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
... -- Stuff for URLs
ChangeLanguage language ->
( { model | language = language }
, Cmd.none
)
type Msg
= LinkClicked Browser.UrlRequest
| UrlChanged Url.Url
| ChangeLanguage Language
This is my model:
type alias Model =
{ key : Nav.Key
, url : Url.Url
, language : Language
}
And this is my init function, which (at least in my mind) defaults to English as a language:
init : flags -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
init _ url key = ( Model key url English, Cmd.none )
The problem:
Whenever I click the links that change the language, they do seem to work: I see the language of the paragraphs in the page change, but then several unwanted things happen:
The page refreshes. Since I already saw the language change across the page, it's clear this is not needed so I'd like to avoid it.
As a consequence of 1, the viewport is brought all the way to the top of the page again.
The language changes to the default English again!
How could I avoid 1 (and thus 2) from happening?
And what am I missing to make it so the change is maintained across the site (at least when refreshing the page, since I haven't tried working with sessions or cookies yet)?
I consider this behaviour a bug in Elm, and have filed an issue for it, as have others with associated PRs. But in the year and a half since have received no attention from anyone in a position to actually do something about it, which is unfortunately par for the course with Elm.
The problem is that Elm "hijacks" onClick on a elements to create navigation events so that anchors can be handled inside Elm. When using Browser.application, clicking an a element will have Elm call onUrlReuqest with a value of Browser.UrlRequest, which will class the URL as either Internal or External and require you to make a decision on what to do with it.
The problem at the root of this is that omitting href will generate an External UrlRequest with the URL being an empty string. By default, and by the usual handling of External, this will tell the browser to load the URL as usual, thus refreshing the page. But it also suggests a possible workaround is to special-case External "":
update msg model =
case msg of
UrlRequested (Browser.Internal _) ->
( model, Cmd.none )
UrlRequested (Browser.External "") ->
( model, Cmd.none )
UrlRequested (Browser.External url) ->
( model, Browser.Navigation.load url )
UrlChanged _ ->
( model, Cmd.none )
Another workaround is to add href="#" to the a elements, which will correctly classify them as Internal
So I started learning Elm today. I use VSCode as my editor.
I followed the docs for the set up and installed elm as well as el-format via npm install -g elm elm-format. I also installed the VSCode Elm extension.
Next, in my settings.json I set:
"[elm]": {
"editor.formatOnSave": true
},
Then I went on with the tutorial. In it the code is formatted like this:
import Browser
import Html exposing (Html, Attribute, div, input, text)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
-- MAIN
main =
Browser.sandbox { init = init, update = update, view = view }
-- MODEL
type alias Model =
{ content : String
}
init : Model
init =
{ content = "" }
-- UPDATE
type Msg
= Change String
update : Msg -> Model -> Model
update msg model =
case msg of
Change newContent ->
{ model | content = newContent }
-- VIEW
view : Model -> Html Msg
view model =
div []
[ input [ placeholder "Text to reverse", value model.content, onInput Change ] []
, div [] [ text (String.reverse model.content) ]
]
But when I hit safe, it formats the code like this:
module Main exposing (Model, Msg(..), init, main, update, view)
import Browser
import Html exposing (Attribute, Html, div, input, text)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
-- MAIN
main =
Browser.sandbox { init = init, update = update, view = view }
-- MODEL
type alias Model =
{ content : String
}
init : Model
init =
{ content = "" }
-- UPDATE
type Msg
= Change String
update : Msg -> Model -> Model
update msg model =
case msg of
Change newContent ->
{ model | content = newContent }
-- VIEW
view : Model -> Html Msg
view model =
div []
[ input [ placeholder "Text to reverse", value model.content, onInput Change ] []
, div [] [ text (String.reverse model.content) ]
]
So it adds extra lines, and extra module Main exposing ... and doubles the number of spaces. I tried setting spaces to 2 again using the footer in VSCode, but that didn't help.
My questions are:
Is it okay that saving adds the extra module Main ...?
Is having 2 spaces best practice / community standard, or 4?
If it is 2 (like it is in the tutorial's default code) how can I get my formatter to respect that?
If it is not, why has the tutorial the non-standard indentation?
First of all, this question is both too broad and primarily opinion-based and will probably be closed because of that. It would have been more suited for the forums I think.
That said, I'm going to try to answer it as best as I can anyway, since it's here:
Yes? Most modules won't be very useful without exposing something and it's good practice to be explicit about what's being exposed.
elm-format is the community standard, so 4 it is.
You can't. This is by design. It has also been discussed to death in various fora. Here's one issue discussing it
You'd have to ask Evan about that. It might be related to formatting for the web, or just Evan being lazy.
How do I use preventDefault in elm? Say on a keyboard event:
keyDown keyCode model =
case keyCode of
13 -> -- Enter key
model
if we don't want the default behaviour?
Html.Events has methods for it, but I don't understand how to use it in practice.
This answer is obsolete with Elm 0.19
Learn how to use Html.Events.on, then it becomes obvious.
myInput =
input
[ on "keydown" (Json.map KeyInput keyCode)]
[]
becomes...
myInputWithOptions =
input
[ onWithOptions "keydown" options (Json.map KeyInput keyCode)]
[]
options =
{ stopPropagation = True
, preventDefault = True
}
(Here, message constructor KeyInput
is defined as: type Msg = KeyInput Int)
#Tosh has shown how to stop event propagation and prevent default behaviour in his answer.
If your specific case is to prevent default behaviour only on Enter but not on any other keys, this is currently not possible in Elm—you'll have to resort to Ports and JS.
You can now use the library elm-community/html-extra for that, it provides you the onClickPreventDefault and onClickStopPropagation functions.
Im using start-app to shape my application. Html.Events supports creating custom events with custom Signal.message. But how to send that message is abstracted behind the Html library. Also there is a library called which implements debouncing (http://package.elm-lang.org/packages/Apanatshka/elm-signal-extra/5.7.0/Signal-Time#settledAfter).
SearchBar.elm:
module PhotosApp.SearchBar (view) where
import Html exposing (input, div, button, text, Html)
import Html.Events exposing (on, onClick, targetValue)
import Html.Attributes exposing (value, type')
import Effects exposing (Effects, Never)
import PhotosApp.Actions as Actions
onTextChange : Signal.Address a -> (String -> a) -> Html.Attribute
onTextChange address contentToValue =
on "input" targetValue (\str -> Signal.message address (contentToValue str))
view : Signal.Address Actions.Action -> String -> Html
view address model =
div []
[ input [ type' "text", value model, onTextChange address Actions.Search] []
, button [ onClick address Actions.Clear ] [text "Clear"]
]
You can set up debouncing from an HTML attribute by going through a proxy mailbox before tying into #Apanatshka's settledAfter function.
(Note that since you're using StartApp, we don't have direct access to the main Address and therefore I've had to compromise a few of these functions by ignoring the address passed into view. You could get something more generalized by not using StartApp, but this should hopefully get you started)
Since event attributes cannot create tasks directly, we have to use an intermediate proxy mailbox. We can set that up like this:
debounceProxy : Signal.Mailbox Action
debounceProxy =
Signal.mailbox NoOp
Note that the above function expects a NoOp Action, common in Elm architecture, which just means the update function makes no changes to the model.
Now we need to set up another Signal which listens to the proxy mailbox, then passes the signal through the settledAfter function. We can define this signal like this:
debounce : Signal Action
debounce =
settledAfter (500 * Time.millisecond) debounceProxy.signal
We can now change the onTextChange function to point at the proxy mailbox. Notice that I've taken out the first Address parameter since it's being ignored (see my earlier comment about conforming to StartApp):
onTextChange : (String -> Action) -> Html.Attribute
onTextChange contentToValue =
on "input" targetValue (\str -> Signal.message debounceProxy.address (contentToValue str))
Lastly, you have to tie in the debounce signal into StartApp's inputs parameter, which means your call to start will look something like this:
app = StartApp.start
{ init = init
, update = update
, view = view
, inputs = [ debounce ]
}
I've pasted the full working example at a gist here.
UPDATE : This has now been covered in the Elm Architecture documentation.
--
I don't understand how you tie the Elm Architecture and Tasks.
-- Action is an enumeration of possible actions
type Action = ..
-- Model is a Model that evolves in time
model : Signal Model
-- View turns a model into Html, potentially sending actions to an address
view : Address Action -> Model -> Html
-- Update turns a model and an action into another model
update : Action -> Model -> Model
-- Main waits for new values of the model to be emitted, and pass then to the view action
main : Signal Html
main =
Signal.map (view actions.address) model
I'm trying to model this:
when a user click on a button, a "DoSomethingAction" is emitted
this action should start a Task, handled by some port
when the task is done, it should emit another Action ("SomethingDoneAction"), with the result
Is that the right way to go?
Which functions should I change (update, main)?
I understand this is what is alluded to here, but the explanation is not too clear and I don't understand what needs to be changed.
So any complete example / or more explanation would be welcome.
EDIT :
A more complete version (that "kinda" works) is available here : http://share-elm.com/sprout/55bf3c3ce4b06aacf0e8ba17
I'm a few steps from having it working, it seems, since the Task is not always run when I click on the button (but hopefully I'm getting somewhere.)
While it is very possible to perform Tasks when a button is pressed, it is often not needed. What you probably want is to send a message to the Action signal, then with that update the Model, then with that the view may change. This is standard Elm architecture.
If you really want to perform Tasks, you could do the following:
type Action = NoOp | ButtonClicked | SetText String
type alias Model =
{ tasks : Task Http.Error String
, text : String
}
init : Model
init =
{ task = Task.succeed "Fetching..."
, text = ""
}
-- We can use, for example, the Http.get task
update : Action -> Model -> Model
update action model =
case action of
ButtonClicked ->
{ model | task <-
Http.getString "http://example.org/some-text.txt"}
SetText t ->
{ model | text <- t }
_ -> model
view : Address Action -> Model -> Html
view address model =
div []
[ div
[ class "btn btn-default"
, onClick address ButtonClicked
]
[ text "Click me!" ]
, h3 "Here is what was fetched:"
, p [] [ text model.text ]
]
-- now possibly in another file
actions : Signal.Mailbox Action
actions : Signal.mailbox NoOp
model : Signal Model
model = Signal.foldp init update actions.signal
-- This is what actually performs the Tasks
-- The Elm task example also details how to send
port taskPort : Signal (Task Http.Error String)
port taskPort =
((.task) <~ model) `andThen`
(Signal.send actions.address << SetText)
main : Signal Html
main = view actions.address <~ model
Note that you can use Signal.dropRepeats if you want to perform the task only when the task changes.
The way to do it is to have the update of the model mapped to a response signal that receive the results of what you want your model to react to.
The view sends the tasks to a query/requests mailbox.
The port would be a map on the query signal that executes the tasks andThen sends the results to the response mailbox.
If you want to see this in action take a look at this file (I've highlighted the relevant parts)
https://github.com/pdamoc/elmChallenges/blob/master/challenge4.elm#L72-L94
Please be advised that what you see there is a little bit more complicated than what you need because it also implements a mechanism to execute the tasks only when the user stops typing.
This is answered in the doc :
https://github.com/evancz/elm-architecture-tutorial#user-content-example-5-random-gif-viewer