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
, title = "Just a simple Int control"
, 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
, title = "Just a simple Int control"
, 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 wrapper type around a given type.
type Id
= Id Int
idControl =
wrapper { wrap = Id, unwrap = \(Id i) -> i } intA 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 ( "First", string ) ( "Second", int ) ( "Third", float )A 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 ( "Key", int ) ( "Value", string )A 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 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.
positiveInt =
int
|> failIf
(\x -> x < 1)
"Must 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 helpful error messages on both fields if they don't. You can achieve this as follows:
passwordControl =
record
(\password confirmation ->
{ password = password
, confirmation = confirmation
}
)
|> field "Enter password"
(string
|> catchFlag
"flag-passwords-do-not-match"
"Must match 'Confirm password' field"
)
|> field "Confirm password"
(string
|> catchFlag
"flag-passwords-do-not-match"
"Must match 'Enter password' field"
)
|> end
|> throwFlagIf
(\{ password, confirmation } -> password /= confirmation)
"flag-passwords-do-not-match"Throw a "flag" from a Control if its output fails to meet the predicate.
This is meant to be used in combination with catchFlag, which catches the "flag"
and displays an error.
Catch a "flag" thrown by another Control and display an error on this
control.
This is meant to be used in combination with throwFlagIf, which throws the "flag".
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
|> catchFlag
"flag-hello-world"
"The first two items in the list must not be \"hello\" and \"world\".")
list myString
|> throwFlagsAt
(\list_ ->
case list of
"hello" :: "world" :: _ -> [ 0, 1 ]
_ -> []
)
"flag-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 )
]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 "Name" string
|> field "Age" int
|> endAdd a field to a record combinator.
type alias Hello =
{ hello : String }
helloControl =
record Hello
|> field "Hello" string
|> endAdd a read-only field to a record combinator.
type alias Hello =
{ hello : String }
helloControl =
record Hello
|> readOnlyfield "Hello" string
|> endFinalise the construction of a record or customType combinator.
type alias Hello =
{ hello : String }
helloControl =
record Hello
|> field "Hello" string
|> endChange the layout of the fields of a record control.
type alias MyRecord =
{ name : String
, age : Int
}
myRecordControl =
record
(\name age ->
{ name = name
, age = age
}
)
|> field "Name" string
|> field "Age" int
|> end
|> layout (\fieldViews staticConfig dynamicConfig -> Html.div [] fieldViews)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", int ) ( "Float", 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"
|> 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 ( "X", float ) ( "Y", float )Add 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 ( "X", float ) ( "Y", float ) ( "Z", float )Add a tag with four arguments to a custom type.
Add a tag with five arguments to a custom type.
These 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
An internal type used to differentiate between builders for record and custom type combinators
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
Some internal stuff needed to view controls
an internal type needed to track the position of a control within a tree of controls
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.