HTTP request without request in Elm 0.18 - elm

I want to do something very un-functional and make an HTTP request in elm without processing any kind of response. Basically something like this:
testView : Html Msg
testView =
div [] [
button [onClick TestAction] [text "Test Action"]
]
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
...
TestAction ->
( model, testActionCmd )
...
import Http
import HttpBuilder exposing (..)
...
testActionCmd : Cmd Msg
testActionCmd =
( "http://localhost:4000/fakeurl" )
|> get -- this is a side effect; unrelated to the Msg below
Cmd.none -- this is what I want to return
Is there a way to do something like this in Elm?

In short, no, you won't be able to do that (not without writing your own effect manager or using ports).
The "problem" is that the Http module allows you to create a Task which you then need to convert into a Cmd to perform the task. But to go from a Task to a Cmd you need to provide a Msg. See http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Task
So what you'll need to do is create one of those Noop messages.

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.

How can I export console log win Elm?

It is difficult to export console.log with Elm.
I want to output the console log by pushing button like below and with console log in function. How shold I do?
sampleView : Html Message
sampleView =
Html.div (class "sample-card" :: Styles.sampleCard)
[ Html.div
(class "sample-card-list" :: Styles.sampleCardList)
[
Html.button
(class "sample-card-button" :: Styles.sampleCardListButton)
[
Html.text "サンプルボタン"
]
]
]
If I use Javascript, I want to do like below.
<button class="sample-card-button" onclick="btnClick();">サンプルボタン</button>
/* JavaScript 側 */
function btnClick() {
console.log("クリックされました");
});
If it's just for debugging, you can use Debug.log, but you can't use the Debug module in production code (running the compiler with --optimize will fail to compile if you have Debug usages). Using it can look like this:
sampleView : Html Message
sampleView =
Html.div (class "sample-card" :: Styles.sampleCard)
[ Html.div
(class "sample-card-list" :: Styles.sampleCardList)
[
Html.button
([class "sample-card-button", onClick CardButtonClicked] ++ Styles.sampleCardListButton)
[
Html.text "サンプルボタン"
]
]
]
update : Message -> Model -> ( Model, Cmd Message )
update msg model =
case msg of
CardButtonClicked ->
let
_ = Debug.log "クリックされました" msg
in
( model, Cmd.none )
If you want to write to the console in a production app, you'll need to use a port. To create a port, you'll need to update the module it's in to be declare a port module (just add port to the beginning of of the module declaration on the first line of the file).
port module Main exposing (..)
port consoleLog : String -> Cmd msg
update : Message -> Model -> ( Model, Cmd Message )
update msg model =
case msg of
CardButtonClicked ->
( model, consoleLog "クリックされました" )
Then you'll need to wire up some JavaScript to handle that port message:
var app = Elm.Main.init({ node: document.querySelector('main') })
app.ports.consoleLog.subscribe(function (msg) {
console.log(msg);
});
Here's an example app using a port to call console.log.

Retrieving a DOM value from Elm ports

My elm app uses an auto scrolling function, which gets the Y position of an element and uses Dom.Scroll.toY to scroll there.
Two do this, I set up two ports; a subscription and sender.
ports.elm
port setYofElementById : Maybe String -> Cmd msg
port getYofElementById : (Value -> msg) -> Sub msg
index.html
app.ports.setYofElementById.subscribe(function(id) {
var element = document.getElementById(id);
var rect = element.getBoundingClientRect();
app.ports.getYofElementById.send({"number": rect.top});
})
The listener is a subscription
subscriptions : Model -> Sub Msg
subscriptions model =
Ports.getYofElementById getYofElementById
getYofElementById : Decode.Value -> Msg
getYofElementById value =
let
result =
Decode.decodeValue bSimpleIntValueDecoder value
in
case result of
Ok simpleIntValue ->
SetSelectedElementYPosition (Just simpleIntValue.number)
Err id ->
SetSelectedElementYPosition Nothing
SetSelectedElementYPosition just sets the model.
Now, the action that executes this does two things: call Port.setYofElementById, then scrolls to the Y value in the model, assuming that it has already been set.
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
ScrollToY idString ->
model
=> Cmd.batch
[ Ports.setYofElementById (Just idString)
, Task.attempt (always NoOp) <| Dom.Scroll.toY "ul" model.selectedElementYPosition
]
However, this doesn't happen sequentially. When the action first fires, nothing happens. If I fire it again, it scrolls to the location called for in the first action. So it seems like it is calling Dom.Scroll.toY before the value is set.
Is there a way to force the Cmds in ScrollToY to happen in sequence? Or is there a better way to do this in general?
You can get the Cmds to execute in sequence by making the second, the one that does the Dom.Scroll.toY, happen as a response to the first, the one that does the setYofElementById. The following update function accomplishes this:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
ScrollToY idString ->
(model, Ports.setYofElementById idString)
SetSelectedElementYPosition (Just newY) ->
(model, Task.attempt (always NoOp) <| Dom.Scroll.toY "ul" newY)
SetSelectedElementYPosition Nothing ->
(model, Cmd.none)
NoOp ->
(model, Cmd.none)
With the Cmds correctly sequenced, you will need to make sure that the newY argument to Dom.Scroll.toY is in the correct frame of reference to get the effect that you want.
I finally got this to work by tacking the action for Task.attempt (always NoOp) <| Dom.Scroll.toY "ul" model.selectedElementYPosition onto the action called by the subscription, not the action. That's the key.
With ports, the subscribe and send actions follow completely different pathways, so anything that reacts to a send from js to elm is not going to be referenced in the actions that go from elm to js.
In this case, since SetSelectedElementYPosition is being called from the subscription, you have to set the update there:
SetSelectedElementYPosition idString ->
({model | selectedElementYPosition = number }, Cmd.none)
|> andThen update GoToSelectedElementYPosition

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.

How do I implement and return a Cmd Msg?

How do I implement and return a Cmd Msg?
For example, the following line generates a Cmd Msg:
Http.send msg request
It's used in the following function:
tryRegister : Form -> (Result Http.Error JsonProfile -> msg) -> Cmd msg
tryRegister form msg =
let
registerUrl =
"http://localhost:5000/register"
body =
encode form |> Http.jsonBody
request =
Http.post registerUrl body decoder
in
Http.send msg request
I'm trying to hand code a similar function within my TestAPI:
tryRegister : Form -> (Result Http.Error JsonProfile -> msg) -> Cmd msg
tryRegister form msg =
Cmd.none
The above code compiles. However, it's not clear to me how to implement a function that returns a Cmd Msg other than Cmd.none.
Appendix:
type Msg
=
...
| Submit
| Response (Result Http.Error JsonProfile)
update : Msg -> Form -> ( Form, Cmd Msg )
update msg model =
case msg of
...
Submit ->
( model, runtime.tryRegister model Response )
Source code on GitHub.
Edit
The original answer suggested mapping over Cmd.none, which compiles and may potentially be useful when mocking out functions for testing, but if you are actually trying to force another update cycle in The Elm Architecture, you will need to convert to a Task, as outlined in the send function described here.
send : msg -> Cmd msg
send msg =
Task.succeed msg
|> Task.perform identity
As #SwiftsNamesake mentioned above, in most cases this is not necessary, and the entire blog post on the subject is worth a read.
Original Answer
You can use Cmd.map over Cmd.none to change it to any Cmd:
Cmd.map (always Submit) Cmd.none