This library helps you build user input forms in Elm by creating and
composing self-contained Widgets.
Field, defineFields, addWidget,
addWidgetWithConfig, endFields
Model, Msg, init, load,
update, ViewConfig, view,
Wizard, viewWizard, Feedback,
subscriptions, submit
succeed, fail, failAt, map,
andMap, andThen, choice,
option
HasId, NoId, identifier,
intercept, send
Widgets are the basic building blocks of this package. Each widget
is effectively a little Elm application, with its own init, update, view
and subscriptions functions, plus a couple of extra features.
This package doesn't supply any prebuilt widgets. Every app is unique, and it's unlikely that a prebuilt widget would precisely fit your use case. But the point is, this package gives you the power to create any types of widgets you choose, and compose them together very easily with minimal boilerplate.
Nevertheless, we'll provide some code samples for a few simple widgets that we can use in the code snippets in these docs.
module Examples exposing (..)
import Html as H
import Html.Attributes as HA
import Html.Events as HE
import Yafl
{-| A basic `Widget` that produces a `String`. Its internal
`model` and `msg` types are also `String`s.
-}
stringWidget : Yafl.Widget String String String
stringWidget =
{ init = ( "", Cmd.none )
, update = \msg model -> ( msg, Cmd.none )
, view =
\{ label, id, feedback } model ->
[ H.label [ HA.for id ] [ H.text label ]
, H.input
[ HA.id id
, HA.type_ "text"
, HA.value model
, HE.onInput identity
]
[]
, H.ul [] (List.map (\f -> H.li [] [ H.text f ]) feedback)
]
, subscriptions = \model -> Sub.none
, submit = \model -> Ok model
, label = "String"
}
{-| A `Widget` that produces an `Int`, based on the counter
example from the Elm Guide. Its internal `model` type is an
`Int`, but its `msg` type is a custom type.
-}
type CounterMsg
= Increment
| Decrement
counterWidget : Yafl.Widget () Int CounterMsg Int
counterWidget () =
{ init = ( 0, Cmd.none )
, update =
\msg model ->
case msg of
Increment ->
( model + 1, Cmd.none )
Decrement ->
( model - 1, Cmd.none )
, view =
\{ label, id } model ->
[ H.label [ HA.for id ] [ H.text label ]
, H.fieldset
[ HA.id id ]
[ H.button [ HA.type_ "button", HE.onClick Decrement ] [ H.text "-" ]
, H.output [] [ H.text (String.fromInt model) ]
, H.button [ HA.type_ "button", HE.onClick Increment ] [ H.text "+" ]
]
]
, subscriptions = \_ -> Sub.none
, submit = \model -> Ok model
, label = "Counter"
}Before we can use our Widgets to create a form, we need to convert them into
Fields. This conversion process effectively combines the internal model and
msg types of each widget to create composite types that we can use as the
top-level model and msg for the entire form.
We perform this conversion using three functions: defineFields, addWidget,
and endFields. The type signatures for these three functions are extremely
terrifying, but fortunately we don't need to understand them - just follow the
example below:
module Examples exposing (Model, Msg, fields)
import Yafl exposing (addWidget, defineFields, endFields)
fields =
Yafl.defineFields
(\string counter -> { string = string, counter = counter })
|> Yafl.addWidget stringWidget
|> Yafl.addWidget counterWidget
|> Yafl.endFields
{- This gives us the following Model and Msg types for
our form:
-}
type alias FormModel =
( Maybe String, ( Maybe Int, () ) )
type alias FormMsg =
( Maybe String, ( Maybe CounterMsg, () ) )Forms are composed of Fields - this is the main data type we'll be using in this package.
Begin a definition of the fields you want to use in your forms.
Add a Widget to the definition of the Fields you want to use in your forms.
Add a configurable Widget to the definition of the Fields you want to use in
your forms. Each time you use a Field derived from this Widget in your form, you
will be able to pass in a config value.
Finalize the definition of the Fields you want to use in your forms.
Once we've defined our Fields, we can start the fun part: making forms!
Imagine we just want a simple form that allows a user to choose an Int:
import Yafl
import Examples exposing (FormModel, FormMsg, fields)
import Html exposing (Html)
-- We can turn any Field into a form:
form =
fields.counter
-- Initialize it with `Yafl.init` to get a (model, cmd)
-- tuple:
init =
Yafl.init form
init
--: ( Yafl.Model FormModel Int, Cmd (Yafl.Msg FormMsg) )
-- The form's model can then be passed to `Yafl.view`,
-- `Yafl.update`, `Yafl.subscriptions` and `Yafl.submit`:
model =
Tuple.first init
Yafl.view form model
--: List (Html (Yafl.Msg FormMsg))
Yafl.subscriptions form model
--: Sub (Yafl.Msg FormMsg)
Yafl.submit form model
--> Ok 0The top-level model type for your form.
The top-level message type for your form.
Initialize your form
import Yafl
import Examples exposing (FormModel, FormMsg, fields)
fields.counter
|> Yafl.init
--: ( Yafl.Model FormModel Int, Cmd (Yafl.Msg FormMsg) )Load data into your form. A bit tricky to explain, but see the examples below:
For simple Fields, you just pass a value of the underlying Widget's msg
type. This will dispatch the msg to the Field's update function, which may
then update its model and optionally send a Cmd.
import Yafl
import Examples exposing (FormModel, FormMsg, CounterMsg(..), fields)
form =
fields.counter
modelBeforeLoading =
form
|> Yafl.init
|> Tuple.first
Yafl.submit form modelBeforeLoading
--> Ok 0
modelAfterLoading =
modelBeforeLoading
|> Yafl.load form Increment
|> Tuple.first
Yafl.submit form modelAfterLoading
--> Ok 1For Fields composed using map2 or andMap, you can pass in a record where
each field is a Maybe widgetMsg. If the record field's value is Just, then
the message will be dispatched to the Field's update function. If it's
Nothing, then no message will be dispatched and the Field's model will
remain unchanged.
import Yafl
import Examples exposing (FormModel, FormMsg, CounterMsg(..), fields)
form =
Yafl.succeed (\int string -> ( int, string ))
|> Yafl.andMap .a fields.counter
|> Yafl.andMap .b fields.string
form
--: Yafl.Field FormModel FormMsg Never Never { a : Maybe CounterMsg, b : Maybe String } ( Int, String )
modelBeforeLoading =
form
|> Yafl.init
|> Tuple.first
Yafl.submit form modelBeforeLoading
--> Ok (0, "")
modelAfterLoading =
modelBeforeLoading
|> Yafl.load form
{ a = Nothing
, b = Just "hello"
}
|> Tuple.first
Yafl.submit form modelAfterLoading
--> Ok ( 0, "hello" )For Fields composed using choice and option, it's a similar story, except
that the options are nested within a record of the form { selected : Maybe Int,
options : {...} }. The selected field is used to pick which of the options
should be selected (it's zero-indexed, so 0 is the first option, 1 is the
second, etc.)
import Yafl
import Examples exposing (FormModel, FormMsg, CounterMsg(..), fields)
type Foo
= Bar Int
| Qux String
form =
Yafl.choice
|> Yafl.option "Bar" .bar barField
|> Yafl.option "Qux" .qux quxField
barField =
Yafl.map Bar fields.counter
quxField =
Yafl.map Qux fields.string
form
--: Yafl.Field FormModel FormMsg Never Never { selected : Maybe Int, options : Maybe { bar : Maybe CounterMsg, qux : Maybe String } } Foo
modelBeforeLoading =
form
|> Yafl.init
|> Tuple.first
Yafl.submit form modelBeforeLoading
--> Ok (Bar 0)
modelAfterLoading =
modelBeforeLoading
|> Yafl.load form
{ selected = Just 1
, options =
Just
{ bar = Nothing
, qux = Just "hello"
}
}
|> Tuple.first
Yafl.submit form modelAfterLoading
--> Ok (Qux "hello")Update your form by supplying a Msg and Model
Configuration passed into the view of each Field in your form.
View your form.
import Yafl
import Examples exposing (FormModel, FormMsg, fields)
import Html exposing (Html)
form =
fields.string
model =
form
|> Yafl.init
|> Tuple.first
Yafl.view form model
--: List (Html (Yafl.Msg FormMsg))A record produced by viewWizard, containing fields that are
useful if you want to render your form as a multi-step "wizard".
stepView is the HTML produced by the view function for the current step of
the wizard.
stepIndex is the zero-indexed number of the current step of the wizard.
totalSteps is the total number of steps in the wizard.
selectStepMsg lets you jump to a specified step of the wizard - for
example, selectStepMsg (currentStep + 1) would move to the next step of
the wizard. (In general, you'll want to take care not to let the user move
to a stepIndex less than 0 or greater than totalSteps - 1, but there
are possible designs where you might want to do this, so Yafl doesn't
guard against it.)
isStepValid indicates whether all the fields displayed in the current step
of the wizard pass validation (i.e. their submit functions return an
Ok). You could use this to decide whether to display a "next" button in
the view of the current step.
View your form as a multi-step wizard. This will only work if the top-level
field of your form is a product type created with succeed and andMap.
viewWizard doesn't produce a fully-fledged wizard view; it returns a value of
type Wizard. Jump over to the Wizard docs for an explanation.
Generate subscriptions for your form.
import Yafl
import Examples exposing (FormModel, FormMsg, fields)
form =
fields.string
model =
form
|> Yafl.init
|> Tuple.first
Yafl.subscriptions form model
--: Sub (Yafl.Msg FormMsg)Submit your form.
import Yafl
import Examples exposing (FormModel, FormMsg, fields)
form =
fields.string
model =
form
|> Yafl.init
|> Tuple.first
Yafl.submit form model
--> Ok ""In addition to the Fields that you define based on your
Widgets, the package also provides succeed and
fail, which can be useful in various ways when
used with other combinators such as andMap. You may be familiar with similar functions from packages
such as elm/json.
The views of these fields return an empty Html element. When
submitted, succeed always returns an Ok, while fail always returns an
Err.
A Field that always successfully generates the value that you supply.
import Yafl
form =
Yafl.succeed "Hurrah!"
model =
form
|> Yafl.init
|> Tuple.first
Yafl.submit form model
--> Ok "Hurrah!"A Field that always fails on submission with the error message that you supply.
import Yafl
form =
Yafl.fail "Oh dear!"
model =
form
|> Yafl.init
|> Tuple.first
Yafl.submit form model
--> Err [ ("0", "Oh dear!") ]Like fail, except it will display the error message on a different
Field. This can be useful in multi-field validation, when you have an error that
results from a combination of several fields, but you only want to display the
error message on one specific field.
import Examples exposing (fields)
import Yafl
form =
Yafl.succeed
(\targetOutput failAtOutput -> targetOutput)
|> Yafl.andMap .target targetField
|> Yafl.andMap .failure failAtField
targetField =
fields.string
|> Yafl.identifier "target-field"
failAtField =
Yafl.failAt targetField "Uh oh!"
output =
Yafl.init form
|> Tuple.first
|> Yafl.submit form
-- The error is assigned to `targetField`,
-- not `failAtField`
output --> Err [ ( "target-field", "Uh oh!" ) ]Convert the output of a Field from one type to another.
A common use case for this function is to create Fields that produce custom
type variants.
import Yafl
import Examples exposing (FormModel, FormMsg, fields)
-- Example: Creating a custom type variant
type MyCustomType
= Foo String
fooField =
Yafl.map Foo (fields.string)
fooField
|> Yafl.init
|> Tuple.first
|> Yafl.submit fooField
--> Ok (Foo "")Convert the input of a Field from one type to another.
This is useful if you want to control how data is loaded into your form with
load.
import Examples exposing (FormModel, FormMsg, fields)
import Yafl
passwordField :
Yafl.Field
FormModel
FormMsg
Never
Never
-- Here's the `input` parameter:
{ password : Maybe String
, confirm : Maybe String
}
String
passwordField =
Yafl.succeed
(\p c -> { password = p, confirm = c })
|> Yafl.andMap .password fields.string
|> Yafl.andMap .confirm fields.string
|> Yafl.validate
(\{ password, confirm } ->
if password == confirm then
Nothing
else
Just "Passwords need to match"
)
|> Yafl.map .password
-- With the `.password` and `.confirm` functions that we
-- pass to `andMap` here, our `Field`'s `input` type
-- parameter is a record containing both of those fields.
-- By using `contraMap`, we can change the `input` type
-- to make the field a bit easier to use.
passwordFieldThatIsEasierToLoad :
Yafl.Field
FormModel
FormMsg
Never
Never
-- after `contraMap`, the `input` parameter
-- is just a `String`
String
String
passwordFieldThatIsEasierToLoad =
passwordField
|> Yafl.contraMap
(\string ->
{ password = Just string
, confirm = Just string
}
)
-- DOC TESTS
passwordField --: Yafl.Field FormModel FormMsg Never Never { password : Maybe String, confirm : Maybe String } String
passwordFieldThatIsEasierToLoad --: Yafl.Field FormModel FormMsg Never Never String StringCombine multiple fields. Use in combination with succeed.
import Yafl
import Examples exposing (FormModel, FormMsg, fields)
form =
Yafl.succeed (\a b c -> { firstName = a, middleName = b, lastName = c })
|> Yafl.andMap .firstName (fields.string |> Yafl.label "First name")
|> Yafl.andMap .middleName (fields.string |> Yafl.label "Middle name")
|> Yafl.andMap .lastName (fields.string |> Yafl.label "Last name")
model =
Yafl.init form
|> Tuple.first
Yafl.submit form model
--> Ok { firstName = "", middleName = "", lastName = "" }Check the result of submitting a Field. This can be useful if
you want to convert an existing Widget to return a different output
type.
(You can also use it for validating a field's output, but it will probably be
better to use validate or validateAt instead.)
Be warned: this is not a fully law-abiding monadic andThen - you shouldn't use
it to return arbitrary Fields, you should only use it with succeed, fail and
failAt
import Yafl
import Examples exposing (FormModel, FormMsg, fields)
-- Example 1: Repurposing an existing widget to return
-- a different type
fields.string
|> Yafl.label "Enter a floating-point number"
|> Yafl.andThen
(\string ->
case String.toFloat string of
Just float ->
Yafl.succeed float
Nothing ->
Yafl.fail
"That's not a valid float"
)
--: Yafl.Field FormModel FormMsg Yafl.NoId String String Float
-- Example 2: Validating a field's output
-- (This works, but it's better to use `validate`
-- and `validateAt`.)
fields.string
|> Yafl.label "Enter the first name of a Beatle"
|> Yafl.andThen
(\name ->
if List.member name
[ "John", "Paul", "George", "Ringo" ]
then
Yafl.succeed name
else
Yafl.fail "Invalid Beatle"
)
--: Yafl.Field FormModel FormMsg Yafl.NoId String String Stringimport Yafl
import Examples exposing (FormModel, FormMsg, fields)
type MyCustomType
= Foo String
| Bar Int
myCustomTypeField =
Yafl.choice
|> Yafl.option "Foo" .foo fooField
|> Yafl.option "Bar" .bar barField
fooField =
fields.string
|> Yafl.map Foo
barField =
fields.counter
|> Yafl.map Bar
model =
myCustomTypeField
|> Yafl.init
|> Tuple.first
Yafl.submit myCustomTypeField model
--> Ok (Foo "")Begin defining a choice between multiple options.
This doesn't do anything useful on its own - it needs to be used in conjunction
with option
import Yafl
import Examples exposing (FormModel, FormMsg, fields)
Yafl.choice
--: Yafl.Field FormModel FormMsg Never Never { selected : Maybe Int, options : Maybe {} } IntAdd an option to a choice.
The option will render as an HTML radio input in the view, so you need to
provide a String to serve as a label, plus a Field that returns the actual
type you want as output.
All the options of a given choice must return the same output type,
although their internal model and msg types can be different.
If the user selects the radio button for this option, then the Field's view
will be rendered underneath the fieldset containing the radio buttons.
import Yafl
import Examples exposing (FormModel, FormMsg, fields)
Yafl.choice
|> Yafl.option
"This is the label for the radio button"
.counter
(fields.counter
|> Yafl.label "This is a label for the `counter` field"
)
--: Yafl.Field FormModel FormMsg Never Never { options : Maybe { counter : Maybe Examples.CounterMsg }, selected : Maybe Int } IntAdd a label to a Field.
import Yafl
import Examples exposing (FormModel, FormMsg, fields)
fields.string
|> Yafl.label "What is your name?"
--: Yafl.Field FormModel FormMsg Yafl.NoId String String StringAdd some arbitrary HTML before the view of a field.
import Examples exposing (FormModel, FormMsg, fields)
import Html
import Yafl
form =
fields.string
|> Yafl.htmlBefore (Html.text "Here's some text")
form --: Yafl.Field FormModel FormMsg Yafl.NoId String String StringAdd some arbitrary HTML after the view of a field.
import Examples exposing (FormModel, FormMsg, fields)
import Html
import Yafl
form =
fields.string
|> Yafl.htmlAfter (Html.text "Here's some text")
form --: Yafl.Field FormModel FormMsg Yafl.NoId String String StringValidate a field and specify an error message if validation fails.
import Yafl
form =
Yafl.succeed 0
|> Yafl.validate
(\int ->
if int > 0 then
Nothing
else
Just
("Must be greater than 0, but the value is "
++ String.fromInt int
)
)
form
|> Yafl.init
|> Tuple.first
|> Yafl.submit form
--> Err [ ( "0", "Must be greater than 0, but the value is 0" ) ]Validate a field and specify an error to display on a different field. This is useful when you are doing validation that involves multiple fields, but you only want to display an error on one field.
import Yafl
import Examples exposing (fields)
passwordField =
fields.string
|> Yafl.identifier "password"
confirmField =
fields.string
|> Yafl.identifier "confirm"
form =
Yafl.succeed
(\password confirm -> { password = password, confirm = confirm })
|> Yafl.andMap .passwordField passwordField
|> Yafl.andMap .confirmField confirmField
|> Yafl.validateAt confirmField
(\{ password, confirm } ->
if password == confirm then
Nothing
else
Just "Passwords do not match"
)
form
|> Yafl.init
|> Tuple.first
|> Yafl.load form
{ passwordField = Just "password123"
, confirmField = Just "password124"
}
|> Tuple.first
|> Yafl.submit form
--> Err [ ( "confirm", "Passwords do not match" ) ]Indicates that a Field has been given an identifier, and can therefore be
used with intercept, send, etc. See the docs for identifier.
Indicates that a Field has not been given an identifier. See the docs for
identifier.
Add a unique identifier to a Field, which can be used to send and intercept
messages to that Field.
import Yafl
import Examples exposing (FormModel, FormMsg, fields)
myField =
fields.string
myField
--: Yafl.Field FormModel FormMsg Yafl.NoId String String String
myFieldWithId =
myField
|> Yafl.identifier "any-string-as-long-as-it's-unique"
myFieldWithId
--: Yafl.Field FormModel FormMsg Yafl.HasId String String String
Yafl.send myFieldWithId "Hello!"
--: Cmd (Yafl.Msg FormMsg)This identifier is also used as the id string in ViewConfig,
which is passed into the view when the Field is rendered. When defining a
Widget, you can use the id field of the ViewConfig to set the
Html.Attributes.id of the HTML input.
Intercept the top-level Msg sent to your form, and if it contains a message sent to the specified field, return that message.
import Yafl
import Examples exposing (CounterMsg, FormModel, FormMsg, fields)
myFieldWithId =
fields.counter
|> Yafl.identifier "any-string-as-long-as-it's-unique"
model =
Yafl.init myFieldWithId
|> Tuple.first
Yafl.intercept myFieldWithId model
--: Yafl.Msg FormMsg -> Maybe CounterMsgCheck that a form doesn't contain fields with duplicate identifiers.
Turn a Field into an Elm Program that you can view in your
browser with in elm reactor for testing purposes.
This should only be used in development - to help you avoid accidentally
deploying it in production, you should pass in Debug.toString as the first
argument.
Convert a Model value into a Graphviz DOT String, which you can visualize
using a tool such as https://dreampuf.github.io/GraphvizOnline
As the first argument, you should pass in Debug.toString.
The
Widgettype is very similar to the record type that you would supply toBrowser.elementto create an ElmProgram.