Introduction
In Cambiatus, we have a very powerful abstraction over forms - they're just a function that takes in a "dirty" model (made up of user input) and returns a valid model (by parsing the dirty model, following Parse don't validate).
This abstraction comes primarily from hecrj/composable-form,
but we have our own Form
module.
Along with the abstraction of how a form should work in a general sense (receive input, and then parse that input), we also have a bunch of components that define how the form fields should work in a specific sense, and also how they should look.
Another powerful feature of our Form
module and all of it's fields is the use
of The builder pattern,
for forms and for form fields (in our abstraction, each form field is considered
as a mini-form itself). This ensures we can have a very composable and flexible
API to define forms. Here's roughly how we could have a login form:
type alias DirtyCredentials = { email : String, password : String }
type alias Credentials = { email : Email, password : String }
loginForm : Bool -> Form DirtyCredentials Credentials
loginForm areCredentialsIncorrect =
Form.succeed Credentials
-- We can have fields defined on their own functions
|> Form.with emailField
-- And we can have them inline
|> Form.with
(Form.Text.init { label = "Password", id = "password-input" }
-- We can add some attributes to the text field
|> Form.Text.withType Form.Text.Password
|> Form.Text.withElements
[ clearInputButton
, toggleInputVisibilityButton
]
|> Form.textField
{ parser =
\password ->
if String.length password > 3 then
Ok password
else
Err "Password must be at least 4 characters long"
, value = .password
, update = \password dirtyCredentials -> { dirtyCredentials | password = password }
, externalError =
\_ ->
if areCredentialsIncorrect then
Just "Your credentials don't match!"
else
Nothing
}
)
emailField : Form String Email
emailField =
Form.Text.init { label = "Email", id = "email-input" }
|> Form.textField
-- Imagine that `Email.fromString` is of type `String -> Result String Email`
{ parser = Email.fromString
, value = .email
, update = \email dirtyCredentials -> { dirtyCredentials | email = email }
, externalError = always Nothing
}
With these functions, we can build generic forms - notice we didn't give them any data!
In order to actually render them and get input from them, we need a
Form.Model
, which has an init
, update
and view
triplet:
type alias Model =
{ dirtyCredentials : Form.Model DirtyCredentials }
init : (Model, Cmd Msg)
init =
( { dirtyCredentials = Form.init { email = "", password = "" } }
, Cmd.none
)
type Msg
= GotLoginFormMsg (Form.Msg DirtyCredentials)
| AttemptedLogin Credentials
update : Msg -> Model -> LoggedIn.Model -> UpdateResult
update msg model loggedIn =
case msg of
GotLoginFormMsg subMsg ->
Form.update loggedIn.shared subMsg model.dirtyCredentials
|> UR.fromChild
(\dirtyCredentials -> { model | dirtyCredentials = dirtyCredentials })
GotLoginFormMsg
LoggedIn.executeFeedback
model
AttemptedLogin credentials ->
UR.init model
|> UR.addCmd (Api.login credentials)
view : LoggedIn.Model -> Model -> Html Msg
view loggedIn model =
Form.view []
{ buttonAttrs = []
, buttonLabel = [ text "Login" ]
, translators = loggedIn.shared.translators
}
loginForm
model.dirtyCredentials
GotLoginFormMsg
AttemptedLogin
In the following chapters, we will take an in-depth look at all of our form components. If you're testing these, please make sure they're keyboard-accessible (you can navigate and use them only with your keyboard) and also screen-reader-friendly!