Controlled number input with Floats in Elm
Tags: elm, beginners, input, number
Published: 2018-07-18 20:19:27

---
title: Controlled number input with Floats in Elm
published: true
description: How to deal with numbers when using controlled inputs in Elm
tags: elm, beginner, input, number
---

The term *controlled* is taken from the [React documentation](https://reactjs.org/docs/forms.html#controlled-components), but it's also applicable to Elm. 
In the context of forms *controlled* means that the model manages the input field value so, when we type something, the change is propagated to the model and, immediately after that, the input value is also updated.


# The Problem

Controlled inputs work well when the value they hold is a string, but we can't state the same when the value is a number.


### Contrived example

( [Ellie link](https://ellie-app.com/NXLv3NJZWma1) )

```elm
module Main exposing (..)

import Html exposing (..)
import Html.Attributes as Attrs exposing (..)
import Html.Events exposing (..)




type Msg
    = SetPrice String


type alias Model =
    { price : Float }


model : Model
model =
    { price = 0 }


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        SetPrice price ->
            ( { model | price = price |> String.toFloat |> Result.withDefault 0 }
            , Cmd.none 
            )


view : Model -> Html Msg
view model =
    div []
        [ Html.form [] 
            [ label [] [text "Price"]
            , input [placeholder "Price", value (toString model.price), onInput SetPrice ] []
            , br [] []
            , p [] [ text ("Price is: " ++ (toString model.price)) ]
            ]
        ]


main : Program Never Model Msg
main =
    Html.program
        { init = ( model, Cmd.none )
        , update = update
        , subscriptions = \_ -> Sub.none
        , view = view
        }
```

The main issue here is that the `string -> float` conversion only allows valid numbers, so any intermediate step that creates an invalid number gets converted to `0`.


### Issues with this approach

- You can't have an empty input field, it always displays `0`.
- When you type a non-numeric character by mistake, the whole number is deleted.
- You can't type decimals because the decimal separator `.` is stripped after the `string -> float` conversion.
- You can't type negative numbers because the negative symbol `-` is stripped after the `string -> float` conversion.


# Research

I have found a few solutions to this problem:
- Html Number Form Inputs (https://discourse.elm-lang.org/t/html-number-form-inputs/740)
-  Various solutions for solving the Elm Guide age exercise (http://www.bravo-kernel.com/2016/06/various-solutions-for-solving-the-elm-guide-age-exercise/)
- Use `defaultValue` instead of `value`.

But all of them have failed to satisfy my needs.


# My contribution

My solution must satisfy the following:
- The input type must be `text` because I have to support iOS and Android, where the input type number support is poor.
- The record value that feeds the input must be a `Float`, not a `String`.
- The input field may be empty.
- All that and, ultimately, fix all the typing issues stated above.

( [Ellie link](https://ellie-app.com/P59jnfvv7wa1) )

```elm
module Main exposing (..)

import Html exposing (..)
import Html.Attributes as Attrs exposing (..)
import Html.Events exposing (..)


type Msg
    = SetPrice String


type PriceField
    = PriceField (Maybe Float) String


type alias Model =
    { price : PriceField }


model : Model
model =
    { price = PriceField Nothing "" }


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        SetPrice price ->
            if String.right 1 price == "." then
                ( { model | price = PriceField Nothing price }, Cmd.none )
            else
                let
                    maybePrice =
                        price |> String.toFloat |> Result.toMaybe
                in
                case maybePrice of
                    Nothing ->
                        ( { model | price = PriceField Nothing price }, Cmd.none )

                    Just p ->
                        ( { model | price = PriceField (Just p) price }, Cmd.none )


priceFieldToString : PriceField -> String
priceFieldToString priceField =
    case priceField of
        PriceField Nothing price ->
            price

        PriceField (Just price) _ ->
            toString price


view : Model -> Html Msg
view model =
    div []
        [ Html.form []
            [ label [] [ text "Price" ]
            , input [ placeholder "Price", value (priceFieldToString model.price), onInput SetPrice ] []
            , br [] []
            , small [] [ text ("Price is: " ++ toString model.price) ]
            ]
        ]


main : Program Never Model Msg
main =
    Html.program
        { init = ( model, Cmd.none )
        , update = update
        , subscriptions = \_ -> Sub.none
        , view = view
        }
```

## Recap

The key here is the `PriceField` product type which allows us to have both the input `String` value and the `Maybe Float` at the same place.

We can also use the `Maybe Float` as an indicator of invalid input state, so we can add custom validation logic:

( [Ellie link - same solution with some basic validation](https://ellie-app.com/P8wPHrWZz8a1) )

```elm
-- (...)
priceValidationStyle : PriceField -> List ( String, String )
priceValidationStyle priceField =
    case priceField of
        PriceField Nothing price ->
            if price == "" then
                []
            else
                [ ( "background-color", "red" ) ]

        PriceField (Just price) _ ->
            []


view : Model -> Html Msg
view model =
    div []
        [ Html.form []
            [ label [] [ text "Price" ]
            , input [ placeholder "Price", value (priceFieldToString model.price), onInput SetPrice, style (priceValidationStyle model.price) ] []
            , br [] []
            , small [] [ text ("Price is: " ++ toString model.price) ]
            ]
        ]

-- (...)
```

I can imagine how this same strategy could be used to add more advanced validation information to the same type, but this is all I have for now.

If you know any, I'd love to hear what other strategies or packages can be used to achieve similar goals.