This is the view snippet taken from elm checkbox examples -
view address model =
div [] <|
span [toStyle model] [text "Hello, how are yo?"]
:: br [] []
:: checkbox address model.red Red "red"
++ checkbox address model.underline Underline "underline"
++ checkbox address model.bold Bold "bold"
checkbox : Address Action -> Bool -> (Bool -> Action) -> String -> List Html
checkbox address isChecked tag name =
[ input
[ type' "checkbox"
, checked isChecked
, on "change" targetChecked (Signal.message address << tag)
]
[]
, text name
, br [] []
]
1) I understand, double colons and double plus are for concatenating lists? How are they different from each other?
2) In the checkbox function in this line, (Signal.message address << tag), what does tag unbind to? Is it Red (or) red? What is this argument used for?
3) What is the type of argument does the address function take?
Answer
double colon (::) adds a single element to the start of a list.
double plus (++) concatenates two lists.
tag would be equal to Red in the when checkbox is called from checkbox address model.red Red "red". Red is a function from Bool to Action. It wraps the event of the check box in this data constructor so that later in the update function, you can distinguish the event of that check box from the events of the other check boxes.
address is not a function. It has type Address Action, meaning it holds the address of a mailbox that can receive Action messages. This "mailbox" is not visible in the code, it's used internally by StartApp, which is used in the main function.
Code references
To keep this question useful even if the linked example changes, these are the relevant code portions I'm referring to:
The update function:
update action model =
case action of
Red bool ->
{ model | red <- bool }
Underline bool ->
{ model | underline <- bool }
Bold bool ->
{ model | bold <- bool }
The Action type:
type Action
= Red Bool
| Underline Bool
| Bold Bool
The main function:
main =
StartApp.start { model = initialModel, view = view, update = update }
Related
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"))
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
How do I get the caption from a button?
decorateOn : String -> Html Msg -> Html Msg
decorateOn selectedCaption button =
if button.text == selectedCaption then
button [ class "selectedNavigationButton" ] []
else
button [ class "navigationButton" ] []
button does not have a field named text. - The type of button
is:
Html Home.Msg
Which does not contain a field named text.
Note, I realize that the "button" is really of type Html Msg.
You need to turn your thinking on its head. Rather than seeing what is in the button text, you need to set the text at the same stage as setting the class. So that gives you something like
decorateOn : String -> Html Msg -> Html Msg
decorateOn selectedCaption button =
if selectedCaption == "the selected value" then
button [ class "selectedNavigationButton" ] [text selectedCaption ]
else
button [ class "navigationButton" ] [text selectedCaption]
You can't get the text from a button without resorting to hacks involving ports and JavaScript. Moreover, you can't really inspect anything about the Elm Virtual DOM from within Elm.
Instead, try to refactor your app so that you can get the information from your model.
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.
What would be the right way to handle a click outside of a single component that is supposed to hide this component?
Example of such component might be a dropdown menu, a datepicker and the like. We typically expect them to hide when we click outside. But to do so, it seems like we have to perform some "impure" hacks that I'm not sure how to avoid in FRP style.
I searched for relevant React examples for ideas and found this but they all seem to rely on attaching callbacks to global objects that then modify internal component's state.
The existing answer doesn't work in elm v0.18 (Signal was removed in 0.17), so I wanted to update it. The idea is to add a top-level transparent backdrop behind the dropdown menu. This has the bonus effect of being able to darken everything behind the menu if you want.
This example model has a list of words, and any word may have a open dropdown (and some associated info), so I map across them to see if any of them are open, in which case I display the backdrop div in front of everything else:
There's a backdrop in the main view function:
view : Model -> Html Msg
view model =
div [] <|
[ viewWords model
] ++ backdropForDropdowns model
backdropForDropdowns : Model -> List (Html Msg)
backdropForDropdowns model =
let
dropdownIsOpen model_ =
List.any (isJust << .menuMaybe) model.words
isJust m =
case m of
Just _ -> True
Nothing -> False
in
if dropdownIsOpen model then
[div [class "backdrop", onClick CloseDropdowns] []]
else
[]
CloseDropdowns is handled in the app's top-level update function:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
CloseDropdowns ->
let
newWords = List.map (\word -> { word | menuMaybe = Nothing } ) model.words
in
({model | words = newWords}, Cmd.none)
And styled things using scss:
.popup {
z-index: 100;
position: absolute;
box-shadow: 0px 2px 3px 2px rgba(0, 0, 0, .2);
}
.backdrop {
z-index: 50;
position: absolute;
background-color: rgba(0, 0, 0, .4);
top: 0;
right: 0;
bottom: 0;
left: 0;
}
The following example that does something similar to what you describe.
modal is presented with an address (to send a 'dismiss' event to), the current window dimensions, and an elm-html Html component (which is the thing to be focussed, like a datepicker or a form).
We attach a click handler to the surrounding element; having given it an appropriate id we can work out if received clicks apply to it or the child, and forward them on appropriately. The only really clever bit is the deployment of customDecoder to filter out clicks on the child element.
Elsewhere, on reception of the 'dismiss' event, our model state changes such that we no longer need to call modal.
This is quite a large code sample that makes use of a fair few elm packages, so please ask if anything requires further explanation
import Styles exposing (..)
import Html exposing (Attribute, Html, button, div, text)
import Html.Attributes as Attr exposing (style)
import Html.Events exposing (on, onWithOptions, Options)
import Json.Decode as J exposing (Decoder, (:=))
import Result
import Signal exposing (Message)
modal : (Signal.Address ()) -> (Int, Int) -> Html -> Html
modal addr size content =
let modalId = "modal"
cancel = targetWithId (\_ -> Signal.message addr ()) "click" modalId
flexCss = [ ("display", "flex")
, ("align-items", "center")
, ("justify-content", "center")
, ("text-align", "center")
]
in div (
cancel :: (Attr.id modalId) :: [style (flexCss ++ absolute ++ dimensions size)]
) [content]
targetId : Decoder String
targetId = ("target" := ("id" := J.string))
isTargetId : String -> Decoder Bool
isTargetId id = J.customDecoder targetId (\eyed -> if eyed == id then Result.Ok True else Result.Err "nope!")
targetWithId : (Bool -> Message) -> String -> String -> Attribute
targetWithId msg event id = onWithOptions event stopEverything (isTargetId id) msg
stopEverything = (Options True True)
A bit late to the party here, but I was struggling with exactly the same problem and the elm community on slack suggested a nice way of detecting click outside an element (let's say, a dropdown).
The idea is that you can attach a global listener to mousedown via BrowserEvents.onMouseDown and pass it a custom decoder that would decode target DOM node from the event object. By "decoding DOM node" I mean decoding only the id and parentNode properties of the node. parentNode will allow recursively travers the DOM tree and for each node check whether its id is the same as the id of the dropdown.
The code for this (in elm 0.19) looks like this:
-- the result answers the question: is the node outside of the dropdown?
isOutsideDropdown : String -> Decode.Decoder Bool
isOutsideDropdown dropdownId =
Decode.oneOf
[ Decode.field "id" Decode.string
|> Decode.andThen
(\id ->
if dropdownId == id then
-- found match by id
Decode.succeed False
else
-- try next decoder
Decode.fail "continue"
)
, Decode.lazy
(\_ -> isOutsideDropdown dropdownId |> Decode.field "parentNode")
-- fallback if all previous decoders failed
, Decode.succeed True
]
-- sends message Close if target is outside the dropdown
outsideTarget : String -> Decode.Decoder Msg
outsideTarget dropdownId =
Decode.field "target" (isOutsideDropdown "dropdown")
|> Decode.andThen
(\isOutside ->
if isOutside then
Decode.succeed Close
else
Decode.fail "inside dropdown"
)
-- subscribes to the global mousedown
subscriptions : Model -> Sub Msg
subscriptions _ =
Browser.Events.onMouseDown (outsideTarget "dropdown")
The code uses Json-Decode package that needs to be installed via elm install elm/json.
I also wrote an article explaining in details how this works, and have an example of a dropdown on github.