Elm: How to transform Html FooMsg to Html Msg - elm

I'm new to Elm. I have a site with two pages: Home and Signup.
Home has it's own view and Signup has it's own view, but they both return Html Msg. I'd like to change it so that Home returns Html HomeMsg and Signup returns Html SignupMsg.
Writing these view functions is easy of course, but I think my top-level view function needs to transform the result into Html Msg.
Here is the Msg type.
type Msg
= Home HomeMsg
| Signup SignupMsg
| OnLocationChange Location
I think I need some sort of map function to do this like
view : Model -> Html Msg
view model =
case model.route of
Model.HomeRoute ->
map Home (homeView model)
Model.SignupRoute ->
map Signup (signupView model)
Model.NotFoundRoute ->
notFoundView

Yes, there is a map function. It belongs to Html module.
http://package.elm-lang.org/packages/elm-lang/html/2.0.0/Html#map
Your code becomes:
view : Model -> Html Msg
view model =
case model.route of
Model.HomeRoute ->
Html.map Home (homeView model)
Model.SignupRoute ->
Html.map Signup (signupView model)
Model.NotFoundRoute ->
notFoundView

Related

How to modify Html.Node in Elm

How would I go about modifying an existing Html.Node in Elm?
I would like to create a function with the following signature that would add an extra attribute to the input node.
addAttribute : Html msg -> Attribute msg -> Html msg
Given
node = Html.div [] [Html.text "dummy"]
attr = Html.Attributes.style "display" : "inline-block"
a call to addAttribute node attr should return
Html.div [Html.Attributes.style "display" "inline-block"] [Html.text "dummy"]
Html msg nodes are opaque by design. There is no way to access their structure and add attributes or children after they have been created.
You will need to find a different approach to building the node.
What I usually do is create some kind of Cfg msg record that is passed to some function that generates the node I need. I can then pass styles or optional children through that record.

onClick message causes unwanted refreshing of the page

