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 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 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.

Search Document