formz
A form is a list of fields and a decoder function. This module uses a
series of callbacks to construct a decoder function as the fields are
being added to the form. The idea is that you’d have a function that
makes the form using the use
syntax, and then be able to use the form
later for parsing or rendering in different contexts.
Examples
fn make_form() {
use name <- formz.require(field("name"), defintions.text_field())
formz.create_form(name)
}
fn process_form() {
make_form()
|> formz.data([#("name", "Louis"))])
|> formz.parse
# -> Ok("Louis")
}
fn make_form() {
use greeting <- optional(field("greeting"), defintions.text_field())
use name <- optional(field("name"), defintions.text_field())
formz.create_form(greeting <> " " <> name)
}
fn process_form() {
make_form()
|> data([#("greeting", "Hello"), #("name", "World")])
|> formz.parse
# -> Ok("Hello World")
}
Types
The widget
type is the set by the Definition
s used to add fields for
this form, and has the details of how to turn given fields into HTML inputs.
The output
type is the type of the decoded data from the form. This is
set by the create_form
function, after all the fields have been added.
pub opaque type Form(widget, output)
A form contains a list of fields and subforms. You primarily only use these
when writing a form generator function. You can also manipulate these
after a form has been created, to change things like labels, help text, etc.
There are specific functions,update_field
and update_subform
, to help
with this, so you don’t have to pattern match when updating a specific item.
let form = make_form()
form.update_field("name", field.set_label(_, "Full Name"))
pub type FormItem(widget) {
Field(field.Field, widget: widget)
SubForm(subform.SubForm, items: List(FormItem(widget)))
}
Constructors
-
Field(field.Field, widget: widget)
-
SubForm(subform.SubForm, items: List(FormItem(widget)))
Functions
pub fn create_form(thing: a) -> Form(b, a)
Create an empty form that only parses to thing
. This is primarily
intended to be the final return value of a chain of callbacks that adds
the form’s fields.
create_form(1)
|> parse
# -> Ok(1)
fn make_form() {
use field1 <- formz.require(field("field1"), definitions.text_field())
use field2 <- formz.require(field("field2"), definitions.text_field())
use field3 <- formz.require(field("field3"), definitions.text_field())
create_form(#(field1, field2, field3))
}
pub fn data(
form: Form(a, b),
input_data: List(#(String, String)),
) -> Form(a, b)
Add input data to this form. This will set the raw string value of the fields. It does not trigger any parsing, so you can also use this to set default values (if you do it in your form generator function) or initial values (if you do it before rendering an empty form).
The input data is a list of tuples, where the first element is the name of the field and the second element is the value to set. If the field does not exist the data is ignored, and if multiple values are given for the same field, the last one wins.
pub fn get(
form: Form(a, b),
name: String,
) -> Result(FormItem(a), Nil)
Get the FormItem
with the
given name. If multiple items have the same name, the first one is returned.
pub fn items(form: Form(a, b)) -> List(FormItem(a))
Get each FormItem
added
to the form. Any time a field or subform are added, a FormItem is created.
pub fn optional(
field: Field,
is definition: Definition(a, b, c),
rest fun: fn(c) -> Form(a, d),
) -> Form(a, d)
Add an optional field to a form.
This will use both the parse
and optional_parse
functions from the
definition to parse the input data when parsing this field. Ultimately
whether a field is actually optional or not comes down to the details
of the definition.
The final argument is a callback that will be called when the form is being constructed to look for more fields; validated to check for errors; and when the form is being parsed, to decode the input data. For this reason, the callback should be a function without side effects. It can be called any number of times. Don’t do anything but create the type with the data you need!
pub fn parse(form: Form(a, b)) -> Result(b, Form(a, b))
Parse the form. This means step through the fields one by one, parsing
them individually. If any field fails to parse, the whole form is considered
invalid, however it will still continue parsing the rest of the fields to
collect all errors. This is useful for showing all errors at once. If no
fields fail to parse, the decoded value is returned, which is the value given
to create_form
.
If you’d like to parse the form but not get the output, so you can give
feedback to a user in response to input, you can use validate
or validate_all
.
pub fn parse_then_try(
form: Form(a, b),
apply fun: fn(Form(a, b), b) -> Result(c, Form(a, b)),
) -> Result(c, Form(a, b))
Parse the form, then apply a function to the output if it was successful.
This is a very thin wrapper around parse
and result.try
, but the
difference being it will pass the form along to the function as a second
argument in addition to the successful result. This allows you to easily
update the form fields with errors or other information based on the output.
This is useful for situations where you can have errors in the form that aren’t easily checked in simple parsing functions. Like, say, hitting a db to check if a username is taken.
make_form()
|> data(form_data)
|> parse_then_try(fn(username, form) {
case is_username_taken(username) {
Ok(false) -> Ok(form)
Ok(true) -> update_field(form, "username", field.set_error(_, "Username is taken"))
}
}
pub fn require(
field: Field,
is definition: Definition(a, b, c),
rest fun: fn(b) -> Form(a, d),
) -> Form(a, d)
Add a required field to a form.
This will use only the parse
function from the definition to parse the
input data when parsing this field. Ultimately whether a field is actually
required or not comes down to the details of the definition.
This will also set the required
value on the field to True
. Form
generators can use this to mark the HTML input elements as required for
accessibility.
The final argument is a callback that will be called when the form is being constructed to look for more fields; validated to check for errors; and when the form is being parsed, to decode the input data. For this reason, the callback should be a function without side effects. It can be called any number of times. Don’t do anything but create the type with the data you need!
pub fn subform(
details: SubForm,
sub: Form(a, b),
fun: fn(b) -> Form(a, c),
) -> Form(a, c)
Add a form as a subform. This will essentially append the fields from the subform to the current form, prefixing their names with the name of the subform. Form generators will still see the fields as a set though, so they can be marked up as a group for accessibility reasons.
The final argument is a callback that will be called when the form is being constructed to look for more fields; validated to check for errors; and when the form is being parsed, to decode the input data. For this reason, the callback should be a function without side effects. It can be called any number of times. Don’t do anything but create the type with the data you need!
pub fn update(
form: Form(a, b),
name: String,
fun: fn(FormItem(a)) -> FormItem(a),
) -> Form(a, b)
Update the FormItem
with
the given name using the provided function. If multiple items have the same
name, it will be called on all of them.
pub fn update_field(
form: Form(a, b),
name: String,
fun: fn(Field) -> Field,
) -> Form(a, b)
Update the Field
with
the given name using the provided function. If multiple items have the same
name, it will be called on all of them.
let form = make_form()
update(form, "name", field.set_label(_, "Full Name"))
pub fn update_subform(
form: Form(a, b),
name: String,
fun: fn(SubForm) -> SubForm,
) -> Form(a, b)
Update the SubForm
with
the given name using the provided function. If multiple subforms have the same
name, it will be called on all of them.
let form = make_form()
update(form, "name", subform.set_help_text(_, "..."))
pub fn validate(
form: Form(a, b),
names: List(String),
) -> Form(a, b)
Validate specific fields of the form. This is similar to parse
, but
instead of returning the decoded output if there are no errors, it returns
the valid form. This is useful for if you want to be able to give feedback
to the user about whether certain fields are valid or not. In this case you
could just validate only fields that the user has interacted with.
pub fn validate_all(form: Form(a, b)) -> Form(a, b)
Validate all the fields in the form. This is similar to parse
, but
instead of returning the decoded output if there are no errors, it returns
the valid form. This is useful for if you want to be able to give feedback
to the user about whether certain fields are valid or not.