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.
Related
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
}
Building on the Elm navigation tutorial, I needed to execute a command to fetch additional data once navigating to my CategoryRoute.
My View.elm looks something like this:
view : Model -> Html Msg
view model =
div []
[ page model ]
page : Model -> Html Msg
page model =
case model.categories of
RemoteData.Success categories ->
case model.currentRoute of
CategoryListRoute ->
CategoryList.view categories
CategoryRoute id ->
let maybeCategory =
categories
|> SubCategories
|> flatten
|> filter (\category -> category.id == id)
|> head
_ = update (OnCategorySelected id) model
in
case maybeCategory of
Just category ->
Category.view category
Nothing ->
notFound
You'll notice that I'm calling update with the OnCategorySelected message myself when the currentRoute changes to the CategoryRoute.
My Update.eml looks something like this:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
OnArticlesFetch response ->
let
_ = log "got response" response
in
( { model | articles = response }, Cmd.none)
OnLocationChange location ->
let
newRoute =
parseLocation location
in
( { model | currentRoute = newRoute }, Cmd.none )
OnCategorySelected id ->
( model, (getArticles model.tenant id) )
And finally, my Commands.eml looks like so:
getArticles : String -> String -> Cmd Msg
getArticles tenant id =
let
url =
"https://" ++ tenant ++ ".my.api"
_ = log "getArticles for " id
in
Http.post url (Http.jsonBody (encoder id)) decoder
|> RemoteData.sendRequest
|> Cmd.map OnArticlesFetch
I was expecting that once I'll call update OnCategorySelected, it will in turn invoke the getArticles function, which is passed a Cmd Msg, which I had thought will be invoked once the response comes in.
The problem I'm facing is that while update OnCategorySelected and getArticles seem to get invoked (as indicated by the log printouts log "getArticles for " id), I'm seeing no outgoing HTTP calls, no errors, no results and no log "got response" response printouts.
I'm confused as to what am I doing wrong here and what's the pattern for actually fetching more data as one navigates to a page in Elm...
Elm is a pure language where side effects are relegated to the framework. Calling the update function does not actually perform any work itself. It simply returns a value that can be handed off to the Elm framework directing it to interact with the outside world. That means when you call update from within the page function and discard the result, nothing happens.
One thing that can cause confusion is that Debug.log actually does get called and prints to the console, which violates the aforementioned purity of the language. It's just a magic function which exists only for debugging so hopefully it doesn't cause too much confusion.
You should instead be handling the RemoteData.Success case in the update function after parsing the route in the OnLocationChange case and returning a result which includes the getArticles result Cmd.
OnLocationChange location ->
let
newRoute =
parseLocation location
cmd =
case newRoute of
CategoryRoute id ->
getArticles model.tenant id
_ ->
Cmd.none
in
( { model | currentRoute = newRoute }, cmd )
I have a textbox that's extremely slow when displaying characters.
I once observed a Stackoverflow exception related to it.
I believe that the performance issue is associated to preparing the portal value:
Sources.InputAccessId _ ->
( { model | portal = portal }, sourceCmd )
Each time a character is keyed-in, I copy and modify a new portal record which consumes more memory.
The code can be found below.
Main:
onSourcesUpdated : Sources.Msg -> Model -> ( Model, Cmd Msg )
onSourcesUpdated subMsg model =
let
pendingPortal =
model.portal
provider =
pendingPortal.provider
profile =
provider.profile
source =
pendingPortal.newSource
( sources, subCmd ) =
Sources.update subMsg
{ profileId = profile.id
, platforms = model.platforms
, source = { source | profileId = profile.id }
, sources = profile.sources
}
sourceCmd =
Cmd.map SourcesUpdated subCmd
pendingProvider =
{ provider | profile = { profile | sources = sources.sources } }
portal =
{ pendingPortal | newSource = sources.source, provider = pendingProvider }
in
case subMsg of
Sources.InputAccessId _ ->
( { model | portal = portal }, sourceCmd )
...
Sources.elm:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
let
source =
model.source
in
case msg of
InputAccessId v ->
( { model | source = { source | accessId = v } }, Cmd.none )
...
How can I pass around state without copying entire structures each time an event occurs?
You might want to debounce these events with libraries like this one: elm-debounce
The consequence of debouncing though is that some events will be discarded so you only get one that sums them all when the user is done typing/interacting with the page (more precisely, after a – configurable - timeout).
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.
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.