How can I add event handlers to the body element in Elm? - elm

I'm trying with the Html.App.beginnerProgram and I want to add event handlers (onKeyDown, etc.) to the <body> element.
Unfortunately everything I put in view becomes the children of <body>. Returning Html.body from view doesn't do the trick. This code:
main = beginnerProgram { model = 0, view = view, update = update }
view model = body [] []
update _ model = model
will generate:
<html>
<head>...</head>
<body>
<body></body>
</body>
</html>
So, how do I get the control of the <body> element?

Since Elm renders as a descendant of <body/>, you cannot bind event handling to it in the normal Elm way (e.g. button [ onClick SomeMsg ] []). Instead you'll have to use ports. And because you're using ports and subscriptions, you will need to use Html.App.program rather than beginnerProgram.
You'll need a port function inside your port module:
port bodyKeyPress : (String -> msg) -> Sub msg
And then you'll be able to send key presses to that port from javascript:
app.ports.bodyKeyPress.send(...);
Here's a full example:
Main.elm
port module Main exposing (main)
import Html.App as App
import Html exposing (..)
main = App.program
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}
type alias Model = { message : String }
type Msg = BodyKeyPress String
init = { message = "Press some keys: " } ! []
update msg model =
case msg of
BodyKeyPress c ->
{ model | message = model.message ++ c } ! []
view model = text model.message
subscriptions _ =
bodyKeyPress BodyKeyPress
port bodyKeyPress : (String -> msg) -> Sub msg
And the html behind the scenes (assuming you built using elm make Main.elm --output=Main.js):
<html>
<body>
<script src="Main.js"></script>
<script>
var app = Elm.Main.fullscreen();
document.body.onkeypress = function(e) {
app.ports.bodyKeyPress.send(String.fromCharCode(e.keyCode));
};
</script>
</body>
</html>
You will need to create and handle a port function for every body event you want to send.

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

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...

Elm modal dialog box

How do I integrate this sweet dialog into my Elm code. I've included the JS and CSS in my index.html. How do I call this JavaScript function in my update function?
update : Action -> Model -> (Model, Effects Action)
update action model =
case action of
Submit ->
let valid = length model.name > 0
in
if valid
then (model, swal({"title": "Invalid name"}))
else (model, swal({"title": "Valid name"}))
It's tricky to rig up a full example without all the view code to check with, but I'm hoping this simpler version helps! Cribbed to some extend from this repo...
index.html
<!DOCTYPE html>
<html>
<head>
<script src="Main.js" type="text/javascript"></script>
<script type="text/javascript"
src="bower_components/sweetalert/dist/sweetalert.min.js"
></script>
<link rel="stylesheet"
href="bower_components/sweetalert/dist/sweetalert.css"
/>
</head>
<body>
<div id="main"></div>
<script type="text/javascript">
var so = Elm.embed(Elm.Main, document.getElementById('main'));
so.ports.callSwal.subscribe(doAlert);
function doAlert(space) {
if (space) swal("Hey, a spacebar!");
}
</script>
</body>
</html>
modal.elm
import Graphics.Element
import Keyboard
port callSwal : Signal Bool
port callSwal =
Keyboard.space
main = Graphics.Element.show "How about pressing a spacebar?"
stuff I did to make it work
$ bower install sweetalert
$ elm-make modal.elm --output=Main.js
Note
Embed the Elm application, to give js an object to access ("so" here)
In js, subscribe to a named port and give it a callback function.
create the port in elm. This one takes a simple Bool, but I guess yours will want at least a String.
A better answer
The trick turns out to be noticing that startApp has a mailbox baked into it, which you can access through app.model.
The alert message becomes part of your model. If it's an empty string, we interpret that as meaning "don't trigger any alerts".
NB. I've no idea why update needs to return a tuple with an Events Action in it. That's not been used here..
Here's an example of it all put together:
var so = Elm.embed(Elm.Main, document.getElementById('main'));
so.ports.alert.subscribe(function(text) {
if (text.length > 0) swal(text);
});
import StartApp
import Task exposing (Task)
import Effects exposing (Effects, Never)
import Html exposing (Html, div, input, button, text)
import Html.Events exposing (on, onClick, targetValue)
import String exposing (length)
app :
{ html : Signal Html
, model : Signal Model
, tasks : Signal (Task Never ())
}
app =
StartApp.start
{ init = init
, update = update
, view = view
, inputs = []
}
port alert : Signal String
port alert =
Signal.map (\n -> n.alert) app.model
main : Signal Html
main =
app.html
-- MODEL
type alias Model =
{ name : String
, alert : String
}
init : (Model, Effects Action)
init =
( { name = ""
, alert = ""
}
, Effects.none
)
-- UPDATE
type Action
= Submit
| TextEntry String
update : Action -> Model -> (Model, Effects Action)
update action model =
case action of
Submit ->
if length model.name > 0 then
({ model | alert = "Valid name" }, Effects.none)
else
({ model | alert = "Invalid name" }, Effects.none)
TextEntry txt ->
({ model | name = txt, alert = "" }, Effects.none)
-- VIEW
view : Signal.Address Action -> Model -> Html
view address model =
let f = (\str -> Signal.message address (TextEntry str)) in
div
[]
[ input
[ on "input" targetValue f ]
[]
, button
[ onClick address Submit ]
[ text "Submit" ]
]

