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)
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)
}
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)
_ ->
(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 trying to partition my Msg values in Elm (0.18) into different types. I'd like it to conform to this kind of typing:
type MsgSession
= LogIn
| LogOut
type MsgSettings
= SetUsername String
= SetPassword String
type Msg
= SessionMsg MsgSession
| SettingsMsg MsgSettings
...
My update function looks like this:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
SessionMsg sessionMsg ->
sessionUpdate sessionMsg model
SettingsMsg settingsMsg ->
settingsUpdate settingsMsg model
...
...which means that I have to define the child updates as well...
sessionUpdate : MsgSession -> Model -> ( Model, Cmd Msg )
sessionUpdate msg model =
case msg of
LogIn ->
-- do stuff
LogOut ->
-- do stuff
My LogOut event looks like this, and works fine:
button [onClick (SessionMsg LogOut)] [text "Log Out"]
But once there is a variable involved with the event, it doesn't work. I have a similar setup for settingsUpdate:
settingsUpdate : MsgSettings -> Model -> ( Model, Cmd Msg )
settingsUpdate msg model =
case msg of
SetUsername string ->
...
SetPassword string ->
...
But I can't get onInput to send as a variable. For example, this code
input [onInput SettingsMsg SetUsername] []
Yields this error:
Function `onInput` is expecting 1 argument, but was given 2.
483| onInput SettingsMsg SetUsername
Parentheses also don't work.
input [onInput (SettingsMsg SetUsername)] []
yields
The argument to function `SettingsMsg` is causing a mismatch.
483| SettingsMsg SetUsername)
^^^^^^^^^^^
Function `SettingsMsg` is expecting the argument to be:
MsgSettings
But it is:
String -> MsgSettings
What do I have to pass into onInput to make this work?
You should be able to use composition:
input [onInput (SettingsMsg << SetUsername)] []
Which, if you're more comfortable with explicit lambdas, looks like this:
input [onInput (\name -> SettingsMsg (SetUsername name))] []
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 ... ->
...