This package is experimental, and the API may change. It's also pretty weird. I'm fairly sure there are still some bugs and oddities. I would not recommend using it in production.
This package allows you to create very sophisticated forms by designing
Widgets,
(which are essentially miniature Elm applications), and converting them into
Fields
which can then be composed to create HTML forms.
Widgets can use any Elm types as their Msg and Model types, and they can
send Cmds and receive Subs, just like a full-scale Elm Program. This gives
you a lot of flexibility to design rich user interface components, because you
have the full power of the Elm architecture at your disposal.
Fields can be transformed and composed using standard functional combinators
such as
map,
andMap
and
andThen.
These combinators make it easy to create forms for complex data structures by
composing simpler Fields, without too much wiring or boilerplate.
Widgets that can draw on the full power of the Elm architectureField communication through message-passingWidgets.Msg and Model types for your forms can look a bit unusual, as they are
built on nested tuples like ( Maybe String, ( Maybe Int, () ) ).Widgets to
Fields are quite terrifyingUntil I have time to write a proper tutorial, here's a taste of what it looks like to use this package:
Let's say we want to create a form for this very boring User type:
module Examples exposing (..)
type alias User =
{ firstName : String
, lastName : String
, numberOfPets : Int
}WidgetsFirst, we'll need some Widgets that can generate the primitive types for our
User's record fields (String and Int). Widgets are just like little Elm
apps! But in addition to init, update, view and subscriptions, they need
a submit function and a label. (They also take a parameter that allows you
to pass in some configuration, but we don't need that here, so we just pass in
()):
import Html as H
import Html.Attributes as HA
import Html.Events as HE
import Yafl
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"
}
type CounterMsg
= Increment
| Decrement
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"
}
-- DOC TESTS
stringWidget --: Yafl.Widget () String String String
counterWidget --: Yafl.Widget () Int CounterMsg IntWidgets into FieldsNext, we define the Fields we're going to use in our form. (You might have a
big library of Widgets, but if you only need to use a handful of them in your
form, there's no need to include them all).
import Examples exposing (..)
import Yafl
fields =
Yafl.defineFields
(\string counter ->
{ string = string
, counter = counter
}
)
|> Yafl.addWidget stringWidget
|> Yafl.addWidget counterWidget
|> Yafl.endFields
-- Defining the fields will also define the `Model` and `Msg`
-- types for our form:
type alias FormModel =
( Maybe String, (Maybe Int, () ) )
type alias FormMsg =
( Maybe String, (Maybe CounterMsg, () ) )
-- DOC TESTS
fields --: { string : Yafl.Field FormModel FormMsg Yafl.NoId String String String, counter : Yafl.Field FormModel FormMsg Yafl.NoId CounterMsg CounterMsg Int }Now whenever we need a field that produces a String, we can use
fields.string, and if we need a field that produces an Int, it's just
fields.counter.
FieldsEach field needs a label, and we might also want to add some validation, so:
import Examples exposing (..)
import Yafl
nonEmptyString =
fields.string
|> Yafl.validate
(\string ->
if String.isEmpty string then
Just "This field must not be blank"
else
Nothing
)
firstName =
nonEmptyString
|> Yafl.label "What is the user's first name?"
lastName =
nonEmptyString
|> Yafl.label "What is the user's last name?"
numberOfPets =
fields.counter
|> Yafl.label "How many pets do they have?"
-- DOC TESTS
nonEmptyString --: Yafl.Field FormModel FormMsg Yafl.NoId String String String
firstName --: Yafl.Field FormModel FormMsg Yafl.NoId String String String
lastName --: Yafl.Field FormModel FormMsg Yafl.NoId String String String
numberOfPets --: Yafl.Field FormModel FormMsg Yafl.NoId CounterMsg CounterMsg IntFields to create a formWe can use a combination of
succeed
and
andMap
to compose our Fields into a form that produces our User type as its output:
import Examples exposing (..)
import Yafl
user =
Yafl.succeed User
|> Yafl.andMap .firstName firstName
|> Yafl.andMap .lastName lastName
|> Yafl.andMap .numberOfPets numberOfPets
-- DOC TESTS
user --: Yafl.Field FormModel FormMsg Never Never { firstName : Maybe String, lastName : Maybe String, numberOfPets : Maybe CounterMsg } UserYafl.studioIf you create a main function with Yafl.studio, you can use elm reactor to
take your form for a quick test-drive in the browser.
import Examples exposing (..)
import Yafl
main =
Yafl.studio Debug.toString user
-- DOC TESTS
main --: Platform.Program () (Yafl.Model FormModel User) (Yafl.Msg FormMsg)Once you are happy with how your form looks and behaves, you'll probably want to integrate it into a larger Elm app. This isn't a very realistic example, but it should get you up and running:
import Examples exposing (..)
import Yafl
import Browser
import Html as H
main =
Browser.element
{ init = \flags -> Yafl.init user
, update = \msg model -> Yafl.update user msg model
, view = \model -> H.form [] (Yafl.view user model)
, subscriptions = \model -> Yafl.subscriptions user model
}
-- DOC TESTS
main --: Program () (Yafl.Model FormModel User) (Yafl.Msg FormMsg )For a slightly larger-scale example, take a look at the
examples folder.
You can run the examples and tests and view this documentation by doing npx
run-pty run-pty.json
That's the end of the Getting Started section of this README. Hopefully it is
enough to get you up and running. The API docs for the
Yafl
module should help you explore the package in a bit more detail.
The rest of this document is about the rationale behind creating this package, design decisions and tradeoffs.
I guess you could say that I have... form for creating form packages in Elm.
My previous attempt was
edkelly303/elm-any-type-forms,
which similarly allowed you to create widgets with arbitrary model and msg
types and compose them into forms. However, the approach I took in that package
had some serious shortcomings:
With elm-any-type-forms, the types of the forms quickly became very complex.
For a type as simple as:
type Foo
= Foo String
| Bar IntThe resulting form's model type was:
type alias FormModel =
( Control.CustomType
( Control.Variant (Control.Arg String Control.EndVariant)
( Control.Variant (Control.Arg String Control.EndVariant)
Control.EndCustomType
)
)
)And this got worse every time you added a variant to a custom type or field to a record.
The key insight for elm-yafl was that the complexity of the types doesn't need
to increase with the number of fields in the form - it only needs to increase
when you add a new type of widget to the form.
For example, once you've added a String widget type to your fields
definition, you can include as many String fields in your form as you like
without blowing up your msg and model types.
So, with elm-yafl, our Foo form's model could be as simple as:
type alias FormModel =
( Maybe String, ( Maybe Int, () ) )And even if the definition of Foo changed to:
type Foo
= Foo String
| Bar Int
| Baz String String String String
| Qux Int Int Int IntThe FormModel type would remain exactly the same.
In elm-any-type-forms, the type signatures of many of the functions are
completely insane - in some cases, they are hundreds of lines of code.
In fact, the type annotations got so long that the Elm 0.19.1 compiler refused to allow me to publish the package, so I had to use an earlier compiler version to get it published (thank you Dillon Kearns for teaching me this trick!)
If you misused any of these functions, the compiler would often spend several seconds printing out thousands of lines of error messages, which was... somewhat offputting for most users?
In elm-yafl, there are only four functions that have crazy type signatures:
defineFields,
addWidget,
addWidgetWithConfig
and
endFields,
and the longest is only 70 lines of code.
These functions only need to be used once per project, and they are designed to be difficult to misuse, so it's less likely that users will be confronted with bizarre error messages.
Wolfgang Schuster once asked me whether it was possible for the state of a field
in elm-any-type-forms to depend on the state of another field. I didn't have a
good answer. If you wanted widgets that were interdependent, your only real
option was to create a custom field from scratch.
With elm-yafl, we now have a mechanism for fields to communicate with each
other through message passing. At the top level of your app, you can
intercept
messages that a field is sending to itself, and then
send
messages to another field via the Elm runtime.
While writing elm-any-type-forms, I blundered into the discovery that both
product and sum types could be represented using nested tuples. But I didn't
come up with a comfortable way to work with nested tuples until much later, when
I distilled the core ideas into my
elm-nested-tuples
package.
As a result, the codebase for elm-any-type-forms is extremely difficult to
work with. It's also 9,500 lines of code in one main file, with literally
thousands of lines of type annotations.
By contrast, elm-yafl builds on elm-nested-tuples, which reduces the
complexity significantly. It also comes in at just over 3,000 lines of code
including type annotations and documentation.
There are some cool features of elm-any-type-forms that haven't made it into
elm-yafl.
elm-any-type-forms, fields could also raise
warnings, which would show the user a message without preventing submission. I'd
like to add this feature to elm-yafl when I have time.There are many other possible features, but I'm dogfooding the current version
first to see what my real-world requirements are. (If I'd done this with
elm-any-type-forms, I could have saved myself a lot of work!)