this repo has no description
4
fork

Configure Feed

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

API tweaks

I don’t even know. Just an absolute ton of little changes that add up to a lot of big changes. Trying to refine the API and make things as simple as possible. Not sure if I’m making any progress.

+1688 -1129
+2 -2
formz/README.md
··· 203 203 204 204 However, often you want to parse a form, and then... you know... act on that 205 205 data, and in doing so you might discover more errors for the form. In this 206 - situation you can use `parse_then_try`: 206 + situation you can use `decode_then_try`: 207 207 208 208 ```gleam 209 209 pub fn handle_form_submission(req: Request) -> Response { ··· 211 211 212 212 let result = make_form() 213 213 |> formz.data(formdata.values) 214 - |> formz.parse_then_try(fn(form, credentials) { 214 + |> formz.decode_then_try(fn(form, credentials) { 215 215 case credentials { 216 216 #("admin" as username, "l33t") -> Ok(username) 217 217 #("admin", _) ->
+752 -272
formz/src/formz.gleam
··· 4 4 //// makes the form using the `use` syntax, and then be able to use the form 5 5 //// later for parsing or rendering in different contexts. 6 6 //// 7 + //// You can use this cheatsheet to navigate the module documentation: 8 + //// 9 + //// <table> 10 + //// <tr> 11 + //// <td>Creating a form</td> 12 + //// <td> 13 + //// <a href="#create_form">create_form</a><br> 14 + //// <a href="#require">require</a><br> 15 + //// <a href="#optional">optional</a><br> 16 + //// <a href="#list">list</a><br> 17 + //// <a href="#limited_list">limited_list</a><br> 18 + //// <a href="#subform">subform</a> 19 + //// </td> 20 + //// </tr> 21 + //// <tr> 22 + //// <td>Decoding and validating a form</td> 23 + //// <td> 24 + //// <a href="#data">data</a><br> 25 + //// <a href="#decode">decode</a><br> 26 + //// <a href="#decode_then_try">decode_then_try</a><br> 27 + //// <a href="#validate">validate</a><br> 28 + //// <a href="#validate_all">validate_all</a> 29 + //// </td> 30 + //// </tr> 31 + //// <tr> 32 + //// <td>Creating a field definition</td> 33 + //// <td> 34 + //// <a href="#definition">definition</a><br> 35 + //// <a href="#definition_with_custom_optional">definition_with_custom_optional</a><br> 36 + //// <a href="#transform">transform</a><br> 37 + //// <a href="#transform_with_custom_optional">transform_with_custom_optional</a><br> 38 + //// <a href="#verify">verify</a><br> 39 + //// <a href="#widget">widget</a> 40 + //// </td> 41 + //// </tr> 42 + //// <tr> 43 + //// <td>Creating a limited list</td> 44 + //// <td> 45 + //// <a href="#limit_at_least">limit_at_least</a><br> 46 + //// <a href="#limit_at_most">limit_at_most</a><br> 47 + //// <a href="#limit_between">limit_between</a><br> 48 + //// <a href="#simple_blank_fields">simple_blank_fields</a> 49 + //// </td> 50 + //// </tr> 51 + //// <tr> 52 + //// <td>Accessing and manipulating form fields</td> 53 + //// <td> 54 + //// <a href="#get">get</a><br> 55 + //// <a href="#items">items</a><br> 56 + //// <a href="#set_field_error">set_field_error</a><br> 57 + //// <a href="#set_listfield_errors">set_listfield_errors</a><br> 58 + //// <a href="#update">update</a><br> 59 + //// <a href="#update_field">update_field</a><br> 60 + //// <a href="#update_listfield">update_listfield</a><br> 61 + //// <a href="#update_subform">update_subform</a> 62 + //// </td> 63 + //// </tr> 64 + //// </table> 65 + //// 7 66 //// ### Examples 8 67 //// 9 68 //// ```gleam ··· 16 75 //// fn process_form() { 17 76 //// make_form() 18 77 //// |> formz.data([#("name", "Louis"))]) 19 - //// |> formz.parse 78 + //// |> formz.decode 20 79 //// # -> Ok("Louis") 21 80 //// } 22 81 //// ``` ··· 32 91 //// fn process_form() { 33 92 //// make_form() 34 93 //// |> data([#("greeting", "Hello"), #("name", "World")]) 35 - //// |> formz.parse 94 + //// |> formz.decode 36 95 //// # -> Ok("Hello World") 37 96 //// } 38 97 //// ``` 39 98 40 - import formz/definition.{type Definition, Definition} 41 99 import formz/field 42 100 import formz/subform 43 101 import gleam/dict.{type Dict} 102 + import gleam/int 44 103 import gleam/list 45 104 import gleam/option.{None, Some} 46 105 import gleam/result 106 + import gleam/string 47 107 48 - /// The `widget` type is the set by the `Definition`s used to add fields for 108 + /// You create this using the `create_form` function. 109 + /// 110 + /// The `widget` type is set by the `Definition`s used to add fields for 49 111 /// this form, and has the details of how to turn given fields into HTML inputs. 50 112 /// 51 113 /// The `output` type is the type of the decoded data from the form. This is 52 - /// set by the `create_form` function, after all the fields have been added. 114 + /// set directly by the `create_form` function, after all the fields have 115 + /// been added. 53 116 pub opaque type Form(widget, output) { 54 117 Form( 55 118 items: List(FormItem(widget)), 56 - parse: fn(List(FormItem(widget))) -> Result(output, List(FormItem(widget))), 119 + decode: fn(List(FormItem(widget))) -> Result(output, List(FormItem(widget))), 57 120 stub: output, 58 121 ) 59 122 } 60 123 61 - /// A form contains a list of fields and subforms. You primarily only use these 62 - /// when writing a form generator function. You can also manipulate these 63 - /// after a form has been created, to change things like labels, help text, etc. 64 - /// There are specific functions,`update_field` and `update_subform`, to help 65 - /// with this, so you don't have to pattern match when updating a specific item. 124 + /// A form is a decode function and a list of `FormItem`s. You add `FormItem`s 125 + /// to a form using the `optional`, `require`, `list`, etc functions. You 126 + /// primarily only use these directly when writing a form generator function to 127 + /// output your function to HTML. 128 + /// 129 + /// You can also manipulate these after a form has been created, to change 130 + /// things like labels, help text, etc. There are specific functions, 131 + /// `update_field`, `update_listfield` and `update_subform`, to help with this, 132 + /// so you don't have to pattern match when updating a specific item. 66 133 /// 67 134 /// ``` 68 135 /// let form = make_form() 69 136 /// form.update_field("name", field.set_label(_, "Full Name")) 70 137 /// ``` 71 138 pub type FormItem(widget) { 72 - Field(detail: field.Field, state: State, widget: widget) 73 - MultiField(detail: field.Field, states: List(State), widget: widget) 139 + /// A single field that (generally speaking) corresponds to a single 140 + /// HTML input 141 + Field(detail: field.Field, state: FieldState, widget: widget) 142 + /// A single field that a consumer can submit multiple values for. 143 + ListField( 144 + detail: field.Field, 145 + states: List(FieldState), 146 + blank_fields: fn(Int) -> List(FieldState), 147 + widget: widget, 148 + ) 149 + /// A group of fields that are added as and then parsed to a single unit. 74 150 SubForm(detail: subform.SubForm, items: List(FormItem(widget))) 75 151 } 76 152 77 - pub type State { 78 - Valid(value: String) 79 - Invalid(value: String, error: String) 153 + /// The state of the field, this is used to track the current raw value, 154 + /// whether the field is required or not, if the field has been validated, 155 + /// and the outcome of that validation. 156 + pub type FieldState { 157 + Unvalidated(value: String, presence: FieldPresence) 158 + Valid(value: String, presence: FieldPresence) 159 + Invalid(value: String, presence: FieldPresence, error: String) 160 + } 161 + 162 + /// Whether a field is required or not. 163 + pub type FieldPresence { 164 + Optional 165 + Required 166 + } 167 + 168 + /// A `Definition` is the second argument needed to [add a field to a form](#require). 169 + /// It is what describes how a field works, e.g. how it looks and how it's 170 + /// parsed. It is the heavy compared to the lightness of a [Field](https://hexdocs.pm/formz/formz/field.html); 171 + /// they take a bit more work to make as they are intended to be reusable. 172 + /// 173 + /// The first role of a `Defintion` is to generate the HTML widget for the field. 174 + /// This library is format-agnostic and you can generate HTML widgets as raw 175 + /// strings, Lustre elements, Nakai nodes, something else, etc. There are 176 + /// currently three `formz` libraries that provide common field 177 + /// definitions (and widgets) for the most common HTML inputs. 178 + /// 179 + /// - [formz_string](https://hexdocs.pm/formz_string/) 180 + /// - [formz_nakai](https://hexdocs.pm/formz_nakai/) 181 + /// - [formz_lustre](https://hexdocs.pm/formz_lustre/) (untested in a browser, 182 + /// would it be useful there??) 183 + /// 184 + /// The second role of a `Definition` is to parse the data from the field. 185 + pub opaque type Definition(widget, required, optional) { 186 + Definition( 187 + widget: widget, 188 + parse: fn(String) -> Result(required, String), 189 + stub: required, 190 + /// If a field is marked as optional, this function is called, with the 191 + /// above parse as an argument. The idea is that this function will 192 + /// call out to the parse function if the field is not empty, and 193 + /// this should only handle the case where the raw input value is empty. 194 + /// This function is necessary because not all fields should just be parsed 195 + /// into an `Option` when they aren't provided. 196 + /// For example, an optional text field might be an empty string, 197 + /// an optional checkbox might be `False`, and an optional select might 198 + /// be `option.None`. 199 + optional_parse: fn(fn(String) -> Result(required, String), String) -> 200 + Result(optional, String), 201 + optional_stub: optional, 202 + ) 203 + } 204 + 205 + type ListParsingResult(input_output) { 206 + ListParsingResult( 207 + value: String, 208 + presence: FieldPresence, 209 + output: Result(input_output, String), 210 + ) 80 211 } 81 212 82 213 /// Create an empty form that only parses to `thing`. This is primarily ··· 85 216 /// 86 217 /// ```gleam 87 218 /// create_form(1) 88 - /// |> parse 219 + /// |> decode 89 220 /// # -> Ok(1) 90 221 /// ``` 91 222 /// ```gleam ··· 94 225 /// use field2 <- formz.require(field("field2"), definitions.text_field()) 95 226 /// use field3 <- formz.require(field("field3"), definitions.text_field()) 96 227 /// 97 - /// create_form(#(field1, field2, field3)) 228 + /// formz.create_form(#(field1, field2, field3)) 98 229 /// } 99 230 /// ``` 100 231 pub fn create_form(thing: thing) -> Form(widget, thing) { 101 232 Form([], fn(_) { Ok(thing) }, thing) 102 233 } 103 234 104 - fn add( 105 - detail: field.Field, 235 + /// Add an optional field to a form. 236 + /// 237 + /// Ultimately whether a field is actually optional or not comes down to the 238 + /// details of the definition. The definition will receive the raw input string 239 + /// and is in charge of returning an error or an optional value. 240 + /// 241 + /// If multiple values are submitted for this field, the last one will be used. 242 + /// 243 + /// The final argument is a callback that will be called when the form 244 + /// is being... constructed to look for more fields; validated to check for 245 + /// errors; and decoded to parse the input data. **For this reason, the 246 + /// callback should be a function without side effects.** It can be called any 247 + /// number of times. Don't do anything but create the type with the data you 248 + /// need! If you need to do decoding that has side effects, you should use 249 + /// `decode_then_try` instead. 250 + pub fn optional( 251 + field: field.Field, 252 + definition: Definition(widget, _, input_output), 253 + next: fn(input_output) -> Form(widget, form_output), 254 + ) -> Form(widget, form_output) { 255 + add_field( 256 + field, 257 + Optional, 258 + definition.widget, 259 + definition.optional_parse(definition.parse, _), 260 + definition.optional_stub, 261 + next, 262 + ) 263 + } 264 + 265 + /// Add a required field to a form. 266 + /// 267 + /// Ultimately whether a field is actually required or not comes down to the 268 + /// details of the definition. The definition will receive the raw input string 269 + /// and is in charge of returning an error or a value. 270 + /// 271 + /// If multiple values are submitted for this field, the last one will be used. 272 + /// 273 + /// The final argument is a callback that will be called when the form 274 + /// is being... constructed to look for more fields; validated to check for 275 + /// errors; and decoded to parse the input data. **For this reason, the 276 + /// callback should be a function without side effects.** It can be called any 277 + /// number of times. Don't do anything but create the type with the data you 278 + /// need! If you need to do decoding that has side effects, you should use 279 + /// `decode_then_try` instead. 280 + pub fn require( 281 + field: field.Field, 282 + is definition: Definition(widget, required_output, _), 283 + next next: fn(required_output) -> Form(widget, form_output), 284 + ) -> Form(widget, form_output) { 285 + add_field( 286 + field, 287 + Required, 288 + definition.widget, 289 + definition.parse, 290 + definition.stub, 291 + next, 292 + ) 293 + } 294 + 295 + fn add_field( 296 + field: field.Field, 297 + required: FieldPresence, 106 298 widget: widget, 107 299 parse_field: fn(String) -> Result(input_output, String), 108 300 stub: input_output, 109 - rest fun: fn(input_output) -> Form(widget, form_output), 301 + next: fn(input_output) -> Form(widget, form_output), 110 302 ) -> Form(widget, form_output) { 111 303 // we pass in our stub value, and we're going to throw away the 112 304 // decoded result here, we just care about pulling out the fields 113 305 // from the form. 114 - let next_form = fun(stub) 306 + let next_form = next(stub) 115 307 116 308 // prepend the new field to the items from the form we got in the previous step. 117 - let updated_items = [Field(detail, Valid(""), widget), ..next_form.items] 309 + let updated_items = [ 310 + Field(field, Unvalidated("", required), widget), 311 + ..next_form.items 312 + ] 118 313 119 - // now create the parse function. parse function accepts most recent 314 + // now create the decode function. decode function accepts most recent 120 315 // version of input list, since data can be added to it. the list 121 316 // above we just needed for the initial setup. 122 - let parse = fn(items: List(FormItem(widget))) { 317 + let decode = fn(items: List(FormItem(widget))) { 123 318 // pull out the latest version of this field to get latest input data 124 319 let assert [Field(field, state, widget), ..next_items] = items 125 320 126 - // transform the input data using the transform/validate/decode/etc function 321 + // transform the input data using the decode function 127 322 let item_output = parse_field(state.value) 128 323 129 324 // pass our transformed input data to the next function/form. if 130 325 // we errored we still do this with our stub so we can continue 131 326 // processing all the fields in the form. if we're on the error track 132 327 // we'll throw away the "output" made with this and just keep the error 133 - let next_form = fun(item_output |> result.unwrap(stub)) 134 - let form_output = next_form.parse(next_items) 328 + let next_form = next(item_output |> result.unwrap(stub)) 329 + let form_output = next_form.decode(next_items) 135 330 136 331 // ok, check which track we're on 137 332 case form_output, item_output { ··· 141 336 // form has errors, but this field was good, so add it to the list 142 337 // of fields as is. 143 338 Error(error_items), Ok(_) -> { 144 - let f = Field(field, Valid(state.value), widget) 339 + let f = Field(field, Valid(state.value, required), widget) 145 340 Error([f, ..error_items]) 146 341 } 147 342 148 343 // form was good so far, but this field errored, so need to 149 - // mark this field as invalid and return all the fields we've got 150 - // so far 344 + // mark this field as invalid, mark all the existing fields as valid, 345 + // and return all the fields we've got so far 151 346 Ok(_), Error(error) -> { 152 - let f = Field(field, Invalid(state.value, error), widget) 153 - Error([f, ..next_items]) 347 + let f = Field(field, Invalid(state.value, required, error), widget) 348 + Error([f, ..mark_all_fields_as_valid(next_items)]) 154 349 } 155 350 156 - // form already has errors and this field errored, so add this field 157 - // to the list of errors 351 + // form already has errors and this field errored, so mark this field 352 + // as invalid, and add it to the list of errors 158 353 Error(error_items), Error(error) -> { 159 - let f = Field(field, Invalid(state.value, error), widget) 354 + let f = Field(field, Invalid(state.value, required, error), widget) 160 355 Error([f, ..error_items]) 161 356 } 162 357 } 163 358 } 164 - Form(items: updated_items, parse:, stub: next_form.stub) 359 + Form(items: updated_items, decode:, stub: next_form.stub) 165 360 } 166 361 167 - /// Add an optional field to a form. 168 - /// 169 - /// This will use both the `parse` and `optional_parse` functions from the 170 - /// definition to parse the input data when parsing this field. Ultimately 171 - /// whether a field is actually optional or not comes down to the details 172 - /// of the definition. 173 - /// 174 - /// The final argument is a callback that will be called when the form 175 - /// is being constructed to look for more fields; validated to check for errors; 176 - /// and when the form is being parsed, to decode the input data. **For this 177 - /// reason, the callback should be a function without side effects.** It can be 178 - /// called any number of times. Don't do anything but create the type with the 179 - /// data you need! 180 - pub fn optional( 181 - detail: field.Field, 182 - is definition: Definition(widget, _, input_output), 183 - rest fun: fn(input_output) -> Form(widget, form_output), 184 - ) -> Form(widget, form_output) { 185 - add( 186 - detail |> field.set_required(False), 187 - definition.widget, 188 - definition.optional_parse(definition.parse, _), 189 - definition.optional_stub, 190 - fun, 191 - ) 362 + pub fn simple_blank_fields( 363 + min: Int, 364 + max: Int, 365 + extra: Int, 366 + ) -> fn(Int) -> List(FieldState) { 367 + fn(num_nonempty) { 368 + int.max(1 - num_nonempty, int.min(extra, max - num_nonempty)) 369 + list.fold([1, extra, min], 0, int.max) 370 + 371 + // if they've specified a minimum required, then start there 372 + let num_required = int.max(min - num_nonempty, 0) 373 + 374 + // at least one field needs to be present. don't want a form that is asking 375 + // for no input 376 + let num_base = case num_nonempty, num_required { 377 + x, y if x > 0 || y > 0 -> 0 378 + _, _ -> 1 379 + } 380 + 381 + // they've asked for extra fields, add those unless doing so would 382 + // exceed the max 383 + let num_extra = int.min(extra, max - num_nonempty) 384 + 385 + // take whatever's bigger for our optional ones, either the bare minimum 386 + // (base) or the extra if they've got room for it and they've asked for it 387 + let num_optional = int.max(num_base, num_extra) - num_required 388 + 389 + list.append( 390 + list.repeat(Unvalidated("", Required), num_required), 391 + list.repeat(Unvalidated("", Optional), num_optional), 392 + ) 393 + } 192 394 } 193 395 194 - /// Add a required field to a form. 396 + /// Convenience function for creating a `BlankFieldsFunc` with a minimum number 397 + /// of required values. This sets the maximum to `1,000,000`, effectively unlimited. 398 + pub fn limit_at_least(min: Int) { 399 + simple_blank_fields(min, 1_000_000, 1) 400 + } 401 + 402 + /// Convenience function for creating a `BlankFieldsFunc` with a maximum number 403 + /// of accepted values. This sets the minimum to `0`. 404 + pub fn limit_at_most(max: Int) { 405 + simple_blank_fields(0, max, 1) 406 + } 407 + 408 + /// Convenience function for creating a `BlankFieldsFunc` with a minimum and maximum 409 + /// number of values. 410 + pub fn limit_between(min: Int, max: Int) { 411 + simple_blank_fields(min, max, 1) 412 + } 413 + 414 + /// Add a list field to a form, but with limits on the number of values that 415 + /// can be submitted. 195 416 /// 196 - /// This will use only the `parse` function from the definition to parse the 197 - /// input data when parsing this field. Ultimately whether a field is actually 198 - /// required or not comes down to the details of the definition. 417 + /// When adding a list field to a form, you have to provide a function that 418 + /// tells the form if and how many blank fields to provide for the 419 + /// consumer to fill out. 199 420 /// 200 - /// This will also set the `required` value on the field to `True`. Form 201 - /// generators can use this to mark the HTML input elements as required for 202 - /// accessibility. 421 + /// For example, you are presenting an empty form to a consumer, and you 422 + /// want to show three blank fields for them to fill out, or you want to always 423 + /// show one more blank field than the number of values that already belong to 424 + /// the form, etc. 425 + /// 426 + /// This function takes as its only argument, the number of fields that already 427 + /// have a value. It should return a list of `Unvalidated` `FieldState` items 428 + /// that specify if the value is required or not. 429 + /// 430 + /// There are helper functions, `limit_at_least`, `limit_at_most`, and 431 + /// `limit_between` or more generally `simple_blank_fields` for the most common 432 + /// use cases. 203 433 /// 204 434 /// The final argument is a callback that will be called when the form 205 - /// is being constructed to look for more fields; validated to check for errors; 206 - /// and when the form is being parsed, to decode the input data. **For this 207 - /// reason, the callback should be a function without side effects.** It can be 208 - /// called any number of times. Don't do anything but create the type with the 209 - /// data you need! 210 - pub fn require( 211 - detail: field.Field, 212 - is definition: Definition(widget, required_output, _), 213 - rest fun: fn(required_output) -> Form(widget, form_output), 214 - ) -> Form(widget, form_output) { 215 - add( 216 - detail |> field.set_required(True), 217 - definition.widget, 218 - definition.parse, 219 - definition.stub, 220 - fun, 221 - ) 222 - } 223 - 224 - pub fn list( 225 - detail: field.Field, 435 + /// is being... constructed to look for more fields; validated to check for 436 + /// errors; and decoded to parse the input data. **For this reason, the 437 + /// callback should be a function without side effects.** It can be called any 438 + /// number of times. Don't do anything but create the type with the data you 439 + /// need! If you need to do decoding that has side effects, you should use 440 + /// `decode_then_try` instead. 441 + pub fn limited_list( 442 + blank_fields: fn(Int) -> List(FieldState), 443 + field: field.Field, 226 444 is definition: Definition(widget, required_output, _), 227 - rest fun: fn(List(required_output)) -> Form(widget, form_output), 445 + next next: fn(List(required_output)) -> Form(widget, form_output), 228 446 ) -> Form(widget, form_output) { 229 447 // we pass in our stub value, and we're going to throw away the 230 448 // decoded result here, we just care about pulling out the fields 231 449 // from the form. 232 - let next_form = fun([definition.stub]) 450 + let next_form = next([definition.stub]) 233 451 234 452 // prepend the new field to the items from the form we got in the previous step. 235 453 let updated_items = [ 236 - MultiField(detail, [Valid("")], definition.widget), 454 + ListField(field, blank_fields(0), blank_fields, definition.widget), 237 455 ..next_form.items 238 456 ] 239 457 240 - // now create the parse function. parse function accepts most recent 458 + // now create the decode function. decode function accepts most recent 241 459 // version of input list, since data can be added to it. the list 242 460 // above we just needed for the initial setup. 243 - let parse = fn(items: List(FormItem(widget))) { 461 + let decode = fn(items: List(FormItem(widget))) { 244 462 // pull out the latest version of this field to get latest input data 245 - let assert [MultiField(field, states, widget), ..next_items] = items 463 + let assert [ListField(field, states, blank_fields, widget), ..next_items] = 464 + items 246 465 247 - // transform the input data using the transform/validate/decode/etc function 466 + // go through all decode all input values. these can have empty rows 467 + // that were offered to the consumer, but they didn't fill out. we need 468 + // to keep track of those so they can be used when generating the form 469 + // again on error 248 470 let item_results = 249 - states 250 - |> list.map(fn(state) { #(state.value, definition.parse(state.value)) }) 471 + list.map(states, parse_list_state(_, definition.parse, definition.stub)) 251 472 252 - let item_outputs = item_results |> list.map(fn(t) { t.1 }) |> result.all 473 + // but we don't want them for considering the output of this list field, 474 + // so remove the empty optional fields and just grab the outputs 475 + let item_outputs = 476 + item_results |> outputs_for_required_or_nonempty |> result.all 253 477 254 478 // pass our transformed input data to the next function/form. if 255 479 // we errored we still do this with our stub so we can continue 256 480 // processing all the fields in the form. if we're on the error track 257 481 // we'll throw away the "output" made with this and just keep the error 258 - let next_form = fun(item_outputs |> result.unwrap([definition.stub])) 259 - let form_output = next_form.parse(next_items) 482 + let next_form = next(item_outputs |> result.unwrap([definition.stub])) 483 + let form_output = next_form.decode(next_items) 260 484 261 485 // ok, check which track we're on 262 486 case form_output, item_outputs { 263 487 // everything is good! pass along the output 264 488 Ok(_), Ok(_) -> form_output 265 489 266 - // form has errors, but this field was good, so add it to the list 267 - // of fields as is. 490 + // form has errors, but this field was good, so mark all states as valid 268 491 Error(error_items), Ok(_) -> { 269 - let f = 270 - MultiField( 271 - field, 272 - states |> list.map(fn(s) { Valid(s.value) }), 273 - widget, 274 - ) 492 + let states = states |> list.map(fn(s) { Valid(s.value, s.presence) }) 493 + let f = ListField(field, states, blank_fields, widget) 275 494 Error([f, ..error_items]) 276 495 } 277 496 278 497 // form was good so far, but this field errored, so need to 279 - // mark this field as invalid and return all the fields we've got 280 - // so far 498 + // mark the errored states as invalid, and mark the successful fields as 499 + // valid, and then return all these fields we've got so far 281 500 Ok(_), Error(_) -> { 282 - let f = 283 - MultiField( 284 - field, 285 - item_results 286 - |> list.map(fn(t) { 287 - case t.1 { 288 - Ok(_) -> Valid(t.0) 289 - Error(e) -> Invalid(t.0, e) 290 - } 291 - }), 292 - widget, 293 - ) 294 - Error([f, ..next_items]) 501 + let states = list.map(item_results, state_from_parse_result) 502 + let f = ListField(field, states, blank_fields, widget) 503 + Error([f, ..mark_all_fields_as_valid(next_items)]) 295 504 } 296 505 297 506 // form already has errors and this field errored, so add this field 298 - // to the list of errors 507 + // to the list of errors, but first marking the invalid states as... invalid 299 508 Error(error_items), Error(_) -> { 300 - let f = 301 - MultiField( 302 - field, 303 - item_results 304 - |> list.map(fn(t) { 305 - case t.1 { 306 - Ok(_) -> Valid(t.0) 307 - Error(e) -> Invalid(t.0, e) 308 - } 309 - }), 310 - widget, 311 - ) 509 + let states = list.map(item_results, state_from_parse_result) 510 + let f = ListField(field, states, blank_fields, widget) 312 511 Error([f, ..error_items]) 313 512 } 314 513 } 315 514 } 316 - Form(items: updated_items, parse:, stub: next_form.stub) 515 + 516 + Form(items: updated_items, decode:, stub: next_form.stub) 517 + } 518 + 519 + fn state_from_parse_result( 520 + result: ListParsingResult(input_output), 521 + ) -> FieldState { 522 + let ListParsingResult(value, presence, output) = result 523 + case output { 524 + Ok(_) -> Valid(value, presence) 525 + Error(error) -> Invalid(value, presence, error) 526 + } 527 + } 528 + 529 + fn outputs_for_required_or_nonempty( 530 + states: List(ListParsingResult(output)), 531 + ) -> List(Result(output, String)) { 532 + list.filter_map(states, fn(result) { 533 + case result.value, result.presence { 534 + "", Optional -> Error(Nil) 535 + _, _ -> Ok(result.output) 536 + } 537 + }) 538 + } 539 + 540 + fn parse_list_state( 541 + state: FieldState, 542 + parse: fn(String) -> Result(a, String), 543 + stub: a, 544 + ) -> ListParsingResult(a) { 545 + case state.value, state.presence { 546 + "", Required -> parse(state.value) 547 + "", Optional -> Ok(stub) 548 + _, _ -> parse(state.value) 549 + } 550 + |> ListParsingResult(state.value, state.presence, _) 551 + } 552 + 553 + /// Add a list field to a form, but with no limits on the number of values that 554 + /// can be submitted. A list field is like a normal field except a consumer can 555 + /// submit multiple values, and it will return a `List` of the parsed values. 556 + /// 557 + /// The final argument is a callback that will be called when the form 558 + /// is being... constructed to look for more fields; validated to check for 559 + /// errors; and decoded to parse the input data. **For this reason, the 560 + /// callback should be a function without side effects.** It can be called any 561 + /// number of times. Don't do anything but create the type with the data you 562 + /// need! If you need to do decoding that has side effects, you should use 563 + /// `decode_then_try` instead. 564 + pub fn list( 565 + field: field.Field, 566 + is definition: Definition(widget, required_output, _), 567 + next next: fn(List(required_output)) -> Form(widget, form_output), 568 + ) -> Form(widget, form_output) { 569 + limited_list(limit_at_most(1_000_000), field, definition, next) 570 + } 571 + 572 + fn add_prefix_to_item( 573 + item: FormItem(widget), 574 + prefix: String, 575 + ) -> FormItem(widget) { 576 + case item { 577 + Field(item_details, state, widget) -> { 578 + let name = prefix <> "." <> item_details.name 579 + Field(item_details |> field.set_name(name), state, widget) 580 + } 581 + ListField(item_details, states, blank_fields, widget) -> { 582 + let name = prefix <> "." <> item_details.name 583 + ListField( 584 + item_details |> field.set_name(name), 585 + states, 586 + blank_fields, 587 + widget, 588 + ) 589 + } 590 + SubForm(item_details, sub_items) -> { 591 + let name = prefix <> "." <> item_details.name 592 + SubForm(item_details |> subform.set_name(name), sub_items) 593 + } 594 + } 317 595 } 318 596 319 597 /// Add a form as a subform. This will essentially append the fields from the ··· 322 600 /// can be marked up as a group for accessibility reasons. 323 601 /// 324 602 /// The final argument is a callback that will be called when the form 325 - /// is being constructed to look for more fields; validated to check for errors; 326 - /// and when the form is being parsed, to decode the input data. **For this 327 - /// reason, the callback should be a function without side effects.** It can be 328 - /// called any number of times. Don't do anything but create the type with the 329 - /// data you need! 603 + /// is being... constructed to look for more fields; validated to check for 604 + /// errors; and decoded to parse the input data. **For this reason, the 605 + /// callback should be a function without side effects.** It can be called any 606 + /// number of times. Don't do anything but create the type with the data you 607 + /// need! If you need to do decoding that has side effects, you should use 608 + /// `decode_then_try` instead. 330 609 pub fn subform( 331 - details: subform.SubForm, 332 - sub: Form(widget, sub_output), 333 - fun: fn(sub_output) -> Form(widget, form_output), 610 + subform: subform.SubForm, 611 + form: Form(widget, sub_output), 612 + next: fn(sub_output) -> Form(widget, form_output), 334 613 ) -> Form(widget, form_output) { 335 - let next_form = fun(sub.stub) 336 - 337 - let sub_items = 338 - sub.items 339 - |> list.map(fn(item) { 340 - case item { 341 - Field(item_details, state, widget) -> { 342 - let name = details.name <> "." <> item_details.name 343 - Field(item_details |> field.set_name(name), state, widget) 344 - } 345 - MultiField(item_details, states, widget) -> { 346 - let name = details.name <> "." <> item_details.name 347 - MultiField(item_details |> field.set_name(name), states, widget) 348 - } 349 - SubForm(item_details, sub_items) -> { 350 - let name = details.name <> "." <> item_details.name 351 - SubForm(item_details |> subform.set_name(name), sub_items) 352 - } 353 - } 354 - }) 614 + let next_form = next(form.stub) 355 615 356 - let updated_items = [SubForm(details, sub_items), ..next_form.items] 616 + let sub_items = form.items |> list.map(add_prefix_to_item(_, subform.name)) 617 + let subform = SubForm(subform, sub_items) 618 + let updated_items = [subform, ..next_form.items] 357 619 358 - let parse = fn(items: List(FormItem(widget))) { 620 + let decode = fn(items: List(FormItem(widget))) { 359 621 // pull out the latest version of this field to get latest input data 360 622 let assert [SubForm(details, sub_items), ..next_items] = items 361 623 362 - let item_output = sub.parse(sub_items) 624 + let item_output = form.decode(sub_items) 363 625 364 - let next_form = fun(item_output |> result.unwrap(sub.stub)) 365 - let form_output = next_form.parse(next_items) 626 + let next_form = next(item_output |> result.unwrap(form.stub)) 627 + let form_output = next_form.decode(next_items) 366 628 367 629 // ok, check which track we're on 368 630 case form_output, item_output { ··· 371 633 372 634 // form has errors, but this sub form was good, so add it to the list 373 635 // of items as is. 374 - Error(next_error_items), Ok(_) -> 375 - Error([SubForm(details, sub_items), ..next_error_items]) 636 + Error(next_error_items), Ok(_) -> { 637 + let sub = SubForm(details, sub_items |> mark_all_fields_as_valid) 638 + Error([sub, ..next_error_items]) 639 + } 376 640 377 641 // form was good so far, but this sub form errored, so need to 378 642 // hop on error track 379 - Ok(_), Error(error_items) -> 380 - Error([SubForm(details, error_items), ..next_items]) 643 + Ok(_), Error(error_items) -> { 644 + let sub = SubForm(details, error_items) 645 + Error([sub, ..mark_all_fields_as_valid(next_items)]) 646 + } 381 647 382 648 // form already has errors and this form errored, so add this field 383 649 // to the list of errors 384 - Error(next_error_items), Error(error_items) -> 385 - Error([SubForm(details, error_items), ..next_error_items]) 650 + Error(next_error_items), Error(error_items) -> { 651 + let sub = SubForm(details, error_items) 652 + Error([sub, ..next_error_items]) 653 + } 386 654 } 387 655 } 388 - Form(updated_items, parse: parse, stub: next_form.stub) 389 - } 390 - 391 - @internal 392 - pub fn get_states(form: Form(widget, output)) -> List(State) { 393 - form.items |> do_get_states |> list.reverse 394 - } 395 - 396 - fn do_get_states(items: List(FormItem(widget))) -> List(State) { 397 - list.fold(items, [], fn(acc, item) { 398 - case item { 399 - Field(_, state, _) -> [state, ..acc] 400 - MultiField(_, states, _) -> list.append(states |> list.reverse, acc) 401 - SubForm(_, sub_items) -> list.flatten([do_get_states(sub_items), acc]) 402 - } 403 - }) 656 + Form(updated_items, decode:, stub: next_form.stub) 404 657 } 405 658 406 659 /// Add input data to this form. This will set the raw string value of the fields. ··· 410 663 /// 411 664 /// The input data is a list of tuples, where the first element is the name of the 412 665 /// field and the second element is the value to set. If the field does not exist 413 - /// the data is ignored, and if multiple values are given for the same field, the 414 - /// last one wins. 666 + /// the data is ignored. 415 667 /// 416 - /// This resets the error state of the fields that have data, so you'll need to 417 - /// re-validate the form after setting data. 668 + /// This resets the validation state of the fields that have data, so you'll need to 669 + /// re-validate or decode the form after setting data. 418 670 pub fn data( 419 671 form: Form(widget, output), 420 672 input_data: List(#(String, String)), 421 673 ) -> Form(widget, output) { 422 674 let data = to_dict(input_data) 423 - let Form(items, parse, stub) = form 424 - Form(do_data(items, data), parse, stub) 675 + let Form(items, decode, stub) = form 676 + Form(do_data(items, data), decode, stub) 425 677 } 426 678 427 - pub fn do_data( 679 + fn do_data( 428 680 items: List(FormItem(widget)), 429 681 data: Dict(String, List(String)), 430 682 ) -> List(FormItem(widget)) { 431 683 list.map(items, fn(item) { 432 - case item { 433 - Field(detail, _, widget) -> 434 - case dict.get(data, item.detail.name) { 435 - Ok([first, ..]) -> { 436 - Field(detail, Valid(first), widget) 437 - } 438 - _ -> item 439 - } 440 - MultiField(detail, _, widget) -> { 441 - case dict.get(data, item.detail.name) { 442 - Ok(values) -> { 443 - let values = list.map(values, fn(v) { Valid(v) }) |> list.reverse 444 - MultiField(detail, values, widget) 445 - } 684 + let values = dict.get(data, get_item_name(item)) 685 + case item, values { 686 + Field(detail, state, widget), Ok([_, ..] as values) -> { 687 + let assert Ok(last) = list.last(values) 688 + Field(detail, Unvalidated(last, state.presence), widget) 689 + } 690 + ListField(detail, states, blank_fields, widget), Ok(values) -> { 691 + let nonempty = list.filter(values, fn(v) { !string.is_empty(v) }) 692 + let num_nonempty = list.length(nonempty) 446 693 447 - _ -> item 448 - } 694 + let items = 695 + list.map2(states, nonempty, fn(s, v) { Unvalidated(v, s.presence) }) 696 + |> list.append( 697 + nonempty 698 + |> list.drop(list.length(states)) 699 + |> list.map(fn(v) { Unvalidated(v, Optional) }), 700 + ) 701 + |> list.append(blank_fields(num_nonempty)) 702 + 703 + ListField(detail, items, blank_fields, widget) 449 704 } 450 - SubForm(detail, items) -> SubForm(detail, do_data(items, data)) 705 + SubForm(detail, items), _ -> SubForm(detail, do_data(items, data)) 706 + _, _ -> item 451 707 } 452 708 }) 453 709 } 454 710 455 711 fn to_dict(values: List(#(String, String))) -> Dict(String, List(String)) { 456 - list.fold_right(values |> list.reverse, dict.new(), fn(acc, kv) { 712 + list.fold_right(values, dict.new(), fn(acc, kv) { 457 713 let #(key, value) = kv 458 714 dict.upsert(acc, key, fn(opt) { 459 715 case opt { ··· 464 720 }) 465 721 } 466 722 467 - /// Parse the form. This means step through the fields one by one, parsing 723 + /// Decode the form. This means step through the fields one by one, parsing 468 724 /// them individually. If any field fails to parse, the whole form is considered 469 725 /// invalid, however it will still continue parsing the rest of the fields to 470 726 /// collect all errors. This is useful for showing all errors at once. If no 471 727 /// fields fail to parse, the decoded value is returned, which is the value given 472 728 /// to `create_form`. 473 729 /// 474 - /// If you'd like to parse the form but not get the output, so you can give 730 + /// If you'd like to decode the form but not get the output, so you can give 475 731 /// feedback to a user in response to input, you can use `validate` or `validate_all`. 476 - pub fn parse(form: Form(widget, output)) -> Result(output, Form(widget, output)) { 477 - case form.parse(form.items) { 732 + pub fn decode( 733 + form: Form(widget, output), 734 + ) -> Result(output, Form(widget, output)) { 735 + case form.decode(form.items) { 478 736 Ok(output) -> Ok(output) 479 737 Error(items) -> Error(Form(..form, items:)) 480 738 } 481 739 } 482 740 483 741 /// Parse the form, then apply a function to the output if it was successful. 484 - /// This is a very thin wrapper around `parse` and `result.try`, but the 742 + /// This is a very thin wrapper around `decode` and `result.try`, but the 485 743 /// difference being it will pass the form along to the function as a second 486 744 /// argument in addition to the successful result. This allows you to easily 487 745 /// update the form fields with errors or other information based on the output. ··· 490 748 /// aren't easily checked in simple parsing functions. Like, say, hitting a 491 749 /// db to check if a username is taken. 492 750 /// 751 + /// As a reminder, parse functions will be called multiple times for each field 752 + /// when the form is being made, validated and parsed, and should not contain 753 + /// side effects. This function is the proper way to add errors to fields from 754 + /// functions that have side effects. 755 + /// 493 756 /// ```gleam 494 757 /// make_form() 495 758 /// |> data(form_data) 496 - /// |> parse_then_try(fn(username, form) { 759 + /// |> decode_then_try(fn(username, form) { 497 760 /// case is_username_taken(username) { 498 761 /// Ok(false) -> Ok(form) 499 - /// Ok(true) -> update_field(form, "username", field.set_error(_, "Username is taken")) 762 + /// Ok(true) -> set_field_error(form, "username", "Username is taken") 500 763 /// } 501 764 /// } 502 - pub fn parse_then_try( 765 + pub fn decode_then_try( 503 766 form: Form(widget, output), 504 767 apply fun: fn(Form(widget, output), output) -> Result(c, Form(widget, output)), 505 768 ) -> Result(c, Form(widget, output)) { 506 - form |> parse |> result.try(fun(form, _)) 769 + form 770 + |> decode 771 + |> result.try(fn(output) { 772 + let items = form.items |> mark_all_fields_as_valid 773 + fun(Form(..form, items:), output) 774 + }) 507 775 } 508 776 509 - /// Validate specific fields of the form. This is similar to `parse`, but 777 + fn mark_all_fields_as_valid( 778 + items: List(FormItem(widget)), 779 + ) -> List(FormItem(widget)) { 780 + list.map(items, fn(item) { 781 + case item { 782 + Field(field, state, widget) -> 783 + Field(field, Valid(state.value, state.presence), widget) 784 + ListField(field, states, blank_fields, widget) -> { 785 + let new_states = 786 + states 787 + |> list.map(fn(state) { Valid(state.value, state.presence) }) 788 + ListField(field, new_states, blank_fields, widget) 789 + } 790 + SubForm(subform, items) -> 791 + SubForm(subform, mark_all_fields_as_valid(items)) 792 + } 793 + }) 794 + } 795 + 796 + /// Validate specific fields of the form. This is similar to `decode`, but 510 797 /// instead of returning the decoded output if there are no errors, it returns 511 798 /// the valid form. This is useful for if you want to be able to give feedback 512 - /// to the user about whether certain fields are valid or not. In this case you 799 + /// to the user about whether certain fields are valid or not. For example, you 513 800 /// could just validate only fields that the user has interacted with. 514 801 pub fn validate( 515 802 form: Form(widget, output), 516 803 names: List(String), 517 804 ) -> Form(widget, output) { 518 - case form.parse(form.items) { 519 - Ok(_) -> form 805 + case form.decode(form.items) { 806 + Ok(_) -> { 807 + let items = 808 + list.map(form.items, fn(parsed_item) { 809 + let item_name = get_item_name(parsed_item) 810 + case list.find(names, fn(name) { item_name == name }) { 811 + Ok(_) -> { 812 + let assert Ok(item) = 813 + [parsed_item] |> mark_all_fields_as_valid |> list.first 814 + item 815 + } 816 + Error(_) -> { 817 + let assert Ok(original_item) = get(form, item_name) 818 + original_item 819 + } 820 + } 821 + }) 822 + Form(..form, items:) 823 + } 520 824 Error(items) -> { 521 825 let items = 522 826 list.map(items, fn(parsed_item) { 523 - let item_name = case parsed_item { 524 - Field(field, _, _) -> field.name 525 - MultiField(field, _, _) -> field.name 526 - SubForm(subform, _) -> subform.name 527 - } 827 + let item_name = get_item_name(parsed_item) 528 828 case list.find(names, fn(name) { item_name == name }) { 529 829 Ok(_) -> parsed_item 530 830 Error(_) -> { ··· 538 838 } 539 839 } 540 840 541 - /// Validate all the fields in the form. This is similar to `parse`, but 841 + /// Validate all the fields in the form. This is similar to `decode`, but 542 842 /// instead of returning the decoded output if there are no errors, it returns 543 843 /// the valid form. This is useful for if you want to be able to give feedback 544 844 /// to the user about whether certain fields are valid or not. 545 845 pub fn validate_all(form: Form(widget, output)) -> Form(widget, output) { 546 - let names = 547 - form.items 548 - |> list.map(fn(f) { 549 - case f { 550 - Field(field, _, _) -> field.name 551 - MultiField(field, _, _) -> field.name 552 - SubForm(subform, _) -> subform.name 553 - } 554 - }) 555 - 556 - validate(form, names) 846 + form.items |> list.map(get_item_name) |> validate(form, _) 557 847 } 558 848 559 849 /// Get each [`FormItem`](https://hexdocs.pm/formz/formz.html#FormItem) added 560 - /// to the form. Any time a field or subform are added, a FormItem is created. 850 + /// to the form. Any time a field, list field, or subform are added, a FormItem is created. 561 851 pub fn items(form: Form(widget, output)) -> List(FormItem(widget)) { 562 852 form.items 563 853 } ··· 568 858 form: Form(widget, output), 569 859 name: String, 570 860 ) -> Result(FormItem(widget), Nil) { 571 - list.find(form.items, fn(item) { 572 - let item_name = case item { 573 - Field(field, _, _) -> field.name 574 - MultiField(field, _, _) -> field.name 575 - SubForm(subform, _) -> subform.name 576 - } 577 - item_name == name 578 - }) 861 + list.find(form.items, fn(item) { name == get_item_name(item) }) 579 862 } 580 863 581 864 /// Update the [`FormItem`](https://hexdocs.pm/formz/formz.html#FormItem) with ··· 586 869 name: String, 587 870 fun: fn(FormItem(widget)) -> FormItem(widget), 588 871 ) { 589 - let items = do_formitems_update(form.items, name, fun) 872 + let items = do_update(form.items, name, fun) 590 873 Form(..form, items:) 591 874 } 592 875 593 - fn do_formitems_update( 876 + fn do_update( 594 877 items: List(FormItem(widget)), 595 878 name: String, 596 879 fun: fn(FormItem(widget)) -> FormItem(widget), ··· 598 881 list.map(items, fn(item) { 599 882 case item { 600 883 Field(detail, _, _) if detail.name == name -> fun(item) 601 - MultiField(detail, _, _) if detail.name == name -> fun(item) 602 - SubForm(detail, _) if detail.name == name -> fun(item) 603 - SubForm(detail, items) -> 604 - SubForm(detail, do_formitems_update(items, name, fun)) 884 + ListField(detail, _, _, _) if detail.name == name -> fun(item) 885 + SubForm(detail, items) -> { 886 + let sub = SubForm(detail, do_update(items, name, fun)) 887 + case detail.name == name { 888 + True -> fun(sub) 889 + False -> sub 890 + } 891 + } 605 892 _ -> item 606 893 } 607 894 }) ··· 609 896 610 897 /// Update the [`Field`](https://hexdocs.pm/formz/formz/field.html) with 611 898 /// the given name using the provided function. If multiple items have the same 612 - /// name, it will be called on all of them. 899 + /// name, it will be called on all of them. If no items have the given name, 900 + /// or an item with the given name exists but isn't a `Field`, this function 901 + /// will do nothing. 613 902 /// 614 903 /// ```gleam 615 904 /// let form = make_form() ··· 622 911 ) -> Form(widget, output) { 623 912 update(form, name, fn(item) { 624 913 case item { 625 - Field(field, state, widget) -> Field(fun(field), state, widget) 626 - MultiField(field, states, widget) -> 627 - MultiField(fun(field), states, widget) 628 - SubForm(..) -> item 914 + Field(field, ..) -> Field(..item, detail: fun(field)) 915 + _ -> item 629 916 } 630 917 }) 631 918 } 632 919 920 + /// Update the [`ListField`](https://hexdocs.pm/formz/formz/field.html) with 921 + /// the given name using the provided function. If multiple items have the same 922 + /// name, it will be called on all of them. If no items have the given name, 923 + /// or an item with the given name exists but isn't a `ListField`, this function 924 + /// will do nothing. 925 + /// 926 + /// ```gleam 927 + /// let form = make_form() 928 + /// update(form, "name", field.set_label(_, "Full Name")) 929 + /// ``` 930 + pub fn update_listfield( 931 + form: Form(widget, output), 932 + name: String, 933 + fun: fn(field.Field) -> field.Field, 934 + ) -> Form(widget, output) { 935 + update(form, name, fn(item) { 936 + case item { 937 + ListField(field, ..) -> ListField(..item, detail: fun(field)) 938 + _ -> item 939 + } 940 + }) 941 + } 942 + 943 + /// Update the [`SubForm`](https://hexdocs.pm/formz/formz/subform.html) with 944 + /// the given name using the provided function. If multiple subforms have the same 945 + /// name, it will be called on all of them. If no items have the given name, 946 + /// or an item with the given name exists but isn't a `SubForm`, this function 947 + /// will do nothing. 948 + /// 949 + /// ```gleam 950 + /// let form = make_form() 951 + /// update(form, "name", subform.set_help_text(_, "...")) 952 + /// ``` 633 953 pub fn update_subform( 634 954 form: Form(widget, output), 635 955 name: String, ··· 651 971 update(form, name, fn(item) { 652 972 case item { 653 973 Field(field, state, widget) -> 654 - Field(field, Invalid(state.value, str), widget) 655 - MultiField(..) -> item 656 - SubForm(..) -> item 974 + Field(field, Invalid(state.value, state.presence, str), widget) 975 + _ -> item 976 + } 977 + }) 978 + } 979 + 980 + pub fn set_listfield_errors( 981 + form: Form(widget, output), 982 + name: String, 983 + errors: List(Result(Nil, String)), 984 + ) -> Form(widget, output) { 985 + update(form, name, fn(item) { 986 + case item { 987 + ListField(field, states, blank_fields, widget) -> { 988 + let invalid_states = 989 + list.map2(states, errors, fn(state, err) { 990 + case err { 991 + Error(str) -> Invalid(state.value, state.presence, str) 992 + Ok(Nil) -> state 993 + } 994 + }) 995 + ListField(field, invalid_states, blank_fields, widget) 996 + } 997 + _ -> item 998 + } 999 + }) 1000 + } 1001 + 1002 + fn get_item_name(item: FormItem(widget)) -> String { 1003 + case item { 1004 + Field(field, _, _) -> field.name 1005 + ListField(field, _, _, _) -> field.name 1006 + SubForm(subform, _) -> subform.name 1007 + } 1008 + } 1009 + 1010 + /// Create a simple `Definition` that is parsed as an `Option` if the field 1011 + /// is empty. 1012 + pub fn definition( 1013 + widget widget: widget, 1014 + parse parse: fn(String) -> Result(required, String), 1015 + stub stub: required, 1016 + ) { 1017 + Definition( 1018 + widget:, 1019 + parse:, 1020 + stub:, 1021 + optional_parse: fn(fun, str) { 1022 + case str { 1023 + "" -> Ok(option.None) 1024 + _ -> fun(str) |> result.map(option.Some) 1025 + } 1026 + }, 1027 + optional_stub: option.None, 1028 + ) 1029 + } 1030 + 1031 + pub fn definition_with_custom_optional( 1032 + widget widget: widget, 1033 + parse parse: fn(String) -> Result(required, String), 1034 + stub stub: required, 1035 + optional_parse optional_parse: fn( 1036 + fn(String) -> Result(required, String), 1037 + String, 1038 + ) -> 1039 + Result(optional, String), 1040 + optional_stub optional_stub: optional, 1041 + ) -> Definition(widget, required, optional) { 1042 + Definition(widget:, parse:, stub:, optional_parse:, optional_stub:) 1043 + } 1044 + 1045 + /// Chain additional validation onto the `parse` function. This is 1046 + /// useful if you don't need to change the returned type, but might have 1047 + /// additional constraints. Like say, requiring a `String` to be at least 1048 + /// a certain length, or that an Int must be positive. 1049 + /// 1050 + /// ### Example 1051 + /// ```gleam 1052 + /// field 1053 + /// |> validate(fn(i) { 1054 + /// case i > 0 { 1055 + /// True -> Ok(i) 1056 + /// False -> Error("must be positive") 1057 + /// } 1058 + /// }), 1059 + /// ``` 1060 + pub fn verify( 1061 + def: Definition(widget, a, b), 1062 + fun: fn(a) -> Result(a, String), 1063 + ) -> Definition(widget, a, b) { 1064 + Definition(..def, parse: fn(val) { val |> def.parse |> result.try(fun) }) 1065 + } 1066 + 1067 + pub fn transform( 1068 + def: Definition(widget, old_required, old_optional), 1069 + transform: fn(old_required) -> Result(new_required, String), 1070 + stub: new_required, 1071 + ) -> Definition(widget, new_required, option.Option(new_required)) { 1072 + let Definition(widget, parse, _, _, _) = def 1073 + Definition( 1074 + widget:, 1075 + parse: fn(val) { val |> parse |> result.try(transform) }, 1076 + stub:, 1077 + optional_parse: fn(parse, str) { 1078 + case str { 1079 + "" -> Ok(option.None) 1080 + _ -> parse(str) |> result.map(option.Some) 1081 + } 1082 + }, 1083 + optional_stub: option.None, 1084 + ) 1085 + } 1086 + 1087 + pub fn transform_with_custom_optional( 1088 + def: Definition(widget, old_required, old_optional), 1089 + transform: fn(old_required) -> Result(new_required, String), 1090 + stub: new_required, 1091 + optional_parse: fn(fn(String) -> Result(new_required, String), String) -> 1092 + Result(new_optional, String), 1093 + optional_stub: new_optional, 1094 + ) -> Definition(widget, new_required, new_optional) { 1095 + let Definition(widget, parse, _, _, _) = def 1096 + Definition( 1097 + widget:, 1098 + parse: fn(val) { val |> parse |> result.try(transform) }, 1099 + stub:, 1100 + optional_parse:, 1101 + optional_stub:, 1102 + ) 1103 + } 1104 + 1105 + pub fn widget( 1106 + def: Definition(widget, a, b), 1107 + widget: widget, 1108 + ) -> Definition(widget, a, b) { 1109 + Definition(..def, widget:) 1110 + } 1111 + 1112 + @internal 1113 + pub fn get_parse( 1114 + def: Definition(widget, a, b), 1115 + ) -> fn(String) -> Result(a, String) { 1116 + def.parse 1117 + } 1118 + 1119 + @internal 1120 + pub fn get_optional_parse( 1121 + def: Definition(widget, a, b), 1122 + ) -> fn(fn(String) -> Result(a, String), String) -> Result(b, String) { 1123 + def.optional_parse 1124 + } 1125 + 1126 + @internal 1127 + pub fn get_states(form: Form(widget, output)) -> List(FieldState) { 1128 + form.items |> do_get_states |> list.reverse 1129 + } 1130 + 1131 + fn do_get_states(items: List(FormItem(widget))) -> List(FieldState) { 1132 + list.fold(items, [], fn(acc, item) { 1133 + case item { 1134 + Field(_, state, _) -> [state, ..acc] 1135 + ListField(_, states, _, _) -> list.append(states |> list.reverse, acc) 1136 + SubForm(_, sub_items) -> list.flatten([do_get_states(sub_items), acc]) 657 1137 } 658 1138 }) 659 1139 }
-152
formz/src/formz/definition.gleam
··· 1 - //// A `Definition` is the second argument needed to add a field to a form. It 2 - //// is what describes how a field works, e.g. how it looks and how it's parsed. 3 - //// It is the heavy compared to the lightness of a [Field](https://hexdocs.pm/formz/formz/field.html); 4 - //// they take a bit more work to make as they are intended to be reusable. 5 - //// 6 - //// The first role of a `Defintion` is to generate the HTML widget for the field. 7 - //// This library is format-agnostic and you can generate HTML widgets as raw 8 - //// strings, Lustre elements, Nakai nodes, something else, etc. There are 9 - //// currently three `formz` libraries that provide common widgets (and field 10 - //// definitions) for the most common HTML formats. 11 - //// 12 - //// - [formz_string](https://hexdocs.pm/formz_string/) 13 - //// - [formz_nakai](https://hexdocs.pm/formz_nakai/) 14 - //// - [formz_lustre](https://hexdocs.pm/formz_lustre/) (untested in a browser, 15 - //// would it be useful there??) 16 - //// 17 - //// The second role of a `Definition` is to parse the data from the field. 18 - //// There are a two parts to this, as how you parse a field's value depends on 19 - //// if it is optional or required. Not all scenarios can be cookie-cutter 20 - //// placed into an `Option`. So you need to provide two parse functions, one 21 - //// for when a field is required, and a second for when it's optional. 22 - //// 23 - //// ### Example password field definition 24 - //// 25 - //// ```gleam 26 - //// /// you won't often need to do this directly (I think??). The idea is that 27 - //// /// there'd be libs with the definitions you need. 28 - //// 29 - //// import formz/definition.{Definition} 30 - //// import formz/field 31 - //// import formz/validation 32 - //// import formz/widget 33 - //// import lustre/attribute 34 - //// import lustre/element 35 - //// import lustre/element/html 36 - //// 37 - //// fn password_widget( 38 - //// field: field.Field, 39 - //// args: widget.Args, 40 - //// ) -> element.Element(msg) { 41 - //// html.input([ 42 - //// attribute.type_("password"), 43 - //// attribute.name(field.name), 44 - //// attribute.id(args.id), 45 - //// attribute.attribute("aria-labelledby", field.label), 46 - //// ]) 47 - //// } 48 - //// 49 - //// pub fn password_field() { 50 - //// Definition( 51 - //// widget: password_widget, 52 - //// parse: validation.string, 53 - //// optional_parse: fn(parse, str) { 54 - //// case str { 55 - //// "" -> Ok(option.None) 56 - //// _ -> parse(str) 57 - //// } 58 - //// }, 59 - //// // We need to have a stub value for each parser. The stubs are used when 60 - //// // building the decoder and parse functions for the form. 61 - //// stub: "", 62 - //// optional_stub: option.None, 63 - //// ) 64 - //// } 65 - //// ``` 66 - 67 - import gleam/option 68 - import gleam/result 69 - 70 - pub type Definition(widget, required, optional) { 71 - Definition( 72 - /// The widget is used by the form generator to construct the HTML for the 73 - /// field. It's up to the form generator to decide on the widget's type and 74 - /// how it's used. 75 - widget: widget, 76 - /// This parse function takes the raw string from the parsed POST data 77 - /// and converts it to a Gleam type. This `parse` is for when a value 78 - /// is required, so it should return an error if the field is 79 - /// considered empty. 80 - parse: fn(String) -> Result(required, String), 81 - /// The callbacks pattern for generating a form requires a stub 82 - /// value for each field, because the actual decode function is called 83 - /// step by step as the fields are added to the form and `formz` learns 84 - /// the form's details as it goes. This stub value is purely used 85 - /// for navigating the decode function, and just needs to match the type 86 - /// of the real value that can be parsed. 87 - stub: required, 88 - /// If a field is marked as optional, this function is called, with the 89 - /// above parse as an argument. The idea is that this function will 90 - /// call out to the parse function if the field is not empty, and 91 - /// this should only handle the case where the raw input value is empty. 92 - /// This function is necessary because not all fields should just be parsed 93 - /// into an `Option` when they aren't provided. 94 - /// For example, an optional text field might be an empty string, 95 - /// an optional checkbox might be `False`, and an optional select might 96 - /// be `option.None`. 97 - optional_parse: fn(fn(String) -> Result(required, String), String) -> 98 - Result(optional, String), 99 - /// stub for the `optional_parse` return value 100 - optional_stub: optional, 101 - ) 102 - } 103 - 104 - /// A convenience function to make the simple optional parse function where 105 - /// if a value isn't provided, just return `option.None`, otherwise call out 106 - /// to the parse function and put it's value in `option.Some`. 107 - pub fn make_simple_optional_parse() -> fn( 108 - fn(String) -> Result(required, String), 109 - String, 110 - ) -> 111 - Result(option.Option(required), String) { 112 - fn(fun, str) { 113 - case str { 114 - "" -> Ok(option.None) 115 - _ -> fun(str) |> result.map(option.Some) 116 - } 117 - } 118 - } 119 - 120 - /// Replace the widget that this `Definition` uses for rendering the field. Most 121 - /// HTML inputs can be interchangeable, they all generate a `String` after all, 122 - /// but not all are the best UX. This allows you to choose the one that is the 123 - /// most appropriate for your field. However, the details of how this works 124 - /// are up to the form generator. 125 - pub fn set_widget( 126 - definition: Definition(widget, a, b), 127 - widget: widget, 128 - ) -> Definition(widget, a, b) { 129 - Definition(..definition, widget:) 130 - } 131 - 132 - /// Chain additional validation onto the `parse` function. This is 133 - /// useful if you don't need to change the returned type, but might have 134 - /// additional constraints. Like say, requiring a `String` to be at least 135 - /// a certain length, or that an Int must be positive. 136 - /// 137 - /// ### Example 138 - /// ```gleam 139 - /// field 140 - /// |> validate(fn(i) { 141 - /// case i > 0 { 142 - /// True -> Ok(i) 143 - /// False -> Error("must be positive") 144 - /// } 145 - /// }), 146 - /// ``` 147 - pub fn validate( 148 - def: Definition(widget, a, b), 149 - fun: fn(a) -> Result(a, String), 150 - ) -> Definition(widget, a, b) { 151 - Definition(..def, parse: fn(val) { val |> def.parse |> result.try(fun) }) 152 - }
-12
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 required. This field is not functional, but is purely 41 - /// for whether or not the form generator should indicate to the user that the 42 - /// field is required. Add the field to the form with either `optional` or 43 - /// `required` methods to control this functionally. Those methods will make 44 - /// sure this field is set correctly. 45 - required: Bool, 46 40 /// Whether the field is hidden. A hidden field is not displayed in the browser. 47 41 hidden: Bool, 48 42 ) ··· 61 55 label: justin.sentence_case(name), 62 56 help_text: "", 63 57 disabled: False, 64 - required: False, 65 58 hidden: False, 66 59 ) 67 60 } ··· 80 73 81 74 pub fn set_hidden(field: Field, hidden: Bool) -> Field { 82 75 Field(..field, hidden:) 83 - } 84 - 85 - @internal 86 - pub fn set_required(field: Field, required: Bool) -> Field { 87 - Field(..field, required:) 88 76 } 89 77 90 78 pub fn make_hidden(field: Field) -> Field {
+1 -1
formz/src/formz/subform.gleam
··· 1 - //// Detials about a subform being added to a form. There is a convenience 1 + //// Details about a subform being added to a form. There is a convenience 2 2 //// function to create a field with just a name, and then you can use the rest 3 3 //// of the functions to set just the values you need to change. 4 4
+1 -1
formz/src/formz/validation.gleam
··· 122 122 } 123 123 124 124 /// Validates that the input is one from a list of allowed values. Takes a list 125 - /// of Gleam values that can be chosen. This uses the index of the item in to 125 + /// of Gleam values that can be chosen. This uses the index of the item to 126 126 /// find the desired value. 127 127 /// 128 128 /// ## Examples
+2 -2
formz/src/formz/widget.gleam formz_string/src/formz_string/widget.gleam
··· 11 11 import formz 12 12 import formz/field 13 13 14 - pub type Widget(format) = 15 - fn(field.Field, formz.State, Args) -> format 14 + pub type Widget = 15 + fn(field.Field, formz.FieldState, Args) -> String 16 16 17 17 pub type Args { 18 18 Args(
+404 -225
formz/test/formz_test.gleam
··· 1 - import formz.{Field, Invalid, Valid} 2 - import formz/definition 1 + import formz.{Invalid, Optional, Required, Unvalidated, Valid} 3 2 import formz/field.{field} 4 3 import formz/subform.{subform} 5 4 import formz/validation 6 - import gleam/option.{None, Some} 7 - import gleam/result 5 + import gleam/option 8 6 import gleam/string 9 7 import gleeunit 10 8 import gleeunit/should ··· 14 12 } 15 13 16 14 fn text_field() { 17 - definition.Definition( 15 + formz.definition_with_custom_optional( 18 16 widget: fn(_, _) { Nil }, 19 17 parse: validation.non_empty_string, 20 18 stub: "", ··· 29 27 } 30 28 31 29 pub fn float_field() { 32 - definition.Definition( 30 + formz.definition( 33 31 widget: fn(_, _) { Nil }, 34 32 parse: validation.number, 35 33 stub: 0.0, 36 - optional_parse: fn(fun, str) { 37 - case str { 38 - "" -> Ok(None) 39 - _ -> fun(str) |> result.map(Some) 40 - } 41 - }, 42 - optional_stub: Some(0.0), 43 34 ) 44 35 } 45 36 46 37 fn integer_field() { 47 - definition.Definition( 48 - widget: fn(_, _) { Nil }, 49 - parse: validation.int, 50 - stub: 0, 51 - optional_parse: fn(fun, str) { 52 - case str { 53 - "" -> Ok(None) 54 - _ -> fun(str) |> result.map(Some) 55 - } 56 - }, 57 - optional_stub: Some(0), 58 - ) 38 + formz.definition(widget: fn(_, _) { Nil }, parse: validation.int, stub: 0) 59 39 } 60 40 61 41 fn boolean_field() { 62 - definition.Definition( 42 + formz.definition_with_custom_optional( 63 43 widget: fn(_, _) { Nil }, 64 44 parse: validation.on, 65 45 stub: False, ··· 73 53 ) 74 54 } 75 55 76 - fn state_should_be(state: formz.State, expected: formz.State) { 56 + fn state_should_be(state: formz.FieldState, expected: formz.FieldState) { 77 57 should.equal(state, expected) 78 58 } 79 59 ··· 104 84 use a <- formz.optional( 105 85 field("x") |> field.set_name("a") |> field.set_label("A"), 106 86 text_field() 107 - |> definition.validate(fn(str) { 87 + |> formz.verify(fn(str) { 108 88 case string.length(str) > 3 { 109 89 True -> Ok(str) 110 90 False -> Error("must be longer than 3") ··· 115 95 use b <- formz.optional( 116 96 field(named: "b"), 117 97 integer_field() 118 - |> definition.validate(fn(i) { 98 + |> formz.verify(fn(i) { 119 99 case i > 0 { 120 100 True -> Ok(i) 121 101 False -> Error("must be positive") ··· 140 120 pub fn parse_empty_form_test() { 141 121 empty_form(1) 142 122 |> formz.data([]) 143 - |> formz.parse 123 + |> formz.decode 144 124 |> should.equal(Ok(1)) 145 125 146 126 empty_form("hello") 147 127 |> formz.data([]) 148 - |> formz.parse 128 + |> formz.decode 149 129 |> should.equal(Ok("hello")) 150 130 } 151 131 152 132 pub fn parse_single_field_form_test() { 153 133 one_field_form() 154 134 |> formz.data([#("a", "world")]) 155 - |> formz.parse 135 + |> formz.decode 156 136 |> should.equal(Ok("hello world")) 157 137 158 138 one_field_form() 159 139 |> formz.data([#("a", "ignored"), #("a", "world")]) 160 - |> formz.parse 140 + |> formz.decode 161 141 |> should.equal(Ok("hello world")) 162 142 } 163 143 164 144 pub fn parse_double_field_form_test() { 165 145 two_field_form() 166 146 |> formz.data([#("a", "hello"), #("b", "world")]) 167 - |> formz.parse 147 + |> formz.decode 168 148 |> should.equal(Ok(#("hello", "world"))) 169 149 170 150 // wrong order 171 151 two_field_form() 172 152 |> formz.data([#("b", "world"), #("a", "hello")]) 173 - |> formz.parse 153 + |> formz.decode 174 154 |> should.equal(Ok(#("hello", "world"))) 175 155 176 156 // takes second ··· 181 161 #("b", "world"), 182 162 #("a", "hello"), 183 163 ]) 184 - |> formz.parse 164 + |> formz.decode 185 165 |> should.equal(Ok(#("hello", "world"))) 186 166 } 187 167 ··· 192 172 formz.create_form(a) 193 173 } 194 174 |> formz.data([#("a", "world")]) 195 - |> formz.parse 175 + |> formz.decode 196 176 197 177 let assert [state] = formz.get_states(f) 198 - state |> state_should_be(Invalid("world", "must be on")) 178 + state |> state_should_be(Invalid("world", Optional, "must be on")) 199 179 } 200 180 201 181 pub fn parse_triple_field_form_with_error_test() { 202 - let assert [statea, stateb, statec] = 203 - three_field_form() 204 - |> formz.data([#("a", "string"), #("b", "1"), #("c", "string")]) 205 - |> formz.parse 206 - |> get_form_from_error_result 207 - |> formz.get_states 182 + three_field_form() 183 + |> formz.data([#("a", "string"), #("b", "1"), #("c", "string")]) 184 + |> formz.decode 185 + |> get_form_from_error_result 186 + |> formz.get_states 187 + |> should.equal([ 188 + Valid("string", Optional), 189 + Valid("1", Optional), 190 + Invalid("string", Optional, "must be a number"), 191 + ]) 208 192 209 - statea |> state_should_be(Valid("string")) 210 - stateb |> state_should_be(Valid("1")) 211 - statec |> state_should_be(Invalid("string", "must be a number")) 212 - 213 - let assert [statea, stateb, statec] = 214 - three_field_form() 215 - |> formz.data([#("a", "string"), #("b", "string"), #("c", "string")]) 216 - |> formz.parse 217 - |> get_form_from_error_result 218 - |> formz.get_states 219 - statea |> state_should_be(Valid("string")) 220 - stateb |> state_should_be(Invalid("string", "must be a whole number")) 221 - statec |> state_should_be(Invalid("string", "must be a number")) 193 + three_field_form() 194 + |> formz.data([#("a", "string"), #("b", "string"), #("c", "string")]) 195 + |> formz.decode 196 + |> get_form_from_error_result 197 + |> formz.get_states 198 + |> should.equal([ 199 + Valid("string", Optional), 200 + Invalid("string", Optional, "must be a whole number"), 201 + Invalid("string", Optional, "must be a number"), 202 + ]) 222 203 223 - let assert [statea, stateb, statec] = 224 - three_field_form() 225 - |> formz.data([#("a", "string"), #("b", "string"), #("c", "3.4")]) 226 - |> formz.parse 227 - |> get_form_from_error_result 228 - |> formz.get_states 229 - statea |> state_should_be(Valid("string")) 230 - stateb |> state_should_be(Invalid("string", "must be a whole number")) 231 - statec |> state_should_be(Valid("3.4")) 204 + three_field_form() 205 + |> formz.data([#("a", "string"), #("b", "string"), #("c", "3.4")]) 206 + |> formz.decode 207 + |> get_form_from_error_result 208 + |> formz.get_states 209 + |> should.equal([ 210 + Valid("string", Optional), 211 + Invalid("string", Optional, "must be a whole number"), 212 + Valid("3.4", Optional), 213 + ]) 232 214 233 - let assert [statea, stateb, statec] = 234 - three_field_form() 235 - |> formz.data([#("a", "."), #("b", "string"), #("c", "3.4")]) 236 - |> formz.parse 237 - |> get_form_from_error_result 238 - |> formz.get_states 239 - statea |> state_should_be(Invalid(".", "must be longer than 3")) 240 - stateb |> state_should_be(Invalid("string", "must be a whole number")) 241 - statec |> state_should_be(Valid("3.4")) 215 + three_field_form() 216 + |> formz.data([#("a", "."), #("b", "string"), #("c", "3.4")]) 217 + |> formz.decode 218 + |> get_form_from_error_result 219 + |> formz.get_states 220 + |> should.equal([ 221 + Invalid(".", Optional, "must be longer than 3"), 222 + Invalid("string", Optional, "must be a whole number"), 223 + Valid("3.4", Optional), 224 + ]) 242 225 243 - let assert [statea, stateb, statec] = 244 - three_field_form() 245 - |> formz.data([#("a", "."), #("b", "1"), #("c", "3.4")]) 246 - |> formz.parse 247 - |> get_form_from_error_result 248 - |> formz.get_states 249 - statea |> state_should_be(Invalid(".", "must be longer than 3")) 250 - stateb |> state_should_be(Valid("1")) 251 - statec |> state_should_be(Valid("3.4")) 252 - } 253 - 254 - pub fn set_required_field_test() { 255 - let f = { 256 - use a <- formz.require( 257 - field("a") |> field.set_required(False), 258 - integer_field(), 259 - ) 260 - use b <- formz.optional( 261 - field("b") |> field.set_required(True), 262 - integer_field(), 263 - ) 264 - formz.create_form(#(a, b)) 265 - } 266 - 267 - let assert [Field(fielda, _, _), Field(fieldb, _, _)] = formz.items(f) 268 - 269 - fielda.required |> should.equal(True) 270 - fieldb.required |> should.equal(False) 226 + three_field_form() 227 + |> formz.data([#("a", "."), #("b", "1"), #("c", "3.4")]) 228 + |> formz.decode 229 + |> get_form_from_error_result 230 + |> formz.get_states 231 + |> should.equal([ 232 + Invalid(".", Optional, "must be longer than 3"), 233 + Valid("1", Optional), 234 + Valid("3.4", Optional), 235 + ]) 271 236 } 272 237 273 238 pub fn sub_form_test() { ··· 293 258 #("name.c", "3"), 294 259 #("d", "4"), 295 260 ]) 296 - |> formz.parse 261 + |> formz.decode 297 262 |> should.equal(Ok(#(#(1, 2, 3), 4))) 298 263 } 299 264 300 265 pub fn get_fields_test() { 301 - let assert [statea, stateb, statec] = 266 + three_field_form() 267 + |> formz.data([#("a", "1"), #("b", "2"), #("c", "3")]) 268 + |> formz.get_states 269 + |> should.equal([ 270 + Unvalidated("1", Optional), 271 + Unvalidated("2", Optional), 272 + Unvalidated("3", Optional), 273 + ]) 274 + } 275 + 276 + pub fn validate_errors_test() { 277 + let f = 302 278 three_field_form() 303 - |> formz.data([#("a", "1"), #("b", "2"), #("c", "3")]) 304 - |> formz.get_states 279 + |> formz.data([#("a", "1"), #("b", "-1"), #("c", "x")]) 280 + 281 + // haven't validated yet 282 + f 283 + |> formz.get_states 284 + |> should.equal([ 285 + Unvalidated("1", Optional), 286 + Unvalidated("-1", Optional), 287 + Unvalidated("x", Optional), 288 + ]) 289 + 290 + // validate first 2 291 + 292 + f 293 + |> formz.validate(["a", "b"]) 294 + |> formz.get_states 295 + |> should.equal([ 296 + Invalid("1", Optional, "must be longer than 3"), 297 + Invalid("-1", Optional, "must be positive"), 298 + Unvalidated("x", Optional), 299 + ]) 300 + 301 + // validate middle 302 + f 303 + |> formz.validate(["b"]) 304 + |> formz.get_states 305 + |> should.equal([ 306 + Unvalidated("1", Optional), 307 + Invalid("-1", Optional, "must be positive"), 308 + Unvalidated("x", Optional), 309 + ]) 305 310 306 - statea |> state_should_be(Valid("1")) 307 - stateb |> state_should_be(Valid("2")) 308 - statec |> state_should_be(Valid("3")) 311 + //validate last 312 + f 313 + |> formz.validate(["c"]) 314 + |> formz.get_states 315 + |> should.equal([ 316 + Unvalidated("1", Optional), 317 + Unvalidated("-1", Optional), 318 + Invalid("x", Optional, "must be a number"), 319 + ]) 309 320 } 310 321 311 - pub fn validate_test() { 322 + pub fn validate_ok_test() { 312 323 let f = 313 324 three_field_form() 314 - |> formz.data([#("a", "1"), #("b", "-1"), #("c", "x")]) 325 + |> formz.data([#("a", "string"), #("b", "1"), #("c", "1.0")]) 315 326 316 327 // haven't validated yet 317 - let assert [statea, stateb, statec] = f |> formz.get_states 318 - statea |> state_should_be(Valid("1")) 319 - stateb |> state_should_be(Valid("-1")) 320 - statec |> state_should_be(Valid("x")) 328 + f 329 + |> formz.get_states 330 + |> should.equal([ 331 + Unvalidated("string", Optional), 332 + Unvalidated("1", Optional), 333 + Unvalidated("1.0", Optional), 334 + ]) 321 335 322 336 // validate first 2 323 - let assert [statea, stateb, statec] = 324 - f |> formz.validate(["a", "b"]) |> formz.get_states 325 - statea |> state_should_be(Invalid("1", "must be longer than 3")) 326 - stateb |> state_should_be(Invalid("-1", "must be positive")) 327 - statec |> state_should_be(Valid("x")) 337 + 338 + f 339 + |> formz.validate(["a", "b"]) 340 + |> formz.get_states 341 + |> should.equal([ 342 + Valid("string", Optional), 343 + Valid("1", Optional), 344 + Unvalidated("1.0", Optional), 345 + ]) 328 346 329 347 // validate middle 330 - let assert [statea, stateb, statec] = 331 - f |> formz.validate(["b"]) |> formz.get_states 332 - statea |> state_should_be(Valid("1")) 333 - stateb |> state_should_be(Invalid("-1", "must be positive")) 334 - statec |> state_should_be(Valid("x")) 348 + f 349 + |> formz.validate(["b"]) 350 + |> formz.get_states 351 + |> should.equal([ 352 + Unvalidated("string", Optional), 353 + Valid("1", Optional), 354 + Unvalidated("1.0", Optional), 355 + ]) 335 356 336 357 //validate last 337 - let assert [statea, stateb, statec] = 338 - f |> formz.validate(["c"]) |> formz.get_states 339 - statea |> state_should_be(Valid("1")) 340 - stateb |> state_should_be(Valid("-1")) 341 - statec |> state_should_be(Invalid("x", "must be a number")) 358 + f 359 + |> formz.validate(["c"]) 360 + |> formz.get_states 361 + |> should.equal([ 362 + Unvalidated("string", Optional), 363 + Unvalidated("1", Optional), 364 + Valid("1.0", Optional), 365 + ]) 342 366 } 343 367 344 368 pub fn validate_all_test() { 345 - let assert [statea, stateb, statec] = 346 - three_field_form() 347 - |> formz.data([#("a", "1"), #("b", "-1"), #("c", "x")]) 348 - |> formz.validate_all 349 - |> formz.get_states 369 + three_field_form() 370 + |> formz.data([#("a", "1"), #("b", "-1"), #("c", "x")]) 371 + |> formz.validate_all 372 + |> formz.get_states 373 + |> should.equal([ 374 + Invalid("1", Optional, "must be longer than 3"), 375 + Invalid("-1", Optional, "must be positive"), 376 + Invalid("x", Optional, "must be a number"), 377 + ]) 350 378 351 - statea |> state_should_be(Invalid("1", "must be longer than 3")) 352 - stateb |> state_should_be(Invalid("-1", "must be positive")) 353 - statec |> state_should_be(Invalid("x", "must be a number")) 379 + three_field_form() 380 + |> formz.data([#("a", "string"), #("b", "1"), #("c", "1.0")]) 381 + |> formz.validate_all 382 + |> formz.get_states 383 + |> should.equal([ 384 + Valid("string", Optional), 385 + Valid("1", Optional), 386 + Valid("1.0", Optional), 387 + ]) 354 388 } 355 389 356 390 pub fn sub_form_error_test() { ··· 369 403 formz.create_form(#(a, b)) 370 404 } 371 405 372 - let tmp = 373 - f2 374 - |> formz.data([ 375 - #("name.a", "a"), 376 - #("name.b", "2"), 377 - #("name.c", "3"), 378 - #("d", "4"), 379 - ]) 380 - |> formz.parse 381 - |> get_form_from_error_result 406 + f2 407 + |> formz.data([ 408 + #("name.a", "a"), 409 + #("name.b", "2"), 410 + #("name.c", "3"), 411 + #("d", "4"), 412 + ]) 413 + |> formz.decode 414 + |> get_form_from_error_result 415 + |> formz.get_states 416 + |> should.equal([ 417 + Invalid("a", Optional, "must be a whole number"), 418 + Valid("2", Required), 419 + Valid("3", Optional), 420 + Valid("4", Optional), 421 + ]) 382 422 383 - tmp |> formz.items 384 - let assert [statea, stateb, statec, stated] = tmp |> formz.get_states 385 - 386 - statea |> state_should_be(Invalid("a", "must be a whole number")) 387 - stateb |> state_should_be(Valid("2")) 388 - statec |> state_should_be(Valid("3")) 389 - stated |> state_should_be(Valid("4")) 390 - 391 - let assert [statea, stateb, statec, stated] = 392 - f2 393 - |> formz.data([ 394 - #("name.a", "1"), 395 - #("name.b", "2"), 396 - #("name.c", "3"), 397 - #("d", "a"), 398 - ]) 399 - |> formz.parse 400 - |> get_form_from_error_result 401 - |> formz.get_states 402 - 403 - statea |> state_should_be(Valid("1")) 404 - stateb |> state_should_be(Valid("2")) 405 - statec |> state_should_be(Valid("3")) 406 - stated |> state_should_be(Invalid("a", "must be a whole number")) 423 + f2 424 + |> formz.data([ 425 + #("name.a", "1"), 426 + #("name.b", "2"), 427 + #("name.c", "3"), 428 + #("d", "a"), 429 + ]) 430 + |> formz.decode 431 + |> get_form_from_error_result 432 + |> formz.get_states 433 + |> should.equal([ 434 + Valid("1", Optional), 435 + Valid("2", Required), 436 + Valid("3", Optional), 437 + Invalid("a", Optional, "must be a whole number"), 438 + ]) 407 439 } 408 440 409 441 pub fn decoded_and_try_test() { ··· 412 444 |> formz.data([#("a", "string"), #("b", "2"), #("c", "3.0")]) 413 445 414 446 // can succeed 415 - formz.parse_then_try(f, fn(_, _) { Ok(3) }) 447 + formz.decode_then_try(f, fn(_, _) { Ok(3) }) 416 448 |> should.equal(Ok(3)) 417 449 418 450 // can change type 419 - formz.parse_then_try(f, fn(_, _) { Ok("it worked") }) 451 + formz.decode_then_try(f, fn(_, _) { Ok("it worked") }) 420 452 |> should.equal(Ok("it worked")) 421 453 422 454 // can error 423 - formz.parse_then_try(f, fn(form, _) { Error(form) }) 424 - |> should.equal(Error(f)) 455 + formz.decode_then_try(f, fn(form, _) { Error(form) }) 456 + |> get_form_from_error_result 457 + |> formz.get_states 458 + |> should.equal([ 459 + Valid("string", Optional), 460 + Valid("2", Optional), 461 + Valid("3.0", Optional), 462 + ]) 425 463 426 464 // can change field 427 465 let assert Error(form) = 428 - formz.parse_then_try(f, fn(form, _) { 466 + formz.decode_then_try(f, fn(form, _) { 429 467 Error(form |> formz.set_field_error("a", "woops")) 430 468 }) 431 - let assert [statea, stateb, statec] = formz.get_states(form) 432 - statea |> state_should_be(Invalid("string", "woops")) 433 - stateb |> state_should_be(Valid("2")) 434 - statec |> state_should_be(Valid("3.0")) 469 + formz.get_states(form) 470 + |> should.equal([ 471 + Invalid("string", Optional, "woops"), 472 + Valid("2", Optional), 473 + Valid("3.0", Optional), 474 + ]) 475 + 476 + let f = { 477 + use a <- formz.list(field("a"), float_field()) 478 + formz.create_form(a) 479 + } 480 + 481 + formz.data(f, [#("a", "1"), #("a", "2")]) 482 + |> formz.decode_then_try(fn(form, _) { 483 + Error( 484 + formz.set_listfield_errors(form, "a", [Error("woops 1"), Error("woops 2")]), 485 + ) 486 + }) 487 + |> get_form_from_error_result 488 + |> formz.get_states 489 + |> should.equal([ 490 + Invalid("1", Optional, "woops 1"), 491 + Invalid("2", Optional, "woops 2"), 492 + ]) 493 + 494 + formz.data(f, [#("a", "1"), #("a", "2")]) 495 + |> formz.decode_then_try(fn(form, _) { 496 + Error(formz.set_listfield_errors(form, "a", [Error("woops"), Ok(Nil)])) 497 + }) 498 + |> get_form_from_error_result 499 + |> formz.get_states 500 + |> should.equal([Invalid("1", Optional, "woops"), Valid("2", Optional)]) 501 + 502 + formz.data(f, [#("a", "1"), #("a", "2")]) 503 + |> formz.decode_then_try(fn(form, _) { 504 + Error(formz.set_listfield_errors(form, "a", [Ok(Nil), Error("woops")])) 505 + }) 506 + |> get_form_from_error_result 507 + |> formz.get_states 508 + |> should.equal([Valid("1", Optional), Invalid("2", Optional, "woops")]) 435 509 } 436 510 437 - pub fn multi_test() { 511 + pub fn list_test() { 438 512 let f = { 439 513 use a <- formz.list(field("a"), float_field()) 440 514 use b <- formz.list(field("b"), float_field()) ··· 445 519 446 520 f 447 521 |> formz.data([#("a", "1"), #("b", "2"), #("c", "3")]) 448 - |> formz.parse 522 + |> formz.decode 449 523 |> should.equal(Ok(#([1.0], [2.0], [3.0]))) 450 524 451 525 f 452 526 |> formz.data([#("a", "1.1"), #("a", "1.2"), #("b", "2"), #("c", "3")]) 453 - |> formz.parse 527 + |> formz.decode 454 528 |> should.equal(Ok(#([1.1, 1.2], [2.0], [3.0]))) 455 529 456 530 f ··· 462 536 #("c", "3.1"), 463 537 #("c", "3.2"), 464 538 ]) 465 - |> formz.parse 539 + |> formz.decode 466 540 |> should.equal(Ok(#([1.1, 1.2], [2.1, 2.2], [3.1, 3.2]))) 467 541 468 - let assert [statea, stateb, statec] = 469 - f 470 - |> formz.data([#("a", "a"), #("b", "2"), #("c", "3")]) 471 - |> formz.parse 472 - |> get_form_from_error_result 473 - |> formz.get_states 542 + f 543 + |> formz.data([#("a", "a"), #("b", "2"), #("c", "3")]) 544 + |> formz.decode 545 + |> get_form_from_error_result 546 + |> formz.get_states 547 + |> should.equal([ 548 + Invalid("a", Optional, "must be a number"), 549 + Valid("", Optional), 550 + Valid("2", Optional), 551 + Valid("", Optional), 552 + Valid("3", Optional), 553 + Valid("", Optional), 554 + ]) 474 555 475 - statea |> state_should_be(Invalid("a", "must be a number")) 476 - stateb |> state_should_be(Valid("2")) 477 - statec |> state_should_be(Valid("3")) 556 + f 557 + |> formz.data([#("a", "a1"), #("a", "a2"), #("b", "2"), #("c", "3")]) 558 + |> formz.decode 559 + |> get_form_from_error_result 560 + |> formz.get_states 561 + |> should.equal([ 562 + Invalid("a1", Optional, "must be a number"), 563 + Invalid("a2", Optional, "must be a number"), 564 + Valid("", Optional), 565 + Valid("2", Optional), 566 + Valid("", Optional), 567 + Valid("3", Optional), 568 + Valid("", Optional), 569 + ]) 570 + 571 + f 572 + |> formz.data([#("a", "1"), #("a", "a2"), #("b", "2"), #("c", "3")]) 573 + |> formz.decode 574 + |> get_form_from_error_result 575 + |> formz.get_states 576 + |> should.equal([ 577 + Valid("1", Optional), 578 + Invalid("a2", Optional, "must be a number"), 579 + Valid("", Optional), 580 + Valid("2", Optional), 581 + Valid("", Optional), 582 + Valid("3", Optional), 583 + Valid("", Optional), 584 + ]) 585 + 586 + f 587 + |> formz.data([#("a", "1"), #("b", "b1"), #("b", "2"), #("c", "c")]) 588 + |> formz.decode 589 + |> get_form_from_error_result 590 + |> formz.get_states 591 + |> should.equal([ 592 + Valid("1", Optional), 593 + Valid("", Optional), 594 + Invalid("b1", Optional, "must be a number"), 595 + Valid("2", Optional), 596 + Valid("", Optional), 597 + Invalid("c", Optional, "must be a number"), 598 + Valid("", Optional), 599 + ]) 600 + } 601 + 602 + pub fn limited_list_test() { 603 + let zero_extra = { 604 + use a <- formz.limited_list( 605 + formz.simple_blank_fields(1, 4, 0), 606 + field("a"), 607 + integer_field(), 608 + ) 609 + 610 + formz.create_form(a) 611 + } 612 + let one_extra = { 613 + use a <- formz.limited_list( 614 + formz.simple_blank_fields(1, 4, 1), 615 + field("a"), 616 + integer_field(), 617 + ) 618 + 619 + formz.create_form(a) 620 + } 621 + 622 + let two_extra = { 623 + use a <- formz.limited_list( 624 + formz.simple_blank_fields(1, 4, 2), 625 + field("a"), 626 + integer_field(), 627 + ) 628 + 629 + formz.create_form(a) 630 + } 631 + 632 + one_extra 633 + |> formz.get_states 634 + |> should.equal([Unvalidated("", Required)]) 635 + 636 + two_extra 637 + |> formz.get_states 638 + |> should.equal([Unvalidated("", Required), Unvalidated("", Optional)]) 478 639 479 - let assert [statea, stateb, statec, stated] = 480 - f 481 - |> formz.data([#("a", "a1"), #("a", "a2"), #("b", "2"), #("c", "3")]) 482 - |> formz.parse 483 - |> get_form_from_error_result 484 - |> formz.get_states 640 + two_extra 641 + |> formz.data([#("a", "1")]) 642 + |> formz.get_states 643 + |> should.equal([ 644 + Unvalidated("1", Required), 645 + Unvalidated("", Optional), 646 + Unvalidated("", Optional), 647 + ]) 485 648 486 - statea |> state_should_be(Invalid("a1", "must be a number")) 487 - stateb |> state_should_be(Invalid("a2", "must be a number")) 488 - statec |> state_should_be(Valid("2")) 489 - stated |> state_should_be(Valid("3")) 649 + two_extra 650 + |> formz.data([#("a", "1"), #("a", "2")]) 651 + |> formz.get_states 652 + |> should.equal([ 653 + Unvalidated("1", Required), 654 + Unvalidated("2", Optional), 655 + Unvalidated("", Optional), 656 + Unvalidated("", Optional), 657 + ]) 490 658 491 - let assert [statea, stateb, statec, stated] = 492 - f 493 - |> formz.data([#("a", "1"), #("a", "a2"), #("b", "2"), #("c", "3")]) 494 - |> formz.parse 495 - |> get_form_from_error_result 496 - |> formz.get_states 659 + one_extra 660 + |> formz.data([#("a", "a"), #("a", "2")]) 661 + |> formz.decode 662 + |> get_form_from_error_result 663 + |> formz.get_states 664 + |> should.equal([ 665 + Invalid("a", Required, "must be a whole number"), 666 + Valid("2", Optional), 667 + Valid("", Optional), 668 + ]) 497 669 498 - statea |> state_should_be(Valid("1")) 499 - stateb |> state_should_be(Invalid("a2", "must be a number")) 500 - statec |> state_should_be(Valid("2")) 501 - stated |> state_should_be(Valid("3")) 670 + zero_extra 671 + |> formz.get_states 672 + |> should.equal([Unvalidated("", Required)]) 502 673 503 - let assert [statea, stateb, statec, stated] = 504 - f 505 - |> formz.data([#("a", "1"), #("b", "b1"), #("b", "2"), #("c", "c")]) 506 - |> formz.parse 507 - |> get_form_from_error_result 508 - |> formz.get_states 674 + zero_extra 675 + |> formz.data([#("a", "1")]) 676 + |> formz.get_states 677 + |> should.equal([Unvalidated("1", Required)]) 678 + 679 + zero_extra 680 + |> formz.data([#("a", "1"), #("a", "2")]) 681 + |> formz.get_states 682 + |> should.equal([Unvalidated("1", Required), Unvalidated("2", Optional)]) 509 683 510 - statea |> state_should_be(Valid("1")) 511 - stateb |> state_should_be(Invalid("b1", "must be a number")) 512 - statec |> state_should_be(Valid("2")) 513 - stated |> state_should_be(Invalid("c", "must be a number")) 684 + zero_extra 685 + |> formz.data([#("a", "a"), #("a", "2")]) 686 + |> formz.decode 687 + |> get_form_from_error_result 688 + |> formz.get_states 689 + |> should.equal([ 690 + Invalid("a", Required, "must be a whole number"), 691 + Valid("2", Optional), 692 + ]) 514 693 }
+5 -4
formz_demo/priv/static/stylesheet.css
··· 107 107 display: block; 108 108 } */ 109 109 110 - .formz_error { 111 - color: #bd2901; 112 - /* font-size: 0.9em; */ 113 - } 110 + 111 + } 112 + .formz_error { 113 + color: #bd2901; 114 + /* font-size: 0.9em; */ 114 115 } 115 116 fieldset { 116 117 margin: 15px 0;
+1 -1
formz_demo/src/formz_demo/example/defaults.gleam
··· 19 19 ) -> Result(output, formz.Form(format, output)) { 20 20 form 21 21 |> formz.data(formdata.values) 22 - |> formz.parse() 22 + |> formz.decode() 23 23 } 24 24 25 25 pub fn format_string_form(form) -> String {
+15 -8
formz_demo/src/formz_demo/example/page.gleam
··· 1 1 import formz 2 - import formz/field 3 2 import gleam/list 4 3 import gleam/option 5 4 import gleam/result ··· 53 52 fn do_get_fields(items: List(formz.FormItem(format)), acc) { 54 53 case items { 55 54 [] -> acc 56 - [formz.Field(field, _), ..rest] -> do_get_fields(rest, [field, ..acc]) 55 + [formz.Field(field, state, _), ..rest] -> 56 + do_get_fields(rest, [#(field, state), ..acc]) 57 + [formz.ListField(field, states, _, _), ..rest] -> { 58 + let tups = list.map(states, fn(s) { #(field, s) }) |> list.reverse 59 + do_get_fields(rest, list.append(tups, acc)) 60 + } 57 61 [formz.SubForm(_, items), ..rest] -> 58 62 do_get_fields(list.flatten([items, rest]), acc) 59 63 } ··· 68 72 option.None -> element.none() 69 73 option.Some(input_data) -> { 70 74 let fields = get_fields(form) 75 + 71 76 let fields_no_post = 72 77 fields 73 78 |> list.map(fn(i) { 74 79 html.tr([], [ 75 - html.td([], [html.text(i.name)]), 80 + html.td([], [html.text({ i.0 }.name)]), 76 81 html.td([], [ 77 82 html.text( 78 - list.key_find(input_data, i.name) 83 + list.key_find(input_data, { i.0 }.name) 84 + |> result.replace({ i.1 }.value) 79 85 |> result.map(string.inspect) 80 86 |> result.unwrap("<EMPTY>"), 81 87 ), 82 88 ]), 83 89 html.td([], [ 84 - html.text(case i { 85 - field.Valid(..) -> "" 86 - field.Invalid(error:, ..) -> error 90 + html.text(case i.1 { 91 + formz.Invalid(error:, ..) -> error 92 + _ -> "" 87 93 }), 88 94 ]), 89 95 ]) ··· 93 99 let unknown_input = 94 100 list.filter_map(input_data, fn(t) { 95 101 let #(k, v) = t 96 - case list.find(fields, fn(f) { f.name == k }) { 102 + 103 + case list.find(fields, fn(f) { { f.0 }.name == k }) { 97 104 Ok(_) -> Error(Nil) 98 105 Error(_) -> 99 106 Ok(
+9 -11
formz_demo/src/formz_demo/example/run.gleam
··· 1 1 import formz 2 - import formz/widget.{type Widget} 3 2 import formz_demo/example/page 4 3 import gleam/http.{Get, Post} 5 4 import gleam/option 6 5 import simplifile 7 6 import wisp.{type Request, type Response} 8 7 9 - pub type ExampleRun(format, output, output2, msg) { 8 + pub type ExampleRun(format, output, output2, msg, widget) { 10 9 ExampleRun( 11 10 dir: String, 12 - make_form: fn() -> formz.Form(Widget(format), output), 13 - get_handler: fn(formz.Form(Widget(format), output)) -> 14 - formz.Form(Widget(format), output), 15 - post_handler: fn(wisp.FormData, formz.Form(Widget(format), output)) -> 16 - Result(output2, formz.Form(Widget(format), output)), 17 - format_form: fn(formz.Form(Widget(format), output)) -> format, 11 + make_form: fn() -> formz.Form(widget, output), 12 + get_handler: fn(formz.Form(widget, output)) -> formz.Form(widget, output), 13 + post_handler: fn(wisp.FormData, formz.Form(widget, output)) -> 14 + Result(output2, formz.Form(widget, output)), 15 + format_form: fn(formz.Form(widget, output)) -> format, 18 16 formatted_form_to_string: fn(format) -> String, 19 17 ) 20 18 } 21 19 22 20 pub fn handle( 23 21 req: Request, 24 - example: ExampleRun(format, output, output2, msg), 22 + example: ExampleRun(format, output, output2, msg, widget), 25 23 ) -> Response { 26 24 case req.method { 27 25 Get -> handle_get(example) ··· 36 34 form_code 37 35 } 38 36 39 - fn handle_get(example: ExampleRun(format, output, output2, msg)) { 37 + fn handle_get(example: ExampleRun(format, output, output2, msg, widget)) { 40 38 let form = example.make_form() |> example.get_handler 41 39 42 40 page.build_page( ··· 53 51 54 52 fn handle_post( 55 53 req: Request, 56 - example: ExampleRun(format, output, output2, msg), 54 + example: ExampleRun(format, output, output2, msg, widget), 57 55 ) -> Response { 58 56 use formdata <- wisp.require_form(req) 59 57
+1 -2
formz_demo/src/formz_demo/examples/all_the_inputs.gleam
··· 1 1 import formz 2 - import formz/definition 3 2 import formz/field.{field} 4 3 import formz_string/definitions 5 4 import formz_string/widgets ··· 22 21 use i <- formz.optional( 23 22 field("textarea_widget"), 24 23 definitions.text_field() 25 - |> definition.set_widget(widgets.textarea_widget()), 24 + |> formz.widget(widgets.textarea_widget()), 26 25 ) 27 26 28 27 formz.create_form(#(a, b, c, d, e, f, g, h, i))
+5 -3
formz_demo/src/formz_demo/examples/custom_output.gleam
··· 1 1 import formz.{Field} 2 2 import formz/field.{field} 3 - import formz/widget 4 3 import formz_lustre/definitions 4 + import formz_lustre/widget 5 5 import lustre/attribute 6 6 import lustre/element/html 7 7 ··· 13 13 } 14 14 15 15 pub fn format_form(form) { 16 - let assert Ok(Field(username_field, username_widget)) = 16 + let assert Ok(Field(username_field, username_state, username_widget)) = 17 17 formz.get(form, "username") 18 18 19 - let assert Ok(Field(password_field, password_widget)) = 19 + let assert Ok(Field(password_field, password_state, password_widget)) = 20 20 formz.get(form, "password") 21 21 22 22 html.div( ··· 32 32 html.label([attribute.for("username")], [html.text("Username")]), 33 33 username_widget( 34 34 username_field, 35 + username_state, 35 36 widget.Args( 36 37 "username", 37 38 widget.LabelledByLabelFor, ··· 43 44 html.label([attribute.for("password")], [html.text("Password")]), 44 45 password_widget( 45 46 password_field, 47 + password_state, 46 48 widget.Args( 47 49 "username", 48 50 widget.LabelledByLabelFor,
+34
formz_demo/src/formz_demo/examples/list_fields.gleam
··· 1 + import formz 2 + import formz/field.{field} 3 + import formz_string/definitions 4 + import gleam/string 5 + 6 + pub fn make_form() { 7 + use cats <- formz.list( 8 + field("cats") |> field.set_help_text("Any number of cats"), 9 + definitions.text_field() 10 + |> formz.verify(fn(value) { 11 + case string.length(value) > 3 { 12 + True -> Ok(value) 13 + _ -> Error("must be longer than 3") 14 + } 15 + }), 16 + ) 17 + use dogs <- formz.limited_list( 18 + formz.limit_at_least(1), 19 + field("dogs") |> field.set_help_text("At least 1 dog"), 20 + definitions.text_field(), 21 + ) 22 + use fish <- formz.limited_list( 23 + formz.limit_at_most(2), 24 + field("fish") |> field.set_help_text("At most 2 fish"), 25 + definitions.text_field(), 26 + ) 27 + use hamsters <- formz.limited_list( 28 + formz.limit_between(2, 4), 29 + field("hamsters") |> field.set_help_text("Between 2 and 4 hamsters"), 30 + definitions.text_field(), 31 + ) 32 + 33 + formz.create_form(#(cats, dogs, fish, hamsters)) 34 + }
+3 -3
formz_demo/src/formz_demo/examples/login.gleam
··· 21 21 pub fn handle_post(formdata: wisp.FormData, form) { 22 22 form 23 23 |> formz.data(formdata.values) 24 - |> formz.parse_then_try(fn(form, credentials) { 24 + |> formz.decode_then_try(fn(form, credentials) { 25 25 case credentials { 26 26 Credentials("admin", "l33t") -> Ok(User(credentials.username)) 27 27 Credentials("admin", _) -> 28 28 form 29 - |> formz.update_field("password", field.set_error(_, "Wrong password")) 29 + |> formz.set_field_error("password", "Wrong password") 30 30 |> Error 31 31 Credentials(_, _) -> 32 32 form 33 - |> formz.update_field("username", field.set_error(_, "Wrong username")) 33 + |> formz.set_field_error("username", "Wrong username") 34 34 |> Error 35 35 } 36 36 })
+1 -2
formz_demo/src/formz_demo/examples/require_all_the_inputs.gleam
··· 1 1 import formz 2 - import formz/definition 3 2 import formz/field.{field} 4 3 import formz_string/definitions 5 4 import formz_string/widgets ··· 22 21 use i <- formz.require( 23 22 field("textarea_widget"), 24 23 definitions.text_field() 25 - |> definition.set_widget(widgets.textarea_widget()), 24 + |> formz.widget(widgets.textarea_widget()), 26 25 ) 27 26 28 27 formz.create_form(#(a, b, c, d, e, f, g, h, i))
+14
formz_demo/src/formz_demo/router.gleam
··· 12 12 import formz_demo/examples/custom_output 13 13 import formz_demo/examples/hello_world 14 14 import formz_demo/examples/labels 15 + import formz_demo/examples/list_fields 15 16 import formz_demo/examples/login 16 17 import formz_demo/examples/require_all_the_inputs 17 18 import formz_demo/examples/sub_form ··· 65 66 run.ExampleRun( 66 67 dir, 67 68 labels.make_form, 69 + defaults.handle_get, 70 + defaults.handle_post, 71 + defaults.format_string_form, 72 + defaults.formatted_string_form_to_string, 73 + ), 74 + ) 75 + }), 76 + #("list_fields", fn(req, dir) { 77 + run.handle( 78 + req, 79 + run.ExampleRun( 80 + dir, 81 + list_fields.make_form, 68 82 defaults.handle_get, 69 83 defaults.handle_post, 70 84 defaults.format_string_form,
+2 -2
formz_lustre/gleam.toml
··· 7 7 repository = { type = "github", user = "bentomas", repo = "formz" } 8 8 9 9 [dependencies] 10 - # formz = { path = "../formz" } 11 - formz = ">= 1.0.0 and < 2.0.0" 10 + formz = { path = "../formz" } 11 + # formz = ">= 1.0.0 and < 2.0.0" 12 12 gleam_stdlib = ">= 0.34.0 and < 2.0.0" 13 13 lustre = ">= 4.5.1 and < 5.0.0" 14 14
+3 -3
formz_lustre/manifest.toml
··· 2 2 # You typically do not need to edit this file 3 3 4 4 packages = [ 5 - { name = "formz", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "justin"], otp_app = "formz", source = "hex", outer_checksum = "2A3FDACEFDDD87E55344C7A8994A3EF5CC6ACA849D5EDB5C2E1074642C367DB5" }, 5 + { name = "formz", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "justin"], source = "local", path = "../formz" }, 6 6 { name = "formz_string", version = "1.0.0", build_tools = ["gleam"], requirements = ["formz", "gleam_stdlib"], source = "local", path = "../formz_string" }, 7 - { name = "gleam_erlang", version = "0.32.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "B18643083A0117AC5CFD0C1AEEBE5469071895ECFA426DCC26517A07F6AD9948" }, 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 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 10 { name = "gleam_stdlib", version = "0.45.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "206FCE1A76974AECFC55AEBCD0217D59EDE4E408C016E2CFCCC8FF51278F186E" }, ··· 14 14 ] 15 15 16 16 [requirements] 17 - formz = { version = ">= 1.0.0 and < 2.0.0" } 17 + formz = { path = "../formz" } 18 18 formz_string = { path = "../formz_string" } 19 19 gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } 20 20 gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
+11 -39
formz_lustre/src/formz_lustre/definitions.gleam
··· 4 4 //// examples of these in action, please see the [formz_demo](https://github.com/bentomas/formz/tree/main/formz_demo) 5 5 //// example project. 6 6 7 - import formz_lustre/widgets 8 - 9 - import formz/definition.{Definition} 7 + import formz 10 8 import formz/validation 9 + import formz_lustre/widgets 11 10 import gleam/int 12 11 import gleam/list 13 - import gleam/option 14 12 15 13 /// Create a basic form input. Parsed as a String. 16 14 pub fn text_field() { 17 - Definition( 15 + formz.definition_with_custom_optional( 18 16 widgets.input_widget("text"), 19 17 validation.non_empty_string, 20 18 "", ··· 31 29 /// Create an email form input. Parsed as a String but must 32 30 /// look like an email address, i.e. the string has an `@`. 33 31 pub fn email_field() { 34 - Definition( 35 - widgets.input_widget("email"), 36 - validation.email, 37 - "", 38 - definition.make_simple_optional_parse(), 39 - option.None, 40 - ) 32 + formz.definition(widgets.input_widget("email"), validation.email, "") 41 33 } 42 34 43 35 /// Create a whole number form input. Parsed as an Int. 44 36 pub fn integer_field() { 45 - Definition( 46 - widgets.number_widget(""), 47 - validation.int, 48 - 0, 49 - definition.make_simple_optional_parse(), 50 - option.None, 51 - ) 37 + formz.definition(widgets.number_widget(""), validation.int, 0) 52 38 } 53 39 54 40 /// Create a number form input. Parsed as a Float. 55 41 pub fn number_field() { 56 - Definition( 57 - widgets.number_widget("0.01"), 58 - validation.number, 59 - 0.0, 60 - definition.make_simple_optional_parse(), 61 - option.None, 62 - ) 42 + formz.definition(widgets.number_widget("0.01"), validation.number, 0.0) 63 43 } 64 44 65 45 /// Create a checkbox form input. Parsed as a Boolean. 66 46 pub fn boolean_field() { 67 - definition.Definition( 47 + formz.definition_with_custom_optional( 68 48 widget: widgets.checkbox_widget(), 69 49 parse: validation.on, 70 50 stub: False, 71 - optional_parse: fn(fun, str) { 51 + optional_parse: fn(parse, str) { 72 52 case str { 73 53 "" -> Ok(False) 74 - _ -> fun(str) 54 + _ -> parse(str) 75 55 } 76 56 }, 77 57 optional_stub: False, ··· 80 60 81 61 /// Create a password form input, which hides the input value. Parsed as a String 82 62 pub fn password_field() { 83 - Definition( 84 - widgets.password_widget(), 85 - validation.non_empty_string, 86 - "", 87 - definition.make_simple_optional_parse(), 88 - option.None, 89 - ) 63 + formz.definition(widgets.password_widget(), validation.non_empty_string, "") 90 64 } 91 65 92 66 /// Creates a `<select>` input. Takes a tuple of `#(String, String)` where the first ··· 102 76 |> list.index_map(fn(t, i) { #(t.0, int.to_string(i)) }) 103 77 let values = variants |> list.map(fn(t) { t.1 }) 104 78 105 - Definition( 79 + formz.definition( 106 80 widgets.select_widget(keys_indexed), 107 81 validation.list_item_by_index(values) 108 82 |> validation.replace_error("is required"), 109 83 stub, 110 - definition.make_simple_optional_parse(), 111 - option.None, 112 84 ) 113 85 } 114 86
+10 -9
formz_lustre/src/formz_lustre/simple.gleam
··· 1 1 import formz 2 - import formz/field 3 - import formz/widget 2 + import formz_lustre/widget 4 3 import gleam/list 5 4 import gleam/string 6 5 import lustre/attribute ··· 15 14 } 16 15 17 16 pub fn generate_item( 18 - item: formz.FormItem(widget.Widget(element.Element(msg))), 17 + item: formz.FormItem(widget.Widget(msg)), 19 18 ) -> element.Element(msg) { 20 19 case item { 21 - formz.Field(field, _) if field.hidden == True -> 20 + formz.Field(field, state, _) if field.hidden == True -> 22 21 html.input([ 23 22 attribute.type_("hidden"), 24 23 attribute.name(field.name), 25 - attribute.value(field.value), 24 + attribute.value(state.value), 26 25 ]) 27 26 28 - formz.Field(field, make_widget) -> { 27 + formz.Field(field, state, make_widget) -> { 29 28 let id = field.name 30 29 31 30 let label_el = ··· 48 47 ) 49 48 } 50 49 51 - let error = case field { 52 - field.Valid(..) -> #(element.none(), "") 53 - field.Invalid(error:, ..) -> #( 50 + let error = case state { 51 + formz.Invalid(error:, ..) -> #( 54 52 html.span( 55 53 [attribute.id(id <> "_error"), attribute.class("formz_error")], 56 54 [html.text(error)], 57 55 ), 58 56 id <> "_error", 59 57 ) 58 + _ -> #(element.none(), "") 60 59 } 61 60 62 61 let widget_el = 63 62 make_widget( 64 63 field, 64 + state, 65 65 widget.Args( 66 66 id: id, 67 67 labelled_by: widget.LabelledByLabelFor, ··· 84 84 let children = items |> list.map(generate_item) 85 85 html.fieldset([], [legend, html.div([], children)]) 86 86 } 87 + _ -> element.none() 87 88 } 88 89 }
+45
formz_lustre/src/formz_lustre/widget.gleam
··· 1 + //// The goal of a "widget" in `formz` is to produce an HTML input like 2 + //// `<input>`, `<select>`, or `<textarea>`. In a [`Definition`](https://hexdocs.pm/formz/formz/definition.html), 3 + //// a widget can be any Gleam type, and it's up to the form generator being 4 + //// used to know the exact type you need. 5 + //// 6 + //// That said, in the bundled form generators a widget is a function that 7 + //// takes the details of a field and some render time arguments that the form 8 + //// generator needs to construct an input. This module is for those form 9 + //// generators, and it's use is optional if you have different needs. 10 + 11 + import formz 12 + import formz/field 13 + import lustre/element 14 + 15 + pub type Widget(msg) = 16 + fn(field.Field, formz.FieldState, Args) -> element.Element(msg) 17 + 18 + pub type Args { 19 + Args( 20 + /// The id of the input element. 21 + id: String, 22 + /// Details of how the input is labelled. Some sort of label is required for accessibility. 23 + labelled_by: LabelledBy, 24 + /// Details of how the input is described. This is optional, but can be useful for accessibility. 25 + described_by: DescribedBy, 26 + ) 27 + } 28 + 29 + pub type LabelledBy { 30 + /// The input is labelled by a `<label>` element with a `for` attribute 31 + /// pointing to this input's id. This has the best accessibility support 32 + /// and should be [preferred when possible](https://www.w3.org/WAI/tutorials/forms/labels/). 33 + LabelledByLabelFor 34 + /// The input should be labelled using the `Field`'s `label` field. 35 + LabelledByFieldValue 36 + /// The input is labelled by elements with the specified ids. 37 + LabelledByElementsWithIds(ids: List(String)) 38 + } 39 + 40 + pub type DescribedBy { 41 + /// The input is described by elements with the specified ids. This is useful 42 + /// for additional instructions or error messages. 43 + DescribedByElementsWithIds(ids: List(String)) 44 + DescribedByNone 45 + }
+43 -27
formz_lustre/src/formz_lustre/widgets.gleam
··· 1 + import formz 1 2 import formz/field.{type Field} 2 - import formz/widget 3 + import formz_lustre/widget 3 4 import gleam/list 4 5 import gleam/string 5 6 import lustre/attribute ··· 61 62 } 62 63 } 63 64 64 - fn required_attr(requried: Bool) -> attribute.Attribute(msg) { 65 - // case requried { 66 - // True -> attribute.required(True) 67 - // False -> attribute.none() 68 - // } 69 - attribute.required(requried) 65 + fn required_attr(presence: formz.FieldPresence) -> attribute.Attribute(msg) { 66 + case presence { 67 + formz.Required -> attribute.required(True) 68 + formz.Optional -> attribute.none() 69 + } 70 70 } 71 71 72 72 fn step_size_attr(step_size: String) -> attribute.Attribute(msg) { ··· 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, args: widget.Args) { 97 - do_input_widget(field |> field.set_raw_value(""), args, "checkbox", [ 98 - checked_attr(field.value), 99 - ]) 96 + fn(field: Field, state: formz.FieldState, args: widget.Args) { 97 + let value = state.value 98 + let state = case state { 99 + formz.Unvalidated(_, presence) -> formz.Unvalidated("", presence) 100 + formz.Valid(_, presence) -> formz.Valid("", presence) 101 + formz.Invalid(_, presence, e) -> formz.Invalid("", presence, e) 102 + } 103 + do_input_widget(field, state, args, "checkbox", [checked_attr(value)]) 100 104 } 101 105 } 102 106 ··· 107 111 /// the step size. If you truly need any float, then a `type="text"` input might be a 108 112 /// better choice. 109 113 pub fn number_widget(step_size: String) { 110 - fn(field: Field, args: widget.Args) { 111 - do_input_widget(field, args, "number", [step_size_attr(step_size)]) 114 + fn(field: Field, state: formz.FieldState, args: widget.Args) { 115 + do_input_widget(field, state, args, "number", [step_size_attr(step_size)]) 112 116 } 113 117 } 114 118 115 119 /// Create an `<input type="password">`. This will not output the value in the 116 120 /// generated HTML for privacy/security concerns. 117 121 pub fn password_widget() { 118 - fn(field: Field, args: widget.Args) { 119 - do_input_widget(field |> field.set_raw_value(""), args, "password", []) 122 + fn(field: Field, state: formz.FieldState, args: widget.Args) { 123 + let state = case state { 124 + formz.Unvalidated(_, presence) -> formz.Unvalidated("", presence) 125 + formz.Valid(_, presence) -> formz.Valid("", presence) 126 + formz.Invalid(_, presence, e) -> formz.Invalid("", presence, e) 127 + } 128 + do_input_widget(field, state, args, "password", []) 120 129 } 121 130 } 122 131 123 132 /// Generate any `<input>` like `type="text"`, `type="email"` or 124 133 /// `type="url"`. 125 134 pub fn input_widget(type_: String) { 126 - fn(field: Field, args: widget.Args) { 127 - do_input_widget(field, args, type_, []) 135 + fn(field: Field, state: formz.FieldState, args: widget.Args) { 136 + do_input_widget(field, state, args, type_, []) 128 137 } 129 138 } 130 139 131 140 fn do_input_widget( 132 141 field: Field, 142 + state: formz.FieldState, 133 143 args: widget.Args, 134 144 type_: String, 135 145 extra_attrs: List(attribute.Attribute(msg)), ··· 140 150 attribute.type_(type_), 141 151 name_attr(field.name), 142 152 id_attr(args.id), 143 - required_attr(field.required), 153 + required_attr(state.presence), 144 154 disabled_attr(field.disabled), 145 - value_attr(field.value), 155 + value_attr(state.value), 146 156 aria_label_attr(args.labelled_by, field.label), 147 157 aria_describedby_attr(args.described_by), 148 158 ], ··· 153 163 154 164 /// Create a `<textarea></textarea>`. 155 165 pub fn textarea_widget() { 156 - fn(field: Field, args: widget.Args) -> element.Element(msg) { 166 + fn(field: Field, state: formz.FieldState, args: widget.Args) -> element.Element( 167 + msg, 168 + ) { 157 169 html.textarea( 158 170 [ 159 171 name_attr(field.name), 160 172 id_attr(args.id), 161 - required_attr(field.required), 173 + required_attr(state.presence), 162 174 aria_label_attr(args.labelled_by, field.label), 163 175 aria_describedby_attr(args.described_by), 164 176 ], 165 - field.value, 177 + state.value, 166 178 ) 167 179 } 168 180 } ··· 171 183 /// passing data around and you don't want it to be visible to the user. Like 172 184 /// say, the ID of a record being edited. 173 185 pub fn hidden_widget() { 174 - fn(field: Field, _args: widget.Args) -> element.Element(msg) { 186 + fn(field: Field, state: formz.FieldState, _args: widget.Args) -> element.Element( 187 + msg, 188 + ) { 175 189 html.input([ 176 190 attribute.type_("hidden"), 177 191 name_attr(field.name), 178 - value_attr(field.value), 192 + value_attr(state.value), 179 193 ]) 180 194 } 181 195 } ··· 184 198 /// of variants is a two-tuple, where the first item is the text to display and 185 199 /// the second item is the value. 186 200 pub fn select_widget(variants: List(#(String, String))) { 187 - fn(field: Field, args: widget.Args) -> element.Element(msg) { 201 + fn(field: Field, state: formz.FieldState, args: widget.Args) -> element.Element( 202 + msg, 203 + ) { 188 204 html.select( 189 205 [ 190 206 name_attr(field.name), 191 207 id_attr(args.id), 192 - required_attr(field.required), 208 + required_attr(state.presence), 193 209 aria_label_attr(args.labelled_by, field.label), 194 210 aria_describedby_attr(args.described_by), 195 211 ], ··· 198 214 list.map(variants, fn(variant) { 199 215 let val = variant.1 200 216 html.option( 201 - [attribute.value(val), attribute.selected(field.value == val)], 217 + [attribute.value(val), attribute.selected(state.value == val)], 202 218 variant.0, 203 219 ) 204 220 }),
+4 -18
formz_lustre/test/formz_lustre/fields_test.gleam
··· 1 1 import formz_lustre/definitions 2 2 3 - import formz/definition.{type Definition} 3 + import formz.{type Definition} 4 4 import formz_string/definitions as string_definitions 5 5 import gleeunit 6 6 import gleeunit/should ··· 10 10 } 11 11 12 12 fn compare_parse_fns(this: Definition(a, b, c), that: Definition(d, b, c), str) { 13 - this.parse(str) |> should.equal(that.parse(str)) 14 - this.optional_parse(this.parse, "") 15 - |> should.equal(that.optional_parse(that.parse, "")) 13 + formz.get_parse(this)(str) |> should.equal(formz.get_parse(that)(str)) 14 + formz.get_optional_parse(this)(formz.get_parse(this), "") 15 + |> should.equal(formz.get_optional_parse(that)(formz.get_parse(that), "")) 16 16 } 17 17 18 18 pub fn text_field_test() { 19 19 let string_definition = string_definitions.text_field() 20 20 let definition = definitions.text_field() 21 21 22 - definition.stub |> should.equal(string_definition.stub) 23 - definition.optional_stub |> should.equal(string_definition.optional_stub) 24 22 compare_parse_fns(definition, string_definition, "") 25 23 compare_parse_fns(definition, string_definition, "a") 26 24 } ··· 29 27 let string_definition = string_definitions.email_field() 30 28 let definition = definitions.email_field() 31 29 32 - definition.stub |> should.equal(string_definition.stub) 33 - definition.optional_stub |> should.equal(string_definition.optional_stub) 34 30 compare_parse_fns(definition, string_definition, "") 35 31 compare_parse_fns(definition, string_definition, "a") 36 32 } ··· 39 35 let string_definition = string_definitions.number_field() 40 36 let definition = definitions.number_field() 41 37 42 - definition.stub |> should.equal(string_definition.stub) 43 - definition.optional_stub |> should.equal(string_definition.optional_stub) 44 38 compare_parse_fns(definition, string_definition, "") 45 39 compare_parse_fns(definition, string_definition, "a") 46 40 } ··· 49 43 let string_definition = string_definitions.integer_field() 50 44 let definition = definitions.integer_field() 51 45 52 - definition.stub |> should.equal(string_definition.stub) 53 - definition.optional_stub |> should.equal(string_definition.optional_stub) 54 46 compare_parse_fns(definition, string_definition, "") 55 47 compare_parse_fns(definition, string_definition, "a") 56 48 } ··· 59 51 let string_definition = string_definitions.boolean_field() 60 52 let definition = definitions.boolean_field() 61 53 62 - definition.stub |> should.equal(string_definition.stub) 63 - definition.optional_stub |> should.equal(string_definition.optional_stub) 64 54 compare_parse_fns(definition, string_definition, "") 65 55 compare_parse_fns(definition, string_definition, "a") 66 56 } ··· 70 60 string_definitions.choices_field([#("a", "A"), #("b", "B")], "") 71 61 let definition = definitions.choices_field([#("a", "A"), #("b", "B")], "") 72 62 73 - definition.stub |> should.equal(string_definition.stub) 74 - definition.optional_stub |> should.equal(string_definition.optional_stub) 75 63 compare_parse_fns(definition, string_definition, "") 76 64 compare_parse_fns(definition, string_definition, "a") 77 65 } ··· 80 68 let string_definition = string_definitions.list_field(["A", "B"]) 81 69 let definition = definitions.list_field(["A", "B"]) 82 70 83 - definition.stub |> should.equal(string_definition.stub) 84 - definition.optional_stub |> should.equal(string_definition.optional_stub) 85 71 compare_parse_fns(definition, string_definition, "") 86 72 compare_parse_fns(definition, string_definition, "a") 87 73 }
+42 -36
formz_lustre/test/formz_lustre/widgets_test.gleam
··· 1 + import formz 1 2 import formz/field 2 - import formz/widget 3 + import formz_lustre/widget 4 + import formz_lustre/widgets 5 + import formz_string/widget as string_widget 6 + import formz_string/widgets as string_widgets 3 7 import gleeunit 4 8 import gleeunit/should 5 9 import lustre/element 6 - 7 - import formz_lustre/widgets 8 - import formz_string/widgets as string_widgets 9 10 10 11 pub fn main() { 11 12 gleeunit.main() ··· 16 17 |> element.to_string 17 18 } 18 19 20 + pub fn to_string_args(args: widget.Args) { 21 + let labelled_by = case args.labelled_by { 22 + widget.LabelledByFieldValue -> string_widget.LabelledByFieldValue 23 + widget.LabelledByElementsWithIds(ids) -> 24 + string_widget.LabelledByElementsWithIds(ids) 25 + widget.LabelledByLabelFor -> string_widget.LabelledByLabelFor 26 + } 27 + 28 + let described_by = case args.described_by { 29 + widget.DescribedByNone -> string_widget.DescribedByNone 30 + widget.DescribedByElementsWithIds(ids) -> 31 + string_widget.DescribedByElementsWithIds(ids) 32 + } 33 + 34 + string_widget.Args(args.id, labelled_by, described_by) 35 + } 36 + 19 37 fn test_inputs( 20 - name name, 21 - label label, 22 - help help_text, 23 - hidden hidden, 24 - disabled disabled, 25 - required required, 26 - value value, 27 - args args, 28 - string string_widget, 29 - widget widget, 38 + name name: String, 39 + label label: String, 40 + help help_text: String, 41 + hidden hidden: Bool, 42 + disabled disabled: Bool, 43 + required required: Bool, 44 + value value: String, 45 + args args: widget.Args, 46 + string string_widget: string_widget.Widget, 47 + widget widget: widget.Widget(msg), 30 48 ) { 31 - let string_field = 32 - field.Valid( 33 - name:, 34 - label:, 35 - help_text:, 36 - hidden:, 37 - value:, 38 - disabled:, 39 - required:, 40 - ) 41 - let field = 42 - field.Valid( 43 - name:, 44 - label:, 45 - help_text:, 46 - hidden:, 47 - value:, 48 - disabled:, 49 - required:, 50 - ) 49 + let string_field = field.Field(name:, label:, help_text:, hidden:, disabled:) 50 + let field = field.Field(name:, label:, help_text:, hidden:, disabled:) 51 + 52 + let presence = case required { 53 + True -> formz.Required 54 + False -> formz.Optional 55 + } 56 + let state = formz.Valid(value, presence) 51 57 52 - widget(field, args) 58 + widget(field, state, args) 53 59 |> convert_to_string 54 - |> should.equal(string_widget(string_field, args)) 60 + |> should.equal(string_widget(string_field, state, args |> to_string_args)) 55 61 } 56 62 57 63 pub fn text_widget_test() {
+2 -2
formz_nakai/gleam.toml
··· 7 7 repository = { type = "github", user = "bentomas", repo = "formz" } 8 8 9 9 [dependencies] 10 - # formz = { path = "../formz" } 11 - formz = ">= 1.0.0 and < 2.0.0" 10 + formz = { path = "../formz" } 11 + # formz = ">= 1.0.0 and < 2.0.0" 12 12 gleam_stdlib = ">= 0.34.0 and < 2.0.0" 13 13 nakai = ">= 1.0.0 and < 2.0.0" 14 14
+2 -2
formz_nakai/manifest.toml
··· 2 2 # You typically do not need to edit this file 3 3 4 4 packages = [ 5 - { name = "formz", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "justin"], otp_app = "formz", source = "hex", outer_checksum = "2A3FDACEFDDD87E55344C7A8994A3EF5CC6ACA849D5EDB5C2E1074642C367DB5" }, 5 + { name = "formz", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "justin"], source = "local", path = "../formz" }, 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_stdlib", version = "0.45.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "206FCE1A76974AECFC55AEBCD0217D59EDE4E408C016E2CFCCC8FF51278F186E" }, 8 8 { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, ··· 11 11 ] 12 12 13 13 [requirements] 14 - formz = { version = ">= 1.0.0 and < 2.0.0" } 14 + formz = { path = "../formz" } 15 15 formz_string = { path = "../formz_string" } 16 16 gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } 17 17 gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
+13 -41
formz_nakai/src/formz_nakai/definitions.gleam
··· 4 4 //// examples of these in action, please see the [formz_demo](https://github.com/bentomas/formz/tree/main/formz_demo) 5 5 //// example project. 6 6 7 + import formz 8 + import formz/validation 7 9 import formz_nakai/widgets 8 - 9 - import formz/definition.{Definition} 10 - import formz/validation 11 10 import gleam/int 12 11 import gleam/list 13 - import gleam/option 14 12 15 13 /// Create a basic form input. Parsed as a String. 16 14 pub fn text_field() { 17 - Definition( 15 + formz.definition_with_custom_optional( 18 16 widgets.input_widget("text"), 19 17 validation.non_empty_string, 20 18 "", 21 - fn(fun, str) { 19 + fn(parse, str) { 22 20 case str { 23 21 "" -> Ok("") 24 - _ -> fun(str) 22 + _ -> parse(str) 25 23 } 26 24 }, 27 25 "", ··· 31 29 /// Create an email form input. Parsed as a String but must 32 30 /// look like an email address, i.e. the string has an `@`. 33 31 pub fn email_field() { 34 - Definition( 35 - widgets.input_widget("email"), 36 - validation.email, 37 - "", 38 - definition.make_simple_optional_parse(), 39 - option.None, 40 - ) 32 + formz.definition(widgets.input_widget("email"), validation.email, "") 41 33 } 42 34 43 35 /// Create a whole number form input. Parsed as an Int. 44 36 pub fn integer_field() { 45 - Definition( 46 - widgets.number_widget(""), 47 - validation.int, 48 - 0, 49 - definition.make_simple_optional_parse(), 50 - option.None, 51 - ) 37 + formz.definition(widgets.number_widget(""), validation.int, 0) 52 38 } 53 39 54 40 /// Create a number form input. Parsed as a Float. 55 41 pub fn number_field() { 56 - Definition( 57 - widgets.number_widget("0.01"), 58 - validation.number, 59 - 0.0, 60 - definition.make_simple_optional_parse(), 61 - option.None, 62 - ) 42 + formz.definition(widgets.number_widget("0.01"), validation.number, 0.0) 63 43 } 64 44 65 45 /// Create a checkbox form input. Parsed as a Boolean. 66 46 pub fn boolean_field() { 67 - definition.Definition( 47 + formz.definition_with_custom_optional( 68 48 widget: widgets.checkbox_widget(), 69 49 parse: validation.on, 70 50 stub: False, 71 - optional_parse: fn(fun, str) { 51 + optional_parse: fn(parse, str) { 72 52 case str { 73 53 "" -> Ok(False) 74 - _ -> fun(str) 54 + _ -> parse(str) 75 55 } 76 56 }, 77 57 optional_stub: False, ··· 80 60 81 61 /// Create a password form input, which hides the input value. Parsed as a String 82 62 pub fn password_field() { 83 - Definition( 84 - widgets.password_widget(), 85 - validation.non_empty_string, 86 - "", 87 - definition.make_simple_optional_parse(), 88 - option.None, 89 - ) 63 + formz.definition(widgets.password_widget(), validation.non_empty_string, "") 90 64 } 91 65 92 66 /// Creates a `<select>` input. Takes a tuple of `#(String, String)` where the first ··· 102 76 |> list.index_map(fn(t, i) { #(t.0, int.to_string(i)) }) 103 77 let values = variants |> list.map(fn(t) { t.1 }) 104 78 105 - Definition( 79 + formz.definition( 106 80 widgets.select_widget(keys_indexed), 107 81 validation.list_item_by_index(values) 108 82 |> validation.replace_error("is required"), 109 83 stub, 110 - definition.make_simple_optional_parse(), 111 - option.None, 112 84 ) 113 85 } 114 86
+11 -11
formz_nakai/src/formz_nakai/simple.gleam
··· 1 1 import formz 2 - import formz/field 3 - import formz/widget 2 + import formz_nakai/widget 4 3 import gleam/list 5 4 import nakai/attr 6 5 import nakai/html ··· 12 11 |> html.div([attr.class("formz_items")], _) 13 12 } 14 13 15 - pub fn generate_visible_item( 16 - item: formz.FormItem(widget.Widget(html.Node)), 17 - ) -> html.Node { 14 + pub fn generate_visible_item(item: formz.FormItem(widget.Widget)) -> html.Node { 18 15 case item { 19 - formz.Field(field, _) if field.hidden == True -> 16 + formz.Field(field, state, _) if field.hidden == True -> 20 17 html.input([ 21 18 attr.type_("hidden"), 22 19 attr.name(field.name), 23 - attr.value(field.value), 20 + attr.value(state.value), 24 21 ]) 25 - formz.Field(field, make_widget) -> { 22 + formz.Field(field, state, make_widget) -> { 26 23 let id = field.name 27 24 28 25 let label_el = ··· 38 35 id <> "_help_text", 39 36 ) 40 37 } 41 - let error = case field { 42 - field.Valid(..) -> #(html.Nothing, "") 43 - field.Invalid(error:, ..) -> #( 38 + let error = case state { 39 + formz.Invalid(error:, ..) -> #( 44 40 html.span([attr.id(id <> "_error"), attr.class("formz_error")], [ 45 41 html.Text(error), 46 42 ]), 47 43 id <> "_error", 48 44 ) 45 + _ -> #(html.Nothing, "") 49 46 } 50 47 51 48 let widget_el = 52 49 make_widget( 53 50 field, 51 + state, 54 52 widget.Args( 55 53 id: id, 56 54 labelled_by: widget.LabelledByLabelFor, ··· 73 71 let children = items |> list.map(generate_visible_item) 74 72 html.fieldset([], [legend, html.div([], children)]) 75 73 } 74 + 75 + _ -> html.Nothing 76 76 } 77 77 }
+45
formz_nakai/src/formz_nakai/widget.gleam
··· 1 + //// The goal of a "widget" in `formz` is to produce an HTML input like 2 + //// `<input>`, `<select>`, or `<textarea>`. In a [`Definition`](https://hexdocs.pm/formz/formz/definition.html), 3 + //// a widget can be any Gleam type, and it's up to the form generator being 4 + //// used to know the exact type you need. 5 + //// 6 + //// That said, in the bundled form generators a widget is a function that 7 + //// takes the details of a field and some render time arguments that the form 8 + //// generator needs to construct an input. This module is for those form 9 + //// generators, and it's use is optional if you have different needs. 10 + 11 + import formz 12 + import formz/field 13 + import nakai/html 14 + 15 + pub type Widget = 16 + fn(field.Field, formz.FieldState, Args) -> html.Node 17 + 18 + pub type Args { 19 + Args( 20 + /// The id of the input element. 21 + id: String, 22 + /// Details of how the input is labelled. Some sort of label is required for accessibility. 23 + labelled_by: LabelledBy, 24 + /// Details of how the input is described. This is optional, but can be useful for accessibility. 25 + described_by: DescribedBy, 26 + ) 27 + } 28 + 29 + pub type LabelledBy { 30 + /// The input is labelled by a `<label>` element with a `for` attribute 31 + /// pointing to this input's id. This has the best accessibility support 32 + /// and should be [preferred when possible](https://www.w3.org/WAI/tutorials/forms/labels/). 33 + LabelledByLabelFor 34 + /// The input should be labelled using the `Field`'s `label` field. 35 + LabelledByFieldValue 36 + /// The input is labelled by elements with the specified ids. 37 + LabelledByElementsWithIds(ids: List(String)) 38 + } 39 + 40 + pub type DescribedBy { 41 + /// The input is described by elements with the specified ids. This is useful 42 + /// for additional instructions or error messages. 43 + DescribedByElementsWithIds(ids: List(String)) 44 + DescribedByNone 45 + }
+36 -25
formz_nakai/src/formz_nakai/widgets.gleam
··· 1 + import formz 1 2 import formz/field.{type Field} 2 - import formz/widget 3 + import formz_nakai/widget 3 4 import gleam/string 4 5 5 6 import gleam/list ··· 61 62 } 62 63 } 63 64 64 - fn required_attr(required: Bool) -> List(attr.Attr) { 65 - case required { 66 - True -> [attr.required("")] 67 - False -> [] 65 + fn required_attr(presence: formz.FieldPresence) -> List(attr.Attr) { 66 + case presence { 67 + formz.Required -> [attr.required("")] 68 + formz.Optional -> [] 68 69 } 69 70 } 70 71 ··· 92 93 /// Create an `<input type="checkbox">`. The checkbox is checked 93 94 /// if the value is "on" (the browser default). 94 95 pub fn checkbox_widget() { 95 - fn(field: Field, args: widget.Args) { 96 - do_input_widget(field |> field.set_raw_value(""), args, "checkbox", [ 97 - checked_attr(field.value), 98 - ]) 96 + fn(field: Field, state: formz.FieldState, args: widget.Args) { 97 + let value = state.value 98 + let state = case state { 99 + formz.Unvalidated(_, presence) -> formz.Unvalidated("", presence) 100 + formz.Valid(_, presence) -> formz.Valid("", presence) 101 + formz.Invalid(_, presence, e) -> formz.Invalid("", presence, e) 102 + } 103 + do_input_widget(field, state, args, "checkbox", [checked_attr(value)]) 99 104 } 100 105 } 101 106 ··· 106 111 /// the step size. If you truly need any float, then a `type="text"` input might be a 107 112 /// better choice. 108 113 pub fn number_widget(step_size: String) { 109 - fn(field: Field, args: widget.Args) { 110 - do_input_widget(field, args, "number", [step_size_attr(step_size)]) 114 + fn(field: Field, state: formz.FieldState, args: widget.Args) { 115 + do_input_widget(field, state, args, "number", [step_size_attr(step_size)]) 111 116 } 112 117 } 113 118 114 119 /// Create an `<input type="password">`. This will not output the value in the 115 120 /// generated HTML for privacy/security concerns. 116 121 pub fn password_widget() { 117 - fn(field: Field, args: widget.Args) { 118 - do_input_widget(field |> field.set_raw_value(""), args, "password", []) 122 + fn(field: Field, state: formz.FieldState, args: widget.Args) { 123 + let state = case state { 124 + formz.Unvalidated(_, presence) -> formz.Unvalidated("", presence) 125 + formz.Valid(_, presence) -> formz.Valid("", presence) 126 + formz.Invalid(_, presence, e) -> formz.Invalid("", presence, e) 127 + } 128 + do_input_widget(field, state, args, "password", []) 119 129 } 120 130 } 121 131 122 132 /// Generate any `<input>` like `type="text"`, `type="email"` or 123 133 /// `type="url"`. 124 134 pub fn input_widget(type_: String) { 125 - fn(field: Field, args: widget.Args) { 126 - do_input_widget(field, args, type_, []) 135 + fn(field: Field, state: formz.FieldState, args: widget.Args) { 136 + do_input_widget(field, state, args, type_, []) 127 137 } 128 138 } 129 139 130 140 fn do_input_widget( 131 141 field: Field, 142 + state: formz.FieldState, 132 143 args: widget.Args, 133 144 type_: String, 134 145 extra_attrs: List(List(attr.Attr)), ··· 138 149 type_attr(type_), 139 150 name_attr(field.name), 140 151 id_attr(args.id), 141 - required_attr(field.required), 142 - value_attr(field.value), 152 + required_attr(state.presence), 153 + value_attr(state.value), 143 154 disabled_attr(field.disabled), 144 155 aria_describedby_attr(args.described_by), 145 156 aria_label_attr(args.labelled_by, field.label), ··· 150 161 151 162 /// Create a `<textarea></textarea>`. 152 163 pub fn textarea_widget() { 153 - fn(field: Field, args: widget.Args) -> html.Node { 164 + fn(field: Field, state: formz.FieldState, args: widget.Args) -> html.Node { 154 165 html.textarea( 155 166 list.flatten([ 156 167 name_attr(field.name), 157 168 id_attr(args.id), 158 - required_attr(field.required), 169 + required_attr(state.presence), 159 170 aria_label_attr(args.labelled_by, field.label), 160 171 ]), 161 - [html.Text(field.value)], 172 + [html.Text(state.value)], 162 173 ) 163 174 } 164 175 } ··· 167 178 /// passing data around and you don't want it to be visible to the user. Like 168 179 /// say, the ID of a record being edited. 169 180 pub fn hidden_widget() { 170 - fn(field: Field, _) -> html.Node { 181 + fn(field: Field, state: formz.FieldState, _) -> html.Node { 171 182 html.input( 172 183 list.flatten([ 173 184 type_attr("hidden"), 174 185 name_attr(field.name), 175 - value_attr(field.value), 186 + value_attr(state.value), 176 187 ]), 177 188 ) 178 189 } ··· 182 193 /// of variants is a two-tuple, where the first item is the text to display and 183 194 /// the second item is the value. 184 195 pub fn select_widget(variants: List(#(String, String))) { 185 - fn(field: Field, args: widget.Args) -> html.Node { 196 + fn(field: Field, state: formz.FieldState, args: widget.Args) -> html.Node { 186 197 html.select( 187 198 list.flatten([ 188 199 name_attr(field.name), 189 200 id_attr(args.id), 190 - required_attr(field.required), 201 + required_attr(state.presence), 191 202 aria_label_attr(args.labelled_by, field.label), 192 203 ]), 193 204 list.flatten([ 194 205 [html.option([attr.value("")], [html.Text("Select...")]), html.hr([])], 195 206 list.map(variants, fn(variant) { 196 207 let val = variant.1 197 - let selected_attr = case field.value == val { 208 + let selected_attr = case state.value == val { 198 209 True -> [attr.selected()] 199 210 _ -> [] 200 211 }
+4 -18
formz_nakai/test/formz_nakai/fields_test.gleam
··· 1 1 import formz_nakai/definitions 2 2 3 - import formz/definition.{type Definition} 3 + import formz.{type Definition} 4 4 import formz_string/definitions as string_definitions 5 5 import gleeunit 6 6 import gleeunit/should ··· 10 10 } 11 11 12 12 fn compare_parse_fns(this: Definition(a, b, c), that: Definition(d, b, c), str) { 13 - this.parse(str) |> should.equal(that.parse(str)) 14 - this.optional_parse(this.parse, "") 15 - |> should.equal(that.optional_parse(that.parse, "")) 13 + formz.get_parse(this)(str) |> should.equal(formz.get_parse(that)(str)) 14 + formz.get_optional_parse(this)(formz.get_parse(this), "") 15 + |> should.equal(formz.get_optional_parse(that)(formz.get_parse(that), "")) 16 16 } 17 17 18 18 pub fn text_field_test() { 19 19 let string_definition = string_definitions.text_field() 20 20 let definition = definitions.text_field() 21 21 22 - definition.stub |> should.equal(string_definition.stub) 23 - definition.optional_stub |> should.equal(string_definition.optional_stub) 24 22 compare_parse_fns(definition, string_definition, "") 25 23 compare_parse_fns(definition, string_definition, "a") 26 24 } ··· 29 27 let string_definition = string_definitions.email_field() 30 28 let definition = definitions.email_field() 31 29 32 - definition.stub |> should.equal(string_definition.stub) 33 - definition.optional_stub |> should.equal(string_definition.optional_stub) 34 30 compare_parse_fns(definition, string_definition, "") 35 31 compare_parse_fns(definition, string_definition, "a") 36 32 } ··· 39 35 let string_definition = string_definitions.number_field() 40 36 let definition = definitions.number_field() 41 37 42 - definition.stub |> should.equal(string_definition.stub) 43 - definition.optional_stub |> should.equal(string_definition.optional_stub) 44 38 compare_parse_fns(definition, string_definition, "") 45 39 compare_parse_fns(definition, string_definition, "a") 46 40 } ··· 49 43 let string_definition = string_definitions.integer_field() 50 44 let definition = definitions.integer_field() 51 45 52 - definition.stub |> should.equal(string_definition.stub) 53 - definition.optional_stub |> should.equal(string_definition.optional_stub) 54 46 compare_parse_fns(definition, string_definition, "") 55 47 compare_parse_fns(definition, string_definition, "a") 56 48 } ··· 59 51 let string_definition = string_definitions.boolean_field() 60 52 let definition = definitions.boolean_field() 61 53 62 - definition.stub |> should.equal(string_definition.stub) 63 - definition.optional_stub |> should.equal(string_definition.optional_stub) 64 54 compare_parse_fns(definition, string_definition, "") 65 55 compare_parse_fns(definition, string_definition, "a") 66 56 } ··· 70 60 string_definitions.choices_field([#("a", "A"), #("b", "B")], "") 71 61 let definition = definitions.choices_field([#("a", "A"), #("b", "B")], "") 72 62 73 - definition.stub |> should.equal(string_definition.stub) 74 - definition.optional_stub |> should.equal(string_definition.optional_stub) 75 63 compare_parse_fns(definition, string_definition, "") 76 64 compare_parse_fns(definition, string_definition, "a") 77 65 } ··· 80 68 let string_definition = string_definitions.list_field(["A", "B"]) 81 69 let definition = definitions.list_field(["A", "B"]) 82 70 83 - definition.stub |> should.equal(string_definition.stub) 84 - definition.optional_stub |> should.equal(string_definition.optional_stub) 85 71 compare_parse_fns(definition, string_definition, "") 86 72 compare_parse_fns(definition, string_definition, "a") 87 73 }
+43 -92
formz_nakai/test/formz_nakai/widgets_test.gleam
··· 1 + import formz 1 2 import formz/field 2 - import formz/widget 3 + import formz_nakai/widget 4 + import formz_nakai/widgets 5 + import formz_string/widget as string_widget 6 + import formz_string/widgets as string_widgets 3 7 import gleam/string 4 8 import gleeunit 5 9 import gleeunit/should 6 10 import nakai 7 - 8 - import formz_nakai/widgets 9 - import formz_string/widgets as string_widgets 10 11 11 12 pub fn main() { 12 13 gleeunit.main() 13 14 } 14 15 16 + pub fn to_string_args(args: widget.Args) { 17 + let labelled_by = case args.labelled_by { 18 + widget.LabelledByFieldValue -> string_widget.LabelledByFieldValue 19 + widget.LabelledByElementsWithIds(ids) -> 20 + string_widget.LabelledByElementsWithIds(ids) 21 + widget.LabelledByLabelFor -> string_widget.LabelledByLabelFor 22 + } 23 + 24 + let described_by = case args.described_by { 25 + widget.DescribedByNone -> string_widget.DescribedByNone 26 + widget.DescribedByElementsWithIds(ids) -> 27 + string_widget.DescribedByElementsWithIds(ids) 28 + } 29 + 30 + string_widget.Args(args.id, labelled_by, described_by) 31 + } 32 + 15 33 fn remove_self_closing_slash(str: String) -> String { 16 34 string.replace(str, " />", ">") 17 35 } ··· 35 53 } 36 54 37 55 fn test_inputs( 38 - name name, 39 - label label, 40 - help help_text, 41 - hidden hidden, 42 - disabled disabled, 43 - required required, 44 - value value, 45 - args args, 46 - string string_widget, 47 - widget widget, 56 + name name: String, 57 + label label: String, 58 + help help_text: String, 59 + hidden hidden: Bool, 60 + disabled disabled: Bool, 61 + required required: Bool, 62 + value value: String, 63 + args args: widget.Args, 64 + string string_widget: string_widget.Widget, 65 + widget widget: widget.Widget, 48 66 ) { 49 - let string_field = 50 - field.Valid( 51 - name:, 52 - label:, 53 - help_text:, 54 - hidden:, 55 - value:, 56 - disabled:, 57 - required:, 58 - ) 59 - let field = 60 - field.Valid( 61 - name:, 62 - label:, 63 - help_text:, 64 - hidden:, 65 - value:, 66 - disabled:, 67 - required:, 68 - ) 67 + let string_field = field.Field(name:, label:, help_text:, hidden:, disabled:) 68 + let field = field.Field(name:, label:, help_text:, hidden:, disabled:) 69 + 70 + let presence = case required { 71 + True -> formz.Required 72 + False -> formz.Optional 73 + } 74 + let state = formz.Valid(value, presence) 69 75 70 - widget(field, args) 76 + widget(field, state, args) 71 77 |> convert_to_string 72 - |> should.equal(string_widget(string_field, args)) 78 + |> should.equal(string_widget(string_field, state, args |> to_string_args)) 73 79 } 74 80 75 81 pub fn text_widget_test() { ··· 81 87 help: "help", 82 88 hidden: False, 83 89 disabled: False, 84 - required: False, 90 + required: True, 85 91 value: "", 86 92 args: widget.Args( 87 93 "id", ··· 138 144 "id", 139 145 labelled_by: widget.LabelledByFieldValue, 140 146 described_by: widget.DescribedByNone, 141 - ), 142 - ) 143 - 144 - test_inputs( 145 - string_widgets.input_widget("text"), 146 - widgets.input_widget("text"), 147 - name: "a", 148 - label: "A", 149 - help: "help", 150 - hidden: False, 151 - disabled: True, 152 - required: False, 153 - value: "", 154 - args: widget.Args( 155 - "id", 156 - labelled_by: widget.LabelledByFieldValue, 157 - described_by: widget.DescribedByNone, 158 - ), 159 - ) 160 - } 161 - 162 - pub fn described_by_ids_test() { 163 - test_inputs( 164 - string_widgets.input_widget("text"), 165 - widgets.input_widget("text"), 166 - name: "a", 167 - label: "A", 168 - help: "help", 169 - hidden: False, 170 - disabled: False, 171 - required: False, 172 - value: "", 173 - args: widget.Args( 174 - "id", 175 - labelled_by: widget.LabelledByLabelFor, 176 - described_by: widget.DescribedByElementsWithIds(["id1", "id2"]), 177 - ), 178 - ) 179 - } 180 - 181 - pub fn described_by_ids_all_empty_test() { 182 - test_inputs( 183 - string_widgets.input_widget("text"), 184 - widgets.input_widget("text"), 185 - name: "a", 186 - label: "A", 187 - help: "help", 188 - hidden: False, 189 - disabled: False, 190 - required: False, 191 - value: "", 192 - args: widget.Args( 193 - "id", 194 - labelled_by: widget.LabelledByLabelFor, 195 - described_by: widget.DescribedByElementsWithIds(["", ""]), 196 147 ), 197 148 ) 198 149 }
+9 -37
formz_string/src/formz_string/definitions.gleam
··· 4 4 //// examples of these in action, please see the [formz_demo](https://github.com/bentomas/formz/tree/main/formz_demo) 5 5 //// example project. 6 6 7 - import formz_string/widgets 8 - 9 - import formz/definition.{Definition} 7 + import formz 10 8 import formz/validation 9 + import formz_string/widgets 11 10 import gleam/int 12 11 import gleam/list 13 - import gleam/option 14 12 15 13 /// Create a basic form input. Parsed as a String. 16 14 pub fn text_field() { 17 - Definition( 15 + formz.definition_with_custom_optional( 18 16 widgets.input_widget("text"), 19 17 validation.non_empty_string, 20 18 "", ··· 31 29 /// Create an email form input. Parsed as a String but must 32 30 /// look like an email address, i.e. the string has an `@`. 33 31 pub fn email_field() { 34 - Definition( 35 - widgets.input_widget("email"), 36 - validation.email, 37 - "", 38 - definition.make_simple_optional_parse(), 39 - option.None, 40 - ) 32 + formz.definition(widgets.input_widget("email"), validation.email, "") 41 33 } 42 34 43 35 /// Create a whole number form input. Parsed as an Int. 44 36 pub fn integer_field() { 45 - Definition( 46 - widgets.number_widget(""), 47 - validation.int, 48 - 0, 49 - definition.make_simple_optional_parse(), 50 - option.None, 51 - ) 37 + formz.definition(widgets.number_widget(""), validation.int, 0) 52 38 } 53 39 54 40 /// Create a number form input. Parsed as a Float. 55 41 pub fn number_field() { 56 - Definition( 57 - widgets.number_widget("0.01"), 58 - validation.number, 59 - 0.0, 60 - definition.make_simple_optional_parse(), 61 - option.None, 62 - ) 42 + formz.definition(widgets.number_widget("0.01"), validation.number, 0.0) 63 43 } 64 44 65 45 /// Create a checkbox form input. Parsed as a `Bool`. If required, the parsed 66 46 /// `Bool` must be `True`. 67 47 pub fn boolean_field() { 68 - definition.Definition( 48 + formz.definition_with_custom_optional( 69 49 widget: widgets.checkbox_widget(), 70 50 parse: validation.on, 71 51 stub: False, ··· 81 61 82 62 /// Create a password form input, which hides the input value. Parsed as a String. 83 63 pub fn password_field() { 84 - Definition( 85 - widgets.password_widget(), 86 - validation.non_empty_string, 87 - "", 88 - definition.make_simple_optional_parse(), 89 - option.None, 90 - ) 64 + formz.definition(widgets.password_widget(), validation.non_empty_string, "") 91 65 } 92 66 93 67 /// Creates a `<select>` input. Takes a tuple of `#(String, String)` where the first ··· 105 79 |> list.index_map(fn(t, i) { #(t.0, int.to_string(i)) }) 106 80 let values = variants |> list.map(fn(t) { t.1 }) 107 81 108 - Definition( 82 + formz.definition( 109 83 widgets.select_widget(keys_indexed), 110 84 validation.list_item_by_index(values) 111 85 |> validation.replace_error("is required"), 112 86 stub, 113 - definition.make_simple_optional_parse(), 114 - option.None, 115 87 ) 116 88 } 117 89
+66 -5
formz_string/src/formz_string/simple.gleam
··· 1 1 import formz 2 - import formz/widget 2 + import formz_string/widget 3 3 import gleam/list 4 4 import gleam/string 5 5 ··· 14 14 <> "</div>" 15 15 } 16 16 17 - pub fn generate_item(item: formz.FormItem(widget.Widget(String))) -> String { 17 + pub fn generate_item(item: formz.FormItem(widget.Widget)) -> String { 18 18 case item { 19 19 formz.Field(field, state, _) if field.hidden == True -> 20 20 "<input" ··· 44 44 } 45 45 46 46 let #(errors_el, errors_id) = case state { 47 - formz.Valid(..) -> #("", "") 48 47 formz.Invalid(error:, ..) -> { 49 48 #( 50 49 "<span" ··· 56 55 id <> "_error", 57 56 ) 58 57 } 58 + _ -> #("", "") 59 59 } 60 60 61 61 let widget_el = ··· 76 76 <> errors_el 77 77 <> "</div>" 78 78 } 79 + formz.ListField(field, states, _, make_widget) -> { 80 + let id = field.name 81 + 82 + let #(legend_el, legend_id) = #( 83 + "<legend id=\"" <> id <> "_legend\">" <> field.label <> ": </legend>", 84 + id <> "_legend", 85 + ) 86 + 87 + let #(help_text_el, help_text_id) = case field.help_text { 88 + "" -> #("", "") 89 + _ -> { 90 + #( 91 + "<span" 92 + <> { " id=\"" <> id <> "_help_text" <> "\"" } 93 + <> { " class=\"formz_help_text\"" } 94 + <> ">" 95 + <> field.help_text 96 + <> "</span>", 97 + id <> "_help_text", 98 + ) 99 + } 100 + } 101 + 102 + let widgets_el = 103 + states 104 + |> list.map(fn(state) { 105 + let #(errors_el, errors_id) = case state { 106 + formz.Invalid(error:, ..) -> { 107 + #( 108 + "<span" 109 + <> { " id=\"" <> id <> "_error" <> "\"" } 110 + <> { " class=\"formz_error\"" } 111 + <> ">" 112 + <> error 113 + <> "</span>", 114 + id <> "_error", 115 + ) 116 + } 117 + _ -> #("", "") 118 + } 119 + 120 + let widget_el = 121 + make_widget( 122 + field, 123 + state, 124 + widget.Args( 125 + id, 126 + widget.LabelledByElementsWithIds([legend_id]), 127 + widget.DescribedByElementsWithIds([help_text_id, errors_id]), 128 + ), 129 + ) 130 + 131 + widget_el <> errors_el 132 + }) 133 + |> string.join("</li><li>") 134 + let widgets_el = "<ol><li>" <> widgets_el <> "</ol>" 135 + 136 + "<fieldset class=\"formz_listfield\">" 137 + <> legend_el 138 + <> help_text_el 139 + <> widgets_el 140 + <> "</fieldset>" 141 + } 79 142 formz.SubForm(subform, items) -> { 80 143 let #(help_text_el, help_text_id) = case subform.help_text { 81 144 "" -> #("", "") ··· 110 173 <> "</div>" 111 174 <> "</fieldset>" 112 175 } 113 - 114 - _ -> "" 115 176 } 116 177 }
+30 -28
formz_string/src/formz_string/widgets.gleam
··· 1 - import formz.{type State} 1 + import formz 2 2 import formz/field 3 - import formz/widget 3 + import formz_string/widget.{type Widget} 4 4 import gleam/list 5 5 import gleam/string 6 6 ··· 97 97 } 98 98 } 99 99 100 - fn required_attr(required: Bool) -> String { 101 - case required { 102 - True -> " required" 103 - False -> "" 100 + fn required_attr(presence: formz.FieldPresence) -> String { 101 + case presence { 102 + formz.Required -> " required" 103 + formz.Optional -> "" 104 104 } 105 105 } 106 106 ··· 113 113 114 114 /// Create an `<input type="checkbox">`. The checkbox is checked 115 115 /// if the value is "on" (the browser default). 116 - pub fn checkbox_widget() -> widget.Widget(String) { 117 - fn(field: field.Field, state: State, args: widget.Args) { 116 + pub fn checkbox_widget() -> Widget { 117 + fn(field: field.Field, state: formz.FieldState, args: widget.Args) { 118 118 let value = state.value 119 119 let state = case state { 120 - formz.Valid(_) -> formz.Valid("") 121 - formz.Invalid(_, e) -> formz.Invalid("", e) 120 + formz.Unvalidated(_, presence) -> formz.Unvalidated("", presence) 121 + formz.Valid(_, presence) -> formz.Valid("", presence) 122 + formz.Invalid(_, presence, e) -> formz.Invalid("", presence, e) 122 123 } 123 124 do_input_widget(field, state, args, "checkbox", [checked_attr(value)]) 124 125 } ··· 130 131 /// this way does mean that a user can only input numbers up to the precision of 131 132 /// the step size. If you truly need any float, then a `type="text"` input might be a 132 133 /// better choice. 133 - pub fn number_widget(step_size: String) -> widget.Widget(String) { 134 - fn(field: field.Field, state: State, args: widget.Args) { 134 + pub fn number_widget(step_size: String) -> Widget { 135 + fn(field: field.Field, state: formz.FieldState, args: widget.Args) { 135 136 do_input_widget(field, state, args, "number", [step_size_attr(step_size)]) 136 137 } 137 138 } 138 139 139 140 /// Create an `<input type="password">`. This will not output the value in the 140 141 /// generated HTML for privacy/security concerns. 141 - pub fn password_widget() -> widget.Widget(String) { 142 - fn(field: field.Field, state: State, args: widget.Args) { 142 + pub fn password_widget() -> Widget { 143 + fn(field: field.Field, state: formz.FieldState, args: widget.Args) { 143 144 let state = case state { 144 - formz.Valid(_) -> formz.Valid("") 145 - formz.Invalid(_, e) -> formz.Invalid("", e) 145 + formz.Unvalidated(_, presence) -> formz.Unvalidated("", presence) 146 + formz.Valid(_, presence) -> formz.Valid("", presence) 147 + formz.Invalid(_, presence, e) -> formz.Invalid("", presence, e) 146 148 } 147 149 do_input_widget(field, state, args, "password", []) 148 150 } ··· 150 152 151 153 /// Generate any `<input>` like `type="text"`, `type="email"` or 152 154 /// `type="url"`. 153 - pub fn input_widget(type_: String) -> widget.Widget(String) { 154 - fn(field: field.Field, state: State, args: widget.Args) { 155 + pub fn input_widget(type_: String) -> Widget { 156 + fn(field: field.Field, state: formz.FieldState, args: widget.Args) { 155 157 do_input_widget(field, state, args, type_, []) 156 158 } 157 159 } 158 160 159 161 fn do_input_widget( 160 162 field: field.Field, 161 - state: State, 163 + state: formz.FieldState, 162 164 args: widget.Args, 163 165 type_: String, 164 166 extra_attrs: List(String), ··· 167 169 <> type_attr(type_) 168 170 <> name_attr(field.name) 169 171 <> id_attr(args.id) 170 - <> required_attr(field.required) 172 + <> required_attr(state.presence) 171 173 <> disabled_attr(field.disabled) 172 174 <> value_attr(state.value) 173 175 <> aria_label_attr(args.labelled_by, field.label) ··· 177 179 } 178 180 179 181 /// Create a `<textarea></textarea>`. 180 - pub fn textarea_widget() -> widget.Widget(String) { 181 - fn(field: field.Field, state: State, args: widget.Args) -> String { 182 + pub fn textarea_widget() -> Widget { 183 + fn(field: field.Field, state: formz.FieldState, args: widget.Args) -> String { 182 184 // https://chriscoyier.net/2023/09/29/css-solves-auto-expanding-textareas-probably-eventually/ 183 185 // https://til.simonwillison.net/css/resizing-textarea 184 186 "<textarea" 185 187 <> name_attr(field.name) 186 188 <> id_attr(args.id) 187 - <> required_attr(field.required) 189 + <> required_attr(state.presence) 188 190 <> disabled_attr(field.disabled) 189 191 <> aria_label_attr(args.labelled_by, field.label) 190 192 <> aria_describedby_attr(args.described_by) ··· 197 199 /// Create a `<input type="hidden">`. This is useful for if a field is just 198 200 /// passing data around and you don't want it to be visible to the user. Like 199 201 /// say, the ID of a record being edited. 200 - pub fn hidden_widget() -> widget.Widget(String) { 201 - fn(field: field.Field, state: State, _args: widget.Args) -> String { 202 + pub fn hidden_widget() -> Widget { 203 + fn(field: field.Field, state: formz.FieldState, _args: widget.Args) -> String { 202 204 "<input" 203 205 <> type_attr("hidden") 204 206 <> name_attr(field.name) ··· 210 212 /// Create a `<select></select>` with `<option>`s for each variant. The list 211 213 /// of variants is a two-tuple, where the first item is the text to display and 212 214 /// the second item is the value. 213 - pub fn select_widget(variants: List(#(String, String))) -> widget.Widget(String) { 214 - fn(field: field.Field, state: State, args: widget.Args) { 215 + pub fn select_widget(variants: List(#(String, String))) -> Widget { 216 + fn(field: field.Field, state: formz.FieldState, args: widget.Args) { 215 217 let choices = 216 218 list.map(variants, fn(variant) { 217 219 let val = variant.1 ··· 229 231 "<select" 230 232 <> name_attr(field.name) 231 233 <> id_attr(args.id) 232 - <> required_attr(field.required) 234 + <> required_attr(state.presence) 233 235 <> disabled_attr(field.disabled) 234 236 <> aria_label_attr(args.labelled_by, field.label) 235 237 <> aria_describedby_attr(args.described_by)
+17 -33
formz_string/test/formz_string/widgets_test.gleam
··· 1 1 import birdie 2 2 import formz 3 3 import formz/field 4 - import formz/widget 4 + import formz_string/widget 5 5 import formz_string/widgets 6 6 import gleeunit 7 7 ··· 16 16 label: "Label", 17 17 help_text: "", 18 18 disabled: False, 19 - required: False, 20 19 hidden: False, 21 20 ), 22 - formz.Valid("hello"), 21 + formz.Valid("hello", formz.Optional), 23 22 widget.Args("", widget.LabelledByFieldValue, widget.DescribedByNone), 24 23 ) 25 24 |> birdie.snap(title: "input labelled by field value") ··· 32 31 label: "Label", 33 32 help_text: "", 34 33 disabled: False, 35 - required: False, 36 34 hidden: False, 37 35 ), 38 - formz.Valid("hello"), 36 + formz.Valid("hello", formz.Optional), 39 37 widget.Args( 40 38 "", 41 39 widget.LabelledByElementsWithIds(["id"]), ··· 52 50 label: "Label", 53 51 help_text: "", 54 52 disabled: False, 55 - required: False, 56 53 hidden: False, 57 54 ), 58 - formz.Valid("hello"), 55 + formz.Valid("hello", formz.Optional), 59 56 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 60 57 ) 61 58 |> birdie.snap(title: "input labelled by label/for") ··· 68 65 label: "Label", 69 66 help_text: "", 70 67 disabled: False, 71 - required: False, 72 68 hidden: False, 73 69 ), 74 - formz.Valid("hello"), 70 + formz.Valid("hello", formz.Optional), 75 71 widget.Args( 76 72 "", 77 73 widget.LabelledByLabelFor, ··· 88 84 label: "Label", 89 85 help_text: "", 90 86 disabled: False, 91 - required: False, 92 87 hidden: False, 93 88 ), 94 - formz.Valid("hello"), 89 + formz.Valid("hello", formz.Optional), 95 90 widget.Args( 96 91 "", 97 92 widget.LabelledByLabelFor, ··· 108 103 label: "Label", 109 104 help_text: "", 110 105 disabled: False, 111 - required: True, 112 106 hidden: False, 113 107 ), 114 - formz.Valid("hello"), 108 + formz.Valid("hello", formz.Required), 115 109 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 116 110 ) 117 111 |> birdie.snap(title: "input required") ··· 124 118 label: "Label", 125 119 help_text: "", 126 120 disabled: True, 127 - required: False, 128 121 hidden: False, 129 122 ), 130 - formz.Valid("hello"), 123 + formz.Valid("hello", formz.Optional), 131 124 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 132 125 ) 133 126 |> birdie.snap(title: "input disabled") ··· 140 133 label: "Label", 141 134 help_text: "", 142 135 disabled: False, 143 - required: False, 144 136 hidden: False, 145 137 ), 146 - formz.Valid("hello\"<-_=>"), 138 + formz.Valid("hello\"<-_=>", formz.Optional), 147 139 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 148 140 ) 149 141 |> birdie.snap(title: "input sanitized value") ··· 156 148 label: "Label", 157 149 help_text: "", 158 150 disabled: False, 159 - required: False, 160 151 hidden: False, 161 152 ), 162 - formz.Valid("on"), 153 + formz.Valid("on", formz.Optional), 163 154 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 164 155 ) 165 156 |> birdie.snap(title: "checkbox checked") ··· 172 163 label: "Label", 173 164 help_text: "", 174 165 disabled: False, 175 - required: False, 176 166 hidden: False, 177 167 ), 178 - formz.Valid(""), 168 + formz.Valid("", formz.Optional), 179 169 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 180 170 ) 181 171 |> birdie.snap(title: "checkbox unchecked") ··· 188 178 label: "Label", 189 179 help_text: "", 190 180 disabled: False, 191 - required: False, 192 181 hidden: False, 193 182 ), 194 - formz.Valid("pass"), 183 + formz.Valid("pass", formz.Optional), 195 184 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 196 185 ) 197 186 |> birdie.snap(title: "password ignores input") ··· 204 193 label: "Label", 205 194 help_text: "", 206 195 disabled: False, 207 - required: False, 208 196 hidden: False, 209 197 ), 210 - formz.Valid("1"), 198 + formz.Valid("1", formz.Optional), 211 199 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 212 200 ) 213 201 |> birdie.snap(title: "number input with no step") ··· 220 208 label: "Label", 221 209 help_text: "", 222 210 disabled: False, 223 - required: False, 224 211 hidden: False, 225 212 ), 226 - formz.Valid("1.0"), 213 + formz.Valid("1.0", formz.Optional), 227 214 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 228 215 ) 229 216 |> birdie.snap(title: "number input with step") ··· 236 223 label: "Label", 237 224 help_text: "", 238 225 disabled: False, 239 - required: False, 240 226 hidden: False, 241 227 ), 242 - formz.Valid(""), 228 + formz.Valid("", formz.Optional), 243 229 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 244 230 ) 245 231 |> birdie.snap(title: "basic select") ··· 252 238 label: "Label", 253 239 help_text: "", 254 240 disabled: False, 255 - required: False, 256 241 hidden: False, 257 242 ), 258 - formz.Valid("1"), 243 + formz.Valid("1", formz.Optional), 259 244 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 260 245 ) 261 246 |> birdie.snap(title: "select selected") ··· 268 253 label: "Label", 269 254 help_text: "", 270 255 disabled: False, 271 - required: True, 272 256 hidden: False, 273 257 ), 274 - formz.Valid(""), 258 + formz.Valid("", formz.Required), 275 259 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 276 260 ) 277 261 |> birdie.snap(title: "required select")