Platform.worker and basic usage of ports - elm

UPDATE: solved by myself (details underneath original question)
I am just getting started with Elm, and I am trying to make a worker to do some logic for me. I made a basic program to try to understand how Platform.worker and ports work. My goal is that whatever I type in the text box should update in elm_output instantaneously, and in my_field whenever I hit the "Submit" button.
It compiles, and it updates in my_field whenever I hit "Submit", however, the elm_output remains empty no matter what I type in the text box. What is my mistake?
Index.html:
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Main</title>
<script src="src/elm.js"></script>
</head>
<body>
<div id="elm"></div>
<input id="my_field"></input>
<button id="my_field_submit">Submit</button>
<p id="my_display"></p>
<br><br>
<p id="elm_output"></p>
</body>
<script src="view.js"></script>
</html>
view.js:
var myField = document.getElementById("my_field");
var app = Elm.Main.init({
node: document.getElementById("elm"),
flags: myField.value
});
var myFieldSubmit = document.getElementById("my_field_submit");
myFieldSubmit.onclick = function() {
document.getElementById("my_display").innerHTML = myField.value;
}
app.ports.textInput.send(document.getElementById("my_field").value);
app.ports.textOutput.subscribe(function(data) {
document.getElementById("elm_output").innerHTML = data;
});
src/Main.elm:
port module Main exposing (..)
main = Platform.worker
{
init = init
, update = update
, subscriptions = subscriptions
}
type alias Model = { myTextVal : String }
init : String -> (Model, Cmd MyMsg)
init str = (Model str, Cmd.none)
type MyMsg = InMsg String
update : MyMsg -> Model -> (Model, Cmd MyMsg)
update msg model = case msg of
InMsg str -> (Model str, textOutput str)
port textInput : (String -> msg) -> Sub msg
port textOutput : String -> Cmd msg
decodeValue : String -> MyMsg
decodeValue str = InMsg str
subscriptions : Model -> Sub MyMsg
subscriptions model = textInput decodeValue
Compiled with:
elm make src/Main.elm --output src/elm.js
Thanks in advance.
Edit 1: also, do I have to use flags in my init? I am always going to initialize myTextVal to "" anyway.
Edit 2: SOLVED THIS QUESTION MYSELF.
I found the problem by adding console.log statements to the parts in my JS code which involved the Elm ports.
The problem was that Javascript does not constantly update Elm whenever a change is made. Instead, Javascript only sends information to Elm exactly once, whenever the app.ports.xyzabc.send function is called. In my old code, I only called this function once, when the document was loaded. Instead, I have to call it whenever the text input is changed in order to get the result I want.
New code:
(Index.html remained unchanged)
NEW view.js
var myField = document.getElementById("my_field");
var app = Elm.Main.init({
node: document.getElementById("elm"),
});
var myFieldSubmit = document.getElementById("my_field_submit");
myFieldSubmit.onclick = function() {
document.getElementById("my_display").innerHTML = myField.value;
};
myField.oninput = function() {
console.log("text changed");
app.ports.textInput.send(myField.value);
};
app.ports.textOutput.subscribe(function(data) {
console.log("received text output from Elm");
document.getElementById("elm_output").innerHTML = data;
});
Console output:
received text output from Elm
text changed
(repeats whenever the text within myField changes)
Additionally, it is possible to initialize a Platform.worker without flags, and the solution is exactly what you think it would be:
NEW Main.elm
port module Main exposing (..)
main = Platform.worker
{
init = init
, update = update
, subscriptions = subscriptions
}
type alias Model = { myTextVal : String }
init : () -> (Model, Cmd MyMsg)
init _ = (Model "", Cmd.none)
type MyMsg = InMsg String
update : MyMsg -> Model -> (Model, Cmd MyMsg)
update msg model = case msg of
InMsg str -> (Model str, textOutput str)
port textInput : (String -> msg) -> Sub msg
port textOutput : String -> Cmd msg
decodeValue : String -> MyMsg
decodeValue str = InMsg str
subscriptions : Model -> Sub MyMsg
subscriptions model = textInput decodeValue
This was actually my first Elm program, and I'm happy that it worked perfectly! As always, JavaScript was the problem, not Elm.

Related

Receive message from an Elm process

