In a few guides, message handling in parent components is made like this;
type Msg
= NavMsg Nav.Msg
| SidebarMsg Sidebar.Msg
| WidgetMsg Widget.Msg
And parent components handle them in updates with:
update : Msg -> AppModel -> (AppModel, Cmd Msg)
update message model =
case message of
WidgetMsg subMsg ->
let
(updatedWidgetModel, widgetCmd) =
Widget.update subMsg model.widgetModel
in
({ model | widgetModel = updatedWidgetModel }, Cmd.map WidgetMsg widgetCmd)
_ ->
However, I couldn't find a simple way to do the same if the child components are inside a list. How can I tell the correct sub component to react to a message directed to him?
I thought of adding the component object to the message:
type Msg
= MessageToParent
| MessageToChild Child Child.Msg
But this seems very inefficient if the Child component is big, and still gives me trouble when trying to delegate the inner Child.Msg to the right Child.
What's the best way to handle message passing to a list of components?
I would suggest using an array (or Dict) and code like this
type Msg
= WidgetMsg Int Widget.Msg
update : Msg -> AppModel -> (AppModel, Cmd Msg)
update message model =
case message of
WidgetMsg idx subMsg ->
model.widgetModels
|> Array.get idx
|> Maybe.map (Widget.update subMsg)
|> Maybe.map (\(m,c) ->
({ model | widgetModels = Array.set idx m model.widgetModels }
, Cmd.map Widgetmsg idx c)
)
|> Maybe.withDefault (model, Cmd.none)
_ ->
Related
I am new to Elm and just read the docs (https://guide.elm-lang.org/). I am modifying an example from there and playing around.
What I want to do is to hit an endpoint which will give me a list of IDs. Later I want to hit another endpoint with each of these IDs and display the results.
https://hacker-news.firebaseio.com/v0/topstories.json -
This endpoint has a list of IDs.
https://hacker-news.firebaseio.com/v0/item/[ID].json -
This endpoint will give the details of the story of given ID.
With what I have till now, I can get the list of all IDs separately and I can get each story separately (hard-coded ID) and display them. But what I am trying achieve here is to
get the list of IDs (500 of them) from endpoint 1
get first 5 of the stories by hitting endpoint 2
have a "load more" button which will load 5 more and so on
I am not sure how to do this. Any help is greatly appreciated.
Thanks
You can fire the second request when you handle the response from the first endpoint. Something like:
type Msg
= GotIds (Result Http.Error (List Int))
| GotStory (Result Http.Error (String))
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
GotIds result ->
case result of
Ok (first::rest) ->
({ model | ids = first::rest }, getStory first)
Ok _ ->
(model, Cmd.none)
Err _ ->
({ model | story = "ERROR"}, Cmd.none)
GotStory result ->
({model | story = Result.withDefault "None" result}, Cmd.none)
If you want to fire multiple Cmd at the same time, you can use Cmd.batch
Here is an Ellie that gets the ids from the first request and then fetches the title for the first ID.
You will want to create a custom type and decoder for each post.
For posterity's sake, here is all of the code from the Ellie:
module Main exposing (main)
import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
import Http
import Json.Decode exposing (Decoder, field, int, list, string )
type alias Model =
{ ids : List Int
, story : String
}
initialModel : Model
initialModel =
{ ids = []
, story = "None"
}
type Msg
= GotIds (Result Http.Error (List Int))
| GotStory (Result Http.Error (String))
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
GotIds result ->
case result of
Ok (first::rest) ->
({ model | ids = first::rest }, getStory first)
Ok [] ->
(model, Cmd.none)
Err _ ->
({ model | story = "ERROR"}, Cmd.none)
GotStory result ->
({model | story = Result.withDefault "None" result}, Cmd.none)
view : Model -> Html Msg
view model =
div []
[ text model.story
]
main : Program () Model Msg
main =
Browser.element
{ init = init
, view = view
, update = update
, subscriptions = (\_ -> Sub.none)
}
init : () -> (Model, Cmd Msg)
init flags =
(initialModel, getIds)
getIds : Cmd Msg
getIds =
Http.get
{ url = "https://hacker-news.firebaseio.com/v0/topstories.json"
, expect = Http.expectJson GotIds (list int)
}
getStory : Int -> Cmd Msg
getStory id =
Http.get
{ url = "https://hacker-news.firebaseio.com/v0/item/" ++ String.fromInt id ++ ".json"
, expect = Http.expectJson GotStory (field "title" string)
}
(This is related to Initializing an empty file value in Elm )
I am using Elm (0.18) and imported simonh1000's FileReader library. To store a file value, we use the following json type:
type alias FileContentArrayBuffer =
Value
and I structure my model thusly:
type alias Model =
{
username : String
, filecontent: Maybe FileContentArrayBuffer
}
initialModel : Model
initialModel =
{
username = "mark"
, filecontent = Nothing
}
When a file is dropped into place, getFileContents is called. The relevant functions and msg's are as follows:
getFileContents : NativeFile -> Cmd Msg
getFileContents nf =
FileReader.readAsArrayBuffer nf.blob
|> Task.attempt OnFileContent
type Msg
...
| OnFileContent (Result FileReader.Error (Maybe FileContentArrayBuffer))
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
...
OnFileContent res ->
case res of
Ok (Just filecontent) ->
( { model | filecontent = filecontent }, Cmd.none )
Ok Nothing ->
Debug.crash "No Content"
Err err ->
Debug.crash (toString err)
When I compile I get this error:
The right side of (|>) is causing a type mismatch.
56| FileReader.readAsArrayBuffer nf.blob
57|> |> Task.attempt OnFileContent
(|>) is expecting the right side to be a:
Task.Task FileReader.Error FileReader.FileContentArrayBuffer -> a
But the right side is:
Task.Task FileReader.Error (Maybe FileReader.FileContentArrayBuffer)
-> Cmd Msg
Not sure why, given that I have included the Maybe in my Type and provided cases. Any ideas?
If you define Msg like this instead:
type Msg
...
| OnFileContent (Result FileReader.Error FileContentArrayBuffer)
Then your update case can set the file value when successful or set to Nothing in failure:
OnFileContent res ->
case res of
Ok filecontent ->
( { model | filecontent = Just filecontent }, Cmd.none )
Err err ->
( { model | filecontent = Nothing }, Cmd.none )
Note that Debug.crash should be avoided at all costs. It really is just there for temporary debugging. It would be better perhaps to add an error message property on your model to notify the user of a problem.
I am unable to trigger a response message when sending an initial message.
I have a button:
button
[ class "register"
, value "Create Account"
, onClick Submit
]
I have the following messages:
type Msg
= Submit
| Response (Result Http.Error JsonProfile)
The message handler that is invoked via button click is the following:
update : Msg -> Form -> ( Form, Cmd Msg )
update msg model =
case msg of
Submit ->
( model, runtime.tryRegister model Response )
...
Here's the other message handlers:
update : Msg -> Form -> ( Form, Cmd Msg )
update msg model =
case msg of
Submit ->
( model, runtime.tryRegister model Response )
Response (Ok json) ->
( model, Navigation.load <| "/#/portal/1" )
Response (Err error) ->
( model, Cmd.none )
My tryRegister implementation is the following:
tryRegister : Form -> (Result Http.Error JsonProfile -> msg) -> Cmd msg
tryRegister form msg =
let
jsonProfile =
JsonProfile 1 form.firstName form.lastName form.email
newMsg v =
msg
in
Cmd.map (newMsg <| Result.Ok jsonProfile) Cmd.none
Here's the client code to the elm module depicted above:
onRegistration : Registration.Msg -> Model -> ( Model, Cmd Msg )
onRegistration subMsg model =
let
( form, _ ) =
Registration.update subMsg model.registration
in
case subMsg of
Registration.Submit ->
( { model | registration = form }, Cmd.none )
Registration.Response result ->
case result of
Result.Ok jsonProfile ->
let
newUser =
jsonProfileToProvider jsonProfile
newState =
{ model
| registration = form
, portal =
{ initPortal
| provider = newUser
, requested = Domain.EditProfile
, linksNavigation = False
, sourcesNavigation = False
}
}
in
( newState, Navigation.load <| "/#/portal/" ++ getId newUser.profile.id )
Result.Err _ ->
( model, Cmd.none )
Expectation:
I expect that when I click the button, that navigation takes place.
However, nothing happens and I don't understand why.
Video
Source code is here.
Apparently Cmd.map (...) Cmd.none is not sufficient to force another update cycle. You can force an update cycle by sending an always-succeeding task with Task.perform.
tryRegister : Form -> (Result Http.Error JsonProfile -> msg) -> Cmd msg
tryRegister form msg =
JsonProfile 1 form.firstName form.lastName form.email
|> Result.Ok
|> msg
|> Task.succeed
|> Task.perform identity
Note: There are good reasons not to do this, as outlined here, but we'll ignore those for now to fit the framework you've outlined
However, that alone will not make your code work. You have a nested update call which ignores the Cmd returned from Register.update:
( form, _ ) =
Registration.update subMsg model.registration
That underscore has the effect of blocking all commands generated from the child update. You will need to retain that child Cmd, map it to the parent Cmd, and return it instead of Cmd.none inside all onRegistration cases. For example:
onRegistration : Registration.Msg -> Model -> ( Model, Cmd Msg )
onRegistration subMsg model =
let
( form, subcmd ) =
Registration.update subMsg model.registration
regcmd =
Cmd.map OnRegistration subcmd
in
case subMsg of
Registration.FirstNameInput _ ->
( { model | registration = form }, regcmd )
...
Cmd.none is used in tryRegister function, which does nothing. I think you should use Http.send which actually fires the message loop after the http request completes.
The shortest example...,
update msg model =
case msg of
Submit ->
(model, Http.send Response request)
Response ... ->
...
I've finished the Elm guide and noticed on very simple examples, the update function grows to 3 cases and the Msg type can have 3 constructors. I imagine on an intermediate project, this would grow to 20 and on an advance project, it might be hundreds. How do you manage this? I foresee this being a source of version control contention if every developer needs to add a new constructor for their feature.
I worked on a react-redux project and it has a concept of combining reducers to solve this problem. I did not run across that concept in Elm. Does it have one?
You can define msg type consists of child/sub msg types, and of course, updater can be combined with sub functions. ie.
-- Counter
type CounterMsg
= Increment
| Decrement
type alias CounterModel =
Int
updateCounter : CounterMsg -> CounterModel -> ( CounterModel, Cmd msg )
updateCounter msg model =
case msg of
Increment ->
( model + 1, Cmd.none )
Decrement ->
( model - 1, Cmd.none )
-- Todo
type TodoMsg
= AddTodo String
type alias TodoModel =
List String
updateTodo : TodoMsg -> TodoModel -> ( TodoModel, Cmd msg )
updateTodo msg model =
case msg of
AddTodo str ->
( str :: model, Cmd.none )
-- unified
type alias Model =
{ counter : CounterModel
, todos : TodoModel
}
type Msg
= Counter CounterMsg
| Todo TodoMsg
initModel =
{ counter = 0, todos = [] }
update : Msg -> Model -> ( Model, Cmd msg )
update msg model =
case Debug.log "message" msg of
Counter countermsg ->
let
( newmodel, cmd ) =
updateCounter countermsg model.counter
in
( { model | counter = newmodel }, cmd )
-- etc...
_ ->
( model, Cmd.none )
Take a look at Richard's implementation for RealWorld/Conduit. It provides a realistic way to structure a large enough app (few thousands lines of code).
In short, on complex projects there is the idea of a Page that can have its own model and update and view.
Within each page you could have a large Msg but that is not really an issue. 20 tags is actually quite manageable. 50 is also manageable as discovered by NoRedInk programmers in their production code.
There's a decent tutorial on the topic here: https://www.elm-tutorial.org/en-v01/02-elm-arch/07-composing-2.html
I wish it showed the source of the Widget, but I can imagine what it looks like. Inlining for posterity.
module Main exposing (..)
import Html exposing (Html, program)
import Widget
-- MODEL
type alias AppModel =
{ widgetModel : Widget.Model
}
initialModel : AppModel
initialModel =
{ widgetModel = Widget.initialModel
}
init : ( AppModel, Cmd Msg )
init =
( initialModel, Cmd.none )
-- MESSAGES
type Msg
= WidgetMsg Widget.Msg
-- VIEW
view : AppModel -> Html Msg
view model =
Html.div []
[ Html.map WidgetMsg (Widget.view model.widgetModel)
]
-- UPDATE
update : Msg -> AppModel -> ( AppModel, Cmd Msg )
update message model =
case message of
WidgetMsg subMsg ->
let
( updatedWidgetModel, widgetCmd ) =
Widget.update subMsg model.widgetModel
in
( { model | widgetModel = updatedWidgetModel }, Cmd.map WidgetMsg widgetCmd )
-- SUBSCRIPTIONS
subscriptions : AppModel -> Sub Msg
subscriptions model =
Sub.none
-- APP
main : Program Never AppModel Msg
main =
program
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
I think this is the same idea behind https://stackoverflow.com/a/44275318/61624 but it has more description.
I'm trying to modify a simple app from the elm-lang tutorial to first update the model, then trigger another update.
update msg model =
case msg of
MorePlease ->
(model, getRandomGif model.topic)
NewGif (Ok newUrl) ->
( { model | gifUrl = newUrl }, Cmd.none)
NewGif (Err _) ->
(model, Cmd.none)
-- my addition
NewTopic newTopic ->
({ model | topic = newTopic}, MorePlease)
This fails in the compiler because the NewTopic branch:
The 3rd branch has this type:
( { gifUrl : String, topic : String }, Cmd Msg )
But the 4th is:
( { gifUrl : String, topic : String }, Msg )
So my Msg needs to be type Cmd Msg. How can I turn" my Msg into a Cmd Msg?
note: I recognize there is a simpler way to make this change, but I'm trying to understand Elm more fundamentally
There is really no need to turn Msg into a Cmd Msg. Remember that update is just a function, so you can call it recursively.
Your NewTopic case handler can be simplified to this:
NewTopic newTopic ->
update MorePlease { model | topic = newTopic}
If you really truly wanted the Elm Architecture to fire off a Cmd for this scenario, you could do a simple map of Cmd.none to your desired Msg:
NewTopic newTopic ->
({ model | topic = newTopic}, Cmd.map (always MorePlease) Cmd.none)
(not actually recommended)
Add the following function:
run : msg -> Cmd msg
run m =
Task.perform (always m) (Task.succeed ())
Your code would then turn into:
NewTopic newTopic ->
({ model | topic = newTopic}, run MorePlease)