MackeyRMS / elm-ui-with-context / Element.WithContext.Input

Input elements have a lot of constraints!

We want all of our input elements to:

While these three goals may seem pretty obvious, Html and CSS have made it surprisingly difficult to achieve!

And incredibly difficult for developers to remember all the tricks necessary to make things work. If you've every tried to make a <textarea> be the height of it's content or restyle a radio button while maintaining accessibility, you may be familiar.

This module is intended to be accessible by default. You shouldn't have to wade through docs, articles, and books to find out exactly how accessible your html actually is.

Focus Styling

All Elements can be styled on focus by using Element.focusStyle to set a global focus style or Element.focused to set a focus style individually for an element.

focusedOnLoad : Element.WithContext.Attribute context msg

Attach this attribute to any Input that you would like to be automatically focused when the page loads.

You should only have a maximum of one per page.

Buttons

button : List (Element.WithContext.Attribute context msg) -> { onPress : Maybe msg, label : Element.WithContext.Element context msg } -> Element.WithContext.Element context msg

A standard button.

The onPress handler will be fired either onClick or when the element is focused and the Enter key has been pressed.

import Element exposing (rgb255, text)
import Element.Background as Background
import Element.Input as Input

blue =
    Element.rgb255 238 238 238

myButton =
    Input.button
        [ Background.color blue
        , Element.focused
            [ Background.color purple ]
        ]
        { onPress = Just ClickMsg
        , label = text "My Button"
        }

Note If you have an icon button but want it to be accessible, consider adding a Region.description, which will describe the button to screen readers.

Checkboxes

A checkbox requires you to store a Bool in your model.

This is also the first input element that has a required label.

import Element exposing (text)
import Element.Input as Input

type Msg
    = GuacamoleChecked Bool

view model =
    Input.checkbox []
        { onChange = GuacamoleChecked
        , icon = Input.defaultCheckbox
        , checked = model.guacamole
        , label =
            Input.labelRight []
                (text "Do you want Guacamole?")
        }

checkbox : List (Element.WithContext.Attribute context msg) -> { onChange : Basics.Bool -> msg, icon : Basics.Bool -> Element.WithContext.Element context msg, checked : Basics.Bool, label : Label context msg } -> Element.WithContext.Element context msg

defaultCheckbox : Basics.Bool -> Element.WithContext.Element context msg

The blue default checked box icon.

You'll likely want to make your own checkbox at some point that fits your design.

Text

text : List (Element.WithContext.Attribute context msg) -> { onChange : String -> msg, text : String, placeholder : Maybe (Placeholder context msg), label : Label context msg } -> Element.WithContext.Element context msg

multiline : List (Element.WithContext.Attribute context msg) -> { onChange : String -> msg, text : String, placeholder : Maybe (Placeholder context msg), label : Label context msg, spellcheck : Basics.Bool } -> Element.WithContext.Element context msg

A multiline text input.

By default it will have a minimum height of one line and resize based on it's contents.


type Placeholder context msg

placeholder : List (Element.WithContext.Attribute context msg) -> Element.WithContext.Element context msg -> Placeholder context msg

Text with autofill

If we want to play nicely with a browser's ability to autofill a form, we need to be able to give it a hint about what we're expecting.

The following inputs are very similar to Input.text, but they give the browser a hint to allow autofill to work correctly.

username : List (Element.WithContext.Attribute context msg) -> { onChange : String -> msg, text : String, placeholder : Maybe (Placeholder context msg), label : Label context msg } -> Element.WithContext.Element context msg

newPassword : List (Element.WithContext.Attribute context msg) -> { onChange : String -> msg, text : String, placeholder : Maybe (Placeholder context msg), label : Label context msg, show : Basics.Bool } -> Element.WithContext.Element context msg

A password input that allows the browser to autofill.

It's newPassword instead of just password because it gives the browser a hint on what type of password input it is.

A password takes all the arguments a normal Input.text would, and also show, which will remove the password mask (e.g. **** vs pass1234)

currentPassword : List (Element.WithContext.Attribute context msg) -> { onChange : String -> msg, text : String, placeholder : Maybe (Placeholder context msg), label : Label context msg, show : Basics.Bool } -> Element.WithContext.Element context msg

email : List (Element.WithContext.Attribute context msg) -> { onChange : String -> msg, text : String, placeholder : Maybe (Placeholder context msg), label : Label context msg } -> Element.WithContext.Element context msg

search : List (Element.WithContext.Attribute context msg) -> { onChange : String -> msg, text : String, placeholder : Maybe (Placeholder context msg), label : Label context msg } -> Element.WithContext.Element context msg

spellChecked : List (Element.WithContext.Attribute context msg) -> { onChange : String -> msg, text : String, placeholder : Maybe (Placeholder context msg), label : Label context msg } -> Element.WithContext.Element context msg

If spell checking is available, this input will be spellchecked.

Sliders

A slider is great for choosing between a range of numerical values.

slider : List (Element.WithContext.Attribute context msg) -> { onChange : Basics.Float -> msg, label : Label context msg, min : Basics.Float, max : Basics.Float, value : Basics.Float, thumb : Thumb context, step : Maybe Basics.Float } -> Element.WithContext.Element context msg

