formal/form

Types

pub type FieldError {
  MustBePresent
  MustBeInt
  MustBeFloat
  MustBeEmail
  MustBePhoneNumber
  MustBeUrl
  MustBeDate
  MustBeTime
  MustBeDateTime
  MustBeColour
  MustBeStringLengthMoreThan(limit: Int)
  MustBeStringLengthLessThan(limit: Int)
  MustBeIntMoreThan(limit: Int)
  MustBeIntLessThan(limit: Int)
  MustBeFloatMoreThan(limit: Float)
  MustBeFloatLessThan(limit: Float)
  MustBeAccepted
  MustConfirm
  MustBeUnique
  CustomError(message: String)
}

Constructors

  • MustBePresent
  • MustBeInt
  • MustBeFloat
  • MustBeEmail
  • MustBePhoneNumber
  • MustBeUrl
  • MustBeDate
  • MustBeTime
  • MustBeDateTime
  • MustBeColour
  • MustBeStringLengthMoreThan(limit: Int)
  • MustBeStringLengthLessThan(limit: Int)
  • MustBeIntMoreThan(limit: Int)
  • MustBeIntLessThan(limit: Int)
  • MustBeFloatMoreThan(limit: Float)
  • MustBeFloatLessThan(limit: Float)
  • MustBeAccepted
  • MustConfirm

    For confirmation of passwords, etc. Must match the first field.

  • MustBeUnique

    For values that must be unique. e.g. user email addresses.

  • CustomError(message: String)

A form! Created from a Schema with the new function.

Supply values to a form with the add_* functions and then pass it to the run function to get either the resulting value or any errors.

Use the language function to supply a new translation function to change the language of the error messages returned by the field_error_text function. The default language is en_gb English.

pub opaque type Form(model)

A parser extracts a value from from values, converting it to a desired type and optionally validating the value. Parsers are used with the field function.

See the parse_* and check_* functions for more information.

Functions that start with parse_* are short-circuiting, so any parser functions that come afterwrads will not be run. For example, given this code:

form.parse_int |> form.check_int_more_than(0)

If the input is not an int then parse_int will fail, causing check_int_more_than not to run, so the errors will be [MustBeInt].

pub opaque type Parser(value)

A description of how to decode from typed value from form data. This can be used to create a new form object using the new function.

pub opaque type Schema(model)

Values

pub fn add_error(
  form: Form(model),
  name: String,
  error: FieldError,
) -> Form(model)

Add an error to one of the fields of the form.

This function may be useful if you have some additional validation that runs outside of the form schema and you want to surface the error messages to the user via the form.

pub fn add_int(
  form: Form(model),
  field: String,
  value: Int,
) -> Form(model)

Add an int value to the form.

You may want to use this to pre-fill a form with some already-saved values for the user to edit.

pub fn add_string(
  form: Form(model),
  field: String,
  value: String,
) -> Form(model)

Add a string value to the form.

You may want to use this to pre-fill a form with some already-saved values for the user to edit.