I'm toying around with Elm processes in order to learn more about how they work. In parts of this, I'm trying to implement a timer.
I bumped into an obstacle, however: I can't find a way to access the result of a process' task in the rest of the code.
For a second, I hoped that if I make the task resolve with a Cmd, the Elm runtime would be kind enough to perform that effect for me, but that was a naive idea:
type Msg
= Spawned Process.Id
| TimeIsUp
init _ =
( Nothing
, Task.perform Spawned (Process.spawn backgroundTask)
)
backgroundTask : Task.Task y (Platform.Cmd.Cmd Msg)
backgroundTask =
Process.sleep 1000
-- pathetic attempt to send a Msg starts here
|> Task.map ( always
<| Task.perform (always TimeIsUp)
<| Task.succeed ()
)
-- and ends here
|> Task.map (Debug.log "Timer finished") -- logs "Timer finished: <internals>"
update msg state =
case msg of
Spawned id ->
(Just id, Cmd.none)
TimeIsUp ->
(Nothing, Cmd.none)
view state =
case state of
Just id ->
text "Running"
Nothing ->
text "Time is up"
The docs say
there is no public API for processes to communicate with each other.
I'm not sure if that implies that a process can't cummunicate with the rest of the app.
Is there any way to have update function receive a TimeIsUp once the process exits?
There is one way but it requires a port of hell:
make a fake HTTP request from the process,
then intercept it via JavaScript
and pass it back to Elm.
port ofHell : (() -> msg) -> Sub msg
subscriptions _ =
ofHell (always TimeIsUp)
backgroundTask : Task.Task y (Http.Response String)
backgroundTask =
Process.sleep 1000
-- nasty hack starts here
|> Task.andThen ( always
<| Http.task { method = "EVIL"
, headers = []
, url = ""
, body = Http.emptyBody
, resolver = Http.stringResolver (always Ok "")
, timeout = Nothing
}
)
Under the hood, Http.task invokes new XMLHttpRequest(), so we can intercept it by redefining that constructor.
<script src="elm-app.js"></script>
<div id=hack></div>
<script>
var app = Elm.Hack.init({
node: document.getElementById('hack')
})
var orig = window.XMLHttpRequest
window.XMLHttpRequest = function () {
var req = new orig()
var orig = req.open
req.open = function (method) {
if (method == 'EVIL') {
app.ports.ofHell.send(null)
}
return orig.open.apply(this, arguments)
}
return req
}
</script>
The solution is not production ready, but it does let you continue playing around with Elm processes.
Elm Processes aren't a fully fledged API at the moment. It's not possible to do what you want with the Process library on its own.
See the notes in the docs for Process.spawn:
Note: This creates a relatively restricted kind of Process because it cannot receive any messages. More flexibility for user-defined processes will come in a later release!
and the whole Future Plans section, eg.:
Right now, this library is pretty sparse. For example, there is no public API for processes to communicate with each other.

Can't decode session from elm port

