Basic Chat using elm-native-ui - elm

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.

Related

How do you iterate a List (Maybe a)

I have the following graphQL result:
[Just { details = Just "Engine failure at 33 seconds and loss of
vehicle", launch_year = Just "2006", links = Just { article_link =
Just
"https://www.space.com/2196-spacex-inaugural-falcon-1-rocket-lost-launch.html"
}, mission_name = Just "FalconSat" }]
Based on the following types:
type alias Launch =
{ mission_name : Maybe String
, details : Maybe String
, launch_year : Maybe String
, links : Maybe LaunchLinks
}
type alias Launches =
Maybe (List (Maybe Launch))
type alias LaunchLinks =
{ article_link : Maybe String
}
I want to List.map through and display the results in unordered list. I started with this:
renderLaunch : Launches -> Html Msg
renderLaunch launches =
div [] <|
case launches of
Nothing ->
[ text "Nothing here" ]
Just launch ->
launch
|> List.map (\x -> x)
|> ul []
But I keep getting this error:
This function cannot handle the argument sent through the (|>) pipe:
141| launch 142| |> List.map (\x
-> x) 143| |> ul []
^^^^^ The argument is:
List (Maybe Launch)
But (|>) is piping it a function that expects:
List (Html msg)
The problem is that the Just launch case needs to result in a List (Html msg) but the code results in a different type being returned.
When you are using List.map (\x -> x), it is essentially a no-op. You are iterating over a List (Maybe Launch) and returning the same thing. I'd recommend creating another function that takes a Maybe Launch value and use that as your mapping function. For example:
displayLaunch : Maybe Launch -> Html Msg
displayLaunch launch =
case launch of
Nothing -> text "No launch"
Just l -> text (Debug.toString l)
Now you can plug that into your mapping function:
Just launch ->
launch
|> List.map displayLaunch
|> ul []
But, whoops! Now you get a new error indicating:
The 2nd branch is:
Html Msg
But all the previous branches result in:
List (Html msg)
The problem here is that we are now returning a ul from the Just launch branch and we need to return a list of html. You can use List.singleton to create a list with just one item:
Just launch ->
launch
|> List.map displayLaunch
|> ul []
|> List.singleton

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

Getting type signatures for a function in elm

I'm using elm 0.18.
Let's say I have a function that strings together a bunch of stuff that I threw together in a hurry. It works, but I'm not sure what it's type signature is, and I'd like elm to tell me (or hint for me) that type signature.
For example, I use graphql and have a function that takes a graphql string, a decoder (which also doesn't have a type signature), and a Cmd Msg, and runs it through HttpBuilder.
graphQLPost graphiql decoder msg =
HttpBuilder.post (url ++ "api")
|> HttpBuilder.withStringBody "text/plain" graphiql
|> HttpBuilder.withExpect (Http.expectJson decoder)
|> HttpBuilder.send msg
This works, though I don't know why. I tried fitting it with the type signature graphQLPost : String -> Json.Decode.Decoder -> Cmd Msg, but I get an error.
Figuring out this type signature is not as important to me as finding a way to induce them through elm. Is there a command that I can enter into elm-repl or something that will tell me the signature?
Elm REPL will do this for you:
> import Http
> import HttpBuilder
> type Msg = Msg
> url = "..."
"..." : String
> graphQLPost graphiql decoder msg = \
| HttpBuilder.post (url ++ "api") \
| |> HttpBuilder.withStringBody "text/plain" graphiql \
| |> HttpBuilder.withExpect (Http.expectJson decoder) \
| |> HttpBuilder.send msg
<function>
: String
-> Json.Decode.Decoder a
-> (Result.Result Http.Error a -> msg)
-> Platform.Cmd.Cmd msg
When you write a function and hit <Enter>, it shows you the signature. In this case the signature is:
graphQLPost : String
-> Json.Decode.Decoder a
-> (Result.Result Http.Error a -> msg)
-> Platform.Cmd.Cmd msg
Running elm-make with the --warn option will cause the compiler to suggest that you include a type annotation on functions that don't have one, and it will provide one for you to copy and paste in.
Also, some editor integrations, such as the Visual Studio Code language extension for Elm, will display these kinds of warnings as a hint icon that you can click to add the missing type annotation automatically. You can set a keyboard shortcut for this to do it without your hands leaving the keyboard.

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