Retrieving a DOM value from Elm ports - elm

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

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.

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

Basic Chat using elm-native-ui

I'm creating a basic native chat app with elm-native-ui.
I can open or close the chat by clicking on a button that change the chatOpen boolean
activeChannelView =
case chatOpen of
True ->
Maybe.map (\a -> chatView users a messages) activeChannel ? viewEmpty
False ->
Maybe.map (cardView users) activeChannel ? viewEmpty
on the False side, it’s working well but on the True side it says that viewEmpty needs to be a (String -> Node Msg) instead of being (Node Msg) but the left side (chatView user a messages) returns Node Msg
Check the error below
The right side of (?) is causing a type mismatch.
Maybe.map (\a -> chatView users a messages) activeChannel ? viewEmpty
^^^^^^^^^
(?) is expecting the right side to be a:
String -> Node Msg
But the right side is:
Node Msg
I suppose it means that:
if activeChannel is (Just a), True returns (String -> Node Msg)
How is it possible to have this error since chatView is defined like that:
chatView : List User -> Request -> List String -> String -> Node Msg
chatView takes four parameters and you are passing only three.

How to filter a Signal on page load

For the sake of learning, I'm trying to load content only when I click on a button. So far I've managed to :
Reload the content when I click the button.
And Filter the Signal when I click (if the String I send is not "GETPERF")
But my problem is that the Ajax call is still triggered once the page loads.
Here's the code:
-- SIGNALS & MAILBOX
inbox : Signal.Mailbox String
inbox =
Signal.mailbox "SOME TEXT"
result : Signal.Mailbox String
result =
Signal.mailbox ""
-- VIEW
view : String -> Html
view msg =
div [] [
h1 [] [text "Mailbox3"],
p [] [text msg],
button
[onClick inbox.address "GETPERF"]
[text "click perf"],
]
main : Signal Html
main =
Signal.map view result.signal
-- TASK & EFFECTS
port fetchReadme : Signal (Task Http.Error ())
port fetchReadme =
inbox.signal
|> Signal.filter (\sig -> sig == "GETPERF" ) "boo"
|> Signal.map (\_ -> Http.getString "http://localhost:3000/dates" `andThen` report)
report : String -> Task x ()
report html =
Signal.send result.address html
Is there any way to prevent the first Ajax call on page load ? (Or some more idiomatic way of doing all this ?)
The reason you're getting an initial ajax request is that Signal.filter is still keeping that initial value of "boo" (See the Signal.filter documentation here). That value is ignored in the next Signal.map statement by your use of the underscore parameter, but the Http Task is still getting returned and that's why you see an initial ajax request on page load.
Instead of using Signal.filter, you could write a conditional that only sends the ajax request in the correct circumstances, when sig is "GETPERF". And if sig is not "GETPERF" (as in page load), you can, in essence, do nothing by returning Task.succeed (). Here is a refactored fetchReadme function with these changes:
port fetchReadme : Signal (Task Http.Error ())
port fetchReadme =
let
fetchAndReport sig =
if sig == "GETPERF" then
Http.getString "http://localhost:3000/dates"
`andThen` report
else
Task.succeed ()
in
Signal.map fetchAndReport inbox.signal