Elm - How to use HTML events? - elm

The Elm getting started guide doesn't seem to use the standard Elm HTML events and I'm not sure how to use them. For example, onBlur:
view model =
div []
[ div []
input [ type_ "text", onInput MsgA ] []
, input [ type_ "text", onBlur MsgB ] []
]
This fails in the compiler because now I am returning type Html (String -> Msg) on line 3 but Html (Msg) in line two.
Why are these two events incompatible? Is there any way to use both at the same time? Additionally, the docs don't make it clear enough for someone like me to understand how to use onBlur.

onBlur has a different type signature than onInput.
onInput : (String -> msg) -> Attribute msg
onBlur : msg -> Attribute msg
That means whatever Msg you use for onInput has to take a single string parameter. Likewise, the Msg used in onBlur cannot take a parameter. If you redefined MsgA and MsgB to the following, it would compile:
type Msg
= MsgA String
| MsgB
Edit
If you wanted your blur handler to be able to accept the target value, you can roll your own 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)

Related

Incorrect View Functon Return Type in elm

I am making a program that changes the rendered text on the screen to be whatever the user inputs in a text box. I think I have the model and the update part of the elm architecture correct, but I really don't understand the view portion.
I'm just having trouble wrapping my head around the square bracket view functions.
Anyway, I am getting this error.
This div call produces:
Html #(Model -> Model)#
But the type annotation on view says it should be:
Html #Msg#Elm
But I am not sure how to change my view function to return Html Msg and I am kinda confused between the difference between that and a string.
Thank you everyone!
Here is my code ...
module Main exposing (..)
import Browser
import Html exposing (Html, div, text, input, Attribute)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
main =
Browser.sandbox { init = init, update = update, view = view }
type alias Model = String
init : Model
init = "Hello, World!"
type alias Msg = String
update : Msg -> Model -> Model
update msg model =
msg
view : Model -> Html Msg
view model =
div []
[ input [ placeholder "Input new string", value model, onInput update ] []
, div [] [ text model ]
]
You're passing the update function as an argument to onInput. Your probably meant to pass it a Msg, which the runtime will then pass to the update function.
Since your Msg type is an alias for String, you can use onInput identity

Sending messages between modules

Can't quite figure out how to go about sending messages between modules when working with reusable components.
I have an expanding text area that I'd like to use on a number of different sections on a site. The Text Area accepts a portion of HTML that makes up the user actions. Typically handling submit, cancel, upload icons, etc..
Tried to write up a quick example of what I'm talking about without throwing a ton of code on here. So essentially, I'd like to just plug and play peices of HTML that are already attached.
I'm assuming CancelNote is getting fired as a TextArea msg, so it never sees a Cancel Note msg. Not sure how I would use Html.map here (or even if I would).....feel like plug and play method is probably a bad approach, but not sure how else I could achieve decent reusability .
SEPERATE MODULE
update model msg =
case msg of
CancelText ->
( { model | note = (Just "") }
, Cmd.none
)
view: stuff
view stuff =
......
TextArea.view
(button [ Html.Events.onClick CancelText] [])
TEXT AREA MODULE
view : Html.Html msg -> Html msg
view actionHtml =
div [ class "extended_text_area_container" ] [
textarea [] [
]
, actionHtml
]
Messages are just values like any other. You can pass them around directly:
-- SEPERATE MODULE
view: stuff
view stuff =
......
TextArea.view CancelText
-- TEXT AREA MODULE
view : msg -> Html msg
view msg =
div [ class "extended_text_area_container" ]
[ textarea [] []
, button [ onClick msg ] []
]
Edit: If you need to also maintain internal state, just use another message to tell the parent to update the state:
-- Main module
type msg =
...
SetTextAreaState TextArea.state
update model msg =
case msg of
...
SetTextAreaState state ->
{ model | textAreaState = state }
view : Model -> Html msg
TextArea.view SetTextAreaState model.textAreaState
-- TextArea module
type State =
...
type Msg =
Clicked
update : State -> Msg -> State
update state msg =
case msg of
Clicked ->
{ state | clicked = True }
view : (State -> msg) -> State -> Html msg
view toMsg state =
let
updateAndWrap msg =
toMsg (update state msg)
in
div [ class "extended_text_area_container" ]
[ textarea [] []
, button [ onClick (updateAndWrap Clicked) ] []
]
Here, instead of passing a msg to onClick directly in TextArea.view, call a function that updates the state and then wraps it in the msg constructor passed in from the parent, which will produce a message of a type that we don't know anything about.
Also, while I use an internal Msg type and update function similarly to the overall Elm architecture, that is in no way mandatory. It's just a nice way of doing it since it's familiar and scales well.

