How can the `Msg` type be separated into many types in Elm? - elm

The standard way to use model and update in Elm is to define the Model and Msg types, and the update function:
type alias Model = { ... }
type Msg = Msg1 | Msg2 | ...
update : Msg -> Model -> (Model, Cmd Msg)
...
When the application grows, all those types and functions become more complex. I'd like to separate them in the following way:
type alias Model1 = { ... }
type alias Model2 = { ... }
type alias Model = { model1 : Model1, model2 : Model2 }
type Msg1 = Msg1a | Msg1b | ...
type Msg2 = Msg2a | Msg2b | ...
type Msg = M1 Msg1 | M2 Msg2 | ...
Then, I'd like to handle all of them separately (and I know how to do it).
I have a problem with the view function, though. I define my view as follows:
view : Model -> Html Msg
view model =
let v1 = view1 model.model1
...
in ...
view1 : Model1 -> Html Msg1
view1 model = ...
The problem is, the result of view1 is Html Msg1, and the view function needs Html Msg.
Is there a way to convert the result from Html Msg1 to Html Msg?

You're looking for Html.map:
view : Model -> Html Msg
view model =
let v1 = view1 model.model1 |> Html.map M1
v2 = view2 model.model2 |> Html.map M2
in ...

Related

How to update specific element in a list in Elm

There are couple apples ( in type of List ) which will expose themselvies in the web view. User can update any size attribute of an Apple. I have a msg type UpdateSize which will be triggered via onInput.
Editing any of the apples will only just trigger the message without knowing which apple to be updated.
Is that possible to pass an id attribute to UpdateSize message?
Thank you for reading this, Elm is great !
module Main exposing (main)
import Browser
import Html exposing (Html, button, div, text, input)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick,onInput)
import String
type alias Apple = {
size: Int}
type alias Model = {
apples: List(Apple)}
initialModel : Model
initialModel =
{ apples = [ Apple 10, Apple 11, Apple 12] }
type Msg
= UpdateSize String
update : Msg -> Model -> Model
update msg model =
case msg of
UpdateSize s -> {model | apples = ??? } -- how to update a single Apple with new size
_ -> model
viewApple : Apple -> Html Msg
viewApple a =
input [ type_ "text" ,placeholder ""
, value (String.fromInt a.size)
, onInput UpdateSize]
[]
view : Model -> Html Msg
view model =
div []
(List.map viewApple model.apples)
main : Program () Model Msg
main =
Browser.sandbox
{ init = initialModel
, view = view
, update = update
}
Code link: https://ellie-app.com/ghd9jrcjKQQa1
With your current implementation it's not possible to know which apple to update since there's no unique attribute about the apples. What if two apples have the same size? If would be better if apples had IDs, or you used a dictionary type to keep track of the apples.
However, for the sake of demonstration, you could say that the list indeces of the apples are unique and you find them accordingly. In real life this will be a fragile solution.
Here's a naive approach using some helper functions from List.Extra.
-- ...
type alias Size =
Int
type Msg
= UpdateSize Int String
update : Msg -> Model -> Model
update msg model =
case msg of
UpdateSize index sizeStr ->
let
maybeSize =
String.toInt sizeStr
in
maybeSize
|> Maybe.withDefault (\size -> { model | apples = updateApple index size model.apples })
|> model
_ ->
model
updateApple : Int -> Size -> List Apple -> List Apple
updateApple index size apples =
let
maybeApple =
List.Extra.getAt index apples
in
maybeApple
|> Maybe.map (\apple -> List.Extra.setAt index { apple | size = size } apples)
|> Maybe.withDefault apples
-- ...
viewApple : Int -> Apple -> Html Msg
viewApple index a =
input
[ type_ "text"
, placeholder ""
, value (String.fromInt a.size)
, onInput (UpdateSize index)
]
[]
view : Model -> Html Msg
view model =
div []
(List.indexedMap viewApple model.apples)

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

What does pipe `|` operator do in case expression of elm-lang?

I have this following code snippet in my Elm code:
type alias Model =
{ content : String
}
update : Msg -> Model -> Model
update msg model =
case msg of
Change newContent ->
{ model | content = newContent }
What does { model | content = newContent } do?
Does it assign (bind) the value of newContent to model as well as content? Is that why the | operator is placed there?
The pipe is not a part of the case expression. It's record update syntax, as described here: https://elm-lang.org/docs/records#updating-records.
{ model | content = newContent }
assigns the value of newContent to the content field in the model record.
Read | as 'with'.
{ model 'with' content (set to) = newContent }

Msg's with extra variables in elm inputs

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))] []

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.