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))] []
Related
I'm currently learning elm, I just stumbled on this problem where the div returns a Html (String -> Msg) instead of Html Msg.
error message I'm receiving
This div call produces:
Html (String -> Msg)
But the type annotation on view says it should be:
Html Msg
type alias Model =
{
firstNum: String,
secondNum: String,
answer: String
}
init: Model
init = { firstNum = "",
secondNum = "",
answer = ""}
type Msg =
Add String| Minus String
update: Msg -> Model -> Model
update msg model =
case msg of
Add x -> { model | answer = x}
Minus y -> { model | answer = y}
view : Model -> Html Msg
view model =
div []
[
input [ placeholder "Text to reverse", value model.firstNum] [],
button [onClick Add] [text "add"],
div [] [text model.answer]
]
main =
Browser.sandbox
{ init = init,
update = update,
view = view
}
You define the Msg type as
type Msg =
Add String| Minus String
with Add taking a String argument, but when you use it here:
button [onClick Add] [text "add"],
you're not giving it any argument at all.
The underlying issue seems to be that your mental model of the Elm Architecture is wrong. You seem to consider messages as "operations" or function calls rather than events, where Add is a function that takes an argument to apply to the model.
You should instead consider a message as a description of what triggered it. Instead of Add String, you might call it AddButtonClicked, with no arguments (in this case). Then have the update function do what it should based on what's in the model alone, which I'm guessing is an arithmetic operation on firstNum and secondNum.
But you're also not populating those fields. To do so you need to use the onInput event, which does ask for a message that takes a String. You might add a new message FirstNumChanged String for example, then use it with input like this:
input [ placeholder "Text to reverse", onInput FirstNumChanged, value model.firstNum] [],
I'll leave it to you to figure out how to handle it in update.
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
}
(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 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'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)