Elm submodule's init command not firing - elm

My app uses Navigate for routing, but, for some reason, the initial http command is not firing for one of the submodules. There could be something wrong in the way commands are wired between parent and child modules, but I could use some help locating the issue. The code and structure is modeled after the elm-taco (https://github.com/ohanhi/elm-taco) example. Below is an excerpt of the relevant parts of the code, and there is no compiler error.
Main.elm
init : Flags -> Location -> ( AppModel, Cmd Msg )
init flags location =
( startModel
, Http2.get ("localhost:4000/graphql?query=" ++ encoded) HandleLogDataResponse decoder)
update : Msg -> AppModel -> ( AppModel, Cmd Msg )
update msg model =
case msg of
HandleLogDataResponse webData ->
updateLogData model webData
RouterMsg routerMsg ->
updateRouter model routerMsg
updateLogData : AppModel -> WebData LogData -> ( AppModel, Cmd Msg )
updateLogData model webData =
case webData of
Success logdata ->
case model.appState of
NotReady time ->
let
initTaco =
{ currentTime = time
, logdata = logdata
}
( initRouterModel, routerCmd ) =
Router.init initTaco model.location
in
( { model | appState = Ready initTaco initRouterModel }
, Cmd.map RouterMsg routerCmd
)
Ready taco routerModel ->
( { model | appState = Ready (updateTaco taco (UpdateLogData logdata)) routerModel }
, Cmd.none
)
Router.elm
init : Taco -> Location -> (Model, Cmd Msg)
init taco location =
let
( chatModel, chatCmd) =
Chat.initModel taco
in
( { chatModel = chatModel
, route = parseLocation location}
, Cmd.map ChatMsg chatCmd
)
update: Msg -> Model -> ( Model,Cmd Msg, TacoUpdate)
update msg model =
case msg of
ChatMsg chatMsg ->
updateChat model chatMsg
updateChat : Model -> Chat.Msg -> (Model, Cmd Msg, TacoUpdate)
updateChat model chatMsg =
let
( nextChatModel, chatCmd, tacoUpdate) =
Chat.update chatMsg model.chatModel
in
( {model | chatModel = nextChatModel }
, Cmd.map ChatMsg chatCmd
, tacoUpdate)
Chat.elm
initModel : Taco -> ( Model, Cmd Msg )
initModel taco =
let
startModel = { newMessage = ""
, messages = taco.messages
, response = "Waiting for a response..."
, logs = []
}
cmd = Http.send FetchHNTopStories request
in
( startModel
, cmd
)

In order to have the result of the execution of a command, you need to provide a way for it to get to the runtime.
This is done through main. If your Cmd do not get to the main of the app, they will never be executed.
In the code samples you gave, it is not obvious how the submodule init gets to the main. Traditionally, the init you have in your Main.elm should use the inits from the submodule, Cmd.map the submodule init Cmds and Cmd.batch them together with the rest of the main init Cmds.

Related

Why isn't my bootstrap request being sent to the server after refreshing the landing page?

The breakpoint on the local server is triggered only once regardless of the page refreshes.
main =
Navigation.program UrlChange
{ init = init
, view = view
, update = update
, subscriptions = (\_ -> Sub.none)
}
init : Navigation.Location -> ( Model, Cmd Msg )
init location =
( { currentRoute = location
, ...
}
, runtime.bootstrap BootstrapResponse
)
The Explore History window reports that only one message was generated after a page refresh:
BootstrapResponse Ok
Question:
Why isn't my bootstrap request being sent to the server after refreshing the landing page?
Appendix:
bootstrap : (Result Http.Error JsonBootstrap -> msg) -> Cmd msg
bootstrap msg =
let
url =
baseUrl ++ "bootstrap"
request =
Http.get url bootstrapDecoder
in
Http.send msg request
Update:
I tested this on both Chrome and Edge.

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 divide subscription?

I'm playing with Elm and WebRTC, so I made a listen port which gets some messages from js:
type alias Message =
{ channel : String
, data : String
}
port listen : (Message -> msg) -> Sub msg
Now I would like to be able to divide the messages to different parts of my app. For instance, the chat uses the "chat" channel, while the game logic uses "game".
Is it possible to create a listenTo String subscription that filters out the messages with the correct channel (only returning the data)? Or perhaps a different way of doing it?
Update:
What I currently have, is something like this:
In my main.elm I have an update that looks like this. It can receive messages (from rtc) itself, and send messages for chat to it. (I would later add a "ForGame" then too)
type Msg = Received WebRTC.Message | ForChat Chat.Msg
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
Received message ->
let
_ = Debug.log ("Received message on \"" ++ message.channel ++ "\": " ++ message.data)
in
( model
, Cmd.none
)
ForChat msg ->
let
(chatModel, chatCmd) = Chat.update msg model.chat
in
({ model | chat = chatModel}, Cmd.map ForChat chatCmd)
Then I have subscriptions that combines all my subscriptions:
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ WebRTC.listen Received
, Sub.map ForChat <| Chat.subscriptions model.chat
]
In Chat.elm I have a similar structure, with an update that handles it's messages. The subscription of the chat listens to all messages from WebRTC, but filters only the ones with channel chat:
subscriptions : Model -> Sub Msg
subscriptions model = WebRTC.listen forChatMessages
forChatMessages : WebRTC.Message -> Msg
forChatMessages webrtcMessage =
if webrtcMessage.channel == "chat"
then
let
message = decodeMessage webrtcMessage.data
in
case message of
Ok msg -> Receive msg
Err error -> Debug.log ("Received unreadable message on chat channel \"" ++ toString webrtcMessage.data ++ "\" with error \"" ++ error ++ "\"") Ignore
else
Ignore
(Ignore is a Msg for chat, which just does nothing case msg of Ignore -> (model, Cmd.none). decodeMessage uses a decoder to decode a message decodeMessage : String -> Result String Message.)
I'm quite happy with this, because this way all logic for chat is in Chat.elm. So main.elm doesn't need to know what channels chat is using. Chat just follows the standard structure (Msg, update, view, subscriptions) and main forwards everything.
The only thing that's still not great, is that in Chat.elm I have the forChatMessages function. Used like: subscriptions model = WebRTC.listen forChatMessages. I would like to make this more reuseable, so it would become something like:
subscriptions model = WebRTC.listen for "chat" decodeMessage Receive Ignore
It would then be reusable by the game:
subscriptions model = WebRTC.listen for "game" decodeGameInfo UpdateInfo Ignore
Update 2:
I managed to generalize the forChatMessages function into:
for : String -> (String -> Result String d) -> (d -> msg) -> msg -> Message -> msg
for channel decoder good bad webrtcMessage =
if
webrtcMessage.channel == channel
then
let
decoded = decoder webrtcMessage.data
in
case decoded of
Ok data -> good data
Err error -> Debug.log ("Failed decoding message on " ++ channel ++ "channel \"" ++ toString webrtcMessage.data ++ "\" with error \"" ++ error ++ "\"") bad
else
bad
So I think I found the solution myself. Unless someones has comments on this. Perhaps there is a cleaner/nicer/better way of doing the same?
Let's say you have the following Msg definition:
type Msg
= Listen Message
| GameChannel String
| ChatChannel String
Your update function could then act upon the channel value and call update again with the correct channel, ignoring all channel values except for "game" and "chat":
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Listen message ->
case message.channel of
"game" ->
update (GameChannel message.data) model
"chat" ->
update (ChatChannel message.data) model
_ ->
model ! []
GameChannel data ->
...
ChatChannel data ->
...
Your subscription function would look something like this:
subscriptions : Model -> Sub Msg
subscriptions model =
listen Listen
I found a solution myself, and added it to the original question.
For clarity, this is the short version:
In my main.elm:
type Msg = Received WebRTC.Message | ForChat Chat.Msg
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
Received message ->
let
_ = Debug.log ("Received message on \"" ++ message.channel ++ "\": " ++ message.data)
in
( model
, Cmd.none
)
ForChat msg ->
let
(chatModel, chatCmd) = Chat.update msg model.chat
in
({ model | chat = chatModel}, Cmd.map ForChat chatCmd)
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ WebRTC.listen Received
, Sub.map ForChat <| Chat.subscriptions model.chat
]
In Chat.elm:
subscriptions : Model -> Sub Msg
subscriptions model = WebRTC.listen <| for "game" decodeGameInfo UpdateInfo Ignore
In WebRTC.elm:
type alias Message =
{ channel : String
, data : String
}
port listen : (Message -> msg) -> Sub msg
for : String -> (String -> Result String d) -> (d -> msg) -> msg -> Message -> msg
for channel decoder good bad webrtcMessage =
if
webrtcMessage.channel == channel
then
let
decoded = decoder webrtcMessage.data
in
case decoded of
Ok data -> good data
Err error -> Debug.log ("Failed decoding message on " ++ channel ++ "channel \"" ++ toString webrtcMessage.data ++ "\" with error \"" ++ error ++ "\"") bad
else
bad

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)