I'm trying to build a website in Elm and it includes a couple links to change the language:
div [ class "language" ][ a [ class englishClass, onClick (ChangeLanguage English) ] [ text "EN" ]
, a [ class germanClass, onClick (ChangeLanguage German) ] [ text "DE" ]
, a [ class italianClass, onClick (ChangeLanguage Italian) ] [ text "IT" ]
]
My Update looks like this:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
... -- Stuff for URLs
ChangeLanguage language ->
( { model | language = language }
, Cmd.none
)
type Msg
= LinkClicked Browser.UrlRequest
| UrlChanged Url.Url
| ChangeLanguage Language
This is my model:
type alias Model =
{ key : Nav.Key
, url : Url.Url
, language : Language
}
And this is my init function, which (at least in my mind) defaults to English as a language:
init : flags -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
init _ url key = ( Model key url English, Cmd.none )
The problem:
Whenever I click the links that change the language, they do seem to work: I see the language of the paragraphs in the page change, but then several unwanted things happen:
The page refreshes. Since I already saw the language change across the page, it's clear this is not needed so I'd like to avoid it.
As a consequence of 1, the viewport is brought all the way to the top of the page again.
The language changes to the default English again!
How could I avoid 1 (and thus 2) from happening?
And what am I missing to make it so the change is maintained across the site (at least when refreshing the page, since I haven't tried working with sessions or cookies yet)?
I consider this behaviour a bug in Elm, and have filed an issue for it, as have others with associated PRs. But in the year and a half since have received no attention from anyone in a position to actually do something about it, which is unfortunately par for the course with Elm.
The problem is that Elm "hijacks" onClick on a elements to create navigation events so that anchors can be handled inside Elm. When using Browser.application, clicking an a element will have Elm call onUrlReuqest with a value of Browser.UrlRequest, which will class the URL as either Internal or External and require you to make a decision on what to do with it.
The problem at the root of this is that omitting href will generate an External UrlRequest with the URL being an empty string. By default, and by the usual handling of External, this will tell the browser to load the URL as usual, thus refreshing the page. But it also suggests a possible workaround is to special-case External "":
update msg model =
case msg of
UrlRequested (Browser.Internal _) ->
( model, Cmd.none )
UrlRequested (Browser.External "") ->
( model, Cmd.none )
UrlRequested (Browser.External url) ->
( model, Browser.Navigation.load url )
UrlChanged _ ->
( model, Cmd.none )
Another workaround is to add href="#" to the a elements, which will correctly classify them as Internal

How to integrate multiple msg types from different elm features into a top-level app

I have a feature called Editor that I'm trying to plug into my app. Its view returns a the type Html EditorMsg. I plug it in here:
edit : Editor.Types.Model -> Html EditorMsg
edit editorModel =
div [ class "edit" ] [ Editor.view editorModel ]
In the app, I have routing and so edit is called by way of my main view function, plus a couple of functions that manage which route to show:
-- VIEW
view : Model -> Html Msg
view model =
div []
[ RoutingMsg navBar
, EditorMsg (render model)
]
render : Model -> Html EditorMsg
render model =
case model.route of
Nothing ->
li [] [ text "Invalid URL" ]
Just route ->
showRoute route model
showRoute : Route -> Model -> Html EditorMsg
showRoute route model =
case route of
Home ->
home
Editor ->
edit model.editor
My view also contains a navBar as you can see, and that returns Html possibly containing a different type of message, RoutingMsg:
navBar : Html RoutingMsg
navBar =
NavBarState.config
|> NavBarState.items
[ navItem "/" "Home" False
, navItem "/editor" "Edit" False
]
|> NavBar.view
This setup didn't compile because in view I have a list of two different types, one returning Html RoutingMsg and the other Html EditorMsg, so I set up a union type to try to contain that difference:
type Msg
= RoutingMsg (Html RoutingMsg)
| EditorMsg (Html EditorMsg)
This seems to work, but then I run into trouble in my update method, where the matching doesn't quite work. Long and short of it is that it feels as though I've just got the wrong pattern. My goal was to make my Editor somewhat independent, like a module, that can be plugged in different places. But it's hard for me to understand in Elm, how to integrate things.
To illustrate the problem more simply, I created this example on ellie-app that approximates what I tried to do but can't get working: https://ellie-app.com/PKmzsV3PC7a1/1
Is my approach here just incorrect, or if not, how can I get this code to work?
You should use Html.map to map children messages to the top-level Msg
Here's what you've been missing:
view : Model -> Html Msg
view model =
div []
[ Html.map ButtonDecMsg buttonDec
, div [] [ text (toString model) ]
, Html.map ButtonIncMsg buttonInc
]
Also the type annotation definition of child update functions should include the message type:
buttonDecUpdate : ButtonDecMsg -> Model -> Int
buttonDecUpdate msg model =
model - 1
Here is an example of working app: https://ellie-app.com/PM4H2dpFsfa1/0

What does a function with 2 values on the right side mean? (Model -> Html msg)

I have encountered that in the guide:
viewValidation : Model -> Html msg
viewValidation model =
let
(color, message) =
if model.password == model.passwordAgain then
("green", "OK")
else
("red", "Passwords do not match!")
in
div [ style [("color", color)] ] [ text message ]
So this is a function, which takes the Model.
Html msg usually looks to me like we are calling the function Html with the argument msg.
msg doesn't seem to play any role in any other part of the viewValidation function, though. So what does it mean and what is it for in this case?
Html Msg is just a type parameter, as List Int is. While List Int denotes a list that contains element of type Int, similarly Html Msg describes some HTML that can treat/emit messages of type Msg.
For example, if you have a button inside your HTML, it could look like this:
button [ onClick DoSomething ] [ text "caption" ]
Where DoSomething is a case of the Msg type.
Don't mix the type definition with the normal execution of code. Html is not a function, it is a type that takes a parameter to define a type for a view function.
Html msg is the most general definition you can have as msg is a variable itself, so this returns Html that is independent of the msg type you are currently using. This could either be because it creates no event messages, or because the view function takes messages as parameters.
As the comments established Html () would be a very narrow type that is constrained to return nothing.
The most common case though will be a view function returning Html Msg - i.e. Html with messages based on user interactions.
As Elm encourages componentization, you also need keep Html.map in mind. It's type signature is Html.map : (a -> b) -> Html a -> Html b. In the context of components this is more easily read as
Html.map : (Child.Msg -> Parent.Msg) -> Html Child.Msg -> Html Parent.Msg
Note that when you define your messages in your parent component, you will have something like:
type Msg = ChildMsg Child.Msg
which means that ChildMsg has type signature:
ChildMsg : Child.Msg -> Parent.Msg
So my view functions have a lot of
parentView model =
-- childView model.child |> Html.map ChildMsg
Html.map ChildMsg (childView model.child)

How to pass an action to a child component in Elm

I've only been trying out Elm for a few days and have come across what seems like a common scenario that I can't figure out.
I have a parent component that holds a list of items. To render the list, I have a child component such that the parent calls the view function of the child passing in the address and model. Since the Action types are different, I think the compiler is complaining about that, but I'm really not sure.
Parent Component:
type alias Model = List ToDoItem.Model
type Action
= Remove Int
update : Action -> Model -> Model
update action model =
case action of
Remove id ->
List.filter (\todo -> todo.id /= id) model
view : Signal.Address Action -> Model -> Html
view address model =
let
buildToDos =
List.map (ToDoItem.view address) model
in
div [] [ buildToDos ]
Child Component:
type alias Model =
{ id : Int
, name : String
, description : String
, complete: Bool
}
type alias ID = Int
type Action
= Toggle Bool
update : Action -> Model -> Model
update action model =
case action of
Toggle toggle ->
if toggle == True then
{ model | complete = False }
else
{ model | complete = True }
view : Signal.Address Action -> Model -> Html
view address model =
let
toggleText : Bool -> String
toggleText complete =
case complete of
True -> "Incomplete"
False -> "Complete"
in
div
[ class "wrapper" ]
[ span [] [ text ("[" ++ toString model.id ++ "]") ]
, span [ class "name" ] [ text model.name ]
, div [ class "description" ] [ text model.description ]
, a [ onClick address (Toggle model.complete)] [ text (toggleText model.complete)]
]
Compiler Error:
-- TYPE MISMATCH ----------------------------------------------
The type annotation for `view` does not match its definition.
20│ view : Signal.Address Action -> Model -> Html
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The type annotation is saying:
Address Action -> List ToDoItem.Model -> Html
But I am inferring that the definition has this type:
Address ToDoItem.Action -> List ToDoItem.Model -> Html
-- TYPE MISMATCH ----------------------------------------------
The 2nd argument to function `div` is causing a mismatch.
26│ div [] [ buildToDos ]
^^^^^^^^^^^^^^
Function `div` is expecting the 2nd argument to be:
List VirtualDom.Node
But it is:
List (List Html)
How do I declare either the child component's view function correctly or pass in the parameters to the child's view function correctly from the parent?
In reality, I don't actually want to pass any action to the child component - I just want to render it. The action in the child component is for the onClick there.
Then again, maybe I'm way off because this is day 2 of my Elm life.
Elm Architecture Tutorial covers the child view issue. Take another look at how the example 4 is implemented.
In short, you need to have an Action in the parent that wraps the child action:
type Action = Remove Int | ToDo Int ToDoItem.Action
And you need to forward this action to the appropriate Item in update.
In view you need to create a forwarding address for each of your ToDoItem views.
view : Signal.Address Action -> Model -> Html
view address model =
let
fwd idx = Signal.forwardTo address (ToDo idx)
toDoList =
List.indexedMap (\(idx, m) -> ToDoItem.view (fwd idx) m) model
in
div [] toDoList
Please note that your old buildToDos is already a list and saying [buildToDos] is actually saying List (List Html) which is why you got the second error.
The compiler had already told you the answer. First of all, the
Address Action -> List ToDoItem.Model -> Html
The Action here has to be specified to the children Action. Just fix it like the compiler tell you:
view : Signal.Address TodoItem.Action -> Model -> Html
Second one, because you buildToDos is already a list, you just need to:
div [] buildToDos
Spend some time understanding type annotation and follow compiler carefully then you are should be able to solve this kind of problem yourself.