DEV Community

Kristian Pedersen
Kristian Pedersen

Posted on • Updated on

#30daysofelm Day 2: HTML element for each item in list

2/30 Return HTML for each list item

This is day 2 of my 30 day Elm challenge

Code + demo: https://ellie-app.com/bS7Gr4mBws5a1

In today's project, I want to display a p tag for each item in an array. No for loops!

Here's what I want to do, written in React:

function App() {
    const people = ["You", "Me", "They"]

    return (
        people.map(person => <p>{person}</p>)
    )
}
Enter fullscreen mode Exit fullscreen mode

In advance, I assume .map() is what people would use in Elm as well.

.map() applies a function to each item in an array, and returns a new array with transformed items. The old array is left intact.

Code walkthrough

This project wasn't too bad, actually. I got stuck one place, but the Elm Slack community quickly helped me understand.

I also found this article useful: Generating HTML from a list in Elm, by Brad Cypert

1. Imports

module Main exposing (initialState, main)

import Browser
import Html exposing (Html, div, p, text)
import Html.Attributes exposing (..)
Enter fullscreen mode Exit fullscreen mode

Nothing fancy here. I'll explain the "text" element later.

The first line was added automatically by elm-format. How do I know what to expose? Do I even need Main.elm to be a module, or is that more useful if I'm working with multiple files?

2. Main function

main : Program () Model a
main =
    Browser.sandbox { init = initialState, update = update, view = view }
Enter fullscreen mode Exit fullscreen mode

I still find the type annotation a bit cryptic, but I'll re-read bukkfrig's comment on yesterday's post a couple of times.

Basically, it's a bit like Java's public static void main(String[] args). To begin with, it just seems like cryptic boilerplate, but the terms make sense eventually.

3. Model

Today's model is very simple. I chose to have it in a record (similar to JavaScript object), because I think it's a good habit that will allow for flexibility as my model grows.

type alias Model =
    { listOfPeople : List String }


initialState : Model
initialState =
    { listOfPeople = [ "You", "Me", "They" ] }
Enter fullscreen mode Exit fullscreen mode

I could have simplified this slightly, like this:

initialState : { listOfPeople : List String }
initialState =
    { listOfPeople = [ "You", "Me", "They" ] }
Enter fullscreen mode Exit fullscreen mode

However, having the Model type alias will be nice if I decide to add new fields to it.

In that case, I don't have to add the new field to all other references to the model.

4. Update function that doesn't update anything

update : a -> Model -> Model
update msg model =
    model
Enter fullscreen mode Exit fullscreen mode

My update function just returns the model it receives.

I haven't defined any Msg type, so I just went with VS Code's a suggestion. I guess it's like TypeScript's any?

For a tiny static project like this, I would be better off just doing it in plain HTML+JS. :)

5. View

view : Model -> Html msg
view model =
    div [] (List.map (\person -> div [] [ text person ]) model.listOfPeople)
Enter fullscreen mode Exit fullscreen mode

The map function works the same as the one in JavaScript, except the list comes last.

  • JavaScript: myList.map(myFunction)
  • Elm: List.map myFunction myList

I've wrapped map in parentheses. Otherwise, Elm perceives each whitespace to indicate a new argument to List.map.

The first div argument is the attributes, which is empty. This is where we would add classes, event listeners and other things.

5.1 View errors, helped by Slack and type annotations

To begin with, I wrote the application without type annotations, thinking it would make things simpler. My view function looked like this, and didn't work:

view model =
    div [] (List.map (\person -> div [] [ person ]) model.listOfPeople)
Enter fullscreen mode Exit fullscreen mode

The error message left me a bit confused. How on earth did I get an Html msg1?

The 1st argument to `sandbox` is not what I expect:

10|     Browser.sandbox { init = initialState, update = update, view = view }
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This argument is a record of type:

    { init : Model
    , update : msg1 -> Model -> Model
    , view : { listOfPeople : List (Html msg) } -> Html msg
    }

But `sandbox` needs the 1st argument to be:

    { init : Model, update : msg1 -> Model -> Model, view : Model -> Html msg1 }
Enter fullscreen mode Exit fullscreen mode

Slack user wolfadex suggested I add type annotations to get better error messages.

New code:

view : Model -> Html msg
view model =
    div [] (List.map (\person -> div [] [ person ]) model.listOfPeople)
Enter fullscreen mode Exit fullscreen mode

New error message:

The 2nd argument to `map` is not what I expect:

29|     div [] (List.map (\person -> div [] [ person ]) model.listOfPeople)
                                                        ^^^^^^^^^^^^^^^^^^
The value at .listOfPeople is a:

    List String

But `map` needs the 2nd argument to be:

    List (Html msg)

Hint: I always figure out the argument types from left to right. If an argument
is acceptable, I assume it is “correct” and move on. So the problem may actually
be in one of the previous arguments!

Enter fullscreen mode Exit fullscreen mode

The part that helped me was this: "map needs the 2nd argument to be List (Html msg)".

Compare these two examples:

-- 1. My initial approach
-- Person is a string, but div expects an Html msg
div [] [ person ]

-- 2. Working example
-- The text function converts the string "person" to an Html msg.
div [] [ text person ]
Enter fullscreen mode Exit fullscreen mode

I was happy to see the great response on yesterday's post, and I'm already looking forward to tomorrow's project!

Top comments (2)

Collapse
 
bukkfrig profile image
bukkfrig • Edited

Most Elm programs will want model, view and update sooner or later, so it makes sense to teach it from the very start, and I'm not saying you shouldn't be doing it... just don't think that Elm's programs always have to include a lot of boilerplate. Elm supports simpler programs too:

module Main exposing (main)

import Html exposing (div, p, text)


people =
    [ "You", "Me", "They" ]


display person =
    p []
        [ text person ]


main =
    div []
        (List.map display people)
Enter fullscreen mode Exit fullscreen mode
Collapse
 
wolfadex profile image
Wolfgang Schuster • Edited

@bukkfrig 's comment is slightly incorrect. In Elm a type variable is a lower case type while a type with multiple words is just a more complex/expressive type. I'd definitely recommend reading the docs on Elm types if you want more information.

Happy to see adding the types helped you out! Elm is definitely, for me, the first language I've worked with where adding the types was more helpful than tedious. Since you're using VSCode, the inferred types are really, really nice too. I find myself using them more and more. They even helped me find a bug in my code yesterday!