pub fn add_values(
  form: Form(a),
  values: List(#(String, String)),
) -> Form(a)

Add multiple values to a form. This function is useful for adding values from a HTTP request form body sent to your server, or from a HTML form element in your browser-based application.

Example

use formdata <- wisp.require_form(request)
let form <- new_user_form() |> form.add_values(formdata.values)
pub fn all_errors(
  form: Form(model),
) -> List(#(String, List(FieldError)))

Get all the errors within a form.

If run or add_error have not been called then there will be no errors.

pub fn all_values(form: Form(model)) -> List(#(String, String))

Get all the values within a form.

pub fn check(
  parser: Parser(b),
  checker: fn(b) -> Result(b, String),
) -> Parser(b)

Add a custom check to the parser.

Examples

let must_not_be_rude = fn(text) {
  case contains_swear_word(text) {
    True -> Error("must not be rude")
    False -> Ok(text)
  }
}
let schema = {
  use name <- form.field("name", {
    form.parse_string
    |> form.check(must_not_be_rude)
  })
  form.success(Profile(name:))
}

Internationalisation

If your application supports multiple languages you will need to ensure the error string returned by your checker function is already in the desired language.

pub fn check_accepted(parser: Parser(Bool)) -> Parser(Bool)

Ensure that a bool is True.

Example

let schema = {
  use discount <- form.field("terms-and-conditions", {
    form.parse_checkbox
    |> form.check_accepted
  })
  form.success(Product(discount:))
}
pub fn check_confirms(parser: Parser(t), other: t) -> Parser(t)

Ensure that a field equals some other value. Useful for password confirmation.

Example

let schema = {
  use password <- form.field("password-confirmation", {
    form.parse_string
    |> form.check_string_length_more_than(8)
  })
  use _ <- form.field("password-confirmation", {
    form.parse_string
    |> form.check_confirms(password)
  })
  form.success(User(password:))
}
pub fn check_float_less_than(
  parser: Parser(Float),
  limit: Float,
) -> Parser(Float)

Ensure that a float is less than a specified limit.

Example

let schema = {
  use discount <- form.field("discount", {
    form.parse_float
    |> form.check_float_less_than(100.0)
  })
  form.success(Product(discount:))
}
pub fn check_float_more_than(
  parser: Parser(Float),
  limit: Float,
) -> Parser(Float)

Ensure that a float is more than a specified limit.

Example

let schema = {
  use price <- form.field("price", {
    form.parse_float
    |> form.check_float_more_than(0.0)
  })
  form.success(Product(price:))
}
pub fn check_int_less_than(
  parser: Parser(Int),
  limit: Int,
) -> Parser(Int)

Ensure that an int is less than a specified limit.

Example

let schema = {
  use age <- form.field("age", {
    form.parse_int
    |> form.check_int_less_than(150)
  })
  form.success(Person(age:))
}
pub fn check_int_more_than(
  parser: Parser(Int),
  limit: Int,
) -> Parser(Int)

Ensure that an int is more than a specified limit.

Example

let schema = {
  use age <- form.field("age", {
    form.parse_int
    |> form.check_int_more_than(0)
  })
  form.success(Person(age:))
}
pub fn check_not_empty(parser: Parser(String)) -> Parser(String)

Ensure that the string value is not an empty string.

Example

let schema = {
  use tags <- form.field("tag", {
    form.parse_string
    |> form.check_not_empty
  })
  form.success(Article(tags:))
}
pub fn check_string_length_less_than(
  parser: Parser(String),
  limit: Int,
) -> Parser(String)

Ensure that a string is less than a specified length.

Example

let schema = {
  use username <- form.field("username", {
    form.parse_string
    |> form.check_string_length_less_than(20)
  })
  form.success(User(username:))
}
pub fn check_string_length_more_than(
  parser: Parser(String),
  limit: Int,
) -> Parser(String)

Ensure that a string is more than a specified length.

Example

let schema = {
  use password <- form.field("password", {
    form.parse_string
    |> form.check_string_length_more_than(8)
  })
  form.success(User(password:))
}
pub fn en_gb(error: FieldError) -> String

Translates FieldErrors into strings suitable for showing to the user.

Examples

assert en_us(MustBeColour) == "must be a hex colour code"
pub fn en_us(error: FieldError) -> String

Translates FieldErrors into strings suitable for showing to the user.

The same as en_gb, but with Americanised spelling of the word “color”.

Examples

assert en_us(MustBeColour) == "must be a hex color code"
pub fn field(
  name: String,
  parser: Parser(value),
  continuation: fn(value) -> Schema(model),
) -> Schema(model)

Add a new parser to the form for a given form field name.

pub fn field_error_messages(
  form: Form(model),
  name: String,
) -> List(String)

Get the error messages for a field, if there are any.

The text is formatted using the translater function given with the langauge function. The default translater is en_gb.

pub fn field_errors(
  form: Form(model),
  name: String,
) -> List(FieldError)

Get all the form.

If the run function or the add_error function have not been called then the form is clean and won’t have any errors yet.

pub fn field_value(form: Form(model), name: String) -> String

Get the first values for a given form field.

Examples

let form = form |> form.add_int("one", 100)
assert form.field_value(form, "one") == "100"
assert form.field_value(form, "two") == ""
pub fn field_values(
  form: Form(model),
  name: String,
) -> List(String)

Get all the values for a given form field.

Examples

let form = form
  |> form.add_int("one", 100)
  |> form.add_string("one", "Hello")
  |> form.add_string("two", "Hi!")
assert form.field_values(form, "one") == ["Hello", "100"]
pub fn language(
  form: Form(model),
  translator: fn(FieldError) -> String,
) -> Form(model)

Supply a transation function that will be used when converting any FieldErrors to text that can be presented to the user.

Build-in languages:

  • en_gb
  • en_us

These functions are named using the IETF language tag for the langauge they translate to.

If no language is supplied then en_gb is used by default.

pub fn map(
  parser: Parser(t1),
  mapper: fn(t1) -> t2,
) -> Parser(t2)

Convert the parsed value into another, similar to list.map or result.map.

pub fn new(schema: Schema(model)) -> Form(model)

Create a new form from a schema.

Add values to the form with the add_* functions and use run to get either the final value or some errors from the form and values.

Example

let schema = {
  use name <- form.field("name", {
    form.parse_string
    |> form.check_not_empty
  })
  use name <- form.field("age", form.parse_int)
  form.success(Person(name:, age:))
}
let form = form.new(schema)
pub fn parse(
  parser: fn(List(String)) -> Result(t, #(t, String)),
) -> Parser(t)

Create a custom parser for any type.

If the parser function fails it must return two values:

  • A default “zero” value of the expected type. This will be used to run the remaining parser code, and then will be discarded.
  • An error message string to present to the user.

Examples

form.parse(fn(input) {
  case input {
    ["Squirtle", ..] -> Ok(Squirtle)
    ["Bulbasaur", ..] -> Ok(Bulbasaur)
    ["Charmander", ..] -> Ok(Charmander)
    _ -> Error(#(Squirtle, "must be a starter Pokémon"))
  }
})

Internationalisation

If your application supports multiple languages you will need to ensure the error string returned by your parser function is already in the desired language.

pub const parse_checkbox: Parser(Bool)

Parse a bool value from a checkbox type input.

No value False, while any value (including empty string) counts as True. A checked checkbox input with no explicitly set value has the value "on", which is True. Unchecked checkbox inputs send no value when the form is submitted, they are absent from the sent payload.

Example

let schema = {
  use agreed <- form.field("terms-and-conditions", {
    form.parse_checkbox
    |> form.check_accepted
  })
  form.success(Signup(agreed:))
}
pub const parse_colour: Parser(String)

A parser for colour values.

Parses color strings in HTML hex format (e.g., “#FF0000”, “#00ff00”) and returns the hex color string.

Returns a MustBeColour error if the input is not a valid hex color.

Example

let schema = {
  use background_color <- form.field("background_color", form.parse_colour)
  form.success(Theme(background_color:))
}
pub const parse_date: Parser(calendar.Date)

A parser for calendar dates.

Parses dates in YYYY-MM-DD format and returns a calendar.Date value. Returns a MustBeDate error if the input cannot be parsed as a valid date.

Example

let schema = {
  use birth_date <- form.field("birth_date", form.parse_date)
  form.success(Person(birth_date:))
}
pub const parse_date_time: Parser(
  #(calendar.Date, calendar.TimeOfDay),
)

A parser for datetime values.

Parses datetime strings in HTML datetime-local format (e.g. “2023-12-25T14:30” or “2023-12-25T14:30:00”) and returns a tuple of (calendar.Date, calendar.TimeOfDay).

Returns a MustBeDateTime error if the input cannot be parsed as a valid datetime.

Example

let schema = {
  use created_at <- form.field("created_at", form.parse_date_time)
  form.success(Event(created_at:))
}
pub const parse_email: Parser(String)

A parser that validates email addresses.

Performs basic email validation by checking for the presence of an “@” symbol. Returns a MustBeEmail error if the input is not a valid email format.

Example

let schema = {
  use email <- form.field("email", form.parse_email)
  form.success(Person(email:))
}
pub const parse_float: Parser(Float)

A parser for floating point numbers.

Returns a MustBeFloat error if the input cannot be parsed as a float.

Example

let schema = {
  use price <- form.field("price", {
    form.parse_float
    |> form.check_float_more_than(0.0)
  })
  form.success(Product(price:))
}
pub const parse_int: Parser(Int)

A parser for a whole number.

Returns a MustBeInt error if the input cannot be parsed as a valid int.

Example

let schema = {
  use age <- form.field("age", {
    form.parse_int
    |> form.check_int_more_than(0)
  })
  form.success(Person(age:))
}
pub fn parse_list(parser: Parser(output)) -> Parser(List(output))

A parser that applies another parser to each input value in a list.

Takes a parser for a single value and returns a parser that can handle multiple values of the same type. This is useful for form fields that can have multiple values, such as checkboxes, multi-selects, or just repeated inputs of other types.

Example

let schema = {
  use tags <- form.field("tag", form.parse_list(form.parse_string))
  form.success(Article(tags:))
}

This would parse multiple “tag” fields into a list of strings.

pub fn parse_optional(
  parser: Parser(output),
) -> Parser(option.Option(output))

A parser that applies another parser if there is a non-empty-string input value for the field.

Example

let schema = {
  use age <- form.field("tag", form.parse_optional(form.parse_int))
  form.success(Person(age:))
}

This would parse an int if the form field has text in it, returning None otherwise.

pub const parse_phone_number: Parser(String)

A parser for phone numbers.

Phone numbers are checked with these rules:

  • Must be between 7 and 15 characters after removing formatting
  • Must contain only digits after removing formatting characters
  • + may optionally be the first character.
  • -, , (, and ) are permitted, but are removed.

Returns a MustBePhoneNumber error if the input doesn’t satisfy these rules.

Example

let schema = {
  use phone <- form.field("phone", form.parse_phone_number)
  form.success(Person(phone:))
}
pub const parse_string: Parser(String)

Parse a string value. This parser can never fail!

Example

let schema = {
  use name <- form.field("name", {
    form.parse_string
    |> form.check_not_empty
  })
  form.success(Person(name:))
}
pub const parse_time: Parser(calendar.TimeOfDay)

A parser for time values.

Parses times in HH:MM:SS or HH:MM format and returns a calendar.TimeOfDay value. Returns a MustBeTime error if the input cannot be parsed as a valid time.

Example

let schema = {
  use start_time <- form.field("start_time", form.parse_time)
  form.success(Event(start_time:))
}
pub const parse_url: Parser(uri.Uri)

A parser for URLs.

Uses the gleam/uri module to parse and validate URLs. Returns a MustBeUrl error if the input cannot be parsed as a valid URI.

Example

let schema = {
  use website <- form.field("website", form.parse_url)
  form.success(Company(website:))
}
pub fn run(form: Form(model)) -> Result(model, Form(model))

Run a form, returning either the successfully parsed value if there are no errors, or a new instance of the form with the errors added to the fields.

pub fn set_values(
  form: Form(a),
  values: List(#(String, String)),
) -> Form(a)

Replace any existing values of a form with new values. This function is useful for adding values from a HTTP request form body sent to your server, or from a HTML form element in your browser-based application.

Example

use formdata <- wisp.require_form(request)
let form <- new_user_form() |> form.set_values(formdata.values)
pub fn success(value: model) -> Schema(model)

Finalise a parser, having successfully parsed a value.

Search Document