How to update an HTML element when the model changes

I’m very much an elm beginner and have come across this problem of trying to change the state of an HTML radio button when the model changes.

I include a stripped down copy of the code which illustrates the problem I’m having.

The page displays a list of indexes, each index in the list has a radio button. The ‘+’ button adds a new index to the list.

  • A Initial state, a single, unselected radio box.

  • B Select the radio box next to ‘0’

  • C Click the ‘+’ button to add a new , index ‘1’

The previous index ‘0’ remains selected. I’d like to change this behaviour so that the most recently created index, in this case 1’, becomes selected.

module Main exposing (main)

import Browser
import Html exposing (..)
import Html.Attributes exposing (name, style, type_, value)
import Html.Events exposing (onClick, onInput)


type Msg
    = EntryAdded
    | EntrySelected String


type alias Entry =
    { index : Int }


type alias Model =
    { selected : Maybe Int
    , entries : List Entry
    }


main =
    Browser.element
        { init = init
        , view = view
        , update = update
        , subscriptions = \_ -> Sub.none
        }


init : () -> ( Model, Cmd Msg )
init _ =
    ( Model Nothing [ Entry 0 ]
    , Cmd.none
    )


view : Model -> Html Msg
view model =
    div []
        [ ul [ style "list-style-type" "none" ]
            (List.map
                (\entry ->
                    div
                        []
                        [ li []
                            [ text (String.fromInt entry.index)
                            , input
                                [ type_ "radio"
                                , name "entrySelector"
                                , value <| String.fromInt entry.index
                                , onInput EntrySelected
                                ]
                                []
                            ]
                        ]
                )
                model.entries
            )
        , button [ onClick EntryAdded ] [ text "+" ]
        ]


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        EntryAdded ->
            let
                last_item =
                    List.length model.entries
            in
            ( { model
                | entries = List.append model.entries [ Entry last_item ]
                , selected = Just last_item
              }
            , Cmd.none
            )

        EntrySelected index ->
            ( { model | selected = String.toInt index }
            , Cmd.none
            )

You can programatically select a radio by setting the HTML checked attribute on it. In Elm, you can use the Html.Attributes.checked function.

I modified your code by adding the following line on the radio input:

checked <| Just entry.index == model.selected

Now the checked status of each radio will depend on whether or not its index is the same as `model.selected. Here’s an executable example: https://ellie-app.com/6mLnKqRmB3na1

1 Like

Many thanks for the prompt reply. That’s exactly what I was looking for.

Michael.

an additional wrinkle here is that this data structure is going to get out of sync pretty easily. What if you have: { selected = Just 10, entries = [ ] }? There may be a better way to model this, depending on your requirements. Why is there a maybe there? If it’s because the list may be empty, but you always want an item to be selected, then a zipper might be a good fit. There are a few good packages for this!

Brian - thanks for the feedback

The list of entries (or rows in the application code) are rows in a table. New rows can be appended to the table and existing rows can be deleted. In order to delete a row, it must be selected by the user, so selected can either point to a row in the table or nothing, hence the use use of Maybe

The application code proper can delete a selected row but, on deletion entries in the list are re-indexed.

deleteSelectedEntry : Model -> Model
deleteSelectedEntry model =
    case model.selected of
        Nothing ->
            model

        Just index ->
            { model
                | rows =
                    List.take index model.rows
                        ++ List.drop (index + 1) model.rows
                        |> reIndex index

                , selected = Nothing
            }


reIndex : Int -> List Entry -> List Entry
reIndex from_idx rows =
    let
        -- no change here
        upper =
            List.take from_idx rows

        -- change indexes in this portion
        lower =
            List.drop from_idx rows

        indexes =
            List.range from_idx <| from_idx + List.length lower
    in
    upper ++ List.map2 (\i entry -> Entry.changeIndex i entry) indexes lower

List.map2 works like a zip function. I’ll have a look at the packages with zip functionality.

As I say, I’m very new to elm and all feedback is very useful.

Michael