this repo has no description
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

remove `hidden` from `Field`. leave it up to definitions to handle

+595 -403
+224 -5
README.md
··· 5 5 6 6 A Gleam library for parsing and generating accessible HTML forms. 7 7 8 + HTML forms rendered in the browser and the data they are parsed into are 9 + intrinsically linked. Treating the markup and the parsing as two separate 10 + problems to solve is inconvenient and leads to bugs. This library aims 11 + to make that link explicit and easy to manage, while making it really easy 12 + to make accessible forms. 13 + 14 + Note: This library is not particularly well-suited for generating one-off 15 + forms, but is more intended for use in projects where you have a few forms to 16 + manage, and would like to keep the form markup and parsing logic in sync. It 17 + takes some amount of effort to make an actual form generator with markup and 18 + styles, and that might not be worth it for a one-off form. That said, a simple 19 + form generator is provided if you aren't opinionated about your markup. 20 + 21 + ```sh 22 + gleam add formz@0.1 23 + ``` 24 + 25 + ## Creating a form 26 + 27 + A `formz` form is a list of fields and a decoder function. You construct the 28 + decoder function as fields are added: 29 + 8 30 ```gleam 31 + import formz 9 32 import formz/field.{field} 10 - import formz/formz_builder as formz 11 33 import formz_string/definitions 12 34 13 35 pub fn make_form() { 14 - formz.decodes(fn(username) { fn(password) { #(username, password) } }) 15 - |> formz.require(field("username"), definitions.text_field()) 16 - |> formz.require(field("password"), definitions.password_field()) 36 + use username <- formz.field( 37 + formz.named("username"), 38 + formz.required(definitions.text_field()), 39 + ) 40 + use password <- formz.field( 41 + formz.named("password"), 42 + formz.required(definitions.password_field()), 43 + ) 44 + 45 + formz.create_form(#(username, password)) 46 + } 47 + ``` 48 + 49 + ## Creating fields 50 + 51 + There are two arguments to adding a field to a form (seen above): 52 + 53 + 1. A [Field](https://hexdocs.pm/formz/formz/field.html), which holds specific, 54 + unique details about the field, such as its name, label, help text, disabled 55 + state, etc. 56 + 2. A [Definition](https://hexdocs.pm/formz/formz/definition.html), which 57 + says (A) how to generate the HTML input element for the field, and (B) how 58 + to parse the data from the field. These definitions are reusable and can be 59 + shared across fields, forms and projects. 60 + 61 + ### Field details 62 + 63 + ```gleam 64 + // name is required, the other details are optional 65 + field(named: "username") 66 + |> field.set_label("Username") 67 + |> field.set_help_text("Only alphanumeric characters are allowed.") 68 + ``` 69 + 70 + ```gleam 71 + field(named: "userid") |> field.make_hidden |> field.set_raw_value("42") 72 + ``` 73 + 74 + ### Field definition 75 + 76 + [Defintions](https://hexdocs.pm/formz/formz/definition.html) are the heavy 77 + compared to the lightness of fields; they take a bit more work to make as they 78 + are intended to be more reusable. 79 + 80 + The first role of a `Defintion` is to generate the HTML widget for the field. 81 + This library is format-agnostic and you can generate HTML widgets as raw 82 + strings, Lustre elements, Nakai nodes, something else, etc. There are 83 + currently three `formz` libraries that provide common field definitions for the 84 + most common HTML formats. 85 + 86 + - [formz_string](https://hexdocs.pm/formz_string/) 87 + - [formz_nakai](https://hexdocs.pm/formz_nakai/) 88 + - [formz_lustre](https://hexdocs.pm/formz_lustre/) (untested in a browser, I've only done this server side) 89 + 90 + The second role of a `Definition` is to parse the data from the field. There 91 + are a two parts to this, as how you parse a field's value depends on if it is 92 + optional or required. Not all scenarios can be cookie-cutter placed into an 93 + `Option`. So you need to provide two parse functions, one for when a field is 94 + required, and a second for when it's optional. 95 + 96 + ```gleam 97 + /// you won't often need to do this directly (I think??). The idea is that 98 + /// there'd be libs with the definitions you need. 99 + 100 + import formz/definition.{Definition} 101 + import formz/field 102 + import formz/validation 103 + import formz/widget 104 + import lustre/attribute 105 + import lustre/element 106 + import lustre/element/html 107 + 108 + fn password_widget( 109 + field: field.Field, 110 + args: widget.Args, 111 + ) -> element.Element(msg) { 112 + html.input([ 113 + attribute.type_("password"), 114 + attribute.name(field.name), 115 + attribute.id(args.id), 116 + attribute.attribute("aria-labelledby", field.label), 117 + ]) 118 + } 119 + 120 + pub fn password_field() { 121 + Definition( 122 + widget: password_widget, 123 + parse: validation.string, 124 + optional_parse: fn(parse, str) { 125 + case str { 126 + "" -> Ok(option.None) 127 + _ -> parse(str) 128 + } 129 + }, 130 + // We need to have a stub value for each parser. The stubs are used when 131 + // building the decoder and parse functions for the form. 132 + stub: "", 133 + optional_stub: option.None, 134 + ) 135 + } 136 + ``` 137 + 138 + 139 + 140 + ## Generating HTML for a form 141 + 142 + Generally speaking, the idea with a `formz` form is that you are not going 143 + to generate the HTML for each field individually, but rather, you'd use 144 + a function to loop through each field, generating semantic, accessible 145 + markup for each one. 146 + 147 + The specifics of how you would do this are going 148 + to vary greatly for each project and its styling/markup needs. 149 + 150 + However, the three `formz_*` libraries mentioned above all provide a 151 + simple form generator function that you can use as is, or as a starting 152 + point for your own. `formz` is BYOS, Bring Your Own Stylesheet, so the 153 + built-in form generators come unstyled. If there is interest, I could add 154 + a super simple CSS file to get the ball rolling and make the default 155 + forms easier to use out of the box. 156 + 157 + That said, you can also create the form HTML yourself, directly for each field. 158 + There's [an example](https://github.com/bentomas/formz/blob/main/formz_demo/src/formz_demo/examples/custom_output.gleam) 159 + in the demo project showing how to do this. 160 + 161 + ### Generating form HTML using the `formz_string` library 162 + 163 + The built-in form generators leave it as homework to add the form tags and 164 + submit buttons. 165 + 166 + ```gleam 167 + import formz_string/simple 168 + 169 + pub fn show_form(form) -> String { 170 + "<form method=\"post\">" 171 + <> simple.generate(form) 172 + <> "<p><button type\"submit\">Submit</button></p>" 173 + <> "</form>" 17 174 } 18 175 ``` 19 176 20 - See the [main package](https://github.com/bentomas/formz/tree/main/formz) for more details 177 + 178 + ## Parsing form data 179 + 180 + You can parse a `formz` form with a tuple of values and names, typically from 181 + a POST request. Here we parse in a `wisp` handler: 182 + 183 + ```gleam 184 + pub fn handle_form_submission(req: Request) -> Response { 185 + use formdata <- wisp.require_form(req) 186 + 187 + let result = make_form() 188 + |> formz.data(formdata.values) 189 + |> formz.parse 190 + 191 + case result { 192 + Ok(credentials) -> { 193 + let #(username, password) = credentials 194 + wisp.ok() 195 + |> wisp.html_body(string_builder.from_string("Hello "<>username<>"!")) 196 + } 197 + Error(form_with_errors) -> { 198 + show_form(form_with_errors) 199 + } 200 + } 201 + } 202 + ``` 203 + 204 + However, often you want to parse a form, and then... you know... act on that 205 + data, and in doing so you might discover more errors for the form. In this 206 + situation you can use `decode_then_try`: 207 + 208 + ```gleam 209 + pub fn handle_form_submission(req: Request) -> Response { 210 + use formdata <- wisp.require_form(req) 211 + 212 + let result = make_form() 213 + |> formz.data(formdata.values) 214 + |> formz.decode_then_try(fn(form, credentials) { 215 + case credentials { 216 + #("admin" as username, "l33t") -> Ok(username) 217 + #("admin", _) -> 218 + Error(form |> formz.set_field_error("password", "Wrong password")) 219 + _ -> 220 + Error(form |> formz.set_field_error("username", "Unknown username")) 221 + } 222 + }) 223 + 224 + case result { 225 + Ok(username) -> { 226 + wisp.ok() 227 + |> wisp.html_body(string_builder.from_string("Hello " <> username <> "!")) 228 + } 229 + Error(form_with_errors) -> { 230 + show_form(form_with_errors) 231 + } 232 + } 233 + } 234 + ``` 235 + 236 + ## See it in action 237 + 238 + There is a [demo wisp app](https://github.com/bentomas/formz/tree/main/formz_demo) 239 + showing a few interactive examples of how `formz` works in the repo.
-11
formz/src/formz/field.gleam
··· 37 37 /// the value or submitting a different value via other means, so (presently) 38 38 /// this doesn't mean the value cannot be tampered with. 39 39 disabled: Bool, 40 - /// Whether the field is hidden. A hidden field is not displayed in the browser. 41 - hidden: Bool, 42 40 ) 43 41 } 44 42 ··· 55 53 label: justin.sentence_case(name), 56 54 help_text: "", 57 55 disabled: False, 58 - hidden: False, 59 56 ) 60 57 } 61 58 ··· 69 66 70 67 pub fn set_help_text(field: Field, help_text: String) -> Field { 71 68 Field(..field, help_text:) 72 - } 73 - 74 - pub fn set_hidden(field: Field, hidden: Bool) -> Field { 75 - Field(..field, hidden:) 76 - } 77 - 78 - pub fn make_hidden(field: Field) -> Field { 79 - set_hidden(field, True) 80 69 } 81 70 82 71 pub fn set_disabled(field: Field, disabled: Bool) -> Field {
+10 -4
formz_demo/src/formz_demo/examples/custom_output.gleam
··· 13 13 } 14 14 15 15 pub fn format_form(form) { 16 - let assert Ok(Field(username_field, username_state, username_widget)) = 17 - formz.get(form, "username") 16 + let assert Ok(Field( 17 + username_field, 18 + username_state, 19 + widget.Widget(username_widget), 20 + )) = formz.get(form, "username") 18 21 19 - let assert Ok(Field(password_field, password_state, password_widget)) = 20 - formz.get(form, "password") 22 + let assert Ok(Field( 23 + password_field, 24 + password_state, 25 + widget.Widget(password_widget), 26 + )) = formz.get(form, "password") 21 27 22 28 html.div( 23 29 [
+26
formz_demo/src/formz_demo/examples/hidden_inputs.gleam
··· 1 + import formz 2 + import formz/field.{field} 3 + import formz_string/definitions 4 + import formz_string/widgets 5 + 6 + pub fn make_form() { 7 + use id1 <- formz.require( 8 + field("id_1"), 9 + definitions.make_hidden(definitions.integer_field()), 10 + ) 11 + use id2 <- formz.require( 12 + field("id_2"), 13 + definitions.integer_field() |> definitions.make_hidden, 14 + ) 15 + use id3 <- formz.require( 16 + field("id_3"), 17 + definitions.integer_field() |> formz.widget(widgets.hidden_widget()), 18 + ) 19 + formz.create_form(#(id1, id2, id3)) 20 + } 21 + 22 + pub fn handle_get( 23 + form: formz.Form(format, output), 24 + ) -> formz.Form(format, output) { 25 + form |> formz.data([#("id_1", "1"), #("id_2", "2"), #("id_3", "3")]) 26 + }
+14
formz_demo/src/formz_demo/router.gleam
··· 11 11 import formz_demo/examples/all_the_inputs 12 12 import formz_demo/examples/custom_output 13 13 import formz_demo/examples/hello_world 14 + import formz_demo/examples/hidden_inputs 14 15 import formz_demo/examples/labels 15 16 import formz_demo/examples/list_fields 16 17 import formz_demo/examples/login ··· 54 55 dir, 55 56 require_all_the_inputs.make_form, 56 57 defaults.handle_get, 58 + defaults.handle_post, 59 + defaults.format_string_form, 60 + defaults.formatted_string_form_to_string, 61 + ), 62 + ) 63 + }), 64 + #("hidden_inputs", fn(req, dir) { 65 + run.handle( 66 + req, 67 + run.ExampleRun( 68 + dir, 69 + hidden_inputs.make_form, 70 + hidden_inputs.handle_get, 57 71 defaults.handle_post, 58 72 defaults.format_string_form, 59 73 defaults.formatted_string_form_to_string,
+2 -2
formz_lustre/manifest.toml
··· 6 6 { name = "formz_string", version = "1.0.0", build_tools = ["gleam"], requirements = ["formz", "gleam_stdlib"], source = "local", path = "../formz_string" }, 7 7 { name = "gleam_erlang", version = "0.33.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "A1D26B80F01901B59AABEE3475DD4C18D27D58FA5C897D922FCB9B099749C064" }, 8 8 { name = "gleam_json", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "0A57FB5666E695FD2BEE74C0428A98B0FC11A395D2C7B4CDF5E22C5DD32C74C6" }, 9 - { name = "gleam_otp", version = "0.14.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "5A8CE8DBD01C29403390A7BD5C0A63D26F865C83173CF9708E6E827E53159C65" }, 10 - { name = "gleam_stdlib", version = "0.45.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "206FCE1A76974AECFC55AEBCD0217D59EDE4E408C016E2CFCCC8FF51278F186E" }, 9 + { name = "gleam_otp", version = "0.15.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "E9ED3DF7E7285DA0C440F46AE8236ADC8475E8CCBEE4899BF09A8468DA3F9187" }, 10 + { name = "gleam_stdlib", version = "0.46.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "53940A91251A6BE9AEBB959D46E1CB45B510551D81342A52213850947732D4AB" }, 11 11 { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, 12 12 { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, 13 13 { name = "lustre", version = "4.6.3", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "BDF833368F6C8F152F948D5B6A79866E9881CB80CB66C0685B3327E7DCBFB12F" },
+2 -2
formz_lustre/src/formz_lustre/simple.gleam
··· 17 17 item: formz.Item(widget.Widget(msg)), 18 18 ) -> element.Element(msg) { 19 19 case item { 20 - formz.Field(field, state, _) if field.hidden == True -> 20 + formz.Field(field, state, widget.Hidden) -> 21 21 html.input([ 22 22 attribute.type_("hidden"), 23 23 attribute.name(field.name), 24 24 attribute.value(state.value), 25 25 ]) 26 26 27 - formz.Field(field, state, make_widget) -> { 27 + formz.Field(field, state, widget.Widget(make_widget)) -> { 28 28 let id = field.name 29 29 30 30 let label_el =
+4 -2
formz_lustre/src/formz_lustre/widget.gleam
··· 12 12 import formz/field 13 13 import lustre/element 14 14 15 - pub type Widget(msg) = 16 - fn(field.Field, formz.InputState, Args) -> element.Element(msg) 15 + pub type Widget(msg) { 16 + Widget(fn(field.Field, formz.InputState, Args) -> element.Element(msg)) 17 + Hidden 18 + } 17 19 18 20 pub type Args { 19 21 Args(
+50 -54
formz_lustre/src/formz_lustre/widgets.gleam
··· 93 93 /// Create an `<input type="checkbox">`. The checkbox is checked 94 94 /// if the value is "on" (the browser default). 95 95 pub fn checkbox_widget() { 96 - fn(field: Field, state: formz.InputState, args: widget.Args) { 96 + widget.Widget(fn(field: Field, state: formz.InputState, args: widget.Args) { 97 97 let value = state.value 98 98 let state = case state { 99 99 formz.Unvalidated(_, requirement) -> formz.Unvalidated("", requirement) ··· 101 101 formz.Invalid(_, requirement, e) -> formz.Invalid("", requirement, e) 102 102 } 103 103 do_input_widget(field, state, args, "checkbox", [checked_attr(value)]) 104 - } 104 + }) 105 105 } 106 106 107 107 /// Create a `<input type="number">`. Normally browsers only allow whole numbers, ··· 111 111 /// the step size. If you truly need any float, then a `type="text"` input might be a 112 112 /// better choice. 113 113 pub fn number_widget(step_size: String) { 114 - fn(field: Field, state: formz.InputState, args: widget.Args) { 114 + widget.Widget(fn(field: Field, state: formz.InputState, args: widget.Args) { 115 115 do_input_widget(field, state, args, "number", [step_size_attr(step_size)]) 116 - } 116 + }) 117 117 } 118 118 119 119 /// Create an `<input type="password">`. This will not output the value in the 120 120 /// generated HTML for privacy/security concerns. 121 121 pub fn password_widget() { 122 - fn(field: Field, state: formz.InputState, args: widget.Args) { 122 + widget.Widget(fn(field: Field, state: formz.InputState, args: widget.Args) { 123 123 let state = case state { 124 124 formz.Unvalidated(_, requirement) -> formz.Unvalidated("", requirement) 125 125 formz.Valid(_, requirement) -> formz.Valid("", requirement) 126 126 formz.Invalid(_, requirement, e) -> formz.Invalid("", requirement, e) 127 127 } 128 128 do_input_widget(field, state, args, "password", []) 129 - } 129 + }) 130 130 } 131 131 132 132 /// Generate any `<input>` like `type="text"`, `type="email"` or 133 133 /// `type="url"`. 134 134 pub fn input_widget(type_: String) { 135 - fn(field: Field, state: formz.InputState, args: widget.Args) { 135 + widget.Widget(fn(field: Field, state: formz.InputState, args: widget.Args) { 136 136 do_input_widget(field, state, args, type_, []) 137 - } 137 + }) 138 138 } 139 139 140 140 fn do_input_widget( ··· 163 163 164 164 /// Create a `<textarea></textarea>`. 165 165 pub fn textarea_widget() { 166 - fn(field: Field, state: formz.InputState, args: widget.Args) -> element.Element( 167 - msg, 168 - ) { 169 - html.textarea( 170 - [ 171 - name_attr(field.name), 172 - id_attr(args.id), 173 - required_attr(state.requirement), 174 - aria_label_attr(args.labelled_by, field.label), 175 - aria_describedby_attr(args.described_by), 176 - ], 177 - state.value, 178 - ) 179 - } 166 + widget.Widget( 167 + fn(field: Field, state: formz.InputState, args: widget.Args) -> element.Element( 168 + msg, 169 + ) { 170 + html.textarea( 171 + [ 172 + name_attr(field.name), 173 + id_attr(args.id), 174 + required_attr(state.requirement), 175 + aria_label_attr(args.labelled_by, field.label), 176 + aria_describedby_attr(args.described_by), 177 + ], 178 + state.value, 179 + ) 180 + }, 181 + ) 180 182 } 181 183 182 184 /// Create a `<input type="hidden">`. This is useful for if a field is just 183 185 /// passing data around and you don't want it to be visible to the user. Like 184 186 /// say, the ID of a record being edited. 185 187 pub fn hidden_widget() { 186 - fn(field: Field, state: formz.InputState, _args: widget.Args) -> element.Element( 187 - msg, 188 - ) { 189 - html.input([ 190 - attribute.type_("hidden"), 191 - name_attr(field.name), 192 - value_attr(state.value), 193 - ]) 194 - } 188 + widget.Hidden 195 189 } 196 190 197 191 /// Create a `<select></select>` with `<option>`s for each variant. The list 198 192 /// of variants is a two-tuple, where the first item is the text to display and 199 193 /// the second item is the value. 200 194 pub fn select_widget(variants: List(#(String, String))) { 201 - fn(field: Field, state: formz.InputState, args: widget.Args) -> element.Element( 202 - msg, 203 - ) { 204 - html.select( 205 - [ 206 - name_attr(field.name), 207 - id_attr(args.id), 208 - required_attr(state.requirement), 209 - aria_label_attr(args.labelled_by, field.label), 210 - aria_describedby_attr(args.described_by), 211 - ], 212 - list.flatten([ 213 - [html.option([attribute.value("")], "Select..."), html.hr([])], 214 - list.map(variants, fn(variant) { 215 - let val = variant.1 216 - html.option( 217 - [attribute.value(val), attribute.selected(state.value == val)], 218 - variant.0, 219 - ) 220 - }), 221 - ]), 222 - ) 223 - } 195 + widget.Widget( 196 + fn(field: Field, state: formz.InputState, args: widget.Args) -> element.Element( 197 + msg, 198 + ) { 199 + html.select( 200 + [ 201 + name_attr(field.name), 202 + id_attr(args.id), 203 + required_attr(state.requirement), 204 + aria_label_attr(args.labelled_by, field.label), 205 + aria_describedby_attr(args.described_by), 206 + ], 207 + list.flatten([ 208 + [html.option([attribute.value("")], "Select..."), html.hr([])], 209 + list.map(variants, fn(variant) { 210 + let val = variant.1 211 + html.option( 212 + [attribute.value(val), attribute.selected(state.value == val)], 213 + variant.0, 214 + ) 215 + }), 216 + ]), 217 + ) 218 + }, 219 + ) 224 220 }
+14 -25
formz_lustre/test/formz_lustre/widgets_test.gleam
··· 38 38 name name: String, 39 39 label label: String, 40 40 help help_text: String, 41 - hidden hidden: Bool, 42 41 disabled disabled: Bool, 43 42 required required: Bool, 44 43 value value: String, ··· 46 45 string string_widget: string_widget.Widget, 47 46 widget widget: widget.Widget(msg), 48 47 ) { 49 - let string_field = field.Field(name:, label:, help_text:, hidden:, disabled:) 50 - let field = field.Field(name:, label:, help_text:, hidden:, disabled:) 48 + let string_field = field.Field(name:, label:, help_text:, disabled:) 49 + let field = field.Field(name:, label:, help_text:, disabled:) 51 50 52 51 let requirement = case required { 53 52 True -> formz.Required ··· 55 54 } 56 55 let state = formz.Valid(value, requirement) 57 56 58 - widget(field, state, args) 59 - |> convert_to_string 60 - |> should.equal(string_widget(string_field, state, args |> to_string_args)) 57 + case widget, string_widget { 58 + widget.Hidden, string_widget.Hidden -> Nil 59 + widget.Widget(make_widget), string_widget.Widget(make_string_widget) -> 60 + make_widget(field, state, args) 61 + |> convert_to_string 62 + |> should.equal(make_string_widget( 63 + string_field, 64 + state, 65 + args |> to_string_args, 66 + )) 67 + _, _ -> should.fail() 68 + } 61 69 } 62 70 63 71 pub fn text_widget_test() { ··· 67 75 name: "a", 68 76 label: "A", 69 77 help: "help", 70 - hidden: False, 71 78 disabled: False, 72 79 required: True, 73 80 value: "", ··· 84 91 name: "", 85 92 label: "A", 86 93 help: "help", 87 - hidden: False, 88 94 disabled: False, 89 95 required: True, 90 96 value: "", ··· 101 107 name: "a", 102 108 label: "A", 103 109 help: "help", 104 - hidden: False, 105 110 disabled: False, 106 111 required: True, 107 112 value: "val", ··· 118 123 name: "a", 119 124 label: "A", 120 125 help: "help", 121 - hidden: False, 122 126 disabled: False, 123 127 required: True, 124 128 value: "", ··· 137 141 name: "a", 138 142 label: "A", 139 143 help: "help", 140 - hidden: False, 141 144 disabled: False, 142 145 required: True, 143 146 value: "", ··· 153 156 name: "a", 154 157 label: "A", 155 158 help: "help", 156 - hidden: False, 157 159 disabled: False, 158 160 required: True, 159 161 value: "on", ··· 170 172 name: "a", 171 173 label: "A", 172 174 help: "help", 173 - hidden: False, 174 175 disabled: False, 175 176 required: True, 176 177 value: "on", ··· 189 190 name: "a", 190 191 label: "A", 191 192 help: "help", 192 - hidden: False, 193 193 disabled: False, 194 194 required: True, 195 195 value: "", ··· 205 205 name: "a", 206 206 label: "A", 207 207 help: "help", 208 - hidden: False, 209 208 disabled: False, 210 209 required: True, 211 210 value: "xxxx", ··· 222 221 name: "a", 223 222 label: "A", 224 223 help: "help", 225 - hidden: False, 226 224 disabled: False, 227 225 required: True, 228 226 value: "xxxx", ··· 241 239 name: "a", 242 240 label: "A", 243 241 help: "help", 244 - hidden: False, 245 242 disabled: False, 246 243 required: True, 247 244 value: "", ··· 257 254 name: "a", 258 255 label: "A", 259 256 help: "help", 260 - hidden: False, 261 257 disabled: False, 262 258 required: True, 263 259 value: "1", ··· 274 270 name: "a", 275 271 label: "A", 276 272 help: "help", 277 - hidden: False, 278 273 disabled: False, 279 274 required: True, 280 275 value: "1", ··· 293 288 name: "a", 294 289 label: "A", 295 290 help: "help", 296 - hidden: False, 297 291 disabled: False, 298 292 required: True, 299 293 value: "", ··· 309 303 name: "a", 310 304 label: "A", 311 305 help: "help", 312 - hidden: False, 313 306 disabled: False, 314 307 required: True, 315 308 value: "1", ··· 326 319 name: "a", 327 320 label: "A", 328 321 help: "help", 329 - hidden: False, 330 322 disabled: False, 331 323 required: True, 332 324 value: "1", ··· 346 338 name: "a", 347 339 label: "A", 348 340 help: "help", 349 - hidden: False, 350 341 disabled: False, 351 342 required: True, 352 343 value: "", ··· 362 353 name: "a", 363 354 label: "A", 364 355 help: "help", 365 - hidden: False, 366 356 disabled: False, 367 357 required: True, 368 358 value: "1", ··· 379 369 name: "a", 380 370 label: "A", 381 371 help: "help", 382 - hidden: False, 383 372 disabled: False, 384 373 required: True, 385 374 value: "1",
+2 -2
formz_nakai/src/formz_nakai/simple.gleam
··· 13 13 14 14 pub fn generate_visible_item(item: formz.Item(widget.Widget)) -> html.Node { 15 15 case item { 16 - formz.Field(field, state, _) if field.hidden == True -> 16 + formz.Field(field, state, widget.Hidden) -> 17 17 html.input([ 18 18 attr.type_("hidden"), 19 19 attr.name(field.name), 20 20 attr.value(state.value), 21 21 ]) 22 - formz.Field(field, state, make_widget) -> { 22 + formz.Field(field, state, widget.Widget(make_widget)) -> { 23 23 let id = field.name 24 24 25 25 let label_el =
+4 -2
formz_nakai/src/formz_nakai/widget.gleam
··· 12 12 import formz/field 13 13 import nakai/html 14 14 15 - pub type Widget = 16 - fn(field.Field, formz.InputState, Args) -> html.Node 15 + pub type Widget { 16 + Widget(fn(field.Field, formz.InputState, Args) -> html.Node) 17 + Hidden 18 + } 17 19 18 20 pub type Args { 19 21 Args(
+48 -52
formz_nakai/src/formz_nakai/widgets.gleam
··· 92 92 93 93 /// Create an `<input type="checkbox">`. The checkbox is checked 94 94 /// if the value is "on" (the browser default). 95 - pub fn checkbox_widget() { 96 - fn(field: Field, state: formz.InputState, args: widget.Args) { 95 + pub fn checkbox_widget() -> widget.Widget { 96 + widget.Widget(fn(field: Field, state: formz.InputState, args: widget.Args) { 97 97 let value = state.value 98 98 let state = case state { 99 99 formz.Unvalidated(_, requirement) -> formz.Unvalidated("", requirement) ··· 101 101 formz.Invalid(_, requirement, e) -> formz.Invalid("", requirement, e) 102 102 } 103 103 do_input_widget(field, state, args, "checkbox", [checked_attr(value)]) 104 - } 104 + }) 105 105 } 106 106 107 107 /// Create a `<input type="number">`. Normally browsers only allow whole numbers, ··· 111 111 /// the step size. If you truly need any float, then a `type="text"` input might be a 112 112 /// better choice. 113 113 pub fn number_widget(step_size: String) { 114 - fn(field: Field, state: formz.InputState, args: widget.Args) { 114 + widget.Widget(fn(field: Field, state: formz.InputState, args: widget.Args) { 115 115 do_input_widget(field, state, args, "number", [step_size_attr(step_size)]) 116 - } 116 + }) 117 117 } 118 118 119 119 /// Create an `<input type="password">`. This will not output the value in the 120 120 /// generated HTML for privacy/security concerns. 121 121 pub fn password_widget() { 122 - fn(field: Field, state: formz.InputState, args: widget.Args) { 122 + widget.Widget(fn(field: Field, state: formz.InputState, args: widget.Args) { 123 123 let state = case state { 124 124 formz.Unvalidated(_, requirement) -> formz.Unvalidated("", requirement) 125 125 formz.Valid(_, requirement) -> formz.Valid("", requirement) 126 126 formz.Invalid(_, requirement, e) -> formz.Invalid("", requirement, e) 127 127 } 128 128 do_input_widget(field, state, args, "password", []) 129 - } 129 + }) 130 130 } 131 131 132 132 /// Generate any `<input>` like `type="text"`, `type="email"` or 133 133 /// `type="url"`. 134 134 pub fn input_widget(type_: String) { 135 - fn(field: Field, state: formz.InputState, args: widget.Args) { 135 + widget.Widget(fn(field: Field, state: formz.InputState, args: widget.Args) { 136 136 do_input_widget(field, state, args, type_, []) 137 - } 137 + }) 138 138 } 139 139 140 140 fn do_input_widget( ··· 161 161 162 162 /// Create a `<textarea></textarea>`. 163 163 pub fn textarea_widget() { 164 - fn(field: Field, state: formz.InputState, args: widget.Args) -> html.Node { 165 - html.textarea( 166 - list.flatten([ 167 - name_attr(field.name), 168 - id_attr(args.id), 169 - required_attr(state.requirement), 170 - aria_label_attr(args.labelled_by, field.label), 171 - ]), 172 - [html.Text(state.value)], 173 - ) 174 - } 164 + widget.Widget( 165 + fn(field: Field, state: formz.InputState, args: widget.Args) -> html.Node { 166 + html.textarea( 167 + list.flatten([ 168 + name_attr(field.name), 169 + id_attr(args.id), 170 + required_attr(state.requirement), 171 + aria_label_attr(args.labelled_by, field.label), 172 + ]), 173 + [html.Text(state.value)], 174 + ) 175 + }, 176 + ) 175 177 } 176 178 177 179 /// Create a `<input type="hidden">`. This is useful for if a field is just 178 180 /// passing data around and you don't want it to be visible to the user. Like 179 181 /// say, the ID of a record being edited. 180 182 pub fn hidden_widget() { 181 - fn(field: Field, state: formz.InputState, _) -> html.Node { 182 - html.input( 183 - list.flatten([ 184 - type_attr("hidden"), 185 - name_attr(field.name), 186 - value_attr(state.value), 187 - ]), 188 - ) 189 - } 183 + widget.Hidden 190 184 } 191 185 192 186 /// Create a `<select></select>` with `<option>`s for each variant. The list 193 187 /// of variants is a two-tuple, where the first item is the text to display and 194 188 /// the second item is the value. 195 189 pub fn select_widget(variants: List(#(String, String))) { 196 - fn(field: Field, state: formz.InputState, args: widget.Args) -> html.Node { 197 - html.select( 198 - list.flatten([ 199 - name_attr(field.name), 200 - id_attr(args.id), 201 - required_attr(state.requirement), 202 - aria_label_attr(args.labelled_by, field.label), 203 - ]), 204 - list.flatten([ 205 - [html.option([attr.value("")], [html.Text("Select...")]), html.hr([])], 206 - list.map(variants, fn(variant) { 207 - let val = variant.1 208 - let selected_attr = case state.value == val { 209 - True -> [attr.selected()] 210 - _ -> [] 211 - } 212 - html.option(list.flatten([value_attr(val), selected_attr]), [ 213 - html.Text(variant.0), 214 - ]) 215 - }), 216 - ]), 217 - ) 218 - } 190 + widget.Widget( 191 + fn(field: Field, state: formz.InputState, args: widget.Args) -> html.Node { 192 + html.select( 193 + list.flatten([ 194 + name_attr(field.name), 195 + id_attr(args.id), 196 + required_attr(state.requirement), 197 + aria_label_attr(args.labelled_by, field.label), 198 + ]), 199 + list.flatten([ 200 + [html.option([attr.value("")], [html.Text("Select...")]), html.hr([])], 201 + list.map(variants, fn(variant) { 202 + let val = variant.1 203 + let selected_attr = case state.value == val { 204 + True -> [attr.selected()] 205 + _ -> [] 206 + } 207 + html.option(list.flatten([value_attr(val), selected_attr]), [ 208 + html.Text(variant.0), 209 + ]) 210 + }), 211 + ]), 212 + ) 213 + }, 214 + ) 219 215 }
+14 -25
formz_nakai/test/formz_nakai/widgets_test.gleam
··· 56 56 name name: String, 57 57 label label: String, 58 58 help help_text: String, 59 - hidden hidden: Bool, 60 59 disabled disabled: Bool, 61 60 required required: Bool, 62 61 value value: String, ··· 64 63 string string_widget: string_widget.Widget, 65 64 widget widget: widget.Widget, 66 65 ) { 67 - let string_field = field.Field(name:, label:, help_text:, hidden:, disabled:) 68 - let field = field.Field(name:, label:, help_text:, hidden:, disabled:) 66 + let string_field = field.Field(name:, label:, help_text:, disabled:) 67 + let field = field.Field(name:, label:, help_text:, disabled:) 69 68 70 69 let requirement = case required { 71 70 True -> formz.Required ··· 73 72 } 74 73 let state = formz.Valid(value, requirement) 75 74 76 - widget(field, state, args) 77 - |> convert_to_string 78 - |> should.equal(string_widget(string_field, state, args |> to_string_args)) 75 + case widget, string_widget { 76 + widget.Hidden, string_widget.Hidden -> Nil 77 + widget.Widget(make_widget), string_widget.Widget(make_string_widget) -> 78 + make_widget(field, state, args) 79 + |> convert_to_string 80 + |> should.equal(make_string_widget( 81 + string_field, 82 + state, 83 + args |> to_string_args, 84 + )) 85 + _, _ -> should.fail() 86 + } 79 87 } 80 88 81 89 pub fn text_widget_test() { ··· 85 93 name: "a", 86 94 label: "A", 87 95 help: "help", 88 - hidden: False, 89 96 disabled: False, 90 97 required: True, 91 98 value: "", ··· 102 109 name: "", 103 110 label: "A", 104 111 help: "help", 105 - hidden: False, 106 112 disabled: False, 107 113 required: True, 108 114 value: "", ··· 119 125 name: "a", 120 126 label: "A", 121 127 help: "help", 122 - hidden: False, 123 128 disabled: False, 124 129 required: True, 125 130 value: "val", ··· 136 141 name: "a", 137 142 label: "A", 138 143 help: "help", 139 - hidden: False, 140 144 disabled: False, 141 145 required: True, 142 146 value: "", ··· 155 159 name: "a", 156 160 label: "A", 157 161 help: "help", 158 - hidden: False, 159 162 disabled: False, 160 163 required: True, 161 164 value: "", ··· 171 174 name: "a", 172 175 label: "A", 173 176 help: "help", 174 - hidden: False, 175 177 disabled: False, 176 178 required: True, 177 179 value: "on", ··· 188 190 name: "a", 189 191 label: "A", 190 192 help: "help", 191 - hidden: False, 192 193 disabled: False, 193 194 required: True, 194 195 value: "on", ··· 207 208 name: "a", 208 209 label: "A", 209 210 help: "help", 210 - hidden: False, 211 211 disabled: False, 212 212 required: True, 213 213 value: "", ··· 223 223 name: "a", 224 224 label: "A", 225 225 help: "help", 226 - hidden: False, 227 226 disabled: False, 228 227 required: True, 229 228 value: "xxxx", ··· 240 239 name: "a", 241 240 label: "A", 242 241 help: "help", 243 - hidden: False, 244 242 disabled: False, 245 243 required: True, 246 244 value: "xxxx", ··· 259 257 name: "a", 260 258 label: "A", 261 259 help: "help", 262 - hidden: False, 263 260 disabled: False, 264 261 required: True, 265 262 value: "", ··· 275 272 name: "a", 276 273 label: "A", 277 274 help: "help", 278 - hidden: False, 279 275 disabled: False, 280 276 required: True, 281 277 value: "1", ··· 292 288 name: "a", 293 289 label: "A", 294 290 help: "help", 295 - hidden: False, 296 291 disabled: False, 297 292 required: True, 298 293 value: "1", ··· 311 306 name: "a", 312 307 label: "A", 313 308 help: "help", 314 - hidden: False, 315 309 disabled: False, 316 310 required: True, 317 311 value: "", ··· 327 321 name: "a", 328 322 label: "A", 329 323 help: "help", 330 - hidden: False, 331 324 disabled: False, 332 325 required: True, 333 326 value: "1", ··· 344 337 name: "a", 345 338 label: "A", 346 339 help: "help", 347 - hidden: False, 348 340 disabled: False, 349 341 required: True, 350 342 value: "1", ··· 364 356 name: "a", 365 357 label: "A", 366 358 help: "help", 367 - hidden: False, 368 359 disabled: False, 369 360 required: True, 370 361 value: "", ··· 380 371 name: "a", 381 372 label: "A", 382 373 help: "help", 383 - hidden: False, 384 374 disabled: False, 385 375 required: True, 386 376 value: "1", ··· 397 387 name: "a", 398 388 label: "A", 399 389 help: "help", 400 - hidden: False, 401 390 disabled: False, 402 391 required: True, 403 392 value: "1",
+37 -8
formz_string/src/formz_string/definitions.gleam
··· 6 6 7 7 import formz 8 8 import formz/validation 9 + import formz_string/widget 9 10 import formz_string/widgets 10 11 import gleam/int 11 12 import gleam/list 13 + import gleam/option 12 14 13 15 /// Create a basic form input. Parsed as a String. 14 - pub fn text_field() { 16 + pub fn text_field() -> formz.Definition(widget.Widget, String, String) { 15 17 formz.definition_with_custom_optional( 16 18 widgets.input_widget("text"), 17 19 validation.non_empty_string, ··· 28 30 29 31 /// Create an email form input. Parsed as a String but must 30 32 /// look like an email address, i.e. the string has an `@`. 31 - pub fn email_field() { 33 + pub fn email_field() -> formz.Definition( 34 + widget.Widget, 35 + String, 36 + option.Option(String), 37 + ) { 32 38 formz.definition(widgets.input_widget("email"), validation.email, "") 33 39 } 34 40 35 41 /// Create a whole number form input. Parsed as an Int. 36 - pub fn integer_field() { 42 + pub fn integer_field() -> formz.Definition( 43 + widget.Widget, 44 + Int, 45 + option.Option(Int), 46 + ) { 37 47 formz.definition(widgets.number_widget(""), validation.int, 0) 38 48 } 39 49 40 50 /// Create a number form input. Parsed as a Float. 41 - pub fn number_field() { 51 + pub fn number_field() -> formz.Definition( 52 + widget.Widget, 53 + Float, 54 + option.Option(Float), 55 + ) { 42 56 formz.definition(widgets.number_widget("0.01"), validation.number, 0.0) 43 57 } 44 58 45 59 /// Create a checkbox form input. Parsed as a `Bool`. If required, the parsed 46 60 /// `Bool` must be `True`. 47 - pub fn boolean_field() { 61 + pub fn boolean_field() -> formz.Definition(widget.Widget, Bool, Bool) { 48 62 formz.definition_with_custom_optional( 49 63 widget: widgets.checkbox_widget(), 50 64 parse: validation.on, ··· 60 74 } 61 75 62 76 /// Create a password form input, which hides the input value. Parsed as a String. 63 - pub fn password_field() { 77 + pub fn password_field() -> formz.Definition( 78 + widget.Widget, 79 + String, 80 + option.Option(String), 81 + ) { 64 82 formz.definition(widgets.password_widget(), validation.non_empty_string, "") 65 83 } 66 84 ··· 73 91 /// Because of how you build `formz` forms, you need to provide a stub of 74 92 /// the value type. Is this annoying? Would it be more or less annoying if I 75 93 /// required a non-empty list for the variants instead? I'm not sure. Let me know! 76 - pub fn choices_field(variants: List(#(String, enum)), stub stub: enum) { 94 + pub fn choices_field( 95 + variants: List(#(String, enum)), 96 + stub stub: enum, 97 + ) -> formz.Definition(widget.Widget, enum, option.Option(enum)) { 77 98 let keys_indexed = 78 99 variants 79 100 |> list.index_map(fn(t, i) { #(t.0, int.to_string(i)) }) ··· 89 110 90 111 /// Creates a `<select>` input from a list of strings. Validates that the parsed 91 112 /// value is one of the strings in the list. 92 - pub fn list_field(variants: List(String)) { 113 + pub fn list_field( 114 + variants: List(String), 115 + ) -> formz.Definition(widget.Widget, String, option.Option(String)) { 93 116 let labels_and_values = list.map(variants, fn(s) { #(s, s) }) 94 117 choices_field(labels_and_values, "") 95 118 } 119 + 120 + pub fn make_hidden( 121 + def: formz.Definition(widget.Widget, a, b), 122 + ) -> formz.Definition(widget.Widget, a, b) { 123 + def |> formz.widget(widgets.hidden_widget()) 124 + }
+13 -3
formz_string/src/formz_string/simple.gleam
··· 16 16 17 17 pub fn generate_item(item: formz.Item(widget.Widget)) -> String { 18 18 case item { 19 - formz.Field(field, state, _) if field.hidden == True -> 19 + formz.Field(field, state, widget.Hidden) -> 20 20 "<input" 21 21 <> { " type=\"hidden\"" } 22 22 <> { " name=\"" <> field.name <> "\"" } 23 23 <> { " value=\"" <> state.value <> "\"" } 24 24 <> ">" 25 - formz.Field(field, state, make_widget) -> { 25 + formz.ListField(field, states, _, widget.Hidden) -> 26 + states 27 + |> list.map(fn(state) { 28 + "<input" 29 + <> { " type=\"hidden\"" } 30 + <> { " name=\"" <> field.name <> "[]\"" } 31 + <> { " value=\"" <> state.value <> "\"" } 32 + <> ">" 33 + }) 34 + |> string.join("") 35 + formz.Field(field, state, widget.Widget(make_widget)) -> { 26 36 let id = field.name 27 37 28 38 let label_el = ··· 76 86 <> errors_el 77 87 <> "</div>" 78 88 } 79 - formz.ListField(field, states, _, make_widget) -> { 89 + formz.ListField(field, states, _, widget.Widget(make_widget)) -> { 80 90 let id = field.name 81 91 82 92 let #(legend_el, legend_id) = #(
+4 -2
formz_string/src/formz_string/widget.gleam
··· 11 11 import formz 12 12 import formz/field 13 13 14 - pub type Widget = 15 - fn(field.Field, formz.InputState, Args) -> String 14 + pub type Widget { 15 + Widget(fn(field.Field, formz.InputState, Args) -> String) 16 + Hidden 17 + } 16 18 17 19 pub type Args { 18 20 Args(
+80 -74
formz_string/src/formz_string/widgets.gleam
··· 114 114 /// Create an `<input type="checkbox">`. The checkbox is checked 115 115 /// if the value is "on" (the browser default). 116 116 pub fn checkbox_widget() -> Widget { 117 - fn(field: field.Field, state: formz.InputState, args: widget.Args) { 118 - let value = state.value 119 - let state = case state { 120 - formz.Unvalidated(_, requirement) -> formz.Unvalidated("", requirement) 121 - formz.Valid(_, requirement) -> formz.Valid("", requirement) 122 - formz.Invalid(_, requirement, e) -> formz.Invalid("", requirement, e) 123 - } 124 - do_input_widget(field, state, args, "checkbox", [checked_attr(value)]) 125 - } 117 + widget.Widget( 118 + fn(field: field.Field, state: formz.InputState, args: widget.Args) { 119 + let value = state.value 120 + let state = case state { 121 + formz.Unvalidated(_, requirement) -> formz.Unvalidated("", requirement) 122 + formz.Valid(_, requirement) -> formz.Valid("", requirement) 123 + formz.Invalid(_, requirement, e) -> formz.Invalid("", requirement, e) 124 + } 125 + do_input_widget(field, state, args, "checkbox", [checked_attr(value)]) 126 + }, 127 + ) 126 128 } 127 129 128 130 /// Create a `<input type="number">`. Normally browsers only allow whole numbers, ··· 132 134 /// the step size. If you truly need any float, then a `type="text"` input might be a 133 135 /// better choice. 134 136 pub fn number_widget(step_size: String) -> Widget { 135 - fn(field: field.Field, state: formz.InputState, args: widget.Args) { 136 - do_input_widget(field, state, args, "number", [step_size_attr(step_size)]) 137 - } 137 + widget.Widget( 138 + fn(field: field.Field, state: formz.InputState, args: widget.Args) { 139 + do_input_widget(field, state, args, "number", [step_size_attr(step_size)]) 140 + }, 141 + ) 138 142 } 139 143 140 144 /// Create an `<input type="password">`. This will not output the value in the 141 145 /// generated HTML for privacy/security concerns. 142 146 pub fn password_widget() -> Widget { 143 - fn(field: field.Field, state: formz.InputState, args: widget.Args) { 144 - let state = case state { 145 - formz.Unvalidated(_, requirement) -> formz.Unvalidated("", requirement) 146 - formz.Valid(_, requirement) -> formz.Valid("", requirement) 147 - formz.Invalid(_, requirement, e) -> formz.Invalid("", requirement, e) 148 - } 149 - do_input_widget(field, state, args, "password", []) 150 - } 147 + widget.Widget( 148 + fn(field: field.Field, state: formz.InputState, args: widget.Args) { 149 + let state = case state { 150 + formz.Unvalidated(_, requirement) -> formz.Unvalidated("", requirement) 151 + formz.Valid(_, requirement) -> formz.Valid("", requirement) 152 + formz.Invalid(_, requirement, e) -> formz.Invalid("", requirement, e) 153 + } 154 + do_input_widget(field, state, args, "password", []) 155 + }, 156 + ) 151 157 } 152 158 153 159 /// Generate any `<input>` like `type="text"`, `type="email"` or 154 160 /// `type="url"`. 155 161 pub fn input_widget(type_: String) -> Widget { 156 - fn(field: field.Field, state: formz.InputState, args: widget.Args) { 157 - do_input_widget(field, state, args, type_, []) 158 - } 162 + widget.Widget( 163 + fn(field: field.Field, state: formz.InputState, args: widget.Args) { 164 + do_input_widget(field, state, args, type_, []) 165 + }, 166 + ) 159 167 } 160 168 161 169 fn do_input_widget( ··· 180 188 181 189 /// Create a `<textarea></textarea>`. 182 190 pub fn textarea_widget() -> Widget { 183 - fn(field: field.Field, state: formz.InputState, args: widget.Args) -> String { 184 - // https://chriscoyier.net/2023/09/29/css-solves-auto-expanding-textareas-probably-eventually/ 185 - // https://til.simonwillison.net/css/resizing-textarea 186 - "<textarea" 187 - <> name_attr(field.name) 188 - <> id_attr(args.id) 189 - <> required_attr(state.requirement) 190 - <> disabled_attr(field.disabled) 191 - <> aria_label_attr(args.labelled_by, field.label) 192 - <> aria_describedby_attr(args.described_by) 193 - <> ">" 194 - <> state.value 195 - <> "</textarea>" 196 - } 191 + widget.Widget( 192 + fn(field: field.Field, state: formz.InputState, args: widget.Args) -> String { 193 + // https://chriscoyier.net/2023/09/29/css-solves-auto-expanding-textareas-probably-eventually/ 194 + // https://til.simonwillison.net/css/resizing-textarea 195 + "<textarea" 196 + <> name_attr(field.name) 197 + <> id_attr(args.id) 198 + <> required_attr(state.requirement) 199 + <> disabled_attr(field.disabled) 200 + <> aria_label_attr(args.labelled_by, field.label) 201 + <> aria_describedby_attr(args.described_by) 202 + <> ">" 203 + <> state.value 204 + <> "</textarea>" 205 + }, 206 + ) 197 207 } 198 208 199 209 /// Create a `<input type="hidden">`. This is useful for if a field is just 200 210 /// passing data around and you don't want it to be visible to the user. Like 201 211 /// say, the ID of a record being edited. 202 212 pub fn hidden_widget() -> Widget { 203 - fn(field: field.Field, state: formz.InputState, _args: widget.Args) -> String { 204 - "<input" 205 - <> type_attr("hidden") 206 - <> name_attr(field.name) 207 - <> value_attr(state.value) 208 - <> ">" 209 - } 213 + widget.Hidden 210 214 } 211 215 212 216 /// Create a `<select></select>` with `<option>`s for each variant. The list 213 217 /// of variants is a two-tuple, where the first item is the text to display and 214 218 /// the second item is the value. 215 219 pub fn select_widget(variants: List(#(String, String))) -> Widget { 216 - fn(field: field.Field, state: formz.InputState, args: widget.Args) { 217 - let choices = 218 - list.map(variants, fn(variant) { 219 - let val = variant.1 220 - let selected_attr = case state.value == val { 221 - True -> " selected" 222 - _ -> "" 223 - } 224 - { "<option" <> value_attr(val) <> selected_attr <> ">" } 225 - <> variant.0 226 - <> "</option>" 227 - }) 228 - |> string.join("") 220 + widget.Widget( 221 + fn(field: field.Field, state: formz.InputState, args: widget.Args) { 222 + let choices = 223 + list.map(variants, fn(variant) { 224 + let val = variant.1 225 + let selected_attr = case state.value == val { 226 + True -> " selected" 227 + _ -> "" 228 + } 229 + { "<option" <> value_attr(val) <> selected_attr <> ">" } 230 + <> variant.0 231 + <> "</option>" 232 + }) 233 + |> string.join("") 229 234 230 - { 231 - "<select" 232 - <> name_attr(field.name) 233 - <> id_attr(args.id) 234 - <> required_attr(state.requirement) 235 - <> disabled_attr(field.disabled) 236 - <> aria_label_attr(args.labelled_by, field.label) 237 - <> aria_describedby_attr(args.described_by) 238 - <> ">" 239 - } 240 - // TODO make this placeholder option not selectable? with disabled selected attributes 241 - // https://stackoverflow.com/questions/5805059/how-do-i-make-a-placeholder-for-a-select-box 242 - <> { "<option value>Select...</option>" } 243 - <> { "<hr>" } 244 - <> choices 245 - <> { "</select>" } 246 - } 235 + { 236 + "<select" 237 + <> name_attr(field.name) 238 + <> id_attr(args.id) 239 + <> required_attr(state.requirement) 240 + <> disabled_attr(field.disabled) 241 + <> aria_label_attr(args.labelled_by, field.label) 242 + <> aria_describedby_attr(args.described_by) 243 + <> ">" 244 + } 245 + // TODO make this placeholder option not selectable? with disabled selected attributes 246 + // https://stackoverflow.com/questions/5805059/how-do-i-make-a-placeholder-for-a-select-box 247 + <> { "<option value>Select...</option>" } 248 + <> { "<hr>" } 249 + <> choices 250 + <> { "</select>" } 251 + }, 252 + ) 247 253 }
+2 -2
formz_string/test/formz_string/simple_test.gleam
··· 27 27 28 28 pub fn hidden_field_form() { 29 29 use a <- formz.require( 30 - field("a") |> field.make_hidden, 31 - definitions.integer_field(), 30 + field("a"), 31 + definitions.integer_field() |> definitions.make_hidden, 32 32 ) 33 33 34 34 formz.create_form(#(a))
+45 -128
formz_string/test/formz_string/widgets_test.gleam
··· 9 9 gleeunit.main() 10 10 } 11 11 12 + fn get_make_fun( 13 + widget: widget.Widget, 14 + ) -> fn(field.Field, formz.InputState, widget.Args) -> String { 15 + let assert widget.Widget(fun) = widget 16 + fun 17 + } 18 + 12 19 pub fn input_labelled_by_field_value_test() { 13 - widgets.input_widget("text")( 14 - field.Field( 15 - name: "name", 16 - label: "Label", 17 - help_text: "", 18 - disabled: False, 19 - hidden: False, 20 - ), 20 + get_make_fun(widgets.input_widget("text"))( 21 + field.Field(name: "name", label: "Label", help_text: "", disabled: False), 21 22 formz.Valid("hello", formz.Optional), 22 23 widget.Args("", widget.LabelledByFieldValue, widget.DescribedByNone), 23 24 ) ··· 25 26 } 26 27 27 28 pub fn input_labelled_by_element_with_id_test() { 28 - widgets.input_widget("text")( 29 - field.Field( 30 - name: "name", 31 - label: "Label", 32 - help_text: "", 33 - disabled: False, 34 - hidden: False, 35 - ), 29 + get_make_fun(widgets.input_widget("text"))( 30 + field.Field(name: "name", label: "Label", help_text: "", disabled: False), 36 31 formz.Valid("hello", formz.Optional), 37 32 widget.Args( 38 33 "", ··· 44 39 } 45 40 46 41 pub fn input_labelled_by_label_for_test() { 47 - widgets.input_widget("text")( 48 - field.Field( 49 - name: "name", 50 - label: "Label", 51 - help_text: "", 52 - disabled: False, 53 - hidden: False, 54 - ), 42 + get_make_fun(widgets.input_widget("text"))( 43 + field.Field(name: "name", label: "Label", help_text: "", disabled: False), 55 44 formz.Valid("hello", formz.Optional), 56 45 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 57 46 ) ··· 59 48 } 60 49 61 50 pub fn input_described_by_elements_with_ids_test() { 62 - widgets.input_widget("text")( 63 - field.Field( 64 - name: "name", 65 - label: "Label", 66 - help_text: "", 67 - disabled: False, 68 - hidden: False, 69 - ), 51 + get_make_fun(widgets.input_widget("text"))( 52 + field.Field(name: "name", label: "Label", help_text: "", disabled: False), 70 53 formz.Valid("hello", formz.Optional), 71 54 widget.Args( 72 55 "", ··· 78 61 } 79 62 80 63 pub fn input_described_by_elements_with_ids_all_empty_test() { 81 - widgets.input_widget("text")( 82 - field.Field( 83 - name: "name", 84 - label: "Label", 85 - help_text: "", 86 - disabled: False, 87 - hidden: False, 88 - ), 64 + get_make_fun(widgets.input_widget("text"))( 65 + field.Field(name: "name", label: "Label", help_text: "", disabled: False), 89 66 formz.Valid("hello", formz.Optional), 90 67 widget.Args( 91 68 "", ··· 97 74 } 98 75 99 76 pub fn input_required_test() { 100 - widgets.input_widget("text")( 101 - field.Field( 102 - name: "name", 103 - label: "Label", 104 - help_text: "", 105 - disabled: False, 106 - hidden: False, 107 - ), 77 + get_make_fun(widgets.input_widget("text"))( 78 + field.Field(name: "name", label: "Label", help_text: "", disabled: False), 108 79 formz.Valid("hello", formz.Required), 109 80 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 110 81 ) ··· 112 83 } 113 84 114 85 pub fn input_disabled_test() { 115 - widgets.input_widget("text")( 116 - field.Field( 117 - name: "name", 118 - label: "Label", 119 - help_text: "", 120 - disabled: True, 121 - hidden: False, 122 - ), 86 + get_make_fun(widgets.input_widget("text"))( 87 + field.Field(name: "name", label: "Label", help_text: "", disabled: True), 123 88 formz.Valid("hello", formz.Optional), 124 89 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 125 90 ) ··· 127 92 } 128 93 129 94 pub fn input_sanitized_value_test() { 130 - widgets.input_widget("text")( 131 - field.Field( 132 - name: "name", 133 - label: "Label", 134 - help_text: "", 135 - disabled: False, 136 - hidden: False, 137 - ), 95 + get_make_fun(widgets.input_widget("text"))( 96 + field.Field(name: "name", label: "Label", help_text: "", disabled: False), 138 97 formz.Valid("hello\"<-_=>", formz.Optional), 139 98 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 140 99 ) ··· 142 101 } 143 102 144 103 pub fn checkbox_checked_test() { 145 - widgets.checkbox_widget()( 146 - field.Field( 147 - name: "name", 148 - label: "Label", 149 - help_text: "", 150 - disabled: False, 151 - hidden: False, 152 - ), 104 + get_make_fun(widgets.checkbox_widget())( 105 + field.Field(name: "name", label: "Label", help_text: "", disabled: False), 153 106 formz.Valid("on", formz.Optional), 154 107 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 155 108 ) ··· 157 110 } 158 111 159 112 pub fn checkbox_unchecked_test() { 160 - widgets.checkbox_widget()( 161 - field.Field( 162 - name: "name", 163 - label: "Label", 164 - help_text: "", 165 - disabled: False, 166 - hidden: False, 167 - ), 113 + get_make_fun(widgets.checkbox_widget())( 114 + field.Field(name: "name", label: "Label", help_text: "", disabled: False), 168 115 formz.Valid("", formz.Optional), 169 116 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 170 117 ) ··· 172 119 } 173 120 174 121 pub fn password_test() { 175 - widgets.password_widget()( 176 - field.Field( 177 - name: "name", 178 - label: "Label", 179 - help_text: "", 180 - disabled: False, 181 - hidden: False, 182 - ), 122 + get_make_fun(widgets.password_widget())( 123 + field.Field(name: "name", label: "Label", help_text: "", disabled: False), 183 124 formz.Valid("pass", formz.Optional), 184 125 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 185 126 ) ··· 187 128 } 188 129 189 130 pub fn numeric_no_step_test() { 190 - widgets.number_widget("")( 191 - field.Field( 192 - name: "name", 193 - label: "Label", 194 - help_text: "", 195 - disabled: False, 196 - hidden: False, 197 - ), 131 + get_make_fun(widgets.number_widget(""))( 132 + field.Field(name: "name", label: "Label", help_text: "", disabled: False), 198 133 formz.Valid("1", formz.Optional), 199 134 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 200 135 ) ··· 202 137 } 203 138 204 139 pub fn numeric_step_test() { 205 - widgets.number_widget("0.1")( 206 - field.Field( 207 - name: "name", 208 - label: "Label", 209 - help_text: "", 210 - disabled: False, 211 - hidden: False, 212 - ), 140 + get_make_fun(widgets.number_widget("0.1"))( 141 + field.Field(name: "name", label: "Label", help_text: "", disabled: False), 213 142 formz.Valid("1.0", formz.Optional), 214 143 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 215 144 ) ··· 217 146 } 218 147 219 148 pub fn select_test() { 220 - widgets.select_widget([#("One", "0"), #("Two", "1"), #("Three", "2")])( 221 - field.Field( 222 - name: "name", 223 - label: "Label", 224 - help_text: "", 225 - disabled: False, 226 - hidden: False, 227 - ), 149 + get_make_fun( 150 + widgets.select_widget([#("One", "0"), #("Two", "1"), #("Three", "2")]), 151 + )( 152 + field.Field(name: "name", label: "Label", help_text: "", disabled: False), 228 153 formz.Valid("", formz.Optional), 229 154 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 230 155 ) ··· 232 157 } 233 158 234 159 pub fn select_selected_test() { 235 - widgets.select_widget([#("One", "0"), #("Two", "1"), #("Three", "2")])( 236 - field.Field( 237 - name: "name", 238 - label: "Label", 239 - help_text: "", 240 - disabled: False, 241 - hidden: False, 242 - ), 160 + get_make_fun( 161 + widgets.select_widget([#("One", "0"), #("Two", "1"), #("Three", "2")]), 162 + )( 163 + field.Field(name: "name", label: "Label", help_text: "", disabled: False), 243 164 formz.Valid("1", formz.Optional), 244 165 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 245 166 ) ··· 247 168 } 248 169 249 170 pub fn select_required_test() { 250 - widgets.select_widget([#("One", "0"), #("Two", "1"), #("Three", "2")])( 251 - field.Field( 252 - name: "name", 253 - label: "Label", 254 - help_text: "", 255 - disabled: False, 256 - hidden: False, 257 - ), 171 + get_make_fun( 172 + widgets.select_widget([#("One", "0"), #("Two", "1"), #("Three", "2")]), 173 + )( 174 + field.Field(name: "name", label: "Label", help_text: "", disabled: False), 258 175 formz.Valid("", formz.Required), 259 176 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 260 177 )