formz

Package Version Hex Docs

A Gleam library for parsing and generating accessible HTML forms.

Note: This library currently has two non-interoperable ways to define forms, one using the builder pattern, and one using a series of use calls like with the toy or decode/zero packages. After gathering some feedback, only one of them will be kept.

HTML forms rendered in the browser and the data they are parsed into are intrinsically linked. Treating the markup and the parsing as two separate problems to solve is inconvenient and leads to bugs. This library aims to make that link explicit and easy to manage, while making it really easy to make accessible forms.

gleam add formz@0.1

Creating a form

A formz form is a list of fields and a decoder function.

builder pattern

With the builder pattern, you add the fields and then explicitly specify the decoder function…

import formz/field.{field}
import formz/formz_builder as formz
import formz_string/definitions

pub fn make_form() {
  formz.new()
  |> formz.add(field("username"), definitions.text_field())
  |> formz.add(field("password"), definitions.password_field())
  |> formz.decodes(fn(username) { fn(password) { #(username, password) } })
}

use/callbacks pattern

With the use/callbakcks pattern, you create the decoder function as you add the fields…

import formz/field.{field}
import formz/formz_use as formz
import formz_string/definitions

pub fn make_form() {
  use username <- formz.with(field("username"), definitions.text_field())
  use password <- formz.with(field("password"), definitions.password_field())

  formz.create_form(#(username, password))
}

Creating fields

There are two parts to adding a field to a form (seen above):

  1. Specific, unique details about the field, such as its name, label, help text, disabled/enabled state, etc.
  2. A field “definition” which says (A) how to generate the HTML “widget” for the field, and (B) how to parse, or “transform” the data from the field. These definitions are reusable and can be shared between fields, forms and projects.

Field details

// name is required, the other details are optional
field(named: "username")
|> field.set_label("Username")
|> field.set_help_text("Only alphanumeric characters are allowed.")
field(named: "userid") |> field.make_hidden |> field.set_value("42")

Field definition

This library is format-agnostic and you can generate HTML widgets as raw strings, Lustre elements, Nakai nodes, something else, etc. There are currently three formz libraries that provide common field definitions in different formats.

There is also a simple validation module with some examples, and to cover some basics.

/// you won't often need to do this directly (I think??).  The idea is that
/// there'd be libs with the definitions you need.

import formz/definition.{Definition}
import formz/field
import formz/validation
import formz/widget
import lustre/attribute
import lustre/element
import lustre/element/html

fn password_widget(
  field: field.Field,
  args: widget.Args,
) -> element.Element(msg) {
  html.input([
    attribute.type_("password"),
    attribute.name(field.name),
    attribute.id(args.id),
    attribute.attribute("aria-labelledby", field.label),
  ])
}

pub fn password_field() {
  Definition(password_widget, validation.string, "")
}

Generating HTML for a form

Generally speaking, the idea with a formz form is that you are not going to generate the HTML for each field individually, but rather, you’d use a function to loop through each field, generating semantic, accessible markup for each one.

The specifics of how you would do this are going to vary greatly for each project and its styling/markup needs.

However, the three formz_* libraries mentioned above all provide a simple form generator function that you can use as is, or as a starting point for your own. formz is BYOS, Bring Your Own Stylesheet, so the built-in form generators come unstyled. If there is interest, I could add a super simple CSS file to get the ball rolling and make the default forms easier to use out of the box.

That said, you can create the form HTML yourself, directly for each field. There’s an example in the demo project showing how to do this.

Generating form HTML using the formz_string library

The built-in form generators all leave it as homework to add the form tags and submit buttons.

import formz_string/simple

pub fn show_form(form) -> String {
  "<form method=\"post\">"
  <> simple.generate_form(form)
  <> "<p><button type\"submit\">Submit</button></p>"
  <> "</form>"
}

Parsing form data

You can parse a formz form with a tuple of values and names, typically from a POST request. Here we parse in a wisp handler:

pub fn handle_form_submission(req: Request) -> Response {
  use formdata <- wisp.require_form(req)

  let result = make_form()
  |> formz.data(formdata.values)
  |> formz.parse

  case result {
    Ok(credentials) -> {
      let #(username, password) = credentials
      wisp.ok()
      |> wisp.html_body(string_builder.from_string("Hello "<>username<>"!"))
    }
    Error(form_with_errors) -> {
      show_form(form_with_errors)
    }
  }
}

However, often you want to parse a form, and then… you know… act on that data, and in doing so you might discover more errors for the form. In this situation you can use parse_then_try:

pub fn handle_form_submission(req: Request) -> Response {
  use formdata <- wisp.require_form(req)

  let result = make_form()
  |> formz.data(formdata.values)
  |> formz.parse_then_try(fn(form, credentials) {
    case credentials {
      #("admin" as username, "l33t") -> Ok(username)
      #("admin", _) ->
        form
        |> formz.update_field("password", field.set_error(_, "Wrong password"))
        |> Error
      _ ->
        form
        |> formz.update_field("username", field.set_error(_, "Wrong username"))
        |> Error
    }
  })

  case result {
    Ok(username) -> {
      wisp.ok()
      |> wisp.html_body(string_builder.from_string("Hello " <> username <> "!"))
    }
    Error(form_with_errors) -> {
      show_form(form_with_errors)
    }
  }
}

See it in action

There is a demo wisp app showing a few interactive examples of how formz works in the repo.

Search Document