Ukulele Chord Finder

I made a Ukulele chord finder app. You can play with it at https://www.knowyourtheory.com/resources/ukulele-chord-finder/

You can find the code on Ellie at https://ellie-app.com/7VhStTx7rw7a1

It would be great if I could figure out a way to not pass so many strings around. Is there another way to get the user input from the element in a safer way. I had to have a lot of fromString toString methods.

5 Likes

Nice. Are you familiar with Ju’s elm-chord? It has Ukulele chords as well - https://package.elm-lang.org/packages/Arkham/elm-chords/latest

1 Like

To avoid the String -> a conversion functions with an Html.select, you could think that onClick would work on individual options, and actually it does, but unfortunately only with Firefox (https://ellie-app.com/7WqmTQtwKvRa1).

But there is another trick that will work everywhere. You can use the option index in the options list as the value and use it to return the selected option:

https://ellie-app.com/7WqySgBhxtXa1

type Msg
    = ChangeRoot Root
    | ChangeQuality Quality


view : Model -> Html Msg
view model =
    div
        [ SSA.class "uke-chord-container" ]
        [ ukeRoot model.root model.quality
        , div
            [ SSA.class "uke-chord-inputs" ]
            [ select ChangeRoot rootToString C rootList
            , select ChangeQuality qualityToString Major qualityList
            ]
        ]


select : (a -> msg) -> (a -> String) -> a -> List a -> Html msg
select toMsg toString default options =
    let
        toOption idx a =
            Html.option [ value (String.fromInt idx) ] [ text (toString a) ]
    in
    Html.select
        [ onInput (selectOption toMsg default options) ]
        (List.indexedMap toOption options)


selectOption : (a -> msg) -> a -> List a -> String -> msg
selectOption toMsg default options input =
    case String.toInt input |> Maybe.andThen (\idx -> List.Extra.getAt idx options) of
        Just option ->
            toMsg option

        Nothing ->
            toMsg default

Note that you have to provide a default value in case the index is not found in the list, but you could use a custom decoder that would fail instead if you prefer to avoid a default value:

https://ellie-app.com/7WqG2PKYxrLa1

select : (a -> msg) -> (a -> String) -> List a -> Html msg
select toMsg toString options =
    let
        toOption idx a =
            Html.option [ value (String.fromInt idx) ] [ text (toString a) ]
    in
    Html.select
        [ stopPropagationOn "input" (selectOptionDecoder toMsg options) ]
        (List.indexedMap toOption options)


selectOptionDecoder : (a -> msg) -> List a -> Decoder ( msg, Bool )
selectOptionDecoder toMsg options =
    Decode.at [ "target", "value" ] Decode.string
        |> Decode.andThen
            (\input ->
                case String.toInt input |> Maybe.andThen (\idx -> List.Extra.getAt idx options) of
                    Just option ->
                        Decode.succeed ( toMsg option, True )

                    Nothing ->
                        Decode.fail "Unexpected value"
            )

Lastly I used List.Extra.getAt to get the element in the list, but you could convert your lists to Array instead if you prefer to avoid an additional dependency.

3 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.