Local Storage or other data persistence in elm

I am just beginning to look at Elm with the idea of building a simple web application with it. My idea would require to persist some user data in the browser.
Is there a way to handle data persistence directly with Elm? For example in browser session or even local storage? Or should I use ports to do it with JavaScript?
I would suggest to use localStorage. There is no official support for it in the latest elm (by this time it is 0.17), but you can simply do it via ports. This is a working example (based on an example from the offical docs) of using localStorage via ports for elm 0.17
port module Main exposing (..)
import Html exposing (Html, button, div, text, br)
import Html.App as App
import Html.Events exposing (onClick)
import String exposing (toInt)
main : Program Never
main = App.program
{
init = init
, view = view
, update = update
, subscriptions = subscriptions
}
type alias Model = Int
init : (Model, Cmd Msg)
init = (0, Cmd.none)
subscriptions : Model -> Sub Msg
subscriptions model = load Load
type Msg = Increment | Decrement | Save | Doload | Load String
port save : String -> Cmd msg
port load : (String -> msg) -> Sub msg
port doload : () -> Cmd msg
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
Increment ->
( model + 1, Cmd.none )
Decrement ->
( model - 1, Cmd.none )
Save ->
( model, save (toString model) )
Doload ->
( model, doload () )
Load value ->
case toInt value of
Err msg ->
( 0, Cmd.none )
Ok val ->
( val, Cmd.none )
view : Model -> Html Msg
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (toString model) ]
, button [ onClick Increment ] [ text "+" ]
, br [] []
, button [ onClick Save ] [ text "save" ]
, br [] []
, button [ onClick Doload ] [ text "load" ]
]
And the index.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<script type="text/javascript" src="js/elm.js"></script>
</head>
<body>
</body>
<script type="text/javascript">
var storageKey = "token";
var app = Elm.Main.fullscreen();
app.ports.save.subscribe(function(value) {
localStorage.setItem(storageKey, value);
});
app.ports.doload.subscribe(function() {
app.ports.load.send(localStorage.getItem(storageKey));
});
</script>
</html>
Now, by pressing buttons +/- you change the int value. When you press "save" button, the app stores the actual value into localStorage (by "token" key). Then try to refresh the page and press "load" button - it takes the value back from localStorage (you should see the HTML text control actualized with the restored value).
The official answer is "use ports" (for 0.18 - 0.19.1) while awaiting re-publication of Elm's official local storage library: https://github.com/elm-lang/persistent-cache
You could have a look at TheSeamau5's elm-storage. It makes it possible to store data in local storage.