A slider input, good for capturing float values.

Input.slider
    [ Element.height (Element.px 30)

    -- Here is where we're creating/styling the "track"
    , Element.behindContent
        (Element.el
            [ Element.width Element.fill
            , Element.height (Element.px 2)
            , Element.centerY
            , Background.color grey
            , Border.rounded 2
            ]
            Element.none
        )
    ]
    { onChange = AdjustValue
    , label =
        Input.labelAbove []
            (text "My Slider Value")
    , min = 0
    , max = 75
    , step = Nothing
    , value = model.sliderValue
    , thumb =
        Input.defaultThumb
    }

Element.behindContent is used to render the track of the slider. Without it, no track would be rendered. The thumb is the icon that you can move around.

The slider can be vertical or horizontal depending on the width/height of the slider.

Note If you want a slider for an Int value:


type Thumb context

thumb : List (Element.WithContext.Attribute context Basics.Never) -> Thumb context

defaultThumb : Thumb context

Radio Selection

The fact that we still call this a radio selection is fascinating. I can't remember the last time I actually used an honest-to-goodness button on a radio. Chalk it up along with the floppy disk save icon or the word Dashboard.

Perhaps a better name would be Input.chooseOne, because this allows you to select one of a set of options!

Nevertheless, here we are. Here's how you put one together

Input.radio
    [ padding 10
    , spacing 20
    ]
    { onChange = ChooseLunch
    , selected = Just model.lunch
    , label = Input.labelAbove [] (text "Lunch")
    , options =
        [ Input.option Burrito (text "Burrito")
        , Input.option Taco (text "Taco!")
        , Input.option Gyro (text "Gyro")
        ]
    }

Note we're using Input.option, which will render the default radio icon you're probably used to. If you want compeltely custom styling, use Input.optionWith!

radio : List (Element.WithContext.Attribute context msg) -> { onChange : option -> msg, options : List (Option context option msg), selected : Maybe option, label : Label context msg } -> Element.WithContext.Element context msg

radioRow : List (Element.WithContext.Attribute context msg) -> { onChange : option -> msg, options : List (Option context option msg), selected : Maybe option, label : Label context msg } -> Element.WithContext.Element context msg

Same as radio, but displayed as a row


type Option context value msg

option : value -> Element.WithContext.Element context msg -> Option context value msg

Add a choice to your radio element. This will be rendered with the default radio icon.

optionWith : value -> (OptionState -> Element.WithContext.Element context msg) -> Option context value msg

Customize exactly what your radio option should look like in different states.


type OptionState
    = Idle
    | Focused
    | Selected

Labels

Every input has a required Label.


type Label context msg

labelAbove : List (Element.WithContext.Attribute context msg) -> Element.WithContext.Element context msg -> Label context msg

labelBelow : List (Element.WithContext.Attribute context msg) -> Element.WithContext.Element context msg -> Label context msg

labelLeft : List (Element.WithContext.Attribute context msg) -> Element.WithContext.Element context msg -> Label context msg

labelRight : List (Element.WithContext.Attribute context msg) -> Element.WithContext.Element context msg -> Label context msg

labelHidden : String -> Label context msg

Sometimes you may need to have a label which is not visible, but is still accessible to screen readers.

Seriously consider a visible label before using this.

The situations where a hidden label makes sense:

Basically, a hidden label works when there are other contextual clues that sighted people can pick up on.

Form Elements

You might be wondering where something like <form> is.

What I've found is that most people who want <form> usually want it for the implicit submission behavior or to be clearer, they want to do something when the Enter key is pressed.

Instead of implicit submission behavior, try making an onEnter event handler like in this Ellie Example. Then everything is explicit!

And no one has to look up obtuse html documentation to understand the behavior of their code :).

File Inputs

Presently, elm-ui does not expose a replacement for <input type="file">; in the meantime, an Input.button and elm/file's File.Select may meet your needs.

Disabling Inputs

You also might be wondering how to disable an input.

Disabled inputs can be a little problematic for user experience, and doubly so for accessibility. This is because it's now your priority to inform the user why some field is disabled.

If an input is truly disabled, meaning it's not focusable or doesn't send off a Msg, you actually lose your ability to help the user out! For those wary about accessibility this is a big problem.

Here are some alternatives to think about that don't involve explicitly disabling an input.

Disabled Buttons - Change the Msg it fires, the text that is rendered, and optionally set a Region.description which will be available to screen readers.

import Element.Input as Input
import Element.Region as Region

myButton ready =
    if ready then
        Input.button
            [ Background.color blue
            ]
            { onPress =
                Just SaveButtonPressed
            , label =
                text "Save blog post"
            }

    else
        Input.button
            [ Background.color grey
            , Region.description
                "A publish date is required before saving a blogpost."
            ]
            { onPress =
                Just DisabledSaveButtonPressed
            , label =
                text "Save Blog "
            }

Consider showing a hint if DisabledSaveButtonPressed is sent.

For other inputs such as Input.text, consider simply rendering it in a normal paragraph or el if it's not editable.

Alternatively, see if it's reasonable to not display an input if you'd normally disable it. Is there an option where it's only visible when it's editable?