How do you manage an explosion of update paths/Msg constructors? - elm

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.

Related

Elm: How to use data from one HTTP request in subsequent requests

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

Elm update infinite loop

New to Elm, so I may be missing something obvious.
I'm working on an Elm application that uses annaghi/dnd-list. I'm encountering an infinite loop of calls to update. This happens when clicking on one element, then another one. Here's the code:
config : DnDList.Config Player
config =
{ beforeUpdate = \_ _ list -> list
, movement = DnDList.Free
, listen = DnDList.OnDrag
, operation = DnDList.Swap
}
system : DnDList.System Player Msg
system =
DnDList.create config DndMsg
type alias Model =
{ navKey : Nav.Key
, room : WebData Room
, dnd : DnDList.Model
, startError : Maybe String
}
type Msg
= RoomReceived (WebData Room)
| DndMsg DnDList.Msg
...
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
RoomReceived room ->
( { model | room = room }, Cmd.none )
DndMsg message ->
let
room = model.room
in
case room of
RemoteData.Success actualRoom ->
let
( dnd, players ) =
system.update message model.dnd actualRoom.players
updatedRoom = RemoteData.map
(\roomData ->
{ roomData | players = players }
) room
in
( { model | dnd = dnd, room = updatedRoom }
, system.commands model.dnd
)
_ ->
( model, Cmd.none )
When I change the line system.commands model.dnd to Cmd.none, then there is no infinite looping call to the update function, but also nothing happens. The message that keeps getting called in the dnd-list library is GotDropElement (Ok dropElement)
Again, new to Elm, so this may be a poorly formed question, but any help is appreciated.
Thanks!
Figured it out. Had to add a subscription to listen to mouse events
currentSubs : Model -> Sub Msg
currentSubs model =
case model.page of
GameRoomPage pageModel ->
GameRoom.subscriptions pageModel
|> Sub.map GameRoomMsg
_ ->
always Sub.none model
...
main : Program () Model Msg
main =
Browser.application
{ view = view
, init = init
, update = update
, subscriptions = currentSubs
, onUrlRequest = LinkClicked
, onUrlChange = UrlChanged
}
Try following the flow from system.commands. Probably it is eventually sending again the message DndMsg and this is what is causing the issue.
It is usually considered not a good practice to send messages from commands.
In case you cannot solve the issue, having a working example of the problematic code in Ellie (https://ellie-app.com/new) would help.

Type Mismatch - 1st argument to sandbox is not what I expect

I am trying to add subscriptions as I have a dropdown, this helps ensure that the dropdowns automatically close when you click outside of them. On doing so, I had to change the model as well as my update.
This link (will take you to the Elm Bootstrap site) is the dropdown I am working with which is using Bootstrap 4.
Error I am getting
The 1st argument to sandbox is not what I expect:
295| Browser.sandbox 296|> { init = initialModel 297|>
, update = update 298|> , view = view 299|> }
This argument is a record of type:
{ init : ( Model, Cmd Msg )
, update : Msg -> Model -> ( Model, Cmd Msg )
, view : Model -> Html Msg
}
But sandbox needs the 1st argument to be:
{ init : ( Model, Cmd Msg )
, update : Msg -> ( Model, Cmd Msg ) -> ( Model, Cmd Msg )
, view : ( Model, Cmd Msg ) -> Html Msg
}
Alias Model
type alias Model =
{ currentNumber : Int, clicks : Int, outputList : List(String), uniqueValues : Dict Int Int, firstNumber : String, secondNumber : String, myDropState : Dropdown.State, items : List String, selectedItem : String, dictKeyToRemove : String,
modalVisibility : Modal.Visibility }
Initial Model
initialModel : (Model, Cmd Msg)
initialModel =
({ currentNumber = 0, clicks = 0, outputList = [""], uniqueValues = Dict.empty, firstNumber = "", secondNumber = "", myDropState = Dropdown.initialState, items = ["Small", "Medium", "Large"], selectedItem = "Small", dictKeyToRemove = "",
modalVisibility = Modal.hidden }, Cmd.none)
Main
main : Program () Model Msg
main =
Browser.sandbox
{ init = initialModel
, update = update
, view = view
}
Subscriptions
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ Dropdown.subscriptions model.myDropState DropMsg ]
Update
update : Msg -> Model -> ( Model, Cmd Msg)
update msg model =
case msg of
DropMsg state ->
({model | myDropState = state }, Cmd.none)
I am not sure what I am missing at this point, I have tried changing the argument with no luck.
Browser.sandbox will create a simple and very limited program. The dropdown requires capabilities beyond that, namely subscriptions, which means you need to use either Browser.element or Browser.document instead.
The type of Browser.element is:
element :
{ init : flags -> ( model, Cmd msg )
, view : model -> Html msg
, update : msg -> model -> ( model, Cmd msg )
, subscriptions : model -> Sub msg
}
-> Program flags model msg
Compared to Browser.sandbox:
sandbox :
{ init : model
, view : model -> Html msg
, update : msg -> model -> model
}
-> Program () model msg
There are three differences here:
init takes an argument, flags, which can be anything and will be interpreted by the runtime according to its type. For your purpose just using () should be enough (which is essentially what sandbox does), but see the flags section of the guide for more details.
init and update returns ( model, Cmd msg ) instead of just model. This is the root cause of your error, because you have update and init functions which return ( model, Cmd msg ) as element would expect, but try to feed them to sandbox. This makes the compiler unhappy, because it thinks that model should be ( Model, Cmd msg ) instead of just Model.
element expects an additional subscriptions function, which you have defined but currently aren't doing anything with since sandbox doesn't accept it.
Putting this all together, substituting the following main function should work for you:
main : Program () Model Msg
main =
Browser.element
{ init = \() -> initialModel
, update = update
, view = view
, subscriptions = subscriptions
}

