I am uneasy about publishing this package in its current rough and unfinished state. However, impending fatherhood means I won't be doing much programming over the next few months, and I wanted to share the work I've done so far.
I would not recommend using this package in production, and you'll probably see why if you read to the end of this document.
Nevertheless, I think the techniques I used to implement control combinators with nested tuples may be of interest to people who like using Elm to do weird, unElmish things.
Perhaps by the time our baby lets me get back to coding again, someone will have been inspired to build something less ridiculous and more useful with nested tuples.
This package allows you to build up user input forms that you can use to create or update any arbitrary Elm type. Forms are composed from individual controls, and the internal state of each control can be any arbitrary Elm type.
This gives the user a lot of flexibility to create whatever controls they like, as well as making it easy to create forms for complex data structures by combining simpler controls, without too much wiring or boilerplate.
The package provides basic controls that produce primitive Elm types, such as
String, Int, Float, etc. If you need a completely custom
control, you can easily create one from scratch using Control.create.
It also provides combinators that allow the user to combine controls to create
more complex types. In addition to standard combinators for List, Dict,
Maybe, Tuple and Wrapper types, you can also create combinators
for records and custom types, with an API similar to miniBill/elm-codec.
Finally, it provides functions to validate and debounce controls, without worrying about any annoying state management or book-keeping.
Model and Msg types?").Control module are... interesting.Here's an example that shows the record and customType combinators in action:
type alias User =
{ name : String
, age : Int
, role : Role
}
userControl =
Control.record
(\name age role ->
{ name = name
, age = age
, role = role
}
)
|> Control.field "Name" .name Control.string
|> Control.field "Age" .age Control.int
|> Control.field "Role" .role roleControl
|> Control.end
type Role
= Regular
| AdminLevel Int
roleControl =
Control.customType
(\regular adminLevel tag ->
case tag of
Regular ->
regular
AdminLevel level ->
adminLevel level
)
|> Control.tag0 "Regular" Regular
|> Control.tag1 "Admin Level" AdminLevel Control.int
|> Control.endThe big tradeoff of this package is that its forms build up quite large and
complex State and Delta types (which are the equivalent of an Elm program's
Model and Msg types, respectively).
When you want to hook up a form in your Elm application, your Model and Msg
will need to include those State and Delta types - which means you have to
be able to work out what they are.
Fortunately, the Elm compiler has our back here, and we can use it to tell us what types we need. We'll give it some deliberately incorrect type annotations, and see what error messages it gives us.
type alias Model =
{ state : () } -- we'll get an error here, because the form's `state` won't be `()`
type Msg
= FormUpdated () -- we'll get an error here, because the form's `delta` won't be `()`
| FormSubmitted
userForm =
Control.toForm
"Let's make a User"
FormUpdated
FormSubmitted
userControl
main : Program () Model Msg
main =
Browser.element
{ init = \() -> ( { state = userForm.init }, Cmd.none )
, view = \model -> userForm.view model.state
, update =
\msg model ->
case msg of
FormUpdated delta ->
let
( state, cmd ) =
userForm.update delta model.state
in
( { model | state = state }, cmd )
_ ->
( model, Cmd.none )
, subscriptions = \_ -> Sub.none
}When you compile this, you should get some errors like this:
-- TYPE MISMATCH ----------------------------------------------
Something is off with the body of the `main` definition:
Browser.element
{ init = \() -> ( { state = myForm.init }, Cmd.none )
, view = \model -> myForm.view model.state
, update =
\msg model ->
case msg of
FormUpdated delta ->
let
( state, cmd ) =
myForm.update delta model.state
in
( { model | state = state }, cmd )
_ ->
( model, Cmd.none )
, subscriptions = \_ -> Sub.none
}
This `element` call produces:
Program
()
{ state :
Control.State
( Control.State String
, ( Control.State String
, ( Control.State
( Control.State ()
, ( Control.State
( Control.State String, Control.End )
, Control.End
)
)
, Control.End
)
)
)
}
Msg
But the type annotation on `main` says it should be:
Program () Model Msg
-- TYPE MISMATCH ----------------------------------------------
The 1st argument to this function is not what I expect:
myForm.update delta model.state
^^^^^
This `delta` value is a:
()
But this function needs the 1st argument to be:
Control.Delta
( Control.Delta String
, ( Control.Delta String
, ( Control.Delta
( Control.Delta ()
, ( Control.Delta ( Control.Delta String, Control.End )
, Control.End
)
)
, Control.End
)
)
)So, your types are going to be:
type alias Model =
{ state : Control.State
( Control.State String
, ( Control.State String
, ( Control.State
( Control.State ()
, ( Control.State
( Control.State String, Control.End )
, Control.End
)
)
, Control.End
)
)
)
}
type Msg
= FormUpdated
( Control.Delta
( Control.Delta String
, ( Control.Delta String
, ( Control.Delta
( Control.Delta ()
, ( Control.Delta ( Control.Delta String, Control.End )
, Control.End
)
)
, Control.End
)
)
))
| FormSubmittedYeah. Do you hate it yet?
$ cd examples
$ npm install
$ . run