How can I map values out of an onClick event into an action in Elm? - elm

I am experimenting with an Elm app in which I wish to generate actions from clicking within a div, but I wish the actions to contain the coordinates of the click.
I can see that I can generate actions from clicks like this:
div [onClick address MyAction] [text "click in here"]
But I can't see how to apply a mapping function to the original click data to populate fields in MyAction

You can pull different properties out of the javascript event object using Json Decoders. To get the clientX and clientY values as a tuple, you could create the following decoder:
import Json.Decode as Json exposing ((:=))
eventPos : Json.Decoder (Int, Int)
eventPos =
Json.object2
(,)
("clientX" := Json.int)
("clientY" := Json.int)
In order to do something with those values, you'll need to add an Action that can accept the tuple as a parameter:
type Action
= MyAction
| DivClicked (Int, Int)
Finally, instead of using the onClick function to generate the event handler attribute on your div, you'll need to use the on function, passing in your Json decoder:
on "click" eventPos (Signal.message address << DivClicked)
Here's a gist containing a working example that you can paste right into http://elm-lang.org/try.

Related

Access attribute name with on "click" event handler

I am trying to access an elements HTML properties when clicked. I figured out that Events.onClick will only return event.target.value and I need to use Events.on to handle custom behaviours.
What I need is: when click on a div I should be able to access its HTML properties, for now id and name and send this value to update with some message.
I tried like this:
onClickData : msg -> Attribute msg
onClickData handler =
on "click" (Decode.succeed handler )
---
view: ...
....
div[onClickData HandleClick] []
This way i am able to trigger HandleClick action when the div is clicked but cannot access its HTML properties.
As #glennsl has noted, you normally would want to do something more like this:
view identification =
button [ id identification, onClick (MyMsg identification) ]
[ text "Click me"
]
i.e. you can pass data straight into your Msg type.
That said, there are some unusual inter-op situations where you might want to do this. The general principle is that you can get the element that you bound your event handler to from event.currentTarget, so you can use the following decoder:
decodeProperty : String -> Decoder a -> Decoder a
decodeProperty property decoder =
Decode.at ["currentTarget", property] decoder
--- use
onClickId : (String -> msg) -> Attribute msg
onClickId tagger =
on "click" (Decode.map tagger (decodeProperty "id"))

View with multiple textareas, how to identify which was modified?

