formz
A form is a list of fields and a decoder function. This module uses a
series of callbacks to construct the 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.
You can use this cheatsheet to navigate the module documentation:
Creating a form |
create_form required_field field list limited_list subform |
Decoding and validating a form |
data decode decode_then_try validate validate_all |
Creating a field definition |
definition definition_with_custom_optional verify widget |
Creating a limited list |
limit_at_least limit_at_most limit_between simple_limit_check |
Accessing and manipulating form items |
get items field_error listfield_errors update |
Accessing and manipulating config for a form item |
update_config set_name set_label set_help_text set_disabled make_disabled |
Examples
fn make_form() {
use name <- formz.required_field(field("name"), definitions.text_field())
formz.create_form(name)
}
fn process_form() {
make_form()
|> formz.data([#("name", "Louis"))])
|> formz.decode
# -> Ok("Louis")
}
fn make_form() {
use greeting <- field(field("greeting"), definitions.text_field())
use name <- field(field("name"), definitions.text_field())
formz.create_form(greeting <> " " <> name)
}
fn process_form() {
make_form()
|> data([#("greeting", "Hello"), #("name", "World")])
|> formz.decode
# -> Ok("Hello World")
}
Types
Configuration for the form Item
. The only required information here is
name
and there is a named
function to make a config with a name. You
can then chain the set_x
functions to add the other information as needed.
Example
let config = formz.named("name") |> formz.set_label("Full Name")
let config =
formz.named("name")
|> formz.set_label("Full Name")
|> formz.set_help_text("Enter your full name")
let config = formz.named("name") |> formz.make_disabled
pub type Config {
Config(
name: String,
label: String,
help_text: String,
disabled: Bool,
)
}
Constructors
-
Config( name: String, label: String, help_text: String, disabled: Bool, )
Arguments
-
name
The name of the field or subform. The only truly required information for a form
Item
. This is used to identify the field in the form. It should be unique for each form, and is untested with any values other than strings solely consisting of alphanumeric characters and underscores. -
label
This library thinks of a label as required, but will make one for you from the name if you don’t provide one via the
field
function. For accessibility reasons, a field should always provide a label and all the maintained form generators will output one. -
help_text
Optional help text for the field. This is used to provide additional instructions or context for the field. It is up to the form generator to decide if and how to display this text.
-
disabled
Whether the field is disabled. A disabled field is not editable in the browser. However, there is nothing stopping a user from changing the value or submitting a different value via other means, so (presently) this doesn’t mean the value cannot be tampered with.
-
A Definition
describes how an input works, e.g. how it looks and how it’s
parsed. Definitions are intended to be reusable.
The first role of a Defintion
is to generate the HTML input for the field.
This library is format-agnostic and you can generate inputs as raw
strings, Lustre elements, Nakai nodes, something else, etc. The second role
of a Definition
is to parse the raw string data from the input into a
Gleam type.
There are currently three formz
libraries that provide common field
definitions for the most common HTML inputs:
How a definition parses an input value depends on whether a value is required
for that input (i.e. whether field
, required_field
, list
, or limited_list
was used to add it to the form). If a value is required, the definition is
expected to return a string error if the input is empty, or the required
type
if it isn’t. You can use the definition
function to create a simple
definition that just parses to an Option
if an input is empty.
However, not all fields should be parsed into an Option
when
given an empty input value. For example, an optional text field might be an
empty string or an optional checkbox might be False. For these cases, you
can use the definition_with_custom_optional
function to create a definition
that can parse to any type when the input is empty.
pub opaque type Definition(widget, required, optional)
You create this using the create_form
function.
The widget
type is 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 directly by the create_form
function, after all the fields have
been added.
pub opaque type Form(widget, output)
The state of the an input for a field. This is used to track the current raw value, whether a value is required or not, if the value has been validated, and the outcome of that validation.
pub type InputState {
Unvalidated(value: String, requirement: Requirement)
Valid(value: String, requirement: Requirement)
Invalid(value: String, requirement: Requirement, error: String)
}
Constructors
-
Unvalidated(value: String, requirement: Requirement)
-
Valid(value: String, requirement: Requirement)
-
Invalid(value: String, requirement: Requirement, error: String)
You add an Item
to a form using the field
, required_field
, list
,
limited_list
and subform
functions. A form is a list of Item
s and
each item is parsed to a single value, and then passed to the decode
function.
You primarily only use an Item
directly when writing a form generator
function to output your form to HTML.
You can also manipulate an Item
after a form has been created, to change
things like labels, help text, etc with update
. Each item has a Config
that describes how it works, and this can be updated directly
with update_config
.
let form = make_form()
formz.update_config("name", formz.set_label(_, "Full Name"))
pub type Item(widget) {
Field(config: Config, state: InputState, widget: widget)
ListField(
config: Config,
states: List(InputState),
limit_check: LimitCheck,
widget: widget,
)
SubForm(config: Config, items: List(Item(widget)))
}
Constructors
-
Field(config: Config, state: InputState, widget: widget)
A single field that (generally speaking) corresponds to a single HTML input
-
ListField( config: Config, states: List(InputState), limit_check: LimitCheck, widget: widget, )
A single field that a consumer can submit multiple values for.
-
SubForm(config: Config, items: List(Item(widget)))
A group of fields that are added as and then parsed to a single unit.
When adding a list field to a form with limited_list
, you have to provide
a LimitCheck
function that checks the number of inputs (and associated
values) for the field. This function can either say the number of inputs
is Ok and optionally add more, or say the number of inputs was too high
and return an error. For example, you are presenting a blank form to a
consumer, and you want to show three initial fields for them to fill out,
or you want to always show one more additional field than the number of
values that have already belong to the form, etc.
There are helper functions, limit_at_least
, limit_at_most
, and
limit_between
or more generally simple_limit_check
to make a
LimitCheck
function for you. I would imagine that those will cover 99.9%
of cases and almost no one will need to write their own LimitCheck
. But
if you do, look at the source for simple_limit_check
for a better idea
of how to write one.
This function takes as its only argument, the number of fields that already
have a value. It should return either Ok
with a list of Unvalidated
InputState
items if it wants to offer more inputs to consumers of the
form, or Error
amount of inputs that were too many.
This is used multiple times… when the form is created so we know how many initial inputs to present, when data is added so we know if we need to add more inputs so users can add more items, and when the form is decoded and we are checking if too many fields have been added.
pub type LimitCheck =
fn(Int) -> Result(List(InputState), Int)
Whether an input value is required for an input field.
pub type Requirement {
Optional
Required
}
Constructors
-
Optional
-
Required
Functions
pub fn create_form(thing: a) -> Form(b, a)
Create an empty form that “decodes” directly to thing
. This is
intended to be the final return value of a chain of callbacks that adds
the form’s fields.
create_form(1)
|> decode
# -> Ok(1)
fn make_form() {
use field1 <- formz.required_field(field("field1"), definitions.text_field())
use field2 <- formz.required_field(field("field2"), definitions.text_field())
use field3 <- formz.required_field(field("field3"), definitions.text_field())
formz.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 or decoding, 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 a blank 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.
This resets the validation state of the fields that have data, so you’ll need to re-validate or decode the form after setting data.
pub fn decode(form: Form(a, b)) -> Result(b, Form(a, b))
Decode 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 decode 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 decode_then_try(
form: Form(a, b),
apply fun: fn(Form(a, b), b) -> Result(c, Form(a, b)),
) -> Result(c, Form(a, b))
Decode the form, then apply a function to the output if it was successful.
This is a very thin wrapper around decode
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.
As a reminder, parse functions will be called multiple times for each field when the form is being made, validated and parsed, and should not contain side effects. This function is the proper way to add errors to fields from functions that have side effects.
make_form()
|> data(form_data)
|> decode_then_try(fn(username, form) {
case is_username_taken(username) {
Ok(false) -> Ok(form)
Ok(true) -> field_error(form, "username", "Username is taken")
}
}
pub fn definition(
widget widget: a,
parse parse: fn(String) -> Result(b, String),
stub stub: b,
) -> Definition(a, b, Option(b))
Create a simple Definition
that is parsed as an Option
if the field
is empty. See formz_string
for more examples of making widgets and definitions.
pub fn definition_with_custom_optional(
widget widget: a,
parse parse: fn(String) -> Result(b, String),
stub stub: b,
optional_parse optional_parse: fn(
fn(String) -> Result(b, String),
String,
) -> Result(c, String),
optional_stub optional_stub: c,
) -> Definition(a, b, c)
Create a Definition
that can parse to any type if the field is optional.
This takes two functions. The first, parse
, is the “required” parse
function, which takes the raw string value, and turns it into the required
type. The second, optional_parse
, is a function that takes the normal
parse function and the raw string value, and it is supposed to check the
input string: if it is empty, return an Ok
with a value of the optional
type; and if it’s not empty use the normal parse function.
See formz_string for more examples of making widgets and definitions.
pub fn field(
config: Config,
definition: Definition(a, b, c),
next: fn(c) -> Form(a, d),
) -> Form(a, d)
Add an optional field to a form.
Ultimately whether a field is actually optional or not comes down to the details of the definition. The definition will receive the raw input string and is in charge of returning an error or an optional value.
If multiple values are submitted for this field, the last one will be used.
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 decoded to parse 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! If you need to do decoding that has side effects, you should use
decode_then_try
.
pub fn field_error(
form: Form(a, b),
name: String,
str: String,
) -> Form(a, b)
Convenience function for setting the InputState
of a field to
Invalid with a given error message.
Example
field_error(form, "username", "Username is taken")
pub fn get(
form: Form(a, b),
name: String,
) -> Result(Item(a), Nil)
Get the Item
with the
given name. If multiple items have the same name, the first one is returned.
pub fn items(form: Form(a, b)) -> List(Item(a))
Get each Item
added
to the form. Any time a field, list field, or subform are added, a Item
is created. Use this to loop through all the fields of your form and
generate HTML for them.
pub fn limit_at_least(
min: Int,
) -> fn(Int) -> Result(List(InputState), Int)
Convenience function for creating a LimitCheck
with a minimum number
of required values. This sets the maximum to 1,000,000
, effectively unlimited.
pub fn limit_at_most(
max: Int,
) -> fn(Int) -> Result(List(InputState), Int)
Convenience function for creating a LimitCheck
with a maximum number
of accepted values. This sets the minimum to 0
.
pub fn limit_between(
min: Int,
max: Int,
) -> fn(Int) -> Result(List(InputState), Int)
Convenience function for creating a LimitCheck
with a minimum and maximum
number of values.
pub fn limited_list(
limit_check: fn(Int) -> Result(List(InputState), Int),
config: Config,
is definition: Definition(a, b, c),
next next: fn(List(b)) -> Form(a, d),
) -> Form(a, d)
Add a list field to a form, but with limits on the number of values that
can be submitted. The limit_check
function is used to impose those
limits, and the limit_at_least
, limit_at_most
, and limit_between
functions help you create this function for the most likely scenarios.
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 decoded to parse 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! If you need to do decoding that has side effects, you should use
decode_then_try
.
Example
fn make_form() {
use names <- formz.limited_list(formz.limit_at_most(4), field("name"), definitions.text_field())
// names is a List(String)
formz.create_form(name)
}
pub fn list(
config: Config,
is definition: Definition(a, b, c),
next next: fn(List(b)) -> Form(a, d),
) -> Form(a, d)
Add a list field to a form, but with no limits on the number of values that
can be submitted. A list field is like a normal field except a consumer can
submit multiple values, and it will return a List
of the parsed values.
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 decoded to parse 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! If you need to do decoding that has side effects, you should use
decode_then_try
.
pub fn listfield_errors(
form: Form(a, b),
name: String,
errors: List(Result(Nil, String)),
) -> Form(a, b)
Convenience function for setting the InputState
s of a list field. This
takes a list of Results, where the Ok means the input is Valid
and
Error
means the input is Invalid
with the given error message.
This does not clear any existing errors, it will just set the errors marked
in the input list. If you want to clear errors you’ll have to use the
update
function and do it manually.
Example
listfield_errors(form, "pet_names", [Ok(Nil), Ok(Nil), Error("Must be a cat")])
pub fn make_disabled(config: Config) -> Config
Mark the form Item
as disabled. This will prevent the user from
interacting with the field. If you do this for a subform, it will
only work if the form generator renders the subform as a fieldset
.
For example, HTML does not allow you to mark inputs in a <div>
disabled as group but you can do this with a <fieldset>
.
pub fn named(name: String) -> Config
Create a field with the given name.
It uses
justin.sentence_case
to create an initial label. You can override the label with the set_label
function. I don’t know if this is very english-centric, so let me know if
this is a bad experience in other languages and I’ll consider
something else.
field("name")
|> set_label("Full Name")
pub fn required_field(
config: Config,
is definition: Definition(a, b, c),
next next: fn(b) -> Form(a, d),
) -> Form(a, d)
Add a required field to a form.
Ultimately whether a field is actually required or not comes down to the details of the definition. The definition will receive the raw input string and is in charge of returning an error or a value.
If multiple values are submitted for this field, the last one will be used.
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 decoded to parse 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! If you need to do decoding that has side effects, you should use
decode_then_try
.
pub fn set_disabled(config: Config, disabled: Bool) -> Config
Set the disabled
flag directly.
pub fn set_help_text(config: Config, help_text: String) -> Config
Additional instructions or help text to display to the user about the field.
pub fn set_label(config: Config, label: String) -> Config
Set the label of the field. This is the primary text that will be displayed to the user about the field.
pub fn set_name(config: Config, name: String) -> Config
Set the name of the field. This is the key that will be used for the data.
pub fn simple_limit_check(
min: Int,
max: Int,
extra: Int,
) -> fn(Int) -> Result(List(InputState), Int)
Convenience function for creating a LimitCheck
function. This takes
the minimum number of required values, the maximum number of allowed values,
and the number of “extra” blank inputs that should be offered to the user
for filling out.
If “extra” is 0, (say, to manage blank fields via javascript), then this will show 1 blank field initially.
pub fn subform(
config: Config,
form: Form(a, b),
next: 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 decoded to parse 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! If you need to do decoding that has side effects, you should use
decode_then_try
.
pub fn update(
form: Form(a, b),
name: String,
fun: fn(Item(a)) -> Item(a),
) -> Form(a, b)
Update the Item
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_config(
form: Form(a, b),
name: String,
fun: fn(Config) -> Config,
) -> Form(a, b)
Update the Field
details with
the given name using the provided function. If multiple items have the same
name, it will be called on all of them. If no items have the given name,
or an item with the given name exists but isn’t a Field
, this function
will do nothing.
let form = make_form()
update_field(form, "name", field.set_label(_, "Full Name"))
pub fn validate(
form: Form(a, b),
names: List(String),
) -> Form(a, b)
Validate specific fields of the form. This is similar to decode
, 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. For example, 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 decode
, 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.
pub fn verify(
def: Definition(a, b, c),
fun: fn(b) -> Result(b, String),
) -> Definition(a, b, c)
Chain additional validation onto the parse
function. This is
useful if you don’t need to change the returned type, but might have
additional constraints. Like say, requiring a String
to be at least
a certain length, or that an Int must be positive.
Example
field
|> validate(fn(i) {
case i > 0 {
True -> Ok(i)
False -> Error("must be positive")
}
}),