Locally scoped updates in elm 0.18

I have an elm 0.18 web app with a number of pages and routes. In main.elm I define my update function.
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
FirstUpdateAction ->
...
Every action goes through this function and it's getting big. Is it possible to create an update function to a smaller module that is nested within the overall structure?
For example, I have a settings page that gives the user the ability to change password. There are three fields/states (passwordOld, passwordNew, passwordConfirm) which have update actions associated with onInput and onBlur events. Those states and actions are only relevent to the user settings page, and become irrelevent to the rest of the model when the user leaves the page.
How could I go about setting up a scope for the user settings?
You could break down your code into independent submodules, each with it's own Msg type, update and view functions.
For example you could have a file SubmoduleA.elm looking like this:
module SubmoduleA exposing (Model, Msg, update, view)
type Msg = SubMessageA
| SubMessageB
[..]
type alias model =
{ fieldA : TypeA
, fieldB : TypeB
, [..]
}
update msg model =
case msg of
MessageA ->
{model | fieldA = [..] } ! []
[..]
view model =
div [id "submoduleAView"]
[..]
this module would be connected to your main program like this:
module Main exposing (..)
import SubmoduleA exposing (Model, Msg, update, view)
type Msg = MessageA
| MessageB
| ToSubmoduleA (SubmoduleA.Msg)
[..]
type alias model =
{ fieldA : TypeA
, fieldB : TypeB
, [..]
, subModuleA : SubmoduleA.Model
}
update msg model =
case msg of
MessageA ->
{model | fieldA = [..] } ! []
[..]
ToSubmoduleA msg =
let (newSubmoduleA, newSubmoduleACmd) = SubmoduleA.update msg (.subModuleA model)
in { model | subModuleA = newSubmoduleA } ! [Cmd.map ToSubmoduleA newSubmoduleACmd]
view model =
div [id "mainView"]
[ ..
, Html.map ToSubmoduleA <| SubmoduleA.view (.subModuleA model)
]
this way all the information and state that are relevant to your sub module stay encapsulated in your sub module, and you just have one case in your main update function responsible for the correct routing of messages.

Unable to trigger response message when sending initial message

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