I'm making a simple page with multiple textarea elements in Elm. I'm struggling to get the data saved and especially identifying which textarea was updated. Maybe an example illustrates the point better
I have multiple elements made in view from a list
type alias Model = { List Comment, ... }
type alias Comment = {id: Int, content: String, draftContent: String, ...}
type Event = Save Comment | SaveDraft String
-- view
model.comments
|> List.map( --iterate the list of comments and render html
div [attrubute "name" "comment"] [
textarea [onInput SaveDraft] [text comment.content],
button [onClick (Save comment)] [text "Post comment"]
]
-- update
case event of
Save comment ->
-- Replace the comment content with draft data and clear draft
SaveDraft draftText ->
-- Update the draft content with text from event
-- Which Comment is it?
Based on examples here I came up with the idea of sending each textarea input as an event to update function and saving the draft data.
Now the problem here is that onInput only accepts types with a String parameter and I have no means of identifying which one of the comments was modified.
Change the Event union type to include the comment (SaveDraft String --> SaveDraft Comment String)
type Event = Save Comment | SaveDraft Comment String
-- view
model.comments
|> List.map( --iterate the list of comments and render html
div [attrubute "name" "comment"] [
textarea [onInput (SaveDraft comment.content)] [text comment.content],
button [onClick (Save comment)] [text "Post comment"]
]
Currying causes (SaveDraft comment.content) to have the same return value as before

HTML textarea in Elm where pressing tab adds \t and doesn't change focus

I know how to listen for tab key presses in Elm. And I know how to stop the focus from being changed using onWithOptions:
textarea
[ onWithOptions "keydown" (Options False True) <| Decode.map KeyDown keyCode ] []
I can then check, in my update function, if the keyCode pressed was a 9, representing a tab. The problem is now the default behavior of a textarea doesn't work. Anything I type doesn't appear in the textarea. Easy enough, I simply add whatever I type to the model and make the value of the textarea the model. Now I have issues with the cursor and, more importantly, clipboard pasting doesn't work...
How do I get tabs to work properly with textareas in Elm? Normally, it would seem to make sense to only call preventDefault() if the tab key was pressed. How can I conditionally call preventDefault() in Elm?
Elm does support conditional event propagation through a Decoder that either succeeds or fails. Simply map the message type you want to react to in your update function:
succeededIfTabKey : Int -> Decode.Decoder Int
succeededIfTabKey key =
if key == 9 then
Decode.succeed key
else
Decode.fail "non-tab"
tabPressed : Decode.Decoder Msg
tabPressed =
Decode.andThen succeededIfTabKey keyCode
|> Decode.map (always TabPressed)
And then use this as your attribute for your input element:
onWithOptions "keydown" { defaultOptions | preventDefault = True } tabPressed
This isn't ideal for all situations. If you want some keydown events to not preventDefault(), and other keydown events to preventDefault(), then you're out of luck.

Combine multiple signals for one model (using Html.Events and Keyboard signals)

I am fairly new in Elm. I am currently discovering that language while experimenting it. Although the use of signals appears complex. Here below is a working example of my webpage, which handles multiple paragraphs (addition, removal, edition, ...) .
-- start
main: Signal Html.Html
main = Signal.map (view actions.address) model
view: (Signal.Address Action) -> Model -> Html
view address model = ... (displaying paragraphs with buttons, ect)
This snippet initiates the model. By mapping the actions to the model using Signal.map, i am able to handle click events from buttons (imported Html + Event module)
model: Signal Model
model = Signal.foldp update makeEmptyModel actions.signal
Here, i start with an empty model. The update function allows me to update the model after click events on buttons.
update: Action -> Model -> Model
update action model = ....
The Action is a type which handles multiple actions that i have defined like "AddParagraph", "RemoveParagraph" ...
This works so far. Now, when checking the packages, i found Keyboard. That appears interesting, so I want to add a new functionality. If the user presses Alt+A, all paragraphs got selected. (Like Ctrl+A in file explorer)
But it appears that it's not easy to combine that with the current signal mapping that I have. I decided to dig into Signal and found Signal.merge. So i can use it right ? My attempt to merge the keyboard signal to my current signals is
main = Signal.merge
(Signal.map (view actions.address) model)
(Signal.map (view actions.address) keysDown)
(the keysDown is from import Keyboard exposing (keysDown) import)
But that doesn't work. I get the next error
The 2nd argument to function `map` is causing a mismatch.
160│ Signal.map (view actions.address) keysDown)
^^^^^^^^
Function `map` is expecting the 2nd argument to be:
Signal { focused : Bool, selected : Bool, ... }
But it is:
Signal (Set.Set Char.KeyCode)
It appears that when using Signal.merge, it expects multiple signals which handles the same output. Well that's what i want. But that doesn't work it seems.
Question: How can i add Keyboard signal to the current design ?
Or am I wrong with my expectations about Signal.merge ? That I'm using Signal.merge for wrong purpose ? Should I use Signal.map2 instead ? If so, how can I use it with my current example* ? Or is there a better approach ?
if the solution is map2, can you add an explanation ? That is because I don't understand that "map2" stuff
The Signals you send to merge must be of the same type. You'll need to add another function that maps the keyboard input to an Action prior to merging.
There are a couple ways to achieve this. Here's an example using the basic Counter examples and Signal.merge. Of importance is the use of the keyPressesToAction signal mapping function
import Signal
import Html exposing (..)
import Html.Events exposing (..)
import Keyboard
import Char
type Action = NoOp | Increment | Decrement
actions : Signal.Mailbox Action
actions =
Signal.mailbox NoOp
update action model =
case action of
NoOp -> model
Increment -> model + 1
Decrement -> model - 1
model =
Signal.foldp update 0 (Signal.merge actions.signal keyPressesToAction)
keyPressesToAction =
let
keyCodeToAction keyCode =
case Char.fromCode keyCode of
'+' -> Increment
'-' -> Decrement
_ -> NoOp
in
Signal.map keyCodeToAction Keyboard.presses
main =
Signal.map (view actions.address) model
view address model =
div []
[ button [ onClick address Decrement ] [ text "-" ]
, text <| toString model
, button [ onClick address Increment ] [ text "+" ]
]
Another way to achieve the desired results is by using a port dedicated to converting key presses to action signals. This gets rid of the need to merge signals because it instead causes key presses to trigger the Mailbox you've already set up.
In this scenario, the model function goes back to the way it was, then we add the port below called triggerActionOnKeyPress, which uses the identical keyPressesToAction signal mapping function from above. Here are the relevant lines:
model =
Signal.foldp update 0 actions.signal
port triggerActionOnKeyPress : Signal (Task.Task Effects.Never ())
port triggerActionOnKeyPress =
Signal.map (Signal.send actions.address) keyPressesToAction

Dynamic Element created inside a div must be clicked

I need get the id of my dynamic created element inside a div.
I'm looking for a way to get it's id by clicking it.
Example:
$("#div").on("click", '*', function () {
alert($(this).id);
});
The element can be an image, link, button, etc...
I need to dynamic bind click to it, and get the id when gets clicked.
I use append to add sub elements to div.
$("#div").append(element);
Elements are right shown, but i can't retrive the ids.
Is there anyway to get it?
Example in jsfiddle:
http://jsfiddle.net/StTvn
http://jsfiddle.net/StTvn/2/
changed the listener selector and it works
$("#myDiv").on("click", "*", function () {
txt = $(this).attr('id');
alert(txt);
});
as a side note, id that start with a number are not valid html - you might want to change the way you generate them like this:
el.id= "img" + i.toString();