A Form is an interface containing functions that you can use in your
program to initialise, view, update, subscribe, and submit your form.
Test and debug a Control by turning it into a Program that you can run as a standalone Elm application.
This is useful when you're rapidly iterating on designing a form, and you don't yet want the hassle of plumbing it into
your main Elm application's Modeland Msg types.
Once you're happy with the form, you can then ask the Elm repl (or your editor's Elm plugin) to tell you the type
signature of the Program, which will give you the form's State and Delta types. You can then plug these into your
main Modeland Msg types wherever appropriate.
main : Program () (State String) (Delta String)
main =
sandbox
{ control = int
, outputToString = Debug.toString
}Convert a Control into a Form, which you can then plumb into your Elm application. You will need to supply a
couple of variants from your app's Msg type: one to handle updates of the form's state, and one to handle the
submission of the form.
type alias Msg
= FormUpdated (Delta String)
| FormSubmitted
myForm : Form (State String) (Delta String) Int Msg
myForm =
form
{ control = int
, onUpdate = FormUpdated
, onSubmit = FormSubmitted
}A control that produces a Bool. Renders as an HTML checkbox.
A control that produces an Int. Renders as an HTML number input.
A control that produces a Float. Renders as an HTML number input.
A control that produces a String. Renders as an HTML text input.
A control that produces a Char. Renders as an HTML text input.
A control that produces a custom type where none of the variants have any payload. Renders as an HTML radio input.
type Colour
= Red
| Green
| Blue
colourControl : Control Colour Colour Colour
colourControl =
enum
( "Red", Red )
( "Green", Green )
[ ( "Blue", Blue ) ]A combinator that produces a tuple of two controls of given types.
myTupleControl =
tuple ( "First", string ) ( "Second", int )A combinator that produces a triple of three controls of given types.
myTripleControl =
triple string int floatA combinator that produces a Maybe of a control of a given type.
myMaybeControl =
maybe intA combinator that produces a Result with a given error and
success type.
myResultControl =
result string intA combinator that produces a List of controls of a given type.
myListControl =
list intA combinator that produces a Dict from controls of given key and value
types. The key type must be comparable.
myDictControl =
dict int stringA combinator that produces a Set from controls of a given member
types. The member type must be comparable.
mySetControl =
set stringA combinator that produces an Array from a control of a given type.
myArrayControl =
array intA combinator that converts a Control whose output is of type a to a Control whose output is of type b.
This is particularly useful for implementing "wrapper" types, such as Ids.
Note: with most common map functions, such as List.map, we only need to supply a function from a -> b. In this case,
however, we need to supply two functions that will allow us to both convert the type from a -> b and
revert it back from b -> a.
type Id
= Id Int
idControl =
map
{ convert = \i -> Id i
, revert = \(Id i) -> i
}
intConfiguration for a custom control. You need to supply the following fields:
initEmpty: a default initial "empty" value for the state of the control. For a control whose state is of type String, this might be "", for a number it might be 0.initWith: a function that initialises the control's state from a value of the control's output type. For a control that outputs an Int, but whose state is a String, this might be String.fromInt.update: a function that updates the state of the control based on its existing state and a delta type that it receives. This is exactly analogous to an Elm program's update function, where state = Model and delta = Msg.view: a function that outputs Html delta based on the state of the control. This is similar to an Elm program's view function, except instead of just taking the state as an argument, it takes a record containing the state plus the id, name, class, and label for the input.subscriptions: a function that outputs Sub delta based on the state of the control. This is exactly analogous to an Elm program's subscriptions function.parse: a function that attempts to parse the control's state into its output type, producing a result. If parsing fails, it can provide a list of errors.Create a simple control, with any arbitrary state and delta types, and
producing any arbitrary output type.
Here's how we could create a control like the famous counter example from the Elm Guide
type CounterDelta
= Increment
| Decrement
counterControl : Control Int CounterDelta Int
counterControl =
create
{ initEmpty = 0
, initWith = identity
, update =
\delta state ->
case delta of
Increment ->
state + 1
Decrement ->
state - 1
, view =
\{ state, name, id, label, class } ->
Html.div [ Html.Attributes.class class ]
[ Html.label
[ Html.Attributes.for id ]
[ Html.text label ]
, Html.div
[ Html.Attributes.id id
, Html.Attributes.name name
]
[ Html.button
[ Html.Attributes.type_ "button"
, Html.Events.onClick Increment
]
[ Html.text "+" ]
, Html.div [] [ Html.text <| String.fromInt state ]
, Html.button
[ Html.Attributes.type_ "button"
, Html.Events.onClick Decrement
]
[ Html.text "-" ]
]
]
, subscriptions = \state -> Sub.none
, parse = Ok
}Display an error on a Control if its output fails to meet the predicate.
This causes the Control to fail validation.
positiveInt =
int
|> failIf
(\x -> x < 1)
"This must be greater than zero!"Display an note on a Control if its output fails to meet the predicate.
This just shows the user a message - it doesn't cause the Control to fail
validation.
positiveInt =
int
|> noteIf
(\x -> x < 1)
"Should this be greater than zero?"You may want display errors on one or more controls based on the validation of another control higher up the tree.
Take the example of a password creation form. You want to validate that the "Enter password" and "Confirm password" fields both contain the same string, and show a helpful error message on the "Confirm password" field if they don't. You can achieve this as follows:
passwordControl =
record
(\password confirmation ->
{ password = password
, confirmation = confirmation
}
)
|> field string
|> field
(string
|> respond
{ alert = "passwords-do-not-match"
, fail = True
, message = "Must match 'Enter password' field"
}
)
|> endRecord
|> alertIf
(\{ password, confirmation } ->
password /= confirmation
)
"passwords-do-not-match"Emit an alert from a Control if its output fails to meet the predicate.
This is meant to be used in combination with respond, which listens for the
alert and displays an error or notification message on the appropriate
control(s).
Respond to an alert emitted by another Control, display an error or
notification message on this control, and if desired, cause validation to fail.
This is meant to be used in combination with alertIf, which emits the alert.
Conditionally display an error on one or more items in a list control,
based on the output of the list control.
The following example will display errors on the first two items in a list of strings, if and only if those first two items are "hello" and "world":
myString =
string
|> catch
{ alert = "no-hello-world"
, fail = True
, message = "The first two items in the list must not be \"hello\" and \"world\"."
}
myList =
list myString
|> alertAtIndexes
(\list_ ->
case list of
"hello" :: "world" :: _ -> [ 0, 1 ]
_ -> []
)
"no-hello-world"Initialise a Control using a value of the type that the Control outputs.
oneAndHello =
tuple ( "Int", int ) ( "String", string )
|> initWith ( 1, "hello" )Debounce a Control for a number of milliseconds before it will display
any parsing or validation errors. This can be used to allow the user to finish
typing into a text input before we show them feedback.
sluggishStringControl =
string
|> debounce 2000Set an id for a Control.
myStringControlHasAnId =
string
|> id "my-string"Set a name for a Control.
myStringControlHasAName =
string
|> name "My string"Set a label for a Control.
myStringControlHasALabel =
string
|> label "Enter your name"Add an HTML class attribute for a Control. See docs for Html.Attributes.class in elm-html.
myStringControlHasAClass =
string
|> class "important"
|> class "no-really"Add a list of HTML class attributes to a Control. See docs for Html.Attributes.classList in the elm/html
package.
myStringControlHasClasses =
string
|> classList
[ ( "important", True )
, ( "no-really", True )
, ( "ignore-me", False )
]Transform the HTML output of a Control's view function.
wrappedInt =
int
|> wrapView (\view -> [ Html.div [] view ])A combinator that produces a record type, or any 'product' type that needs to
be constructed from multiple controls. (For example, the tuple combinator in
this library is built using the record combinator).
type alias MyRecord =
{ name : String
, age : Int
}
myRecordControl =
record
(\name age ->
{ name = name
, age = age
}
)
|> field string
|> field int
|> endAdd a field to a record combinator.
type alias Hello =
{ hello : String }
helloControl =
record Hello
|> field string
|> endAdd a read-only field to a record combinator.
type alias Hello =
{ hello : String }
helloControl =
record Hello
|> readOnlyfield string
|> endFinalise the construction of a record combinator.
type alias Hello =
{ hello : String }
helloControl =
record Hello
|> field string
|> endDefine the HTML output for a combinator.
This is useful if you want to alter the way that the combinator's subcontrols are laid out on the screen.
For example, you could use it to present a record control as a wizard, rather than a list. The wizard example below is very naive, but hopefully gives you the idea.
type alias MyRecord =
{ name : String
, age : Int
}
myRecordControl =
record
(\name age ->
{ name = name
, age = age
}
)
|> field string
|> field int
|> end
|> layout wizard
wizard config subcontrols =
let
currentPage =
config.selected
totalPages =
List.length subcontrols
currentPageView =
subcontrols
|> List.filter (\{ index } -> index == currentPage)
|> List.map .html
|> List.concat
nextClicked =
config.selectMsg (currentPage + 1)
backClicked =
config.selectMsg (currentPage - 1)
navigationButton txt msg =
Html.button
[ Html.Attributes.type_ "button"
, Html.Events.onClick msg
]
[ Html.text txt ]
in
Html.div
[ Html.Attributes.id config.id
, Html.Attributes.class config.class
]
([ Html.h1 [] [ Html.text "Wizard!" ]
, navigationButton "Back" backClicked
, Html.text
(String.join " "
[ "page"
, String.fromInt currentPage
, "of"
, String.fromInt totalPages
]
)
, navigationButton "Next" nextClicked
]
++ currentPageView
)Configuration for the layout function
A combinator that produces a custom type.
type MyCustomType
= NoArgs
| OneArg String
| TwoArgs Int Float
myCustomTypeControl =
customType
(\noArgs oneArg twoArgs tag ->
case tag of
NoArgs ->
noArgs
OneArg arg1 ->
oneArg arg1
TwoArgs arg1 arg2 ->
TwoArgs arg1 arg2
)
|> tag0 "NoArgs" NoArgs
|> tag1 "OneArg" OneArg string
|> tag2 "TwoArgs" TwoArgs int float
|> endAdd a tag with no arguments to a custom type.
type Unit
= Unit
unitControl =
customType
(\unit tag ->
case tag of
Unit ->
unit
)
|> tag0 "Unit" Unit
|> endAdd a tag with one argument to a custom type.
type alias MyResult =
Result String Int
myResultControl =
customType
(\ok err tag ->
case tag of
Ok value ->
ok value
Err error ->
err error
)
|> tag1 "Ok" Ok int
|> tag1 "Err" Err string
|> endAdd a tag with two arguments to a custom type.
type Point
= Point Float Float
pointControl =
customType
(\point tag ->
case tag of
Point x y ->
point x y
)
|> tag2 "Point" Point float floatAdd a tag with three arguments to a custom type.
type Point3D
= Point3D Float Float Float
point3DControl =
customType
(\point3D tag ->
case tag of
Point3D x y z ->
point3D x y z
)
|> tag3 "Point3D" Point3D float float floatAdd a tag with four arguments to a custom type.
Add a tag with five arguments to a custom type.
Finalise the construction of a customType combinator.
type alias Foo
= Bar
| Baz
helloControl =
customType
(\bar baz tag ->
case tag ofcustomType
Bar -> bar
Baz -> baz
)
|> tag0 "Bar" Bar
|> tag0 "Baz" Baz
|> endCustomTypeThese are types that you will see in your form's State and Delta type signatures.
State is the form's equivalent of an Elm program's Model type. It is
used to manage the form's internal state.
A Delta is the form's equivalent of an Elm program's Msg type. It is
used internally within a form to update its state.
Special Delta type used only by list controls
Under the hood, this library encodes complex Elm types (such as records and
custom types) as nested tuples, which are used in both the State and Delta
types for each Control. The End type is used to mark the end of a nested
tuple.
For example, the State for this Elm type:
type alias User =
{ name : String, age : Int }Could be encoded as:
( String, ( Int, End ) )A user of this package shouldn't need to know about any of these types - they are only exposed to make it possible to write type signatures.
An internal type used to represent whether a record field is editable, read-only, or hidden
A slightly more flexible version of Control
Some internal stuff needed to build up record and custom type controls
Some internal stuff needed to handle validations
Some internal stuff needed to build up a record control
an internal type needed to represent the validation state of a control
Some internal stuff needed to view controls
an internal type needed to track the position of a control within a tree of controls
Some internal stuff to handle validation
A
Controlis the basic unit from which forms are composed. This library provides various example controls and combinators that you can use to build a form for any arbitrarily complex Elm type.