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 Definitions 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 Items 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 InputStates 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")
    }
  }),
pub fn widget(
  def: Definition(a, b, c),
  widget: a,
) -> Definition(a, b, c)

Update the widget of a definition.

Search Document