Elm and VSCode: Formatter messes up spacing

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.

Elm no data except isTrusted in JSON events

I wrote a simple program based on time example, to test what data are in events. It decodes JSON to value then encodes it back to JSON, then show it in SVG text element. And the only thing I get is {"isTrusted":true}.
Why that happens? How do I get another data of event? I'm using Firefox 49 and online compiler:
import Html exposing (Html)
import Svg exposing (..)
import Svg.Attributes exposing (..)
import Svg.Events exposing(on)
import Json.Decode as Json
import Json.Encode exposing (encode)
main =
Html.program
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
-- MODEL
type alias Model = String
init : (Model, Cmd Msg)
init =
("No event", Cmd.none)
-- UPDATE
type Msg
= Event String
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
Event event ->
(event, Cmd.none)
subscriptions model = Sub.none
stringifyEvent: Json.Encode.Value -> Msg
stringifyEvent x =
Event (encode 2 x)
-- VIEW
view : Model -> Svg Msg
view model =
svg [ viewBox "0 0 300 300", width "300px", on "mousedown" (Json.map stringifyEvent Json.value) ] [
text_ [x "0", y "30"] [text model]
]
When I try in console
svgElement.addEventListener('click', function(e) {console.log(e)})
It works with all the attributes.
I do not know a way to achieve your goal.
But I can give you an answer why it does the way you described.
If you look at the source code, you'll find that Elm runtime uses
JSON.stringify() for converting Value to String.
And guess what...
svgElement.addEventListener('click', function(e) {console.log(JSON.stringify(e))})
will give you {"isTrusted":true} when you click...

What does a function with 2 values on the right side mean? (Model -> Html msg)

I have encountered that in the guide:
viewValidation : Model -> Html msg
viewValidation model =
let
(color, message) =
if model.password == model.passwordAgain then
("green", "OK")
else
("red", "Passwords do not match!")
in
div [ style [("color", color)] ] [ text message ]
So this is a function, which takes the Model.
Html msg usually looks to me like we are calling the function Html with the argument msg.
msg doesn't seem to play any role in any other part of the viewValidation function, though. So what does it mean and what is it for in this case?
Html Msg is just a type parameter, as List Int is. While List Int denotes a list that contains element of type Int, similarly Html Msg describes some HTML that can treat/emit messages of type Msg.
For example, if you have a button inside your HTML, it could look like this:
button [ onClick DoSomething ] [ text "caption" ]
Where DoSomething is a case of the Msg type.
Don't mix the type definition with the normal execution of code. Html is not a function, it is a type that takes a parameter to define a type for a view function.
Html msg is the most general definition you can have as msg is a variable itself, so this returns Html that is independent of the msg type you are currently using. This could either be because it creates no event messages, or because the view function takes messages as parameters.
As the comments established Html () would be a very narrow type that is constrained to return nothing.
The most common case though will be a view function returning Html Msg - i.e. Html with messages based on user interactions.
As Elm encourages componentization, you also need keep Html.map in mind. It's type signature is Html.map : (a -> b) -> Html a -> Html b. In the context of components this is more easily read as
Html.map : (Child.Msg -> Parent.Msg) -> Html Child.Msg -> Html Parent.Msg
Note that when you define your messages in your parent component, you will have something like:
type Msg = ChildMsg Child.Msg
which means that ChildMsg has type signature:
ChildMsg : Child.Msg -> Parent.Msg
So my view functions have a lot of
parentView model =
-- childView model.child |> Html.map ChildMsg
Html.map ChildMsg (childView model.child)