Trying to get elm ports working to maintain the session.
In index.html, the script includes the following listener:
window.addEventListener("load", function(event) {
app.ports.onSessionChange.send(localStorage.session);
}, false);
localStorage.session looks like this (and it stays there until I've logged out):
{"email":"user#fake.com","token":"eyJhbG...","user_id":1,"handle":"me"}
The definition in Ports.elm is:
port onSessionChange : (Value -> msg) -> Sub msg
This port is connected to Main.elm here (let me know if I've forgotten to include some of the definitions below):
subscriptions : Model -> Sub Msg
subscriptions model =
Ports.onSessionChange sessionChange
sessionChange : Json.Decode.Value -> Msg
sessionChange value =
let
result =
Json.Decode.decodeValue sessionDecoder value
in
case result of
Ok sess ->
SetSession (Just sess)
Err err ->
SetSession Nothing
...
type alias Session =
{ email : String
, token : String
, user_id : Int
, handle : String
}
...
import Json.Decode as Decode exposing (..)
import Json.Decode.Pipeline as Pipeline exposing (decode, required)
sessionDecoder : Decode.Decoder Session
sessionDecoder =
Pipeline.decode Session
|> Pipeline.required "email" Decode.string
|> Pipeline.required "token" Decode.string
|> Pipeline.required "user_id" Decode.int
|> Pipeline.required "handle" Decode.string
...
type Msg
= NoOp
| SetSession (Maybe Session)
...
update msg model =
case msg of
SetSession session ->
case Debug.log "session = " session of
Just sess ->
({ model | session = sess } , Cmd.none)
Nothing ->
(model, Cmd.none)
Debug.log "session" displays Nothing in the console when the page loads, so JS is talking to elm, but the decoder seems to be failing. Any ideas?
I've plugged your code into a minimal working example and everything works fine. You might want to log the value of localStorage.session from inside the javascript portion to make sure it's a valid JSON value.

Handling missing keys in Flags gracefully in Elm

My app gets init model values from localstorage through flags. I added a new key to the model and it causes an error while starting the Elm app because of the missing key ("bar") in the value passed through flags. Considering that more new keys can be added in the future, and I don't want to have to clear localstorage every time it happens, is there a way to tell Elm to assign a default value when there is a missing key in the flag?
type alias Model =
{ foo : String, bar : Int }
update : msg -> Model -> ( Model, Cmd msg )
update _ model =
model ! []
view : Model -> Html msg
view model =
text <| toString model
main : Program Flags Model msg
main =
Html.programWithFlags
{ init = init
, update = update
, view = view
, subscriptions = always Sub.none
}
HTML code
<body>
<script>
var app = Elm.Main.fullscreen({foo: "abc"})
</script>
</body>
Here is a great solution that #ilias at the Elm Slack channel kindly provided.
https://ellie-app.com/mWrNyQWYBa1/0
module Main exposing (main)
import Html exposing (Html, text)
import Json.Decode as Decode exposing (Decoder)
import Json.Decode.Extra as Decode --"elm-community/json-extra"
type alias Model =
{ foo : String, bar : Int }
flagsDecoder : Decoder Model
flagsDecoder =
Decode.map2 Model
(Decode.field "foo" Decode.string |> Decode.withDefault "hello")
(Decode.field "bar" Decode.int |> Decode.withDefault 12)
init : Decode.Value -> ( Model, Cmd msg )
init flags =
case Decode.decodeValue flagsDecoder flags of
Err _ ->
Debug.crash "gracefully handle complete failure"
Ok model ->
( model, Cmd.none )
update : msg -> Model -> ( Model, Cmd msg )
update _ model =
model ! []
view : Model -> Html msg
view model =
text <| toString model
main : Program Decode.Value Model msg
main =
Html.programWithFlags
{ init = init
, update = update
, view = view
, subscriptions = always Sub.none
}
HTML
<body>
<script>
var app = Elm.Main.fullscreen({foo: "abc"})
</script>
</body>

elm-architecture separate signal handling?

I can't find an example anywhere online that answers the question: how does a parent component respond to different actions coming out of a child module?
Consider a simple chat message input with a submit button:
// child component: text input w/ a submit button
type Action
= InputChanged String
| MessageSent String
view : Signal.Address Action -> Model -> Html
view addr model =
div []
[ input
[ type' "text"
, value model.content
, on "input" targetValue (\val -> Signal.message addr (InputChanged val))
]
[]
, button
[ type' "submit"
, onClick addr (MessageSent model.content)
]
[ text "Send" ]
]
How does the parent component holding onto this input box respond to the two actions that might come out of that input box? A traditional "just passing through" looks like this:
// parent component, holds onto a list of posts and the child component
-- update
type Action
= MessageBoxAction MessageBox.Action
update : Action -> Model -> Model
update act model =
case act of
MessageBoxAction msg ->
{ model |
currentMessage = MessageBox.update msg model.currentMessage
}
-- view
view : Signal.Address Action -> Model -> Html
view addr model =
div []
[ MessageBox.view (Signal.forwardTo addr MessageBoxAction) model.currentMessage ]
What I want to be able to do is capture a message coming out of that child component and respond to it beyond the normal "just passing through". Something like this:
case act of
MessageBoxSubmit msg ->
let updatedMessage = MessageBox.update msg model.currentMessage
newPost = Posts.update msg model.posts
in
{ model |
posts = model.posts :: [ newPost ]
, currentMessage = updatedMessage
}
But I have no idea how to do this, particularly because when forwarding the address to a child it's not like you have the opportunity to provide more than one address...
MessageBox.view (Signal.forwardTo addr MessageBoxAction) model.currentMessage
There are two main routes to do this.
You can change the signature of MessageBox update to return a parent action that you provide to MessageBox init.
init : (String -> parentAction) -> Model
init onSend =
{ onSend = onSend
, content = ""
}
update : Action -> Model -> (Model, Maybe parentAction)
update action model =
case action of
MessageSent msg ->
let
model' = ...
in
(model', Just (model.onSend msg))
InputChanged str ->
let
model' = ...
in
(model', Nothing)
and in the parent module you do:
init =
{ posts = []
, currentMessage = MessageBox.init HandleSent
}
update : Action -> Model -> Model
update act model =
case act of
MessageBoxAction msg ->
let
(currentMessage', send) = MessageBox.update msg model.currentMessage
model' = {model | currentMessage = currentMessage'}
in
case send of
Nothing -> model'
Just act -> update act model' -- you recursively call the update function with the new action that you received from the MessageBox.update
HandleSent str -> { model | posts = str::model.posts }
You can provide a decoder for the action in the MessageBox module.
in MessageBox module
sentMessage action =
case action of
MessageSent msg -> Just msg
_ -> Nothing
in parent
update : Action -> Model -> Model
update act model =
case act of
MessageBoxAction msg ->
let
currentMessage' = MessageBox.update msg model.currentMessage
model' = {model | currentMessage = currentMessage'}
in
case MessageBox.sentMessage msg of
Nothing -> model'
Just str -> update (HandleSent str) model'
HandleSent str -> { model | posts = str::model.posts }

How to get query parameters in Elm?

In my Elm program, I'd like to initialize my model based on the query string.
For example, if the query string is ?w=3&h=5 I'd like to have:
initialModel =
{ width = 3
, height = 5
}
Is that possible to achieve this in Elm, or the only way to do this is to get the query parameters in Javascript and pass them via a port?
Elm 0.19
For elm 0.19 the below concept is the same. Both of these packages still exist but have been moved and relabeled as the official elm/url and elm/browser libraries.
Elm 0.18
This example uses evancz/url-parser and elm-lang/navigation. There are a few kinks that aren't straightforward in the documentation, but I've explained them briefly below. The example should speak for itself.
module Main exposing (..)
import Html as H exposing (..)
import Navigation exposing (Location)
import UrlParser as UP exposing ((</>), (<?>), top, parsePath, oneOf, s, stringParam, Parser)
import Maybe.Extra as MaybeExtra exposing (unwrap)
type Route
= UrlRoute (Maybe String) (Maybe String)
| NotFoundRoute
type Msg
= UrlParser Navigation.Location
type alias Model =
{ location : Route
, w : String
, h : String
}
type alias SearchParams =
{ w : Maybe String, h : Maybe String }
main =
Navigation.program UrlParser
{ init = init
, view = view
, update = update
, subscriptions = (\_ -> Sub.none)
}
init : Location -> ( Model, Cmd Msg )
init location =
let
currentPath =
parseLocation location
in
( initialModel currentPath
, Cmd.none
)
parseLocation : Location -> Route
parseLocation location =
case (parsePath matchers location) of
Just route ->
route
Nothing ->
NotFoundRoute
matchers : Parser (Route -> a) a
matchers =
UP.map UrlRoute (UP.s "index" <?> UP.stringParam "w" <?> UP.stringParam "h")
initialModel : Route -> Model
initialModel route =
{ location = route
, w = MaybeExtra.unwrap "" (\x -> Maybe.withDefault "" x.w) (parseParams route)
, h = MaybeExtra.unwrap "" (\x -> Maybe.withDefault "" x.h) (parseParams route)
}
parseParams : Route -> Maybe SearchParams
parseParams route =
case route of
UrlRoute w h ->
Just { w = w, h = h }
NotFoundRoute ->
Nothing
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
UrlParser location ->
( model
, Cmd.none
)
view : Model -> Html msg
view model =
div []
[ h1 [] [ text "URL Info" ]
, div [] [ text ("W is: " ++ model.w) ]
, div [] [ text ("H is: " ++ model.h) ]
]
The "trick" is to create another type alias to place your query params inside of. In the above example I've created the type SearchParams. After creating this type we just use an initialModel that takes in the currentPath.
From there, our model can extract the query params with Maybe.withDefault (it needs to be a Maybe type because the params may not be there). Once we have our data in the model we just print it out in the view.
Hope this helps!
There is no built-in core library way to access the URL. You can use ports and the community library jessitron/elm-param-parsing.
If you also want to set the URL, you can again use ports, or you can use the History API, for which there are bindings in TheSeamau5/elm-history.
Unfortunately jessitron/elm-param-parsing doesn't work with Elm 0.18.
Use elm-lang/navigation package:
http://package.elm-lang.org/packages/elm-lang/navigation/latest/Navigation
https://github.com/elm-lang/navigation/tree/2.1.0
especially this function:
program
: (Location -> msg)
-> { init : Location -> (model, Cmd msg), update : msg -> model -> (model, Cmd msg), view : model -> Html msg, subscriptions : model -> Sub msg }
-> Program Never model msg
In the second parameter you can see "init : Location -> (model, Cmd msg)". This should handle reading of initial URL. To complement that, first parameter is a function which gets called every time URL changes.
(I am aware it's an old question, but this link popped out when I was looking for the solution to the same problem and accepted answer didn't help)