Reset select option correctly

Hi all
I have the following code, that will generate selection option dynamically depends on received values from server.

module Account.Page exposing (Msg(..), update, view, Model, init)

import API.Keycloak as Keycloak exposing (..)
import API.Env as Env exposing (..)
import API.Coins as Coins exposing (..)
import Html as Html exposing (..)
import Html.Attributes as Attr exposing (..)
import Http exposing (..)
import Html.Events as Events exposing (..)
import Json.Decode as Decode exposing (..)


---- MODEL ----

type alias Model =
    { 
        env : Env.Model,
        kc : Keycloak.Struct,
        currencies : List String,
        coin : String,
        currency : String,
        disableAdd: Bool,
        userCoins: Coins.UserCoins,
        resetOption: Bool,
        error : String
    }


init : Env.Model -> Keycloak.Struct -> ( Model, Cmd Msg )
init env kc =
    ( Model env kc [] "" "" True [] True "" 
    , Http.send GotCurrencies (Coins.reqCoins env kc)
    )

---- UPDATE ----

type Msg
    = GotCurrencies (Result Http.Error (List String))
    | GotUserCoins (Result Http.Error Coins.UserCoins)
    | GotCoinAdded (Result Http.Error ())
    | OnChangeCurr String
    | OnInputCoin String
    | OnClickAdd
    | ValidNumbers
    | UnvalidNumbers

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        GotCurrencies resp ->
            case resp of
                Ok value ->
                    ({model | currencies = value}, queryUserCoins model.env model.kc)
                Err _ ->
                    ({model | error = "Can not query coins currency" }, Cmd.none)
        GotUserCoins resp ->
            case resp of
                Ok value ->
                    ({model | userCoins = value}, Cmd.none)
                Err _ ->
                    (model, Cmd.none)
        GotCoinAdded resp ->
            case resp of
                Ok value ->
                    (resetValues model, queryUserCoins model.env model.kc)
                Err _ ->
                    ({model | error = "Error occurs during add new coin." }, Cmd.none)
        OnChangeCurr curr ->
            ({model | currency = curr, disableAdd = (setAddStatus model.coin curr), resetOption = False}, Cmd.none)
        ValidNumbers ->
            ({model | error = "" }, Cmd.none)
        UnvalidNumbers ->
            (model, Cmd.none)
        OnInputCoin value ->
            ({model | coin = value, disableAdd = (setAddStatus value model.currency)}, Cmd.none)
        OnClickAdd ->
            case (String.toFloat model.coin) of
                Just value ->
                    (model, sendAddCoin model.env model.kc (Coins.UserCoin value model.currency))
                Nothing ->
                    ({model | error = "Coin amount is not valid."}, Cmd.none)
            

--- SUBSCRIPTIONS ---

subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.none 


---- VIEW ----


view : Model -> Html Msg
view model =
    div [] [
        input [Attr.type_ "number", Attr.min "0", Events.onInput OnInputCoin , preventCharPress, Attr.value model.coin, Attr.step "0.001"] [],
        select [onChangeCurr]  <| buildCurrencyOption model.resetOption model.currencies,
        button [ onClick OnClickAdd, disabled model.disableAdd ] [ text "Add" ],
        p [] [text model.error],
        userCoinsView model.userCoins
    ]

userCoinsView : Coins.UserCoins -> Html Msg
userCoinsView userCoins = 
    let header = tr [] [th [] [text "Amount"],th [] [text "Currency"]]
        data = List.map (\x -> tr [] [td [] [text (String.fromFloat x.amount)], td [] [text x.curr]]) userCoins
    in table [] (header :: data)


---- FUNCTIONS ----

-- Prevent the letter e press
preventCharPress : Attribute Msg
preventCharPress =
    preventDefaultOn "keydown"
    (Decode.field "keyCode" Decode.int
        |> Decode.andThen
        (\key ->
            if key == 69 || key == 189 || key == 109 then
                Decode.succeed (UnvalidNumbers, True)
            else
                Decode.succeed (ValidNumbers, False)
        )
    )

setAddStatus : String -> String -> Bool
setAddStatus amt curr = 
    if (String.length amt > 0) && (String.length curr > 0) then
      False
    else 
      True

onChangeCurr : Attribute Msg
onChangeCurr = Events.on "change" ( Decode.andThen (\value -> Decode.succeed <| OnChangeCurr value) Events.targetValue   )


sendAddCoin : Env.Model -> Keycloak.Struct -> Coins.UserCoin -> Cmd Msg
sendAddCoin env kc coin = 
    Http.send GotCoinAdded (Coins.reqAddCoin env kc coin)

queryUserCoins :  Env.Model -> Keycloak.Struct -> Cmd Msg
queryUserCoins env kc =
    Http.send GotUserCoins (Coins.reqUserCoins env kc)


resetValues : Model -> Model
resetValues model = 
    {model| coin = "", currency = "", disableAdd = True, resetOption = True}

buildCurrencyOption : Bool -> List String -> List (Html Msg)
buildCurrencyOption reset list = 
    let tail =
            List.map (\x -> option [Attr.value x] [text x]) list
        head = 
            option [Attr.value "", if reset then (Attr.selected True) else (Attr.selected False), Attr.disabled True, Attr.hidden True] [text ""]
    in head :: tail 

The last function generates select option from a list and the reset parameter determines, if the select option should be set the initial state or not.

I think, the way that I did is not clean. Does anyone have another suggestion?

Thanks

It’s okay, it’s quite clear and explicit. Is it possible for a currency to have the name ""? If not, then you don’t really need the resetOption in your model you can just check if the currently set currency is equal to "". Better yet, you could have the currency as Maybe String rather than String. Then whether the default Please select option should be selected is just a case of checking whether model.currency == Nothing.

Note, your expression if reset then (Attr.selected True) else (Attr.selected False) is the same as Attr.selected reset.

So to summarise, I would change your Model so that the currency field is Maybe String, the initial value is Nothing.

Then in your buildCurrencyOption you can have:

buildCurrencyOption : Maybe String -> List String -> List (Html Msg)
buildCurrencyOption currentCurrency list = 
    let tail =
            List.map (\x -> option [Attr.value x] [text x]) list
        reset =
            currentCurrency == Nothing
        head = 
            option [Attr.value "", Attr.selected reset, Attr.disabled True, Attr.hidden True] [text "Please select"]
    in head :: tail

Then you don’t need resetOption on your model.

One thing I forgot to mention. If you do go with Maybe String be careful that Just "" is different from Nothing, so in particular you have the "" disabled so that it cannot be selected, but if it could it would result in the model having Just "" as the currently selected currency.

You can avoid this by checking for "" in OnChangeCurr update.

Note as well that you need to have the currently selected currency selected when it is not "". To do this I tend to create a list of pairs, of values and labels, and then use the same code to determine which one is selected.
Something like:

buildCurrencyOption : String -> List String -> List (Html Msg)
buildCurrencyOption currentlySelected currencyNames =
     let
          currencyOptions =
               List.map (\n -> (n, n)) currencyNames
          allOptions =
               -- So note here I do not even add the 'Please select' option if something else is chosen.
               case currentlySelected == "" of
                   True ->
                       ("", "Please select") :: currencyOptions
                   False ->
                        currencyOptions
          makeOption (value, name) =
               Html.option
                   -- This 'selected' attribute works for both the default 'Please select' option and all others.
                   [ Attr.selected (value ==  currentlySelected) ]
                   [ text name ]
    in
    List.map makeOption allOptions

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