···5566A Gleam library for parsing and generating accessible HTML forms.
7788+HTML forms rendered in the browser and the data they are parsed into are
99+intrinsically linked. Treating the markup and the parsing as two separate
1010+problems to solve is inconvenient and leads to bugs. This library aims
1111+to make that link explicit and easy to manage, while making it really easy
1212+to make accessible forms.
1313+1414+Note: This library is not particularly well-suited for generating one-off
1515+forms, but is more intended for use in projects where you have a few forms to
1616+manage, and would like to keep the form markup and parsing logic in sync. It
1717+takes some amount of effort to make an actual form generator with markup and
1818+styles, and that might not be worth it for a one-off form. That said, a simple
1919+form generator is provided if you aren't opinionated about your markup.
2020+2121+```sh
2222+gleam add formz@0.1
2323+```
2424+2525+## Creating a form
2626+2727+A `formz` form is a list of fields and a decoder function. You construct the
2828+decoder function as fields are added:
2929+830```gleam
3131+import formz
932import formz/field.{field}
1010-import formz/formz_builder as formz
1133import formz_string/definitions
12341335pub fn make_form() {
1414- formz.decodes(fn(username) { fn(password) { #(username, password) } })
1515- |> formz.require(field("username"), definitions.text_field())
1616- |> formz.require(field("password"), definitions.password_field())
3636+ use username <- formz.field(
3737+ formz.named("username"),
3838+ formz.required(definitions.text_field()),
3939+ )
4040+ use password <- formz.field(
4141+ formz.named("password"),
4242+ formz.required(definitions.password_field()),
4343+ )
4444+4545+ formz.create_form(#(username, password))
4646+}
4747+```
4848+4949+## Creating fields
5050+5151+There are two arguments to adding a field to a form (seen above):
5252+5353+1. A [Field](https://hexdocs.pm/formz/formz/field.html), which holds specific,
5454+ unique details about the field, such as its name, label, help text, disabled
5555+ state, etc.
5656+2. A [Definition](https://hexdocs.pm/formz/formz/definition.html), which
5757+ says (A) how to generate the HTML input element for the field, and (B) how
5858+ to parse the data from the field. These definitions are reusable and can be
5959+ shared across fields, forms and projects.
6060+6161+### Field details
6262+6363+```gleam
6464+// name is required, the other details are optional
6565+field(named: "username")
6666+|> field.set_label("Username")
6767+|> field.set_help_text("Only alphanumeric characters are allowed.")
6868+```
6969+7070+```gleam
7171+field(named: "userid") |> field.make_hidden |> field.set_raw_value("42")
7272+```
7373+7474+### Field definition
7575+7676+[Defintions](https://hexdocs.pm/formz/formz/definition.html) are the heavy
7777+compared to the lightness of fields; they take a bit more work to make as they
7878+are intended to be more reusable.
7979+8080+The first role of a `Defintion` is to generate the HTML widget for the field.
8181+This library is format-agnostic and you can generate HTML widgets as raw
8282+strings, Lustre elements, Nakai nodes, something else, etc. There are
8383+currently three `formz` libraries that provide common field definitions for the
8484+most common HTML formats.
8585+8686+- [formz_string](https://hexdocs.pm/formz_string/)
8787+- [formz_nakai](https://hexdocs.pm/formz_nakai/)
8888+- [formz_lustre](https://hexdocs.pm/formz_lustre/) (untested in a browser, I've only done this server side)
8989+9090+The second role of a `Definition` is to parse the data from the field. There
9191+are a two parts to this, as how you parse a field's value depends on if it is
9292+optional or required. Not all scenarios can be cookie-cutter placed into an
9393+`Option`. So you need to provide two parse functions, one for when a field is
9494+required, and a second for when it's optional.
9595+9696+```gleam
9797+/// you won't often need to do this directly (I think??). The idea is that
9898+/// there'd be libs with the definitions you need.
9999+100100+import formz/definition.{Definition}
101101+import formz/field
102102+import formz/validation
103103+import formz/widget
104104+import lustre/attribute
105105+import lustre/element
106106+import lustre/element/html
107107+108108+fn password_widget(
109109+ field: field.Field,
110110+ args: widget.Args,
111111+) -> element.Element(msg) {
112112+ html.input([
113113+ attribute.type_("password"),
114114+ attribute.name(field.name),
115115+ attribute.id(args.id),
116116+ attribute.attribute("aria-labelledby", field.label),
117117+ ])
118118+}
119119+120120+pub fn password_field() {
121121+ Definition(
122122+ widget: password_widget,
123123+ parse: validation.string,
124124+ optional_parse: fn(parse, str) {
125125+ case str {
126126+ "" -> Ok(option.None)
127127+ _ -> parse(str)
128128+ }
129129+ },
130130+ // We need to have a stub value for each parser. The stubs are used when
131131+ // building the decoder and parse functions for the form.
132132+ stub: "",
133133+ optional_stub: option.None,
134134+ )
135135+}
136136+```
137137+138138+139139+140140+## Generating HTML for a form
141141+142142+Generally speaking, the idea with a `formz` form is that you are not going
143143+to generate the HTML for each field individually, but rather, you'd use
144144+a function to loop through each field, generating semantic, accessible
145145+markup for each one.
146146+147147+The specifics of how you would do this are going
148148+to vary greatly for each project and its styling/markup needs.
149149+150150+However, the three `formz_*` libraries mentioned above all provide a
151151+simple form generator function that you can use as is, or as a starting
152152+point for your own. `formz` is BYOS, Bring Your Own Stylesheet, so the
153153+built-in form generators come unstyled. If there is interest, I could add
154154+a super simple CSS file to get the ball rolling and make the default
155155+forms easier to use out of the box.
156156+157157+That said, you can also create the form HTML yourself, directly for each field.
158158+There's [an example](https://github.com/bentomas/formz/blob/main/formz_demo/src/formz_demo/examples/custom_output.gleam)
159159+in the demo project showing how to do this.
160160+161161+### Generating form HTML using the `formz_string` library
162162+163163+The built-in form generators leave it as homework to add the form tags and
164164+submit buttons.
165165+166166+```gleam
167167+import formz_string/simple
168168+169169+pub fn show_form(form) -> String {
170170+ "<form method=\"post\">"
171171+ <> simple.generate(form)
172172+ <> "<p><button type\"submit\">Submit</button></p>"
173173+ <> "</form>"
17174}
18175```
191762020-See the [main package](https://github.com/bentomas/formz/tree/main/formz) for more details
177177+178178+## Parsing form data
179179+180180+You can parse a `formz` form with a tuple of values and names, typically from
181181+a POST request. Here we parse in a `wisp` handler:
182182+183183+```gleam
184184+pub fn handle_form_submission(req: Request) -> Response {
185185+ use formdata <- wisp.require_form(req)
186186+187187+ let result = make_form()
188188+ |> formz.data(formdata.values)
189189+ |> formz.parse
190190+191191+ case result {
192192+ Ok(credentials) -> {
193193+ let #(username, password) = credentials
194194+ wisp.ok()
195195+ |> wisp.html_body(string_builder.from_string("Hello "<>username<>"!"))
196196+ }
197197+ Error(form_with_errors) -> {
198198+ show_form(form_with_errors)
199199+ }
200200+ }
201201+}
202202+```
203203+204204+However, often you want to parse a form, and then... you know... act on that
205205+data, and in doing so you might discover more errors for the form. In this
206206+situation you can use `decode_then_try`:
207207+208208+```gleam
209209+pub fn handle_form_submission(req: Request) -> Response {
210210+ use formdata <- wisp.require_form(req)
211211+212212+ let result = make_form()
213213+ |> formz.data(formdata.values)
214214+ |> formz.decode_then_try(fn(form, credentials) {
215215+ case credentials {
216216+ #("admin" as username, "l33t") -> Ok(username)
217217+ #("admin", _) ->
218218+ Error(form |> formz.set_field_error("password", "Wrong password"))
219219+ _ ->
220220+ Error(form |> formz.set_field_error("username", "Unknown username"))
221221+ }
222222+ })
223223+224224+ case result {
225225+ Ok(username) -> {
226226+ wisp.ok()
227227+ |> wisp.html_body(string_builder.from_string("Hello " <> username <> "!"))
228228+ }
229229+ Error(form_with_errors) -> {
230230+ show_form(form_with_errors)
231231+ }
232232+ }
233233+}
234234+```
235235+236236+## See it in action
237237+238238+There is a [demo wisp app](https://github.com/bentomas/formz/tree/main/formz_demo)
239239+showing a few interactive examples of how `formz` works in the repo.
-11
formz/src/formz/field.gleam
···3737 /// the value or submitting a different value via other means, so (presently)
3838 /// this doesn't mean the value cannot be tampered with.
3939 disabled: Bool,
4040- /// Whether the field is hidden. A hidden field is not displayed in the browser.
4141- hidden: Bool,
4240 )
4341}
4442···5553 label: justin.sentence_case(name),
5654 help_text: "",
5755 disabled: False,
5858- hidden: False,
5956 )
6057}
6158···69667067pub fn set_help_text(field: Field, help_text: String) -> Field {
7168 Field(..field, help_text:)
7272-}
7373-7474-pub fn set_hidden(field: Field, hidden: Bool) -> Field {
7575- Field(..field, hidden:)
7676-}
7777-7878-pub fn make_hidden(field: Field) -> Field {
7979- set_hidden(field, True)
8069}
81708271pub fn set_disabled(field: Field, disabled: Bool) -> Field {
···6677import formz
88import formz/validation
99+import formz_string/widget
910import formz_string/widgets
1011import gleam/int
1112import gleam/list
1313+import gleam/option
12141315/// Create a basic form input. Parsed as a String.
1414-pub fn text_field() {
1616+pub fn text_field() -> formz.Definition(widget.Widget, String, String) {
1517 formz.definition_with_custom_optional(
1618 widgets.input_widget("text"),
1719 validation.non_empty_string,
···28302931/// Create an email form input. Parsed as a String but must
3032/// look like an email address, i.e. the string has an `@`.
3131-pub fn email_field() {
3333+pub fn email_field() -> formz.Definition(
3434+ widget.Widget,
3535+ String,
3636+ option.Option(String),
3737+) {
3238 formz.definition(widgets.input_widget("email"), validation.email, "")
3339}
34403541/// Create a whole number form input. Parsed as an Int.
3636-pub fn integer_field() {
4242+pub fn integer_field() -> formz.Definition(
4343+ widget.Widget,
4444+ Int,
4545+ option.Option(Int),
4646+) {
3747 formz.definition(widgets.number_widget(""), validation.int, 0)
3848}
39494050/// Create a number form input. Parsed as a Float.
4141-pub fn number_field() {
5151+pub fn number_field() -> formz.Definition(
5252+ widget.Widget,
5353+ Float,
5454+ option.Option(Float),
5555+) {
4256 formz.definition(widgets.number_widget("0.01"), validation.number, 0.0)
4357}
44584559/// Create a checkbox form input. Parsed as a `Bool`. If required, the parsed
4660/// `Bool` must be `True`.
4747-pub fn boolean_field() {
6161+pub fn boolean_field() -> formz.Definition(widget.Widget, Bool, Bool) {
4862 formz.definition_with_custom_optional(
4963 widget: widgets.checkbox_widget(),
5064 parse: validation.on,
···6074}
61756276/// Create a password form input, which hides the input value. Parsed as a String.
6363-pub fn password_field() {
7777+pub fn password_field() -> formz.Definition(
7878+ widget.Widget,
7979+ String,
8080+ option.Option(String),
8181+) {
6482 formz.definition(widgets.password_widget(), validation.non_empty_string, "")
6583}
6684···7391/// Because of how you build `formz` forms, you need to provide a stub of
7492/// the value type. Is this annoying? Would it be more or less annoying if I
7593/// required a non-empty list for the variants instead? I'm not sure. Let me know!
7676-pub fn choices_field(variants: List(#(String, enum)), stub stub: enum) {
9494+pub fn choices_field(
9595+ variants: List(#(String, enum)),
9696+ stub stub: enum,
9797+) -> formz.Definition(widget.Widget, enum, option.Option(enum)) {
7798 let keys_indexed =
7899 variants
79100 |> list.index_map(fn(t, i) { #(t.0, int.to_string(i)) })
···8911090111/// Creates a `<select>` input from a list of strings. Validates that the parsed
91112/// value is one of the strings in the list.
9292-pub fn list_field(variants: List(String)) {
113113+pub fn list_field(
114114+ variants: List(String),
115115+) -> formz.Definition(widget.Widget, String, option.Option(String)) {
93116 let labels_and_values = list.map(variants, fn(s) { #(s, s) })
94117 choices_field(labels_and_values, "")
95118}
119119+120120+pub fn make_hidden(
121121+ def: formz.Definition(widget.Widget, a, b),
122122+) -> formz.Definition(widget.Widget, a, b) {
123123+ def |> formz.widget(widgets.hidden_widget())
124124+}