Part 3 — Spinner, Floating Labels, Checkboxes, Date Picker, Autocomplete
This is part 3 of a 3 parts series. Have a look at part 1 for an introduction.
Parts
- Part 1 — Examples 1~6:
x-www-form-urlencoded
andjson
encoding, validation - Part 2 — Examples 7~12: Removing
<form>
, on-the-fly validation, Focus detection, Show/Hide the password - Part 3 — Examples 13~21: Spinner, Floating Labels, Checkboxes, Date Picker, Autocomplete
☞ Example 13 — Adding a spinner while the app is waiting for an answer
Time to add a spinner after the form is submitted and before the server answer. Being a Single Page Application we need to give some feedback to the user while the app is waiting for a server to answer.
Let’s add a formState
value in the modal of the type
type FormState = Editing | Fetching
Then in the update
function
SubmitForm ->
case validate model of
...
[] ->
( { model | errors = []
, response = Nothing
, formState = Fetching
}
, Http.send Response (postRequest model)
) errors ->
( { model | errors = errors, showErrors = True }
, Cmd.none
)
Response (Ok response) ->
( { model | response = Just response
, formState = Editing
}
, Cmd.none
)Response (Err error) ->
( { model | response = Just (toString error)
, formState = Editing
}
, Cmd.none
)
then on the view, if the formState
is Fetching
, we add a section with class form-cover
that we can use from CSS to add a spinner
viewForm : Model -> Html Msg
viewForm model =
div [ class "form-container" ]
[ div
[ onEnter SubmitForm
]
[ node "style" [] [ text "" ]
, viewInput model Email "text" "Email"
, viewInput model Password "password" "Password"
, button [ onClick SubmitForm ] [ text "Submit" ]
]
, if model.formState == Fetching then
div [ class "form-cover" ] []
else
text ""
]
In the CSS:
.form-cover {
background-color: #dddddddd;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}@keyframes spinner {
to {transform: rotate(360deg);}
}.form-cover:before {
content: '';
box-sizing: border-box;
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
margin-top: -10px;
margin-left: -10px;
border-radius: 50%;
border-top: 2px solid orange;
border-right: 2px solid transparent;
animation: spinner .6s linear infinite;
}
☞ Example 14 — Adding ”Floating Labels”
Floating Labels are labels in the input fields that occupy the placeholder space until the user focus on that field. Then it moves out of the field. Something described in the Material Design. You can find an example of this functionality at elm-mdl
.
To implement this is very simple at this point.
- We remove the placeholder attribute from the <input> element as we don’t need it anymore
- In the viewInput we add a new section just after the input field
div
[ classList
[ ( "placeholder", True )
, ( "upperPosition", hasFocus || content /= "" )
]
]
The class upperPosition is the one that will move the label above. It is not active only when the field has no focus and it is empty.
This is all about the code. For the styles:
.placeholder {
position: absolute;
top: 15px;
left: 10px;
pointer-events: none;
font-size: 18px;
transition: all 0.2s;
color: #bbb;
}
.placeholder.upperPosition {
top: -17px;
left: 0px;
font-size: 15px;
color: #888;
}
Not very elegant but it should work.
☞ Example 15 — Adding Checkboxes
Time to add something else. Let’s try checkboxes. Usually checkbox could have any pair of name/value and the pair is sent over submission if the checkbox is checked. We ignore this behaviour and we just keep the status in our model, as usual.
To store the data I choose a Dict but other forms of data structure are also fine.
Let’s suppose we have a list of fruits and we want the user to select 0, one or more of them.
We define the alias for Fruit
type alias Fruit =
String
Then in the model we add
fruits : Dict.Dict Fruit Bool
Keys will be the name of the fruit as String, the value is just a boolean for checked and unchecked.
We initialise the value with a list of fruit and False values:
fruits =
Dict.fromList
[ ( "Apple", False )
, ( "Banana", False )
, ( "Orange", False )
, ( "Mango", False )
, ( "Pear", False )
, ( "Strawberry", False )
]
We add a new message to handle the clicks on the checkboxes
type Msg
= NoOp
| SubmitForm
| SetField FormField String
| Response (Result Http.Error String)
| OnFocus FormField
| OnBlur FormField
| ToggleShowPasssword
| ToggleFruit Fruit
In the update we simply toggle the boolean of the corresponding key
ToggleFruit fruit ->
( { model | fruits = toggle fruit model.fruits }, Cmd.none )
Dict doesn’t have a toggle function, so we create an helper for that:
toggle :
comparable
-> Dict.Dict comparable Bool
-> Dict.Dict comparable Bool
toggle key dict =
Dict.update key
(\oldValue ->
case oldValue of
Just value ->
Just <| not value Nothing ->
Nothing
)
dict
Last thing, we need to update the view:
div []
(List.map
(\fruit ->
let
value =
Dict.get fruit model.fruits
in
label [ class "checkbox" ]
[ input
[ type_ "checkbox"
, checked <| Maybe.withDefault False value
, onClick <| ToggleFruit fruit
]
[]
, text <| " " ++ fruit ++ " is "
, text <| toString <| value
]
)
(Dict.keys
model.fruits
)
)
Here we iterate all the item of the Dict and for each element we create an <input> element of the type checkbox. In case the value of the element is True we also add the checked parameter. Moreover, in case of click we toggle the value of the element.
The checkbox could move into an helper the same we did for the input text and input password but for the moment we will leave it in the view.
We can confirm now, using the History Explorer, that the model is updating accordingly on the checkboxes that are checked.
☞ Example 16 — Encoding Checkboxes
This should be straigforward at this point. First we create a filter that take the fruit Dict and return a list of Fruit that have the value set to True (it means that they have been checked in the form):
filteredFruits : Dict.Dict Fruit Bool -> List Fruit
filteredFruits fruits =
Dict.keys
(Dict.filter (\key value -> value) fruits)
We can now add the fruit to the encoder in the post request
postRequest : Model -> Http.Request String
postRequest model =
let
body =
Encode.object
[ ( "email", Encode.string model.email )
, ( "password", Encode.string model.password )
, ( "fruits"
, Encode.list <|
List.map (\key -> Encode.string key)
(filteredFruits model.fruits)
)
]
|> Http.jsonBody
in
Http.request
{ method = "POST"
, headers = []
, url = Utils.urlMirrorService
, body = body
, expect = Http.expectString
, timeout = Nothing
, withCredentials = False
}
Our encoded json will be something like this now
"json": {
"email": "foo",
"fruits": [
"Apple",
"Banana",
"Mango"
],
"password": "bar"
}
☞ Example 17 — Adding maximum number of checkable fruits
To create this validation let’s first create a parameter and an helper:
maxFruitSelectable : Int
maxFruitSelectable =
3fruitsQuantityHaveReachedTheLimit : Dict.Dict comparable Bool ->
Bool
fruitsQuantityHaveReachedTheLimit fruits =
List.length (filteredFruits fruits) >= maxFruitSelectable
To check if we reached the limit we can just measure the length of the List of filtered Fruits (see Example 16 about filteredFruits
)
Then in the section of the view that display the checkbox we make them disabled if the limit of selectable fruit is already reached
(\fruit ->
let
value =
Dict.get fruit model.fruits isDisabled =
fruitsQuantityHaveReachedTheLimit model.fruits && not
(Maybe.withDefault False value)
in
label
[ classList
[ ( "checkbox", True )
, ( "disabled", isDisabled )
]
]
[ input
[ type_ "checkbox"
, checked <| Maybe.withDefault False value
, disabled isDisabled
, onClick <| ToggleFruit fruit
]
[]
, text <| " " ++ fruit
]
)
We both add a class disabled, to visually disable the check box with css and we also add the attribute disabled
to the <input>
element so that users cannot click on them.
Now all the check boxes will be disabled after the number os checked fruit reached 3. Is still possible to change the selection but is necessary to remove some fruit first.
☞ Example 18 — Adding svg fruit icons
Well, this is not at all about forms so feel free to skip at Example 19.
We got bored at watching to a text-only form that it was natural to add some fruit icon to make it more colorful.
This is how the svg icons are implemented:
svgLemmon : Html.Html msg
svgLemmon =
Svg.svg [ SA.viewBox "0 0 55 55" ]
[ Svg.path [ SA.fill "#f4c44e", SA.d "M55 31H13a21 21 0 1 0
42 0z" ] []
, Svg.path [ SA.fill "#f9ea80", SA.d "M51 31H17a17 17 0 1 0
34 0z" ] []
, Svg.path [ SA.fill "#f9da49", SA.d "M33 31h2v17h-2z" ] []
, Svg.path [ SA.fill "#f9da49", SA.d "M34.7 30.3l12 12-1.4
1.4-12-12z" ] []
, Svg.path [ SA.fill "#f9da49", SA.d "M33.3 30.3l1.4 1.4-12
12-1.4-1.4z" ] []
, Svg.path [ SA.fill "#f9d70b", SA.d "M48 11.3l.4-.4a4 4 0 0
0 0-5.7 4 4 0 0 0-5.7 0l-.2.3c-9.1-6.2-23.1-
3.8-33 6s-12.3 24-6.1 33l-.3.3c-1.5 1.6-1.5 4.1
0 5.7s4.1 1.5 5.7 0l.4-.4a23.4 23.4 0 0 0 19.6
1.2A21 21 0 0 1 13 31h36.2c2.4-7 2.1-14.1-1.2-
19.7zM32.9 9c-.5 1.2-1.6 1.6-2.8 1.3.6.1 0 0-.2
0a6 6 0 0 0-.4 0h-.8c-1.1.2-2.3-.5-2.5-1.7-.2-
1.1.6-2.3 1.8-2.5 1.2-.2 2.4-.1 3.6.2 1.1.2 1.6
1.7 1.3 2.7z" ] []
]
Where SA is coming from
import Svg.Attributes as SA
☞ Example 19 — Adding Date Picker
Let’s add a Data Picker widget. We can leverage this library: http://package.elm-lang.org/packages/elm-community/elm-datepicker/latest
After installation, we need to import with
import Date
import DatePicker
In the init we setup some configuration:
init : ( Model, Cmd Msg )
init =
let
isDisabled date =
Date.dayOfWeek date
|> flip List.member [ Date.Sat, Date.Sun ] ( datePicker, datePickerFx ) =
DatePicker.init
in
( { errors = []
, email = ""
, password = ""
, fruits =
Dict.fromList
[ ( "Apple", False )
, ( "Banana", False )
, ( "Orange", False )
, ( "Pear", False )
, ( "Strawberry", False )
, ( "Cherry", False )
, ( "Grapes", False )
, ( "Watermelon", False )
, ( "Pineapple", False )
]
, response = Nothing
, focus = Nothing
, showErrors = False
, showPassword = False
, formState = Editing
, date = Nothing
, datePicker = datePicker
, defaultSettings = DatePicker.defaultSettings
}
, Cmd.map ToDatePicker datePickerFx
)
In the configuration above, for example, we are disabling Saturday and Sundays from the calendar.
Then we add a new message
type Msg
= NoOp
| SubmitForm
| SetField FormField String
| Response (Result Http.Error String)
| OnFocus FormField
| OnBlur FormField
| ToggleShowPasssword
| ToggleFruit Fruit
| ToDatePicker DatePicker.Msg
That we handle this way:
ToDatePicker msg ->
let
( newDatePicker, _, mDate ) =
DatePicker.update settings msg model.datePicker date =
case mDate of
DatePicker.Changed date ->
date _ ->
model.date
in
( { model
| date = date
, datePicker = newDatePicker
}
, Cmd.none
)
Now we prepare a little helper to convert the date into a string. This will become a bit handy also later when we add the date into the Json request
formatDate : Date.Date -> String
formatDate d =
toString (Date.month d) ++ " " ++ toString (Date.day d) ++ ", "
++ toString (Date.year d)
Then we add it to the view using Html.map
, so that the messages will be properly transformed as explained here:
DatePicker.view model.date settings model.datePicker
|> Html.map ToDatePicker
Done!
☞ Example 20 — Adding HTML5 Date Picker
Out of curiosity let’s add and an <input>
element with type = date
so that we can appreciate the difference between the Elm date picker and the Default Browser data picker. This is just simple view stuff.
☞ Example 21— Adding Autocomplete Drop Down Menu
Let’s add another widget to the form, an Autocomplete input field. In this case we use the thebritican/elm-autocomplete package. For a better implementation of this package, read the post Autocomplete widget in Elm.
Part 1 ⬅ Part 2 ⬅ __END_OF_PART_3__
Parts
- Part 1 — Examples 1~6:
x-www-form-urlencoded
andjson
encoding, validation - Part 2 — Examples 7~12: Removing
<form>
, on-the-fly validation, Focus detection, Show/Hide the password - Part 3 — Examples 13~21: Spinner, Floating Labels, Checkboxes, Date Picker, Autocomplete
Let me know if you have any suggestion or comment.
Thank you for reading.