this repo has no description
4
fork

Configure Feed

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

rework api to allow optional or required fields

also use disabled value from field record

+1628 -605
+49 -21
formz/README.md
··· 37 37 38 38 pub fn make_form() { 39 39 formz.new() 40 - |> formz.add(field("username"), definitions.text_field()) 41 - |> formz.add(field("password"), definitions.password_field()) 40 + |> formz.require(field("username"), definitions.text_field()) 41 + |> formz.require(field("password"), definitions.password_field()) 42 42 |> formz.decodes(fn(username) { fn(password) { #(username, password) } }) 43 43 } 44 44 ``` ··· 54 54 import formz_string/definitions 55 55 56 56 pub fn make_form() { 57 - use username <- formz.with(field("username"), definitions.text_field()) 58 - use password <- formz.with(field("password"), definitions.password_field()) 57 + use username <- formz.require(field("username"), definitions.text_field()) 58 + use password <- formz.require(field("password"), definitions.password_field()) 59 59 60 60 formz.create_form(#(username, password)) 61 61 } ··· 63 63 64 64 ## Creating fields 65 65 66 - There are two parts to adding a field to a form (seen above): 66 + There are two arguments to adding a field to a form (seen above): 67 67 68 - 1. Specific, unique details about the field, such as its name, label, help text, 69 - disabled/enabled state, etc. 70 - 2. A field "definition" which says (A) how to generate the HTML "widget" 71 - for the field, and (B) how to parse, or "transform" the data from the field. These 72 - definitions are reusable and can be shared between fields, forms and projects. 68 + 1. Specific, unique [details](https://hexdocs.pm/formz/formz/field.html) about 69 + the field, such as its name, label, help text, disabled state, etc. 70 + 2. A field [definition](https://hexdocs.pm/formz/formz/definition.html) which 71 + says (A) how to generate the HTML *widget* for the field, and (B) how to 72 + parse the data from the field. These definitions are reusable and can be 73 + shared across fields, forms and projects. 73 74 74 75 ### Field details 75 76 ··· 81 82 ``` 82 83 83 84 ```gleam 84 - field(named: "userid") |> field.make_hidden |> field.set_value("42") 85 + field(named: "userid") |> field.make_hidden |> field.set_raw_value("42") 85 86 ``` 86 87 87 88 ### Field definition 88 89 90 + [Defintions](https://hexdocs.pm/formz/formz/definition.html) are the heavy 91 + compared to the lightness of fields; they take a bit more work to make as they 92 + are intended to be more reusable. 93 + 94 + The first role of a defintion is to generate the HTML widget for the field. 89 95 This library is format-agnostic and you can generate HTML widgets as raw 90 - strings, Lustre elements, Nakai nodes, something else, etc. There are 96 + strings, Lustre elements, Nakai nodes, something else, etc, etc. There are 91 97 currently three formz libraries that provide common field definitions in 92 98 different formats. 93 99 ··· 96 102 - [formz_lustre](https://hexdocs.pm/formz_lustre/) (untested in a browser, 97 103 would it be useful there??) 98 104 105 + The second role is to parse the data from the field. There are a two parts 106 + to this, as how you parse a field's value depends on if it is optional or 107 + required. For example, an optional text field might be an empty string, 108 + an optional checkbox might be `false`, and an optional select might 109 + be `option.None`. So you need to provide two parse functions, one for when 110 + a field is required, and a second for when it's optional (and it uses the first 111 + one). 99 112 100 - There is also a *simple* validation module with some examples, and to cover 101 - some basics. 113 + There is also a basic validation module with the simple parsers required to make 114 + the basic form definitions provided above work. You can use this as a starting 115 + point for your own parse functions. 102 116 103 117 ```gleam 104 118 /// you won't often need to do this directly (I think??). The idea is that ··· 125 139 } 126 140 127 141 pub fn password_field() { 128 - Definition(password_widget, validation.string, "") 142 + Definition( 143 + widget: password_widget, 144 + parse: validation.string, 145 + optional_parse: fn(parse, str) { 146 + case str { 147 + "" -> Ok(option.None) 148 + _ -> parse(str) 149 + } 150 + }, 151 + // We need to have a stub value for each parser that's used 152 + // when building the decoder and parse functions for the form as the fields 153 + // are being added 154 + stub: "", 155 + optional_stub: option.None, 156 + ) 129 157 } 130 158 ``` 131 159 ··· 141 169 The specifics of how you would do this are going 142 170 to vary greatly for each project and its styling/markup needs. 143 171 144 - 145 172 However, the three `formz_*` libraries mentioned above all provide a 146 173 simple form generator function that you can use as is, or as a starting 147 174 point for your own. `formz` is BYOS, Bring Your Own Stylesheet, so the ··· 149 176 a super simple CSS file to get the ball rolling and make the default 150 177 forms easier to use out of the box. 151 178 152 - That said, you can create the form HTML yourself, directly for each field. 153 - There's an example in the demo project showing how to do this. 179 + That said, you can also create the form HTML yourself, directly for each field. 180 + There's [an example](https://github.com/bentomas/formz/blob/main/formz_demo/src/formz_demo/examples/custom_output.gleam) 181 + in the demo project showing how to do this. 154 182 155 183 ### Generating form HTML using the `formz_string` library 156 184 157 - The built-in form generators all leave it as homework to add the form tags 158 - and submit buttons. 185 + The built-in form generators leave it as homework to add the form tags and 186 + submit buttons. 159 187 160 188 ```gleam 161 189 import formz_string/simple ··· 214 242 |> Error 215 243 _ -> 216 244 form 217 - |> formz.update_field("username", field.set_error(_, "Wrong username")) 245 + |> formz.update_field("username", field.set_error(_, "Unknown username")) 218 246 |> Error 219 247 } 220 248 })
+2 -2
formz/TODO.md
··· 3 3 - csrf token? 4 4 - emit warning on duplicate named fields? 5 5 - clean names so snake_case? 6 - - disabled fields 7 - - optional fields? (can do optional validation, but that isn't reflected in the html) 8 6 - errors on hidden fields? 9 7 - custom types for input.hidden, input.disabled, input.required? 10 8 - date fields? https://hexdocs.pm/birl/ https://hexdocs.pm/rada/ 9 + - provide password/email/number_field? or just make them change the widget? 10 + - provide a text_area field? or make them change the widget?
+29 -41
formz/src/formz/definition.gleam
··· 1 1 import formz/widget 2 + import gleam/option 3 + import gleam/result 2 4 3 - pub type Definition(format, output) { 5 + pub type Definition(format, required, optional) { 4 6 Definition( 5 7 widget: widget.Widget(format), 6 - transform: fn(String) -> Result(output, String), 7 - placeholder: output, 8 + parse: fn(String) -> Result(required, String), 9 + stub: required, 10 + optional_parse: fn(fn(String) -> Result(required, String), String) -> 11 + Result(optional, String), 12 + optional_stub: optional, 8 13 ) 9 14 } 10 15 11 - pub fn validates( 12 - kind: Definition(format, output), 13 - next: fn(output) -> Result(output, String), 14 - ) -> Definition(format, output) { 15 - let Definition(widget, previous_transform, placeholder) = kind 16 - 17 - Definition( 18 - widget, 19 - fn(str) { 20 - case previous_transform(str) { 21 - Ok(value) -> next(value) 22 - Error(error) -> Error(error) 23 - } 24 - }, 25 - placeholder, 26 - ) 16 + pub fn set_widget( 17 + definition: Definition(format, a, b), 18 + widget: widget.Widget(format), 19 + ) -> Definition(format, a, b) { 20 + Definition(..definition, widget:) 27 21 } 28 22 29 - pub fn transforms( 30 - kind: Definition(format, a), 31 - placeholder: b, 32 - next: fn(a) -> Result(b, String), 33 - ) -> Definition(format, b) { 34 - let Definition(widget, previous_transform, _) = kind 35 - 36 - Definition( 37 - widget, 38 - fn(str) { 39 - case previous_transform(str) { 40 - Ok(value) -> next(value) 41 - Error(error) -> Error(error) 42 - } 43 - }, 44 - placeholder, 45 - ) 23 + pub fn validate( 24 + def: Definition(format, a, b), 25 + fun: fn(a) -> Result(a, String), 26 + ) -> Definition(format, a, b) { 27 + Definition(..def, parse: fn(val) { val |> def.parse |> result.try(fun) }) 46 28 } 47 29 48 - pub fn set_widget( 49 - kind: Definition(format, a), 50 - widget: widget.Widget(format), 51 - ) -> Definition(format, a) { 52 - Definition(..kind, widget:) 30 + pub fn make_simple_optional_parse() -> fn( 31 + fn(String) -> Result(required, String), 32 + String, 33 + ) -> 34 + Result(option.Option(required), String) { 35 + fn(fun, str) { 36 + case str { 37 + "" -> Ok(option.None) 38 + _ -> fun(str) |> result.map(option.Some) 39 + } 40 + } 53 41 }
+67 -11
formz/src/formz/field.gleam
··· 115 115 } 116 116 } 117 117 118 - fn set_hidden(field: Field, hidden: Bool) -> Field { 118 + pub fn set_hidden(field: Field, hidden: Bool) -> Field { 119 119 case field { 120 120 Valid(_hidden, name:, label:, help_text:, value:, disabled:, required:) -> 121 121 Valid(name:, label:, help_text:, hidden:, value:, disabled:, required:) ··· 142 142 } 143 143 } 144 144 145 - pub fn make_hidden(field: Field) -> Field { 146 - set_hidden(field, True) 145 + @internal 146 + pub fn set_required(field: Field, required: Bool) -> Field { 147 + case field { 148 + Valid(name:, label:, help_text:, value:, hidden:, disabled:, required: _) -> 149 + Valid(name:, label:, help_text:, hidden:, value:, disabled:, required:) 150 + Invalid( 151 + name:, 152 + label:, 153 + help_text:, 154 + hidden:, 155 + disabled:, 156 + required: _, 157 + value:, 158 + error:, 159 + ) -> 160 + Invalid( 161 + name:, 162 + label:, 163 + help_text:, 164 + hidden:, 165 + value:, 166 + error:, 167 + disabled:, 168 + required:, 169 + ) 170 + } 147 171 } 148 172 149 - pub fn make_visible(field: Field) -> Field { 150 - set_hidden(field, False) 173 + @internal 174 + pub fn make_required(field: Field) -> Field { 175 + case field { 176 + Valid(_required, name:, label:, help_text:, hidden:, value:, disabled:) -> 177 + Valid( 178 + name:, 179 + label:, 180 + help_text:, 181 + hidden:, 182 + value:, 183 + disabled:, 184 + required: False, 185 + ) 186 + Invalid( 187 + _required, 188 + name:, 189 + label:, 190 + help_text:, 191 + hidden:, 192 + disabled:, 193 + value:, 194 + error:, 195 + ) -> 196 + Invalid( 197 + name:, 198 + label:, 199 + help_text:, 200 + hidden:, 201 + value:, 202 + error:, 203 + disabled:, 204 + required: False, 205 + ) 206 + } 151 207 } 152 208 153 - fn set_disabled(field: Field, disabled: Bool) -> Field { 209 + pub fn make_hidden(field: Field) -> Field { 210 + set_hidden(field, True) 211 + } 212 + 213 + pub fn set_disabled(field: Field, disabled: Bool) -> Field { 154 214 case field { 155 215 Valid(_disabled, name:, label:, help_text:, value:, hidden:, required:) -> 156 216 Valid(name:, label:, help_text:, hidden:, value:, disabled:, required:) ··· 181 241 set_disabled(field, True) 182 242 } 183 243 184 - pub fn make_enabled(field: Field) -> Field { 185 - set_disabled(field, False) 186 - } 187 - 188 - pub fn set_string_value(field: Field, value: String) -> Field { 244 + pub fn set_raw_value(field: Field, value: String) -> Field { 189 245 case field { 190 246 Valid(_value, name:, label:, help_text:, hidden:, disabled:, required:) -> 191 247 Valid(name:, label:, help_text:, hidden:, value:, disabled:, required:)
-29
formz/src/formz/form_details.gleam
··· 1 - import justin 2 - 3 - pub type FormDetails { 4 - FormDetails(name: String, label: String, help_text: String, disabled: Bool) 5 - } 6 - 7 - pub fn form_details(name) { 8 - FormDetails(name, justin.sentence_case(name), "", False) 9 - } 10 - 11 - pub fn set_name(sub: FormDetails, name: String) -> FormDetails { 12 - FormDetails(..sub, name:) 13 - } 14 - 15 - pub fn set_label(sub: FormDetails, label: String) -> FormDetails { 16 - FormDetails(..sub, label:) 17 - } 18 - 19 - pub fn set_help_text(sub: FormDetails, help_text: String) -> FormDetails { 20 - FormDetails(..sub, help_text:) 21 - } 22 - 23 - pub fn make_disabled(sub: FormDetails) -> FormDetails { 24 - FormDetails(..sub, disabled: True) 25 - } 26 - 27 - pub fn make_enabled(sub: FormDetails) -> FormDetails { 28 - FormDetails(..sub, disabled: False) 29 - }
+52 -15
formz/src/formz/formz_builder.gleam
··· 1 - import formz/definition.{type Definition} 1 + import formz/definition.{type Definition, Definition} 2 2 import formz/field 3 - import formz/form_details 3 + import formz/subform 4 4 import formz/widget 5 5 6 6 import gleam/dict ··· 23 23 24 24 pub type FormItem(format) { 25 25 Element(field.Field, widget: widget.Widget(format)) 26 - Set(form_details.FormDetails, items: List(FormItem(format))) 26 + Set(subform.SubForm, items: List(FormItem(format))) 27 27 } 28 28 29 29 pub fn new() -> Form(format, a, a, NoDecoder) { 30 30 Form([], fn(_, output) { Ok(output) }, None) 31 31 } 32 32 33 - pub fn add( 33 + fn add( 34 34 previous_form: Form( 35 35 format, 36 36 fn(decoder_step_input) -> decoder_step_output, ··· 38 38 has_decoder, 39 39 ), 40 40 field: field.Field, 41 - definition: Definition(format, decoder_step_input), 41 + widget: widget.Widget(format), 42 + parse_field: fn(String) -> Result(decoder_step_input, String), 42 43 ) -> Form(format, decoder_step_output, form_output, has_decoder) { 43 - let updated_items = [Element(field, definition.widget), ..previous_form.items] 44 + let updated_items = [Element(field, widget), ..previous_form.items] 44 45 45 46 let parse_with = fn(items, decoder: form_output) { 46 47 // can do let assert because we know there's at least one field since 47 48 // we just added one 48 49 let assert Ok(#(Element(field, widget), rest)) = pop_element(items) 49 50 50 - let input_output = definition.transform(field.value) 51 - 52 51 let previous_form_output = previous_form.parse_with(rest, decoder) 52 + let input_output = parse_field(field.value) 53 + 53 54 case previous_form_output, input_output { 54 55 // the form we've already parsed has no errors and the field 55 - // we just parsed has no errors. so we can move on to the next 56 - Ok(next), Ok(value) -> Ok(next(value)) 56 + // we just parsed has no errors. each intermediary step of a form 57 + // is a part of a decoder, so since we successfully parsed this form, 58 + // we can call the successful decoder that it returned. 59 + Ok(previous_decoder), Ok(value) -> Ok(previous_decoder(value)) 57 60 58 61 // the form already has errors even though this one succeeded. 59 62 // so add this to the list and stop anymore parsing ··· 83 86 Form(items: updated_items, parse_with:, decoder: previous_form.decoder) 84 87 } 85 88 89 + pub fn optional( 90 + previous_form: Form( 91 + format, 92 + fn(decoder_step_input) -> decoder_step_output, 93 + form_output, 94 + has_decoder, 95 + ), 96 + field: field.Field, 97 + definition: Definition(format, _, decoder_step_input), 98 + ) -> Form(format, decoder_step_output, form_output, has_decoder) { 99 + add(previous_form, field, definition.widget, definition.optional_parse( 100 + definition.parse, 101 + _, 102 + )) 103 + } 104 + 105 + pub fn require( 106 + previous_form: Form( 107 + format, 108 + fn(field_output) -> decoder_step_output, 109 + form_output, 110 + has_decoder, 111 + ), 112 + field: field.Field, 113 + definition: Definition(format, field_output, _), 114 + ) -> Form(format, decoder_step_output, form_output, has_decoder) { 115 + add( 116 + previous_form, 117 + field |> field.set_required(True), 118 + definition.widget, 119 + definition.parse, 120 + ) 121 + } 122 + 86 123 pub fn add_form( 87 124 previous_form: Form( 88 125 format, ··· 90 127 form_output, 91 128 has_decoder, 92 129 ), 93 - details: form_details.FormDetails, 130 + details: subform.SubForm, 94 131 sub: Form(format, sub_output, sub_decoder, HasDecoder), 95 132 ) -> Form(format, decoder_step_output, form_output, has_decoder) { 96 133 let sub_items = ··· 161 198 input_data: List(#(String, String)), 162 199 ) -> Form(format, output, decoder, has_decoder) { 163 200 let data = dict.from_list(input_data) 164 - let Form(items, parse, placeholder) = form 201 + let Form(items, parse, decoder) = form 165 202 items 166 203 |> map_fields(fn(field) { 167 204 case dict.get(data, field.name) { 168 - Ok(value) -> field.set_string_value(field, value) 205 + Ok(value) -> field.set_raw_value(field, value) 169 206 Error(_) -> field 170 207 } 171 208 }) 172 - |> Form(parse, placeholder) 209 + |> Form(parse, decoder) 173 210 } 174 211 175 212 pub fn decodes( ··· 271 308 pub fn update_subform( 272 309 form: Form(format, output, decoder, has_decoder), 273 310 name: String, 274 - fun: fn(form_details.FormDetails) -> form_details.FormDetails, 311 + fun: fn(subform.SubForm) -> subform.SubForm, 275 312 ) -> Form(format, output, decoder, has_decoder) { 276 313 update(form, name, fn(item) { 277 314 case item {
+51 -25
formz/src/formz/formz_use.gleam
··· 1 - import formz/definition.{type Definition} 1 + import formz/definition.{type Definition, Definition} 2 2 import formz/field 3 - import formz/form_details 3 + import formz/subform 4 4 import formz/widget 5 5 import gleam/dict 6 6 import gleam/list ··· 16 16 17 17 pub type FormItem(format) { 18 18 Element(field.Field, widget: widget.Widget(format)) 19 - Set(form_details.FormDetails, items: List(FormItem(format))) 19 + Set(subform.SubForm, items: List(FormItem(format))) 20 20 } 21 21 22 22 pub fn create_form(thing: thing) -> Form(format, thing) { 23 23 Form([], fn(_) { Ok(thing) }, thing) 24 24 } 25 25 26 - pub fn with( 26 + fn add( 27 27 field: field.Field, 28 - is definition: Definition(format, input_output), 28 + widget: widget.Widget(format), 29 + parse_field: fn(String) -> Result(input_output, String), 30 + stub: input_output, 29 31 rest fun: fn(input_output) -> Form(format, form_output), 30 32 ) -> Form(format, form_output) { 31 - // we pass in our placeholder value, and we're going to throw away the 33 + // we pass in our stub value, and we're going to throw away the 32 34 // decoded result here, we just care about pulling out the fields 33 35 // from the form. 34 - let next_form = fun(definition.placeholder) 36 + let next_form = fun(stub) 35 37 36 38 // prepend the new field to the items from the form we got in the 37 39 // previous step. 38 - let updated_items = [Element(field, definition.widget), ..next_form.items] 40 + let updated_items = [Element(field, widget), ..next_form.items] 39 41 40 42 // now create the parse function. parse function accepts most recent 41 43 // version of input list, since data can be added to it. the list ··· 45 47 let assert Ok(#(Element(field, widget), pop_elements)) = pop_element(items) 46 48 47 49 // transform the input data using the transform/validate/decode/etc function 48 - let input_output = definition.transform(field.value) 50 + let input_output = parse_field(field.value) 49 51 50 52 // pass our transformed input data to the next function/form. if 51 53 // we errored we still do this with our placeholder so we can continue 52 54 // processing all the fields in the form. if we're on the error track 53 55 // we'll throw away the "output" made with this and just keep the error 54 - let next_form = fun(input_output |> result.unwrap(definition.placeholder)) 56 + let next_form = fun(input_output |> result.unwrap(stub)) 55 57 let form_output = next_form.parse(pop_elements) 56 58 57 59 // ok, check which track we're on ··· 86 88 Form(items: updated_items, parse:, placeholder: next_form.placeholder) 87 89 } 88 90 91 + pub fn optional( 92 + field: field.Field, 93 + is definition: Definition(format, _, input_output), 94 + rest fun: fn(input_output) -> Form(format, form_output), 95 + ) -> Form(format, form_output) { 96 + add( 97 + field, 98 + definition.widget, 99 + definition.optional_parse(definition.parse, _), 100 + definition.optional_stub, 101 + fun, 102 + ) 103 + } 104 + 105 + pub fn require( 106 + field: field.Field, 107 + is definition: Definition(format, required_output, _), 108 + rest fun: fn(required_output) -> Form(format, form_output), 109 + ) -> Form(format, form_output) { 110 + add( 111 + field |> field.set_required(True), 112 + definition.widget, 113 + definition.parse, 114 + definition.stub, 115 + fun, 116 + ) 117 + } 118 + 89 119 pub fn with_form( 90 - details: form_details.FormDetails, 120 + details: subform.SubForm, 91 121 sub: Form(format, sub_output), 92 122 fun: fn(sub_output) -> Form(format, form_output), 93 123 ) -> Form(format, form_output) { ··· 167 197 items 168 198 |> map_fields(fn(field) { 169 199 case dict.get(data, field.name) { 170 - Ok(value) -> field.set_string_value(field, value) 200 + Ok(value) -> field.set_raw_value(field, value) 171 201 Error(_) -> field 172 202 } 173 203 }) ··· 185 215 form: Form(format, output), 186 216 apply fun: fn(Form(format, output), output) -> Result(c, Form(format, output)), 187 217 ) -> Result(c, Form(format, output)) { 188 - parse(form) |> result.try(fun(form, _)) 218 + form |> parse |> result.try(fun(form, _)) 189 219 } 190 220 191 221 pub fn items(form: Form(format, output)) -> List(FormItem(format)) { ··· 196 226 form: Form(format, output), 197 227 name: String, 198 228 ) -> Result(FormItem(format), Nil) { 199 - form.items 200 - |> list.filter(fn(item) { 229 + list.find(form.items, fn(item) { 201 230 case item { 202 231 Element(i, _) if i.name == name -> True 203 232 Set(s, _) if s.name == name -> True 204 233 _ -> False 205 234 } 206 235 }) 207 - |> list.first 208 236 } 209 237 210 238 pub fn update( ··· 212 240 name: String, 213 241 fun: fn(FormItem(format)) -> FormItem(format), 214 242 ) { 215 - form.items 216 - |> do_formitem_update(name, fun) 217 - |> Form(form.parse, form.placeholder) 243 + let items = do_formitems_update(form.items, name, fun) 244 + Form(..form, items:) 218 245 } 219 246 220 - fn do_formitem_update( 247 + fn do_formitems_update( 221 248 items: List(FormItem(format)), 222 249 name: String, 223 250 fun: fn(FormItem(format)) -> FormItem(format), 224 251 ) -> List(FormItem(format)) { 225 - items 226 - |> list.map(fn(item) { 252 + list.map(items, fn(item) { 227 253 case item { 228 - Element(i, _) if i.name == name -> fun(item) 254 + Element(f, _) if f.name == name -> fun(item) 229 255 Set(s, _) if s.name == name -> fun(item) 230 - Set(s, items) -> Set(s, do_formitem_update(items, name, fun)) 256 + Set(s, items) -> Set(s, do_formitems_update(items, name, fun)) 231 257 _ -> item 232 258 } 233 259 }) ··· 249 275 pub fn update_subform( 250 276 form: Form(format, output), 251 277 name: String, 252 - fun: fn(form_details.FormDetails) -> form_details.FormDetails, 278 + fun: fn(subform.SubForm) -> subform.SubForm, 253 279 ) -> Form(format, output) { 254 280 update(form, name, fn(item) { 255 281 case item {
+29
formz/src/formz/subform.gleam
··· 1 + import justin 2 + 3 + pub type SubForm { 4 + SubForm(name: String, label: String, help_text: String, disabled: Bool) 5 + } 6 + 7 + pub fn subform(name) { 8 + SubForm(name, justin.sentence_case(name), "", False) 9 + } 10 + 11 + pub fn set_name(sub: SubForm, name: String) -> SubForm { 12 + SubForm(..sub, name:) 13 + } 14 + 15 + pub fn set_label(sub: SubForm, label: String) -> SubForm { 16 + SubForm(..sub, label:) 17 + } 18 + 19 + pub fn set_help_text(sub: SubForm, help_text: String) -> SubForm { 20 + SubForm(..sub, help_text:) 21 + } 22 + 23 + pub fn make_disabled(sub: SubForm) -> SubForm { 24 + SubForm(..sub, disabled: True) 25 + } 26 + 27 + pub fn make_enabled(sub: SubForm) -> SubForm { 28 + SubForm(..sub, disabled: False) 29 + }
+51 -66
formz/src/formz/validation.gleam
··· 21 21 /// # -> Ok(2) 22 22 /// 23 23 /// check("hi") 24 - /// # -> Error("bust be a whole number") 24 + /// # -> Error("must be a whole number") 25 25 /// 26 26 /// check("1") 27 27 /// # -> Error("must be even") ··· 37 37 } 38 38 } 39 39 40 - /// Parse the input as a boolean in a permissive way. 41 - /// 42 - /// ## Examples 43 - /// 44 - /// ```gleam 45 - /// number("1") 46 - /// number("true") 47 - /// number("yes") 48 - /// number("on") 49 - /// # -> Ok(True) 50 - /// ``` 40 + /// Parse the input as a String that looks like an email address, i.e. it 41 + /// contains an `@` character, with at least one other character on either 42 + /// side 51 43 /// 52 - /// ```gleam 53 - /// number("0") 54 - /// number("") 55 - /// number("no") 56 - /// number("false") 57 - /// number("off") 58 - /// # -> Ok(False) 59 - /// ``` 60 - /// 61 - /// ```gleam 62 - /// number("hi") 63 - /// # -> Error("Must be true or false") 64 - /// ``` 65 - pub fn boolean(str: String) -> Result(Bool, String) { 66 - case string.trim(str) { 67 - "True" -> Ok(True) 68 - "true" -> Ok(True) 69 - "Yes" -> Ok(True) 70 - "yes" -> Ok(True) 71 - "On" -> Ok(True) 72 - "on" -> Ok(True) 73 - "1" -> Ok(True) 74 - "False" -> Ok(False) 75 - "false" -> Ok(False) 76 - "No" -> Ok(False) 77 - "no" -> Ok(False) 78 - "Off" -> Ok(False) 79 - "off" -> Ok(False) 80 - "0" -> Ok(False) 81 - "" -> Ok(False) 82 - _ -> Error("must be true or false") 83 - } 84 - } 85 - 86 - /// Parse the input as a String that looks like an email address, i.e. it 87 - /// containts an `@` character. 44 + /// (this behavior more closely matches what the browser does than just 45 + /// checking for an `@`). 88 46 /// 89 47 /// ## Examples 90 48 /// ··· 92 50 /// email("hello@example.com") 93 51 /// # -> Ok("hello@example.com") 94 52 /// ``` 95 - /// 96 53 /// ```gleam 97 54 /// email("@") 98 - /// # -> Ok("@") 55 + /// # -> Error("Must be an email address") 99 56 /// ``` 100 - /// 101 57 /// ```gleam 102 - /// number("hello") 58 + /// email("hello") 103 59 /// # -> Error("Must be an email address") 104 60 /// ``` 105 - /// ```gleam 106 - /// number("1") 107 - /// # -> Error("ust be an email address") 108 - /// ``` 109 61 pub fn email(input: String) -> Result(String, String) { 62 + case input |> string.trim { 63 + "" -> Error("is required") 64 + trimmed -> 65 + case trimmed |> string.split("@") |> list.map(string.length) { 66 + [before, after] if before > 0 && after > 0 -> Ok(trimmed) 67 + _ -> Error("must be an email address") 68 + } 69 + } 110 70 // TODO verify both parts have at least one character? 111 - case input |> string.trim |> string.split("@") { 112 - [_, _] -> Ok(input) 113 - _ -> Error("must be an email address") 114 - } 115 71 } 116 72 117 73 /// Parse the input as a float. this is forgiving and will also parse ··· 146 102 /// ## Examples 147 103 /// 148 104 /// ```gleam 149 - /// number("1") 105 + /// int("1") 150 106 /// # -> Ok(1) 151 107 /// ``` 152 108 /// 153 109 /// ```gleam 154 - /// number("3.4") 110 + /// int("3.4") 155 111 /// # -> Error("Must be a whole number") 156 112 /// ``` 157 113 /// ```gleam 158 - /// number("hello") 114 + /// int("hello") 159 115 /// # -> Error("Must be a whole number") 160 116 /// ``` 161 117 pub fn int(str: String) -> Result(Int, String) { ··· 198 154 } 199 155 } 200 156 157 + /// Parse the input as a boolean, where only "on" is True and allowed. 158 + /// All other values are an error. This is useful for HTML checkboxes, which 159 + /// the browser sends the empty string if unchecked, and `"on"` if checked. 160 + /// 161 + /// ## Examples 162 + /// 163 + /// ```gleam 164 + /// on("on") 165 + /// # -> Ok(True) 166 + /// ``` 167 + /// 168 + /// ```gleam 169 + /// on("") 170 + /// # -> Error("is required") 171 + /// ``` 172 + /// 173 + /// ```gleam 174 + /// on("hi") 175 + /// # -> Error("is required") 176 + /// ``` 177 + pub fn on(val: String) -> Result(Bool, String) { 178 + case val { 179 + "on" -> Ok(True) 180 + _ -> Error("must be on") 181 + } 182 + } 183 + 201 184 /// Replace the error message of a validation with a new one. Most of the built-in 202 185 /// error messages are pretty rudimentary. 203 186 pub fn replace_error( ··· 207 190 fn(data) { previous(data) |> result.replace_error(error) } 208 191 } 209 192 210 - /// Default field parser. Trims the input and returns it as is. 211 - pub fn string(str: String) -> Result(String, String) { 212 - Ok(string.trim(str)) 193 + pub fn non_empty_string(str: String) -> Result(String, String) { 194 + case string.trim(str) { 195 + "" -> Error("is required") 196 + trimmed -> Ok(trimmed) 197 + } 213 198 }
+14 -2
formz/src/formz/widget.gleam
··· 10 10 pub type LabelledBy { 11 11 LabelledByLabelFor 12 12 LabelledByFieldValue 13 - LabelledByElementWithId(id: String) 13 + LabelledByElementsWithIds(ids: List(String)) 14 14 } 15 15 16 16 pub type DescribedBy { 17 - DescribedByElementWithId(id: String) 17 + DescribedByElementsWithIds(ids: List(String)) 18 18 DescribedByNone 19 19 } 20 + 21 + pub fn args(labelled_by labelled_by: LabelledBy) { 22 + Args(id: "", labelled_by: labelled_by, described_by: DescribedByNone) 23 + } 24 + 25 + pub fn id(args: Args, str: String) { 26 + Args(..args, id: str) 27 + } 28 + 29 + pub fn described_by(args: Args, db: DescribedBy) { 30 + Args(..args, described_by: db) 31 + }
+116 -33
formz/test/formz/formz_builder_test.gleam
··· 4 4 import formz/subform.{subform} 5 5 import formz/validation 6 6 import gleam/list 7 + import gleam/option 8 + import gleam/result 7 9 import gleeunit 8 10 import gleeunit/should 9 11 ··· 11 13 gleeunit.main() 12 14 } 13 15 14 - pub fn text_field() { 15 - definition.Definition(fn(_, _) { "" }, validation.string, "") 16 + fn text_field() { 17 + definition.Definition( 18 + widget: fn(_, _) { Nil }, 19 + parse: validation.non_empty_string, 20 + stub: "", 21 + optional_parse: fn(fun, str) { 22 + case str { 23 + "" -> Ok("") 24 + _ -> fun(str) 25 + } 26 + }, 27 + optional_stub: "", 28 + ) 16 29 } 17 30 18 - pub fn integer_field() { 19 - definition.Definition(fn(_, _) { "" }, validation.int, 0) 31 + pub fn float_field() { 32 + definition.Definition( 33 + widget: fn(_, _) { Nil }, 34 + parse: validation.number, 35 + stub: 0.0, 36 + optional_parse: fn(fun, str) { 37 + case str { 38 + "" -> Ok(option.None) 39 + _ -> fun(str) |> result.map(option.Some) 40 + } 41 + }, 42 + optional_stub: option.Some(0.0), 43 + ) 44 + } 45 + 46 + 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(option.None) 54 + _ -> fun(str) |> result.map(option.Some) 55 + } 56 + }, 57 + optional_stub: option.Some(0), 58 + ) 20 59 } 21 60 22 - pub fn float_field() { 23 - definition.Definition(fn(_, _) { "" }, validation.number, 0.0) 61 + fn boolean_field() { 62 + definition.Definition( 63 + widget: fn(_, _) { Nil }, 64 + parse: validation.on, 65 + stub: False, 66 + optional_parse: fn(fun, str) { 67 + case str { 68 + "" -> Ok(False) 69 + _ -> fun(str) 70 + } 71 + }, 72 + optional_stub: False, 73 + ) 24 74 } 25 75 26 76 fn get_form_from_error_result( ··· 53 103 54 104 pub fn parse_single_field_form_test() { 55 105 formz.new() 56 - |> formz.add(field("first"), text_field()) 106 + |> formz.optional(field("first"), text_field()) 57 107 |> formz.data([#("first", "world")]) 58 108 |> formz.decodes(fn(str) { "hello " <> str }) 59 109 |> formz.parse ··· 62 112 63 113 pub fn parse_double_field_form_test() { 64 114 formz.new() 65 - |> formz.add(field("first"), text_field()) 66 - |> formz.add(field("second"), text_field()) 115 + |> formz.optional(field("first"), text_field()) 116 + |> formz.optional(field("second"), text_field()) 67 117 |> formz.data([#("first", "hello"), #("second", "world")]) 68 118 |> formz.decodes(fn(a) { fn(b) { a <> " " <> b } }) 69 119 |> formz.parse ··· 72 122 73 123 pub fn parse_double_field_form_extra_data_test() { 74 124 formz.new() 75 - |> formz.add(field("first"), text_field()) 76 - |> formz.add(field("second"), text_field()) 125 + |> formz.optional(field("first"), text_field()) 126 + |> formz.optional(field("second"), text_field()) 77 127 |> formz.data([#("first", "1"), #("second", "2")]) 78 128 |> formz.decodes(fn(a) { fn(b) { a <> " " <> b } }) 79 129 |> formz.parse 80 130 |> should.equal(Ok("1 2")) 81 131 82 132 formz.new() 83 - |> formz.add(field("first"), text_field()) 84 - |> formz.add(field("second"), text_field()) 133 + |> formz.optional(field("first"), text_field()) 134 + |> formz.optional(field("second"), text_field()) 85 135 |> formz.data([#("first", "1"), #("second", "2"), #("second", "3")]) 86 136 |> formz.decodes(fn(a) { fn(b) { a <> " " <> b } }) 87 137 |> formz.parse ··· 90 140 91 141 pub fn integer_field_test() { 92 142 formz.new() 93 - |> formz.add(field("first"), integer_field()) 143 + |> formz.optional(field("first"), integer_field()) 144 + |> formz.data([#("first", " 1 ")]) 145 + |> formz.decodes(fn(i) { i }) 146 + |> formz.parse 147 + |> should.equal(Ok(option.Some(1))) 148 + 149 + formz.new() 150 + |> formz.require(field("first"), integer_field()) 94 151 |> formz.data([#("first", " 1 ")]) 95 152 |> formz.decodes(fn(i) { i }) 96 153 |> formz.parse 97 154 |> should.equal(Ok(1)) 98 155 } 99 156 157 + pub fn boolean_field_test() { 158 + formz.new() 159 + |> formz.optional(field("first"), boolean_field()) 160 + |> formz.data([#("first", "")]) 161 + |> formz.decodes(fn(i) { i }) 162 + |> formz.parse 163 + |> should.equal(Ok(False)) 164 + 165 + formz.new() 166 + |> formz.require(field("first"), boolean_field()) 167 + |> formz.data([#("first", "on")]) 168 + |> formz.decodes(fn(i) { i }) 169 + |> formz.parse 170 + |> should.equal(Ok(True)) 171 + 172 + let assert Error(f) = 173 + formz.new() 174 + |> formz.require(field("first"), boolean_field()) 175 + |> formz.data([#("first", "")]) 176 + |> formz.decodes(fn(i) { i }) 177 + |> formz.parse 178 + 179 + let assert [Element(fielda, _)] = formz.items(f) 180 + fielda |> should_be_field_with_error("must be on") 181 + } 182 + 100 183 pub fn can_decodes_in_any_order_test() { 101 184 formz.new() 102 185 |> formz.decodes(fn(str) { "hello " <> str }) 103 - |> formz.add(field("first"), text_field()) 186 + |> formz.optional(field("first"), text_field()) 104 187 |> formz.data([#("first", "world")]) 105 188 |> formz.parse 106 189 |> should.equal(Ok("hello world")) 107 190 108 191 formz.new() 109 - |> formz.add(field("first"), text_field()) 192 + |> formz.optional(field("first"), text_field()) 110 193 |> formz.data([#("first", "world")]) 111 194 |> formz.decodes(fn(str) { "one " <> str }) 112 195 |> formz.decodes(fn(str) { "hello " <> str }) ··· 117 200 pub fn parse_single_field_form_with_error_test() { 118 201 let assert Error(f) = 119 202 formz.new() 120 - |> formz.add(field("first"), integer_field()) 203 + |> formz.optional(field("first"), integer_field()) 121 204 |> formz.data([#("first", "world")]) 122 205 |> formz.decodes(fn(_) { 1 }) 123 206 |> formz.parse ··· 129 212 pub fn parse_double_field_form_with_error_test() { 130 213 let form = 131 214 formz.new() 132 - |> formz.add(field("a"), integer_field()) 133 - |> formz.add(field("b"), integer_field()) 215 + |> formz.optional(field("a"), integer_field()) 216 + |> formz.optional(field("b"), integer_field()) 134 217 |> formz.decodes(fn(_) { fn(_) { 1 } }) 135 218 136 219 let assert Error(f) = ··· 164 247 pub fn parse_triple_field_form_with_error_test() { 165 248 let form = 166 249 formz.new() 167 - |> formz.add(field("a"), integer_field()) 168 - |> formz.add(field("b"), integer_field()) 169 - |> formz.add(field("c"), integer_field()) 250 + |> formz.optional(field("a"), integer_field()) 251 + |> formz.optional(field("b"), integer_field()) 252 + |> formz.optional(field("c"), integer_field()) 170 253 |> formz.decodes(fn(_) { fn(_) { fn(_) { 1 } } }) 171 254 172 255 let assert Error(f) = ··· 254 337 pub fn try_test() { 255 338 let f = 256 339 formz.new() 257 - |> formz.add(field("a"), integer_field()) 258 - |> formz.add(field("b"), integer_field()) 259 - |> formz.add(field("c"), integer_field()) 340 + |> formz.optional(field("a"), integer_field()) 341 + |> formz.optional(field("b"), integer_field()) 342 + |> formz.optional(field("c"), integer_field()) 260 343 |> formz.decodes(fn(a) { fn(b) { fn(c) { [a, b, c] } } }) 261 344 |> formz.data([#("a", "1"), #("b", "2"), #("c", "3")]) 262 345 ··· 287 370 pub fn sub_form_test() { 288 371 let f1 = 289 372 formz.new() 290 - |> formz.add(field("a"), integer_field()) 291 - |> formz.add(field("b"), integer_field()) 292 - |> formz.add(field("c"), integer_field()) 373 + |> formz.require(field("a"), integer_field()) 374 + |> formz.require(field("b"), integer_field()) 375 + |> formz.require(field("c"), integer_field()) 293 376 |> formz.decodes(fn(a) { fn(b) { fn(c) { #(a, b, c) } } }) 294 377 295 378 let f2 = 296 379 formz.new() 297 380 |> formz.add_form(subform("name"), f1) 298 - |> formz.add(field("d"), integer_field()) 381 + |> formz.require(field("d"), integer_field()) 299 382 |> formz.decodes(fn(a) { fn(b) { #(a, b) } }) 300 383 301 384 f2 ··· 312 395 pub fn sub_form_error_tst() { 313 396 let f1 = 314 397 formz.new() 315 - |> formz.add(field("a"), integer_field()) 316 - |> formz.add(field("b"), integer_field()) 317 - |> formz.add(field("c"), integer_field()) 398 + |> formz.require(field("a"), integer_field()) 399 + |> formz.require(field("b"), integer_field()) 400 + |> formz.require(field("c"), integer_field()) 318 401 |> formz.decodes(fn(a) { fn(b) { fn(c) { #(a, b, c) } } }) 319 402 320 403 let f2 = 321 404 formz.new() 322 405 |> formz.add_form(subform("name"), f1) 323 - |> formz.add(field("d"), integer_field()) 406 + |> formz.optional(field("d"), integer_field()) 324 407 |> formz.decodes(fn(a) { fn(b) { #(a, b) } }) 325 408 326 409 let assert [
+87 -64
formz/test/formz/formz_use_test.gleam
··· 2 2 import formz/field.{field} 3 3 import formz/formz_use.{Element, Set} as formz 4 4 import formz/subform.{subform} 5 + import formz/validation 6 + import gleam/option 7 + import gleam/result 5 8 import gleam/string 6 - 7 - import formz/validation 8 9 import gleeunit 9 10 import gleeunit/should 10 11 11 - pub fn text_field() { 12 - definition.Definition(fn(_, _) { "" }, validation.string, "") 12 + fn text_field() { 13 + definition.Definition( 14 + widget: fn(_, _) { Nil }, 15 + parse: validation.non_empty_string, 16 + stub: "", 17 + optional_parse: fn(fun, str) { 18 + case str { 19 + "" -> Ok("") 20 + _ -> fun(str) 21 + } 22 + }, 23 + optional_stub: "", 24 + ) 13 25 } 14 26 15 - pub fn integer_field() { 16 - definition.Definition(fn(_, _) { "" }, validation.int, 0) 27 + pub fn float_field() { 28 + definition.Definition( 29 + widget: fn(_, _) { Nil }, 30 + parse: validation.number, 31 + stub: 0.0, 32 + optional_parse: fn(fun, str) { 33 + case str { 34 + "" -> Ok(option.None) 35 + _ -> fun(str) |> result.map(option.Some) 36 + } 37 + }, 38 + optional_stub: option.Some(0.0), 39 + ) 17 40 } 18 41 19 - pub fn float_field() { 20 - definition.Definition(fn(_, _) { "" }, validation.number, 0.0) 42 + fn integer_field() { 43 + definition.Definition( 44 + widget: fn(_, _) { Nil }, 45 + parse: validation.int, 46 + stub: 0, 47 + optional_parse: fn(fun, str) { 48 + case str { 49 + "" -> Ok(option.None) 50 + _ -> fun(str) |> result.map(option.Some) 51 + } 52 + }, 53 + optional_stub: option.Some(0), 54 + ) 55 + } 56 + 57 + fn boolean_field() { 58 + definition.Definition( 59 + widget: fn(_, _) { Nil }, 60 + parse: validation.on, 61 + stub: False, 62 + optional_parse: fn(fun, str) { 63 + case str { 64 + "" -> Ok(False) 65 + _ -> fun(str) 66 + } 67 + }, 68 + optional_stub: False, 69 + ) 21 70 } 22 71 23 72 fn should_be_field_no_error(field: field.Field) { ··· 67 116 } 68 117 69 118 fn one_field_form() { 70 - use a <- formz.with(field("a"), text_field()) 119 + use a <- formz.optional(field("a"), text_field()) 71 120 formz.create_form("hello " <> a) 72 121 } 73 122 74 123 fn two_field_form() { 75 124 { 76 - use a <- formz.with(field("a"), text_field()) 77 - use b <- formz.with(field("b"), text_field()) 125 + use a <- formz.optional(field("a"), text_field()) 126 + use b <- formz.optional(field("b"), text_field()) 78 127 79 128 formz.create_form(#(a, b)) 80 129 } 81 130 } 82 131 83 132 fn three_field_form() { 84 - use a <- formz.with( 85 - field("x") 86 - |> field.set_name("a") 87 - |> field.set_label("A"), 133 + use a <- formz.optional( 134 + field("x") |> field.set_name("a") |> field.set_label("A"), 88 135 text_field() 89 - |> definition.validates(fn(str) { 136 + |> definition.validate(fn(str) { 90 137 case string.length(str) > 3 { 91 138 True -> Ok(str) 92 139 False -> Error("must be longer than 3") ··· 94 141 }), 95 142 ) 96 143 97 - use b <- formz.with(field(named: "b"), integer_field()) 98 - use c <- formz.with( 99 - field(named: "c") 100 - |> field.set_name("c") 101 - |> field.set_label("C"), 144 + use b <- formz.optional( 145 + field(named: "b"), 146 + integer_field() 147 + |> definition.validate(fn(i) { 148 + case i > 0 { 149 + True -> Ok(i) 150 + False -> Error("must be positive") 151 + } 152 + }), 153 + ) 154 + 155 + use c <- formz.optional( 156 + field(named: "c") |> field.set_name("c") |> field.set_label("C"), 102 157 float_field(), 103 158 ) 104 159 ··· 159 214 |> should.equal(Ok(#("hello", "world"))) 160 215 } 161 216 162 - // pub fn parse_double_optional_field_form_test() { 163 - // let f = { 164 - // use a <- formz.with(field("a") |> field.set_optional, text_field()) 165 - // use b <- formz.with(field("b") |> field.set_optional, text_field()) 166 - 167 - // formz.create_form(#(a, b)) 168 - // } 169 - 170 - // f 171 - // |> formz.data([#("a", "hello"), #("b", "world")]) 172 - // |> formz.parse 173 - // |> should.equal(Ok(#(option.Some("hello"), option.Some("world")))) 174 - 175 - // // missing second 176 - // f 177 - // |> formz.data([#("a", "hello")]) 178 - // |> formz.parse 179 - // |> should.equal(Ok(#(option.Some("hello"), option.None))) 180 - 181 - // // missing first 182 - // f 183 - // |> formz.data([#("b", "world")]) 184 - // |> formz.parse 185 - // |> should.equal(Ok(#(option.None, option.Some("world")))) 186 - 187 - // // missing both 188 - // f 189 - // |> formz.data([]) 190 - // |> formz.parse 191 - // |> should.equal(Ok(#(option.None, option.None))) 192 - // } 193 - 194 217 pub fn parse_single_field_form_with_error_test() { 195 218 let assert Error(f) = 196 219 { 197 - use a <- formz.with(field("a"), integer_field()) 220 + use a <- formz.optional(field("a"), boolean_field()) 198 221 formz.create_form(a) 199 222 } 200 - |> formz.data([#("first", "world")]) 223 + |> formz.data([#("a", "world")]) 201 224 |> formz.parse 202 225 203 226 let assert [Element(field, _)] = formz.items(f) 204 - field |> should_be_field_with_error("must be a whole number") 227 + field |> should_be_field_with_error("must be on") 205 228 } 206 229 207 230 pub fn parse_triple_field_form_with_error_test() { ··· 259 282 260 283 pub fn sub_form_test() { 261 284 let f1 = { 262 - use a <- formz.with(field("a"), integer_field()) 263 - use b <- formz.with(field("b"), integer_field()) 264 - use c <- formz.with(field("c"), integer_field()) 285 + use a <- formz.require(field("a"), integer_field()) 286 + use b <- formz.require(field("b"), integer_field()) 287 + use c <- formz.require(field("c"), integer_field()) 265 288 266 289 formz.create_form(#(a, b, c)) 267 290 } 268 291 269 292 let f2 = { 270 293 use a <- formz.with_form(subform("name"), f1) 271 - use b <- formz.with(field("d"), integer_field()) 294 + use b <- formz.require(field("d"), integer_field()) 272 295 273 296 formz.create_form(#(a, b)) 274 297 } ··· 286 309 287 310 pub fn sub_form_error_test() { 288 311 let f1 = { 289 - use a <- formz.with(field("a"), integer_field()) 290 - use b <- formz.with(field("b"), integer_field()) 291 - use c <- formz.with(field("c"), integer_field()) 312 + use a <- formz.optional(field("a"), integer_field()) 313 + use b <- formz.optional(field("b"), integer_field()) 314 + use c <- formz.optional(field("c"), integer_field()) 292 315 293 316 formz.create_form(#(a, b, c)) 294 317 } 295 318 296 319 let f2 = { 297 320 use a <- formz.with_form(subform("name"), f1) 298 - use b <- formz.with(field("d"), integer_field()) 321 + use b <- formz.optional(field("d"), integer_field()) 299 322 300 323 formz.create_form(#(a, b)) 301 324 }
+13 -21
formz/test/formz/validation_test.gleam
··· 37 37 } 38 38 39 39 pub fn string_test() { 40 - "" |> validation.string |> should.equal(Ok("")) 41 - " " |> validation.string |> should.equal(Ok("")) 42 - "a" |> validation.string |> should.equal(Ok("a")) 43 - "b " |> validation.string |> should.equal(Ok("b")) 44 - " c" |> validation.string |> should.equal(Ok("c")) 45 - " d " |> validation.string |> should.equal(Ok("d")) 40 + "" |> validation.non_empty_string |> should.equal(Error("is required")) 41 + " " |> validation.non_empty_string |> should.equal(Error("is required")) 42 + "a" |> validation.non_empty_string |> should.equal(Ok("a")) 43 + "b " |> validation.non_empty_string |> should.equal(Ok("b")) 44 + " c" |> validation.non_empty_string |> should.equal(Ok("c")) 45 + " d " |> validation.non_empty_string |> should.equal(Ok("d")) 46 46 } 47 47 48 48 pub fn email_test() { 49 49 "xxxxx" |> validation.email |> should.equal(Error("must be an email address")) 50 - "a@" |> validation.email |> should.equal(Ok("a@")) 51 - "@a" |> validation.email |> should.equal(Ok("@a")) 50 + "a@" |> validation.email |> should.equal(Error("must be an email address")) 51 + " a@" |> validation.email |> should.equal(Error("must be an email address")) 52 + "@a" |> validation.email |> should.equal(Error("must be an email address")) 53 + "@a " |> validation.email |> should.equal(Error("must be an email address")) 52 54 "a@a" |> validation.email |> should.equal(Ok("a@a")) 55 + "a@a " |> validation.email |> should.equal(Ok("a@a")) 53 56 } 54 57 55 58 pub fn int_test() { 56 - "" |> validation.int |> should.equal(Error("must be a whole number")) 57 59 "a" |> validation.int |> should.equal(Error("must be a whole number")) 58 60 "1.0" |> validation.int |> should.equal(Error("must be a whole number")) 61 + "" |> validation.int |> should.equal(Error("must be a whole number")) 59 62 "1" |> validation.int |> should.equal(Ok(1)) 60 63 } 61 64 62 65 pub fn number_test() { 63 - "" |> validation.number |> should.equal(Error("must be a number")) 64 66 "a" |> validation.number |> should.equal(Error("must be a number")) 67 + "" |> validation.number |> should.equal(Error("must be a number")) 65 68 "1.0" |> validation.number |> should.equal(Ok(1.0)) 66 69 "1" |> validation.number |> should.equal(Ok(1.0)) 67 70 } ··· 84 87 |> should.equal(Error("must be an item in list")) 85 88 "0" |> validation.list_item_by_index(alphabet()) |> should.equal(Ok("A")) 86 89 "24" |> validation.list_item_by_index(alphabet()) |> should.equal(Ok("Y")) 87 - } 88 - 89 - pub fn boolean_test() { 90 - "x" |> validation.boolean |> should.equal(Error("must be true or false")) 91 - "" |> validation.boolean |> should.equal(Ok(False)) 92 - "true" |> validation.boolean |> should.equal(Ok(True)) 93 - "false" |> validation.boolean |> should.equal(Ok(False)) 94 - "True" |> validation.boolean |> should.equal(Ok(True)) 95 - "False" |> validation.boolean |> should.equal(Ok(False)) 96 - "on" |> validation.boolean |> should.equal(Ok(True)) 97 - "off" |> validation.boolean |> should.equal(Ok(False)) 98 90 } 99 91 100 92 pub fn and_test() {
+25 -8
formz_demo/src/formz_demo/example/page.gleam
··· 46 46 wisp.ok() |> wisp.html_body(html) 47 47 } 48 48 49 - fn get_inputs(form: formz.Form(format, ouput)) { 50 - form |> formz.items |> do_get_inputs([]) |> list.reverse 49 + fn get_fields(form: formz.Form(format, ouput)) { 50 + form |> formz.items |> do_get_fields([]) |> list.reverse 51 51 } 52 52 53 - fn do_get_inputs(items: List(formz.FormItem(format)), acc) { 53 + fn do_get_fields(items: List(formz.FormItem(format)), acc) { 54 54 case items { 55 55 [] -> acc 56 - [formz.Element(input, _), ..rest] -> do_get_inputs(rest, [input, ..acc]) 56 + [formz.Element(field, _), ..rest] -> do_get_fields(rest, [field, ..acc]) 57 57 [formz.Set(_, items), ..rest] -> 58 - do_get_inputs(list.flatten([items, rest]), acc) 58 + do_get_fields(list.flatten([items, rest]), acc) 59 59 } 60 60 } 61 61 ··· 67 67 case input_data { 68 68 option.None -> element.none() 69 69 option.Some(input_data) -> { 70 + let fields = get_fields(form) 70 71 let fields_no_post = 71 - form 72 - |> get_inputs 72 + fields 73 73 |> list.map(fn(i) { 74 74 html.tr([], [ 75 75 html.td([], [html.text(i.name)]), ··· 90 90 }) 91 91 |> element.fragment 92 92 93 + let unknown_input = 94 + list.filter_map(input_data, fn(t) { 95 + let #(k, v) = t 96 + case list.find(fields, fn(f) { f.name == k }) { 97 + Ok(_) -> Error(Nil) 98 + Error(_) -> 99 + Ok( 100 + html.tr([], [ 101 + html.td([], [html.text(k)]), 102 + html.td([], [html.text(string.inspect(v))]), 103 + html.td([], [html.text("Unknown")]), 104 + ]), 105 + ) 106 + } 107 + }) 108 + |> element.fragment 109 + 93 110 let output_row = case output { 94 111 option.None -> "" 95 112 option.Some(val) -> { ··· 111 128 html.th([], [html.text("Error")]), 112 129 ]), 113 130 // input_rows, 114 - fields_no_post, 131 + element.fragment([fields_no_post, unknown_input]), 115 132 ]), 116 133 html.h2([], [html.text("Output")]), 117 134 html.div(
+15 -12
formz_demo/src/formz_demo/examples/all_the_inputs.gleam
··· 5 5 import formz_string/widgets 6 6 7 7 pub fn make_form() { 8 - use a <- formz.with(field("a"), definitions.text_field()) 9 - use b <- formz.with(field("b"), definitions.integer_field()) 10 - use c <- formz.with(field("c"), definitions.number_field()) 11 - use d <- formz.with(field("d"), definitions.boolean_field()) 12 - use e <- formz.with(field("e"), definitions.email_field()) 13 - use f <- formz.with(field("f"), definitions.password_field()) 14 - use g <- formz.with( 15 - field("g"), 16 - definitions.choices_field(letters(), placeholder: A), 8 + use a <- formz.optional(field("text"), definitions.text_field()) 9 + use b <- formz.optional(field("int"), definitions.integer_field()) 10 + use c <- formz.optional(field("number"), definitions.number_field()) 11 + use d <- formz.optional(field("bool"), definitions.boolean_field()) 12 + use e <- formz.optional(field("email"), definitions.email_field()) 13 + use f <- formz.optional(field("password"), definitions.password_field()) 14 + use g <- formz.optional( 15 + field("choices"), 16 + definitions.choices_field(letters(), A), 17 + ) 18 + use h <- formz.optional( 19 + field("list"), 20 + definitions.list_field(["Dog", "Cat", "Ant"]), 17 21 ) 18 - use h <- formz.with(field("h"), definitions.list_field(["Dog", "Cat", "Ant"])) 19 - use i <- formz.with( 20 - field("i"), 22 + use i <- formz.optional( 23 + field("textarea_widget"), 21 24 definitions.text_field() 22 25 |> definition.set_widget(widgets.textarea_widget()), 23 26 )
+4 -12
formz_demo/src/formz_demo/examples/custom_output.gleam
··· 6 6 import lustre/element/html 7 7 8 8 pub fn make_form() { 9 - use username <- formz.with(field("username"), definitions.text_field()) 10 - use password <- formz.with(field("password"), definitions.password_field()) 9 + use username <- formz.require(field("username"), definitions.text_field()) 10 + use password <- formz.require(field("password"), definitions.password_field()) 11 11 12 12 formz.create_form(#(username, password)) 13 13 } ··· 32 32 html.label([attribute.for("username")], [html.text("Username")]), 33 33 username_widget( 34 34 username_field, 35 - widget.Args( 36 - "username", 37 - widget.LabelledByLabelFor, 38 - widget.DescribedByNone, 39 - ), 35 + widget.args(widget.LabelledByLabelFor) |> widget.id("username"), 40 36 ), 41 37 ]), 42 38 html.li([], [ 43 39 html.label([attribute.for("password")], [html.text("Password")]), 44 40 password_widget( 45 41 password_field, 46 - widget.Args( 47 - "password", 48 - widget.LabelledByLabelFor, 49 - widget.DescribedByNone, 50 - ), 42 + widget.args(widget.LabelledByLabelFor) |> widget.id("password"), 51 43 ), 52 44 ]), 53 45 ]),
+1 -1
formz_demo/src/formz_demo/examples/hello_world.gleam
··· 3 3 import formz_string/definitions 4 4 5 5 pub fn make_form() { 6 - use name <- formz.with(field("name"), definitions.text_field()) 6 + use name <- formz.require(field("name"), definitions.text_field()) 7 7 formz.create_form("Hello " <> name) 8 8 }
+3 -3
formz_demo/src/formz_demo/examples/labels.gleam
··· 3 3 import formz_string/definitions 4 4 5 5 pub fn make_form() { 6 - use name <- formz.with(field(named: "name"), is: definitions.text_field()) 7 - use age <- formz.with( 6 + use name <- formz.require(field(named: "name"), is: definitions.text_field()) 7 + use age <- formz.require( 8 8 field("age") |> field.set_label("Age"), 9 9 is: definitions.integer_field(), 10 10 ) 11 - use height <- formz.with( 11 + use height <- formz.require( 12 12 field("height") 13 13 |> field.set_label("Height (cm)") 14 14 |> field.set_help_text("Please enter your height in centimeters"),
+2 -2
formz_demo/src/formz_demo/examples/login.gleam
··· 12 12 } 13 13 14 14 pub fn make_form() { 15 - use username <- formz.with(field("username"), definitions.text_field()) 16 - use password <- formz.with(field("password"), definitions.password_field()) 15 + use username <- formz.require(field("username"), definitions.text_field()) 16 + use password <- formz.require(field("password"), definitions.password_field()) 17 17 18 18 formz.create_form(Credentials(username, password)) 19 19 }
+89
formz_demo/src/formz_demo/examples/require_all_the_inputs.gleam
··· 1 + import formz/definition 2 + import formz/field.{field} 3 + import formz/formz_use as formz 4 + import formz_string/definitions 5 + import formz_string/widgets 6 + 7 + pub fn make_form() { 8 + use a <- formz.require(field("text"), definitions.text_field()) 9 + use b <- formz.require(field("int"), definitions.integer_field()) 10 + use c <- formz.require(field("number"), definitions.number_field()) 11 + use d <- formz.require(field("bool"), definitions.boolean_field()) 12 + use e <- formz.require(field("email"), definitions.email_field()) 13 + use f <- formz.require(field("password"), definitions.password_field()) 14 + use g <- formz.require( 15 + field("choices"), 16 + definitions.choices_field(letters(), stub: A), 17 + ) 18 + use h <- formz.require( 19 + field("list"), 20 + definitions.list_field(["Dog", "Cat", "Ant"]), 21 + ) 22 + use i <- formz.require( 23 + field("textarea_widget"), 24 + definitions.text_field() 25 + |> definition.set_widget(widgets.textarea_widget()), 26 + ) 27 + 28 + formz.create_form(#(a, b, c, d, e, f, g, h, i)) 29 + } 30 + 31 + pub type Alphabet { 32 + A 33 + B 34 + C 35 + D 36 + E 37 + F 38 + G 39 + H 40 + I 41 + J 42 + K 43 + L 44 + M 45 + N 46 + O 47 + P 48 + Q 49 + R 50 + S 51 + T 52 + U 53 + V 54 + W 55 + X 56 + Y 57 + Z 58 + } 59 + 60 + pub fn letters() { 61 + [ 62 + #("A", A), 63 + #("B", B), 64 + #("C", C), 65 + #("D", D), 66 + #("E", E), 67 + #("F", F), 68 + #("G", G), 69 + #("H", H), 70 + #("I", I), 71 + #("J", J), 72 + #("K", K), 73 + #("L", L), 74 + #("M", M), 75 + #("N", N), 76 + #("O", O), 77 + #("P", P), 78 + #("Q", Q), 79 + #("R", R), 80 + #("S", S), 81 + #("T", T), 82 + #("U", U), 83 + #("V", V), 84 + #("W", W), 85 + #("X", X), 86 + #("Y", Y), 87 + #("Z", Z), 88 + ] 89 + }
+10 -13
formz_demo/src/formz_demo/examples/sub_form.gleam
··· 1 1 import formz/field.{field} 2 - import formz/form_details.{form_details} 3 2 import formz/formz_use as formz 3 + import formz/subform.{subform} 4 4 import formz_string/definitions 5 5 6 6 pub fn make_form() { 7 - use billing_address <- formz.with_form( 8 - form_details("billing"), 9 - address_form(), 10 - ) 11 - use shipping_address <- formz.with_form( 12 - form_details("shipping"), 13 - address_form(), 14 - ) 7 + use billing_address <- formz.with_form(subform("billing"), address_form()) 8 + use shipping_address <- formz.with_form(subform("shipping"), address_form()) 15 9 16 10 formz.create_form(#(billing_address, shipping_address)) 17 11 } 18 12 19 13 fn address_form() { 20 - use street <- formz.with(field("street"), definitions.text_field()) 21 - use city <- formz.with(field("city"), definitions.text_field()) 22 - use state <- formz.with(field("state"), definitions.list_field(states_list())) 23 - use postal_code <- formz.with( 14 + use street <- formz.require(field("street"), definitions.text_field()) 15 + use city <- formz.require(field("city"), definitions.text_field()) 16 + use state <- formz.require( 17 + field("state"), 18 + definitions.list_field(states_list()), 19 + ) 20 + use postal_code <- formz.require( 24 21 field.field("postal_code"), 25 22 definitions.text_field(), 26 23 )
+14
formz_demo/src/formz_demo/router.gleam
··· 13 13 import formz_demo/examples/hello_world 14 14 import formz_demo/examples/labels 15 15 import formz_demo/examples/login 16 + import formz_demo/examples/require_all_the_inputs 16 17 import formz_demo/examples/sub_form 17 18 18 19 import formz_demo/web.{type Context} ··· 38 39 run.ExampleRun( 39 40 dir, 40 41 all_the_inputs.make_form, 42 + defaults.handle_get, 43 + defaults.handle_post, 44 + defaults.format_string_form, 45 + defaults.formatted_string_form_to_string, 46 + ), 47 + ) 48 + }), 49 + #("require_all_the_inputs", fn(req, dir) { 50 + run.handle( 51 + req, 52 + run.ExampleRun( 53 + dir, 54 + require_all_the_inputs.make_form, 41 55 defaults.handle_get, 42 56 defaults.handle_post, 43 57 defaults.format_string_form,
+60 -14
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 + 7 9 import formz/definition.{Definition} 8 10 import formz/validation 9 - import formz_lustre/widgets 10 11 import gleam/int 11 12 import gleam/list 13 + import gleam/option 12 14 13 15 /// Create a basic form input. Parsed as a String. 14 16 pub fn text_field() { 15 - Definition(widgets.text_like_widget("text"), validation.string, "") 17 + Definition( 18 + widgets.text_like_widget("text"), 19 + validation.non_empty_string, 20 + "", 21 + fn(fun, str) { 22 + case str { 23 + "" -> Ok("") 24 + _ -> fun(str) 25 + } 26 + }, 27 + "", 28 + ) 16 29 } 17 30 18 31 /// Create an email form input. Parsed as a String but must 19 32 /// look like an email address, i.e. the string has an `@`. 20 33 pub fn email_field() { 21 - Definition(widgets.text_like_widget("email"), validation.email, "") 34 + Definition( 35 + widgets.text_like_widget("email"), 36 + validation.email, 37 + "", 38 + definition.make_simple_optional_parse(), 39 + option.None, 40 + ) 22 41 } 23 42 24 43 /// Create a whole number form input. Parsed as an Int. 25 44 pub fn integer_field() { 26 - Definition(widgets.text_like_widget("number"), validation.int, 0) 45 + Definition( 46 + widgets.number_widget(""), 47 + validation.int, 48 + 0, 49 + definition.make_simple_optional_parse(), 50 + option.None, 51 + ) 27 52 } 28 53 29 54 /// Create a number form input. Parsed as a Float. 30 55 pub fn number_field() { 31 - Definition(widgets.text_like_widget("number"), validation.number, 0.0) 56 + Definition( 57 + widgets.number_widget("0.01"), 58 + validation.number, 59 + 0.0, 60 + definition.make_simple_optional_parse(), 61 + option.None, 62 + ) 32 63 } 33 64 34 65 /// Create a checkbox form input. Parsed as a Boolean. 35 66 pub fn boolean_field() { 36 - Definition(widgets.checkbox_widget(), validation.boolean, False) 67 + definition.Definition( 68 + widget: widgets.checkbox_widget(), 69 + parse: validation.on, 70 + stub: False, 71 + optional_parse: fn(fun, str) { 72 + case str { 73 + "" -> Ok(False) 74 + _ -> fun(str) 75 + } 76 + }, 77 + optional_stub: False, 78 + ) 37 79 } 38 80 39 81 /// Create a password form input, which hides the input value. Parsed as a String 40 82 pub fn password_field() { 41 - Definition(widgets.password_widget(), validation.string, "") 83 + Definition( 84 + widgets.password_widget(), 85 + validation.non_empty_string, 86 + "", 87 + definition.make_simple_optional_parse(), 88 + option.None, 89 + ) 42 90 } 43 91 44 92 /// Creates a `<select>` input. Takes a tuple of #(String, String) where the first ··· 48 96 /// Because of how you build `formz` forms, you need to provide a placeholder of 49 97 /// the value type. Is this annoying? Would it be more or less annoying if I 50 98 /// required a non-empty list for the variants instead? I'm not sure. Let me know! 51 - pub fn choices_field( 52 - variants: List(#(String, enum)), 53 - placeholder placeholder: enum, 54 - ) { 99 + pub fn choices_field(variants: List(#(String, enum)), stub stub: enum) { 55 100 let keys_indexed = 56 101 variants 57 102 |> list.index_map(fn(t, i) { #(t.0, int.to_string(i)) }) 58 - 59 103 let values = variants |> list.map(fn(t) { t.1 }) 60 104 61 105 Definition( 62 106 widgets.select_widget(keys_indexed), 63 107 validation.list_item_by_index(values) 64 - |> validation.replace_error("Please select an option"), 65 - placeholder, 108 + |> validation.replace_error("is required"), 109 + stub, 110 + definition.make_simple_optional_parse(), 111 + option.None, 66 112 ) 67 113 } 68 114
+1 -5
formz_lustre/src/formz_lustre/simple.gleam
··· 37 37 html.span([attribute.class("widget")], [ 38 38 make_widget( 39 39 f, 40 - widget.Args( 41 - id: f.name, 42 - labelled_by: widget.LabelledByLabelFor, 43 - described_by: widget.DescribedByNone, 44 - ), 40 + widget.args(widget.LabelledByLabelFor) |> widget.id(f.name), 45 41 ), 46 42 ]) 47 43
+94 -39
formz_lustre/src/formz_lustre/widgets.gleam
··· 1 1 import formz/field.{type Field} 2 2 import formz/widget 3 3 import gleam/list 4 + import gleam/string 4 5 import lustre/attribute 5 6 import lustre/element 6 7 import lustre/element/html ··· 25 26 ) -> attribute.Attribute(msg) { 26 27 case labelled_by { 27 28 widget.LabelledByLabelFor -> attribute.none() 28 - widget.LabelledByElementWithId(id) -> 29 - attribute.attribute("aria-labelledby", id) 29 + widget.LabelledByElementsWithIds(ids) -> 30 + attribute.attribute("aria-labelledby", string.join(ids, " ")) 30 31 widget.LabelledByFieldValue -> 31 32 case label { 32 33 "" -> attribute.none() ··· 35 36 } 36 37 } 37 38 39 + fn aria_describedby_attr( 40 + described_by: widget.DescribedBy, 41 + ) -> attribute.Attribute(msg) { 42 + case described_by { 43 + widget.DescribedByNone -> attribute.none() 44 + widget.DescribedByElementsWithIds(ids) -> 45 + attribute.attribute("aria-describedby", string.join(ids, " ")) 46 + } 47 + } 48 + 38 49 fn value_attr(value: String) -> attribute.Attribute(msg) { 39 50 case value { 40 51 "" -> attribute.none() ··· 42 53 } 43 54 } 44 55 56 + fn required_attr(requried: Bool) -> attribute.Attribute(msg) { 57 + // case requried { 58 + // True -> attribute.required(True) 59 + // False -> attribute.none() 60 + // } 61 + attribute.required(requried) 62 + } 63 + 64 + fn step_size_attr(step_size: String) -> attribute.Attribute(msg) { 65 + case step_size { 66 + "" -> attribute.none() 67 + _ -> attribute.attribute("step", step_size) 68 + } 69 + } 70 + 71 + fn checked_attr(value: String) -> attribute.Attribute(msg) { 72 + case value { 73 + "on" -> attribute.checked(True) 74 + _ -> attribute.none() 75 + } 76 + } 77 + 78 + fn disabled_attr(disabled: Bool) -> attribute.Attribute(msg) { 79 + case disabled { 80 + True -> attribute.disabled(True) 81 + False -> attribute.none() 82 + } 83 + } 84 + 85 + // Create a checkbox widget (`<input type="checkbox">`). The checkbox is checked 86 + // if the value is "on" (the browser default). 45 87 pub fn checkbox_widget() { 46 - fn(input: Field, args: widget.Args) -> element.Element(msg) { 47 - html.input([ 48 - attribute.type_("checkbox"), 49 - name_attr(input.name), 50 - id_attr(args.id), 51 - attribute.checked(input.value == "on"), 52 - aria_label_attr(args.labelled_by, input.label), 88 + fn(field: Field, args: widget.Args) { 89 + do_input_widget(field |> field.set_raw_value(""), args, "checkbox", [ 90 + checked_attr(field.value), 53 91 ]) 54 92 } 55 93 } 56 94 57 - pub fn password_widget() { 58 - fn(input: Field, args: widget.Args) -> element.Element(msg) { 59 - html.input([ 60 - attribute.type_("password"), 61 - name_attr(input.name), 62 - id_attr(args.id), 63 - // value_attr(input.value), 64 - aria_label_attr(args.labelled_by, input.label), 65 - ]) 95 + pub fn number_widget(step_size: String) { 96 + fn(field: Field, args: widget.Args) { 97 + do_input_widget(field, args, "number", [step_size_attr(step_size)]) 66 98 } 67 99 } 68 100 69 - pub fn text_widget() { 70 - text_like_widget("text") 101 + pub fn password_widget() { 102 + fn(field: Field, args: widget.Args) { 103 + do_input_widget(field |> field.set_raw_value(""), args, "password", []) 104 + } 71 105 } 72 106 73 107 pub fn text_like_widget(type_: String) { 74 - fn(input: Field, args: widget.Args) -> element.Element(msg) { 75 - html.input([ 76 - attribute.type_(type_), 77 - name_attr(input.name), 78 - id_attr(args.id), 79 - value_attr(input.value), 80 - aria_label_attr(args.labelled_by, input.label), 81 - ]) 108 + fn(field: Field, args: widget.Args) { 109 + do_input_widget(field, args, type_, []) 82 110 } 83 111 } 84 112 113 + fn do_input_widget( 114 + field: Field, 115 + args: widget.Args, 116 + type_: String, 117 + extra_attrs: List(attribute.Attribute(msg)), 118 + ) { 119 + html.input( 120 + list.flatten([ 121 + [ 122 + attribute.type_(type_), 123 + name_attr(field.name), 124 + id_attr(args.id), 125 + required_attr(field.required), 126 + disabled_attr(field.disabled), 127 + value_attr(field.value), 128 + aria_label_attr(args.labelled_by, field.label), 129 + aria_describedby_attr(args.described_by), 130 + ], 131 + extra_attrs, 132 + ]), 133 + ) 134 + } 135 + 85 136 pub fn textarea_widget() { 86 - fn(input: Field, args: widget.Args) -> element.Element(msg) { 137 + fn(field: Field, args: widget.Args) -> element.Element(msg) { 87 138 html.textarea( 88 139 [ 89 - name_attr(input.name), 140 + name_attr(field.name), 90 141 id_attr(args.id), 91 - aria_label_attr(args.labelled_by, input.label), 142 + required_attr(field.required), 143 + aria_label_attr(args.labelled_by, field.label), 144 + aria_describedby_attr(args.described_by), 92 145 ], 93 - input.value, 146 + field.value, 94 147 ) 95 148 } 96 149 } 97 150 98 151 pub fn hidden_widget() { 99 - fn(input: Field, _args: widget.Args) -> element.Element(msg) { 152 + fn(field: Field, _args: widget.Args) -> element.Element(msg) { 100 153 html.input([ 101 154 attribute.type_("hidden"), 102 - name_attr(input.name), 103 - value_attr(input.value), 155 + name_attr(field.name), 156 + value_attr(field.value), 104 157 ]) 105 158 } 106 159 } 107 160 108 161 pub fn select_widget(variants: List(#(String, String))) { 109 - fn(input: Field, args: widget.Args) -> element.Element(msg) { 162 + fn(field: Field, args: widget.Args) -> element.Element(msg) { 110 163 html.select( 111 164 [ 112 - name_attr(input.name), 165 + name_attr(field.name), 113 166 id_attr(args.id), 114 - aria_label_attr(args.labelled_by, input.label), 167 + required_attr(field.required), 168 + aria_label_attr(args.labelled_by, field.label), 169 + aria_describedby_attr(args.described_by), 115 170 ], 116 171 list.flatten([ 117 172 [html.option([attribute.value("")], "Select..."), html.hr([])], 118 173 list.map(variants, fn(variant) { 119 174 let val = variant.1 120 175 html.option( 121 - [attribute.value(val), attribute.selected(input.value == val)], 176 + [attribute.value(val), attribute.selected(field.value == val)], 122 177 variant.0, 123 178 ) 124 179 }),
+52 -24
formz_lustre/test/formz_lustre/fields_test.gleam
··· 1 - import gleeunit 2 - import gleeunit/should 1 + import formz_lustre/definitions 3 2 4 - import formz_lustre/definitions 3 + import formz/definition.{type Definition} 5 4 import formz_string/definitions as string_definitions 5 + import gleeunit 6 + import gleeunit/should 6 7 7 8 pub fn main() { 8 9 gleeunit.main() 9 10 } 10 11 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, "")) 16 + } 17 + 11 18 pub fn text_field_test() { 12 - let string_field = string_definitions.text_field() 13 - let field = definitions.text_field() 19 + let string_definition = string_definitions.text_field() 20 + let definition = definitions.text_field() 14 21 15 - field.transform |> should.equal(string_field.transform) 22 + definition.stub |> should.equal(string_definition.stub) 23 + definition.optional_stub |> should.equal(string_definition.optional_stub) 24 + compare_parse_fns(definition, string_definition, "") 25 + compare_parse_fns(definition, string_definition, "a") 16 26 } 17 27 18 28 pub fn email_field_test() { 19 - let string_field = string_definitions.email_field() 20 - let field = definitions.email_field() 29 + let string_definition = string_definitions.email_field() 30 + let definition = definitions.email_field() 21 31 22 - field.transform |> should.equal(string_field.transform) 32 + definition.stub |> should.equal(string_definition.stub) 33 + definition.optional_stub |> should.equal(string_definition.optional_stub) 34 + compare_parse_fns(definition, string_definition, "") 35 + compare_parse_fns(definition, string_definition, "a") 23 36 } 24 37 25 38 pub fn number_field_test() { 26 - let string_field = string_definitions.number_field() 27 - let field = definitions.number_field() 39 + let string_definition = string_definitions.number_field() 40 + let definition = definitions.number_field() 28 41 29 - field.transform |> should.equal(string_field.transform) 42 + definition.stub |> should.equal(string_definition.stub) 43 + definition.optional_stub |> should.equal(string_definition.optional_stub) 44 + compare_parse_fns(definition, string_definition, "") 45 + compare_parse_fns(definition, string_definition, "a") 30 46 } 31 47 32 48 pub fn integer_field_test() { 33 - let string_field = string_definitions.integer_field() 34 - let field = definitions.integer_field() 49 + let string_definition = string_definitions.integer_field() 50 + let definition = definitions.integer_field() 35 51 36 - field.transform |> should.equal(string_field.transform) 52 + definition.stub |> should.equal(string_definition.stub) 53 + definition.optional_stub |> should.equal(string_definition.optional_stub) 54 + compare_parse_fns(definition, string_definition, "") 55 + compare_parse_fns(definition, string_definition, "a") 37 56 } 38 57 39 58 pub fn boolean_field_test() { 40 - let string_field = string_definitions.boolean_field() 41 - let field = definitions.boolean_field() 59 + let string_definition = string_definitions.boolean_field() 60 + let definition = definitions.boolean_field() 42 61 43 - field.transform |> should.equal(string_field.transform) 62 + definition.stub |> should.equal(string_definition.stub) 63 + definition.optional_stub |> should.equal(string_definition.optional_stub) 64 + compare_parse_fns(definition, string_definition, "") 65 + compare_parse_fns(definition, string_definition, "a") 44 66 } 45 67 46 68 pub fn choices_field_test() { 47 - let string_field = 69 + let string_definition = 48 70 string_definitions.choices_field([#("a", "A"), #("b", "B")], "") 49 - let field = definitions.choices_field([#("a", "A"), #("b", "B")], "") 71 + let definition = definitions.choices_field([#("a", "A"), #("b", "B")], "") 50 72 51 - field.transform |> should.equal(string_field.transform) 73 + definition.stub |> should.equal(string_definition.stub) 74 + definition.optional_stub |> should.equal(string_definition.optional_stub) 75 + compare_parse_fns(definition, string_definition, "") 76 + compare_parse_fns(definition, string_definition, "a") 52 77 } 53 78 54 79 pub fn indexed_enum_field_test() { 55 - let string_field = string_definitions.list_field(["A", "B"]) 56 - let field = definitions.list_field(["A", "B"]) 80 + let string_definition = string_definitions.list_field(["A", "B"]) 81 + let definition = definitions.list_field(["A", "B"]) 57 82 58 - field.transform |> should.equal(string_field.transform) 83 + definition.stub |> should.equal(string_definition.stub) 84 + definition.optional_stub |> should.equal(string_definition.optional_stub) 85 + compare_parse_fns(definition, string_definition, "") 86 + compare_parse_fns(definition, string_definition, "a") 59 87 }
+5 -5
formz_lustre/test/formz_lustre/widgets_test.gleam
··· 137 137 value: "", 138 138 args: widget.Args( 139 139 "id", 140 - labelled_by: widget.LabelledByElementWithId("div"), 140 + labelled_by: widget.LabelledByElementsWithIds(["div"]), 141 141 described_by: widget.DescribedByNone, 142 142 ), 143 143 ) ··· 189 189 value: "", 190 190 args: widget.Args( 191 191 "id", 192 - labelled_by: widget.LabelledByElementWithId("div"), 192 + labelled_by: widget.LabelledByElementsWithIds(["div"]), 193 193 described_by: widget.DescribedByNone, 194 194 ), 195 195 ) ··· 241 241 value: "", 242 242 args: widget.Args( 243 243 "id", 244 - labelled_by: widget.LabelledByElementWithId("div"), 244 + labelled_by: widget.LabelledByElementsWithIds(["div"]), 245 245 described_by: widget.DescribedByNone, 246 246 ), 247 247 ) ··· 293 293 value: "", 294 294 args: widget.Args( 295 295 "id", 296 - labelled_by: widget.LabelledByElementWithId("div"), 296 + labelled_by: widget.LabelledByElementsWithIds(["div"]), 297 297 described_by: widget.DescribedByNone, 298 298 ), 299 299 ) ··· 346 346 value: "", 347 347 args: widget.Args( 348 348 "id", 349 - labelled_by: widget.LabelledByElementWithId("div"), 349 + labelled_by: widget.LabelledByElementsWithIds(["div"]), 350 350 described_by: widget.DescribedByNone, 351 351 ), 352 352 )
+60 -14
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_nakai/widgets 8 + 7 9 import formz/definition.{Definition} 8 10 import formz/validation 9 - import formz_nakai/widgets 10 11 import gleam/int 11 12 import gleam/list 13 + import gleam/option 12 14 13 15 /// Create a basic form input. Parsed as a String. 14 16 pub fn text_field() { 15 - Definition(widgets.text_like_widget("text"), validation.string, "") 17 + Definition( 18 + widgets.text_like_widget("text"), 19 + validation.non_empty_string, 20 + "", 21 + fn(fun, str) { 22 + case str { 23 + "" -> Ok("") 24 + _ -> fun(str) 25 + } 26 + }, 27 + "", 28 + ) 16 29 } 17 30 18 31 /// Create an email form input. Parsed as a String but must 19 32 /// look like an email address, i.e. the string has an `@`. 20 33 pub fn email_field() { 21 - Definition(widgets.text_like_widget("email"), validation.email, "") 34 + Definition( 35 + widgets.text_like_widget("email"), 36 + validation.email, 37 + "", 38 + definition.make_simple_optional_parse(), 39 + option.None, 40 + ) 22 41 } 23 42 24 43 /// Create a whole number form input. Parsed as an Int. 25 44 pub fn integer_field() { 26 - Definition(widgets.text_like_widget("number"), validation.int, 0) 45 + Definition( 46 + widgets.number_widget(""), 47 + validation.int, 48 + 0, 49 + definition.make_simple_optional_parse(), 50 + option.None, 51 + ) 27 52 } 28 53 29 54 /// Create a number form input. Parsed as a Float. 30 55 pub fn number_field() { 31 - Definition(widgets.text_like_widget("number"), validation.number, 0.0) 56 + Definition( 57 + widgets.number_widget("0.01"), 58 + validation.number, 59 + 0.0, 60 + definition.make_simple_optional_parse(), 61 + option.None, 62 + ) 32 63 } 33 64 34 65 /// Create a checkbox form input. Parsed as a Boolean. 35 66 pub fn boolean_field() { 36 - Definition(widgets.checkbox_widget(), validation.boolean, False) 67 + definition.Definition( 68 + widget: widgets.checkbox_widget(), 69 + parse: validation.on, 70 + stub: False, 71 + optional_parse: fn(fun, str) { 72 + case str { 73 + "" -> Ok(False) 74 + _ -> fun(str) 75 + } 76 + }, 77 + optional_stub: False, 78 + ) 37 79 } 38 80 39 81 /// Create a password form input, which hides the input value. Parsed as a String 40 82 pub fn password_field() { 41 - Definition(widgets.password_widget(), validation.string, "") 83 + Definition( 84 + widgets.password_widget(), 85 + validation.non_empty_string, 86 + "", 87 + definition.make_simple_optional_parse(), 88 + option.None, 89 + ) 42 90 } 43 91 44 92 /// Creates a `<select>` input. Takes a tuple of #(String, String) where the first ··· 48 96 /// Because of how you build `formz` forms, you need to provide a placeholder of 49 97 /// the value type. Is this annoying? Would it be more or less annoying if I 50 98 /// required a non-empty list for the variants instead? I'm not sure. Let me know! 51 - pub fn choices_field( 52 - variants: List(#(String, enum)), 53 - placeholder placeholder: enum, 54 - ) { 99 + pub fn choices_field(variants: List(#(String, enum)), stub stub: enum) { 55 100 let keys_indexed = 56 101 variants 57 102 |> list.index_map(fn(t, i) { #(t.0, int.to_string(i)) }) 58 - 59 103 let values = variants |> list.map(fn(t) { t.1 }) 60 104 61 105 Definition( 62 106 widgets.select_widget(keys_indexed), 63 107 validation.list_item_by_index(values) 64 - |> validation.replace_error("Please select an option"), 65 - placeholder, 108 + |> validation.replace_error("is required"), 109 + stub, 110 + definition.make_simple_optional_parse(), 111 + option.None, 66 112 ) 67 113 } 68 114
+1 -5
formz_nakai/src/formz_nakai/simple.gleam
··· 29 29 html.span([attr.class("widget")], [ 30 30 make_widget( 31 31 f, 32 - widget.Args( 33 - id: f.name, 34 - labelled_by: widget.LabelledByLabelFor, 35 - described_by: widget.DescribedByNone, 36 - ), 32 + widget.args(widget.LabelledByLabelFor) |> widget.id(f.name), 37 33 ), 38 34 ]) 39 35
+79 -35
formz_nakai/src/formz_nakai/widgets.gleam
··· 1 1 import formz/field.{type Field} 2 2 import formz/widget 3 + import gleam/string 3 4 4 5 import gleam/list 5 6 import nakai/attr ··· 25 26 ) -> List(attr.Attr) { 26 27 case labelled_by { 27 28 widget.LabelledByLabelFor -> [] 28 - widget.LabelledByElementWithId(id) -> [attr.aria_labelledby(id)] 29 + widget.LabelledByElementsWithIds(ids) -> [ 30 + attr.aria_labelledby(string.join(ids, " ")), 31 + ] 29 32 widget.LabelledByFieldValue -> 30 33 case label { 31 34 "" -> [] ··· 34 37 } 35 38 } 36 39 40 + fn aria_describedby_attr(described_by: widget.DescribedBy) -> List(attr.Attr) { 41 + case described_by { 42 + widget.DescribedByElementsWithIds(ids) -> [ 43 + attr.Attr("aria-describedby", string.join(ids, " ")), 44 + ] 45 + widget.DescribedByNone -> [] 46 + } 47 + } 48 + 37 49 fn type_attr(type_: String) -> List(attr.Attr) { 38 50 [attr.type_(type_)] 39 51 } ··· 45 57 } 46 58 } 47 59 60 + fn required_attr(required: Bool) -> List(attr.Attr) { 61 + case required { 62 + True -> [attr.required("")] 63 + False -> [] 64 + } 65 + } 66 + 67 + fn checked_attr(value: String) -> List(attr.Attr) { 68 + case value { 69 + "on" -> [attr.checked()] 70 + _ -> [] 71 + } 72 + } 73 + 74 + fn disabled_attr(disabled: Bool) -> List(attr.Attr) { 75 + case disabled { 76 + True -> [attr.disabled()] 77 + False -> [] 78 + } 79 + } 80 + 81 + fn step_size_attr(step_size: String) -> List(attr.Attr) { 82 + case step_size { 83 + "" -> [] 84 + _ -> [attr.Attr("step", step_size)] 85 + } 86 + } 87 + 88 + // Create a checkbox widget (`<input type="checkbox">`). The checkbox is checked 89 + // if the value is "on" (the browser default). 48 90 pub fn checkbox_widget() { 49 - fn(field: Field, args: widget.Args) -> html.Node { 50 - let checked_attr = case field.value { 51 - "on" -> [attr.checked()] 52 - _ -> [] 53 - } 91 + fn(field: Field, args: widget.Args) { 92 + do_input_widget(field |> field.set_raw_value(""), args, "checkbox", [ 93 + checked_attr(field.value), 94 + ]) 95 + } 96 + } 54 97 55 - html.input( 56 - list.flatten([ 57 - type_attr("checkbox"), 58 - name_attr(field.name), 59 - id_attr(args.id), 60 - checked_attr, 61 - aria_label_attr(args.labelled_by, field.label), 62 - ]), 63 - ) 98 + pub fn number_widget(step_size: String) { 99 + fn(field: Field, args: widget.Args) { 100 + do_input_widget(field, args, "number", [step_size_attr(step_size)]) 64 101 } 65 102 } 66 103 67 104 pub fn password_widget() { 68 - fn(field: Field, args: widget.Args) -> html.Node { 69 - html.input( 70 - list.flatten([ 71 - type_attr("password"), 72 - name_attr(field.name), 73 - id_attr(args.id), 74 - // value_attr(field.value), 75 - aria_label_attr(args.labelled_by, field.label), 76 - ]), 77 - ) 105 + fn(field: Field, args: widget.Args) { 106 + do_input_widget(field |> field.set_raw_value(""), args, "password", []) 78 107 } 79 108 } 80 109 81 110 pub fn text_like_widget(type_: String) { 82 - fn(field: Field, args: widget.Args) -> html.Node { 83 - html.input( 84 - list.flatten([ 85 - type_attr(type_), 86 - name_attr(field.name), 87 - id_attr(args.id), 88 - value_attr(field.value), 89 - aria_label_attr(args.labelled_by, field.label), 90 - ]), 91 - ) 111 + fn(field: Field, args: widget.Args) { 112 + do_input_widget(field, args, type_, []) 92 113 } 93 114 } 94 115 116 + fn do_input_widget( 117 + field: Field, 118 + args: widget.Args, 119 + type_: String, 120 + extra_attrs: List(List(attr.Attr)), 121 + ) { 122 + html.input( 123 + list.flatten([ 124 + type_attr(type_), 125 + name_attr(field.name), 126 + id_attr(args.id), 127 + required_attr(field.required), 128 + value_attr(field.value), 129 + disabled_attr(field.disabled), 130 + aria_describedby_attr(args.described_by), 131 + aria_label_attr(args.labelled_by, field.label), 132 + extra_attrs |> list.flatten, 133 + ]), 134 + ) 135 + } 136 + 95 137 pub fn textarea_widget() { 96 138 fn(field: Field, args: widget.Args) -> html.Node { 97 139 html.textarea( 98 140 list.flatten([ 99 141 name_attr(field.name), 100 142 id_attr(args.id), 143 + required_attr(field.required), 101 144 aria_label_attr(args.labelled_by, field.label), 102 145 ]), 103 146 [html.Text(field.value)], ··· 123 166 list.flatten([ 124 167 name_attr(field.name), 125 168 id_attr(args.id), 169 + required_attr(field.required), 126 170 aria_label_attr(args.labelled_by, field.label), 127 171 ]), 128 172 list.flatten([
+52 -24
formz_nakai/test/formz_nakai/fields_test.gleam
··· 1 - import gleeunit 2 - import gleeunit/should 1 + import formz_nakai/definitions 3 2 4 - import formz_nakai/definitions 3 + import formz/definition.{type Definition} 5 4 import formz_string/definitions as string_definitions 5 + import gleeunit 6 + import gleeunit/should 6 7 7 8 pub fn main() { 8 9 gleeunit.main() 9 10 } 10 11 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, "")) 16 + } 17 + 11 18 pub fn text_field_test() { 12 - let string_field = string_definitions.text_field() 13 - let field = definitions.text_field() 19 + let string_definition = string_definitions.text_field() 20 + let definition = definitions.text_field() 14 21 15 - field.transform |> should.equal(string_field.transform) 22 + definition.stub |> should.equal(string_definition.stub) 23 + definition.optional_stub |> should.equal(string_definition.optional_stub) 24 + compare_parse_fns(definition, string_definition, "") 25 + compare_parse_fns(definition, string_definition, "a") 16 26 } 17 27 18 28 pub fn email_field_test() { 19 - let string_field = string_definitions.email_field() 20 - let field = definitions.email_field() 29 + let string_definition = string_definitions.email_field() 30 + let definition = definitions.email_field() 21 31 22 - field.transform |> should.equal(string_field.transform) 32 + definition.stub |> should.equal(string_definition.stub) 33 + definition.optional_stub |> should.equal(string_definition.optional_stub) 34 + compare_parse_fns(definition, string_definition, "") 35 + compare_parse_fns(definition, string_definition, "a") 23 36 } 24 37 25 38 pub fn number_field_test() { 26 - let string_field = string_definitions.number_field() 27 - let field = definitions.number_field() 39 + let string_definition = string_definitions.number_field() 40 + let definition = definitions.number_field() 28 41 29 - field.transform |> should.equal(string_field.transform) 42 + definition.stub |> should.equal(string_definition.stub) 43 + definition.optional_stub |> should.equal(string_definition.optional_stub) 44 + compare_parse_fns(definition, string_definition, "") 45 + compare_parse_fns(definition, string_definition, "a") 30 46 } 31 47 32 48 pub fn integer_field_test() { 33 - let string_field = string_definitions.integer_field() 34 - let field = definitions.integer_field() 49 + let string_definition = string_definitions.integer_field() 50 + let definition = definitions.integer_field() 35 51 36 - field.transform |> should.equal(string_field.transform) 52 + definition.stub |> should.equal(string_definition.stub) 53 + definition.optional_stub |> should.equal(string_definition.optional_stub) 54 + compare_parse_fns(definition, string_definition, "") 55 + compare_parse_fns(definition, string_definition, "a") 37 56 } 38 57 39 58 pub fn boolean_field_test() { 40 - let string_field = string_definitions.boolean_field() 41 - let field = definitions.boolean_field() 59 + let string_definition = string_definitions.boolean_field() 60 + let definition = definitions.boolean_field() 42 61 43 - field.transform |> should.equal(string_field.transform) 62 + definition.stub |> should.equal(string_definition.stub) 63 + definition.optional_stub |> should.equal(string_definition.optional_stub) 64 + compare_parse_fns(definition, string_definition, "") 65 + compare_parse_fns(definition, string_definition, "a") 44 66 } 45 67 46 68 pub fn choices_field_test() { 47 - let string_field = 69 + let string_definition = 48 70 string_definitions.choices_field([#("a", "A"), #("b", "B")], "") 49 - let field = definitions.choices_field([#("a", "A"), #("b", "B")], "") 71 + let definition = definitions.choices_field([#("a", "A"), #("b", "B")], "") 50 72 51 - field.transform |> should.equal(string_field.transform) 73 + definition.stub |> should.equal(string_definition.stub) 74 + definition.optional_stub |> should.equal(string_definition.optional_stub) 75 + compare_parse_fns(definition, string_definition, "") 76 + compare_parse_fns(definition, string_definition, "a") 52 77 } 53 78 54 79 pub fn indexed_enum_field_test() { 55 - let string_field = string_definitions.list_field(["A", "B"]) 56 - let field = definitions.list_field(["A", "B"]) 80 + let string_definition = string_definitions.list_field(["A", "B"]) 81 + let definition = definitions.list_field(["A", "B"]) 57 82 58 - field.transform |> should.equal(string_field.transform) 83 + definition.stub |> should.equal(string_definition.stub) 84 + definition.optional_stub |> should.equal(string_definition.optional_stub) 85 + compare_parse_fns(definition, string_definition, "") 86 + compare_parse_fns(definition, string_definition, "a") 59 87 }
+29 -9
formz_nakai/test/formz_nakai/widgets_test.gleam
··· 16 16 string.replace(str, " />", ">") 17 17 } 18 18 19 - fn remove_checked_true(str: String) -> String { 20 - string.replace(str, "checked=\"true\"", "checked") 19 + fn remove_trues(str: String) -> String { 20 + str 21 + |> string.replace("checked=\"true\"", "checked") 22 + |> string.replace("disabled=\"true\"", "disabled") 21 23 } 22 24 23 25 fn remove_empty_attributes(str: String) -> String { ··· 28 30 input 29 31 |> nakai.to_inline_string 30 32 |> remove_self_closing_slash 31 - |> remove_checked_true 33 + |> remove_trues 32 34 |> remove_empty_attributes 33 35 } 34 36 ··· 79 81 help: "help", 80 82 hidden: False, 81 83 disabled: False, 82 - required: True, 84 + required: False, 83 85 value: "", 84 86 args: widget.Args( 85 87 "id", ··· 87 89 described_by: widget.DescribedByNone, 88 90 ), 89 91 ) 92 + 90 93 test_inputs( 91 94 string_widgets.text_like_widget("text"), 92 95 widgets.text_like_widget("text"), ··· 137 140 described_by: widget.DescribedByNone, 138 141 ), 139 142 ) 143 + 144 + test_inputs( 145 + string_widgets.text_like_widget("text"), 146 + widgets.text_like_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 + ) 140 160 } 141 161 142 162 pub fn checkbox_widget_test() { ··· 152 172 value: "", 153 173 args: widget.Args( 154 174 "id", 155 - labelled_by: widget.LabelledByElementWithId("div"), 175 + labelled_by: widget.LabelledByElementsWithIds(["div"]), 156 176 described_by: widget.DescribedByNone, 157 177 ), 158 178 ) ··· 204 224 value: "", 205 225 args: widget.Args( 206 226 "id", 207 - labelled_by: widget.LabelledByElementWithId("div"), 227 + labelled_by: widget.LabelledByElementsWithIds(["div"]), 208 228 described_by: widget.DescribedByNone, 209 229 ), 210 230 ) ··· 256 276 value: "", 257 277 args: widget.Args( 258 278 "id", 259 - labelled_by: widget.LabelledByElementWithId("div"), 279 + labelled_by: widget.LabelledByElementsWithIds(["div"]), 260 280 described_by: widget.DescribedByNone, 261 281 ), 262 282 ) ··· 308 328 value: "", 309 329 args: widget.Args( 310 330 "id", 311 - labelled_by: widget.LabelledByElementWithId("div"), 331 + labelled_by: widget.LabelledByElementsWithIds(["div"]), 312 332 described_by: widget.DescribedByNone, 313 333 ), 314 334 ) ··· 361 381 value: "", 362 382 args: widget.Args( 363 383 "id", 364 - labelled_by: widget.LabelledByElementWithId("div"), 384 + labelled_by: widget.LabelledByElementsWithIds(["div"]), 365 385 described_by: widget.DescribedByNone, 366 386 ), 367 387 )
+7
formz_string/birdie_snapshots/basic_select.accepted
··· 1 + --- 2 + version: 1.2.3 3 + title: basic select 4 + file: ./test/formz_string/widgets_test.gleam 5 + test_name: select_test 6 + --- 7 + <select name="name"><option value>Select...</option><hr><option value="0">One</option><option value="1">Two</option><option value="2">Three</option></select>
+7
formz_string/birdie_snapshots/checkbox_checked.accepted
··· 1 + --- 2 + version: 1.2.3 3 + title: checkbox checked 4 + file: ./test/formz_string/widgets_test.gleam 5 + test_name: checkbox_checked_test 6 + --- 7 + <input type="checkbox" name="name" checked>
+7
formz_string/birdie_snapshots/checkbox_unchecked.accepted
··· 1 + --- 2 + version: 1.2.3 3 + title: checkbox unchecked 4 + file: ./test/formz_string/widgets_test.gleam 5 + test_name: checkbox_unchecked_test 6 + --- 7 + <input type="checkbox" name="name">
+7
formz_string/birdie_snapshots/number_input_with_no_step.accepted
··· 1 + --- 2 + version: 1.2.3 3 + title: number input with no step 4 + file: ./test/formz_string/widgets_test.gleam 5 + test_name: numeric_no_step_test 6 + --- 7 + <input type="number" name="name" value="1">
+7
formz_string/birdie_snapshots/number_input_with_step.accepted
··· 1 + --- 2 + version: 1.2.3 3 + title: number input with step 4 + file: ./test/formz_string/widgets_test.gleam 5 + test_name: numeric_step_test 6 + --- 7 + <input type="number" name="name" value="1.0" step="0.1">
+7
formz_string/birdie_snapshots/password_ignores_input.accepted
··· 1 + --- 2 + version: 1.2.3 3 + title: password ignores input 4 + file: ./test/formz_string/widgets_test.gleam 5 + test_name: password_test 6 + --- 7 + <input type="password" name="name">
+7
formz_string/birdie_snapshots/required_select.accepted
··· 1 + --- 2 + version: 1.2.3 3 + title: required select 4 + file: ./test/formz_string/widgets_test.gleam 5 + test_name: select_required_test 6 + --- 7 + <select name="name" required><option value>Select...</option><hr><option value="0">One</option><option value="1">Two</option><option value="2">Three</option></select>
+7
formz_string/birdie_snapshots/select_selected.accepted
··· 1 + --- 2 + version: 1.2.3 3 + title: select selected 4 + file: ./test/formz_string/widgets_test.gleam 5 + test_name: select_selected_test 6 + --- 7 + <select name="name"><option value>Select...</option><hr><option value="0">One</option><option value="1" selected>Two</option><option value="2">Three</option></select>
+7
formz_string/birdie_snapshots/text_like_disabled.accepted
··· 1 + --- 2 + version: 1.2.3 3 + title: text like disabled 4 + file: ./test/formz_string/widgets_test.gleam 5 + test_name: text_like_disabled_test 6 + --- 7 + <input type="text" name="name" disabled value="hello">
+7
formz_string/birdie_snapshots/text_like_labelled_by_element_with_id.accepted
··· 1 + --- 2 + version: 1.2.3 3 + title: text like labelled by element with id 4 + file: ./test/formz_string/widgets_test.gleam 5 + test_name: hello_birdie_test 6 + --- 7 + <input type="text" name="name" value="hello" aria-labelledby="id">
+7
formz_string/birdie_snapshots/text_like_labelled_by_field_value.accepted
··· 1 + --- 2 + version: 1.2.3 3 + title: text like labelled by field value 4 + file: ./test/formz_string/widgets_test.gleam 5 + test_name: hello_birdie_test 6 + --- 7 + <input type="text" name="name" value="hello" aria-label="Label">
+7
formz_string/birdie_snapshots/text_like_labelled_by_label_for.accepted
··· 1 + --- 2 + version: 1.2.3 3 + title: text like labelled by label/for 4 + file: ./test/formz_string/widgets_test.gleam 5 + test_name: text_like_labelled_by_test 6 + --- 7 + <input type="text" name="name" value="hello">
+7
formz_string/birdie_snapshots/text_like_required.accepted
··· 1 + --- 2 + version: 1.2.3 3 + title: text like required 4 + file: ./test/formz_string/widgets_test.gleam 5 + test_name: text_like_required_test 6 + --- 7 + <input type="text" name="name" required value="hello">
+1
formz_string/gleam.toml
··· 18 18 19 19 [dev-dependencies] 20 20 gleeunit = ">= 1.0.0 and < 2.0.0" 21 + birdie = ">= 1.2.3 and < 2.0.0"
+15
formz_string/manifest.toml
··· 2 2 # You typically do not need to edit this file 3 3 4 4 packages = [ 5 + { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, 6 + { name = "birdie", version = "1.2.3", build_tools = ["gleam"], requirements = ["argv", "edit_distance", "filepath", "glance", "gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "justin", "rank", "simplifile", "trie_again"], otp_app = "birdie", source = "hex", outer_checksum = "AE1207210E9CC8F4170BCE3FB3C23932F314C352C3FD1BCEA44CF4BF8CF60F93" }, 7 + { name = "edit_distance", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "edit_distance", source = "hex", outer_checksum = "A1E485C69A70210223E46E63985FA1008B8B2DDA9848B7897469171B29020C05" }, 8 + { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, 5 9 { name = "formz", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "justin"], source = "local", path = "../formz" }, 10 + { name = "glance", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "8F3314D27773B7C3B9FB58D8C02C634290422CE531988C0394FA0DF8676B964D" }, 11 + { name = "gleam_community_ansi", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "4CD513FC62523053E62ED7BAC2F36136EC17D6A8942728250A9A00A15E340E4B" }, 12 + { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" }, 13 + { name = "gleam_erlang", version = "0.28.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "BE551521F708DCE5CB954AFBBDF08519C1C44986521FD40753608825F48FFA9E" }, 14 + { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, 6 15 { name = "gleam_stdlib", version = "0.40.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86606B75A600BBD05E539EB59FABC6E307EEEA7B1E5865AFB6D980A93BCB2181" }, 7 16 { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, 17 + { name = "glexer", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glexer", source = "hex", outer_checksum = "BD477AD657C2B637FEF75F2405FAEFFA533F277A74EF1A5E17B55B1178C228FB" }, 8 18 { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, 19 + { name = "rank", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "rank", source = "hex", outer_checksum = "5660E361F0E49CBB714CC57CC4C89C63415D8986F05B2DA0C719D5642FAD91C9" }, 20 + { name = "simplifile", version = "2.2.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0DFABEF7DC7A9E2FF4BB27B108034E60C81BEBFCB7AB816B9E7E18ED4503ACD8" }, 21 + { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, 22 + { name = "trie_again", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "trie_again", source = "hex", outer_checksum = "5B19176F52B1BD98831B57FDC97BD1F88C8A403D6D8C63471407E78598E27184" }, 9 23 ] 10 24 11 25 [requirements] 26 + birdie = { version = ">= 1.2.3 and < 2.0.0" } 12 27 formz = { path = "../formz" } 13 28 gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } 14 29 gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
+60 -14
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 + 7 9 import formz/definition.{Definition} 8 10 import formz/validation 9 - import formz_string/widgets 10 11 import gleam/int 11 12 import gleam/list 13 + import gleam/option 12 14 13 15 /// Create a basic form input. Parsed as a String. 14 16 pub fn text_field() { 15 - Definition(widgets.text_like_widget("text"), validation.string, "") 17 + Definition( 18 + widgets.text_like_widget("text"), 19 + validation.non_empty_string, 20 + "", 21 + fn(fun, str) { 22 + case str { 23 + "" -> Ok("") 24 + _ -> fun(str) 25 + } 26 + }, 27 + "", 28 + ) 16 29 } 17 30 18 31 /// Create an email form input. Parsed as a String but must 19 32 /// look like an email address, i.e. the string has an `@`. 20 33 pub fn email_field() { 21 - Definition(widgets.text_like_widget("email"), validation.email, "") 34 + Definition( 35 + widgets.text_like_widget("email"), 36 + validation.email, 37 + "", 38 + definition.make_simple_optional_parse(), 39 + option.None, 40 + ) 22 41 } 23 42 24 43 /// Create a whole number form input. Parsed as an Int. 25 44 pub fn integer_field() { 26 - Definition(widgets.text_like_widget("number"), validation.int, 0) 45 + Definition( 46 + widgets.number_widget(""), 47 + validation.int, 48 + 0, 49 + definition.make_simple_optional_parse(), 50 + option.None, 51 + ) 27 52 } 28 53 29 54 /// Create a number form input. Parsed as a Float. 30 55 pub fn number_field() { 31 - Definition(widgets.text_like_widget("number"), validation.number, 0.0) 56 + Definition( 57 + widgets.number_widget("0.01"), 58 + validation.number, 59 + 0.0, 60 + definition.make_simple_optional_parse(), 61 + option.None, 62 + ) 32 63 } 33 64 34 65 /// Create a checkbox form input. Parsed as a Boolean. 35 66 pub fn boolean_field() { 36 - Definition(widgets.checkbox_widget(), validation.boolean, False) 67 + definition.Definition( 68 + widget: widgets.checkbox_widget(), 69 + parse: validation.on, 70 + stub: False, 71 + optional_parse: fn(fun, str) { 72 + case str { 73 + "" -> Ok(False) 74 + _ -> fun(str) 75 + } 76 + }, 77 + optional_stub: False, 78 + ) 37 79 } 38 80 39 81 /// Create a password form input, which hides the input value. Parsed as a String 40 82 pub fn password_field() { 41 - Definition(widgets.password_widget(), validation.string, "") 83 + Definition( 84 + widgets.password_widget(), 85 + validation.non_empty_string, 86 + "", 87 + definition.make_simple_optional_parse(), 88 + option.None, 89 + ) 42 90 } 43 91 44 92 /// Creates a `<select>` input. Takes a tuple of #(String, String) where the first ··· 48 96 /// Because of how you build `formz` forms, you need to provide a placeholder of 49 97 /// the value type. Is this annoying? Would it be more or less annoying if I 50 98 /// required a non-empty list for the variants instead? I'm not sure. Let me know! 51 - pub fn choices_field( 52 - variants: List(#(String, enum)), 53 - placeholder placeholder: enum, 54 - ) { 99 + pub fn choices_field(variants: List(#(String, enum)), stub stub: enum) { 55 100 let keys_indexed = 56 101 variants 57 102 |> list.index_map(fn(t, i) { #(t.0, int.to_string(i)) }) 58 - 59 103 let values = variants |> list.map(fn(t) { t.1 }) 60 104 61 105 Definition( 62 106 widgets.select_widget(keys_indexed), 63 107 validation.list_item_by_index(values) 64 - |> validation.replace_error("Please select an option"), 65 - placeholder, 108 + |> validation.replace_error("is required"), 109 + stub, 110 + definition.make_simple_optional_parse(), 111 + option.None, 66 112 ) 67 113 } 68 114
+3 -7
formz_string/src/formz_string/simple.gleam
··· 23 23 <> { " name=\"" <> f.name <> "\"" } 24 24 <> { " value\"" <> f.value <> "\"" } 25 25 <> ">" 26 - formz.Element(f, widget) -> { 26 + formz.Element(f, make_widget) -> { 27 27 let label_el = 28 28 "<label for=\"" <> f.name <> "\">" <> f.label <> ": </label>" 29 29 let description_el = case string.is_empty(f.help_text) { ··· 32 32 " <span class=\"formz_help_text\">" <> f.help_text <> " </span>" 33 33 } 34 34 let widget_el = 35 - widget( 35 + make_widget( 36 36 f, 37 - widget.Args( 38 - id: f.name, 39 - labelled_by: widget.LabelledByLabelFor, 40 - described_by: widget.DescribedByNone, 41 - ), 37 + widget.args(widget.LabelledByLabelFor) |> widget.id(f.name), 42 38 ) 43 39 44 40 let errors_el = case f {
formz_string/src/formz_string/table.gleam

This is a binary file and will not be displayed.

+81 -29
formz_string/src/formz_string/widgets.gleam
··· 23 23 widget.LabelledByLabelFor -> "" 24 24 25 25 // we have the id of the element that labels this input 26 - widget.LabelledByElementWithId(id) -> " aria-labelledby=\"" <> id <> "\"" 26 + widget.LabelledByElementsWithIds(ids) -> 27 + " aria-labelledby=\"" <> string.join(ids, " ") <> "\"" 27 28 28 29 // we'll use the label value as the aria-label 29 30 widget.LabelledByFieldValue -> { ··· 39 40 } 40 41 } 41 42 43 + fn aria_describedby_attr(described_by: widget.DescribedBy) -> String { 44 + case described_by { 45 + // there should be a label with a for attribute pointing to this id 46 + widget.DescribedByNone -> "" 47 + 48 + // we have the id of the element that labels this input 49 + widget.DescribedByElementsWithIds(ids) -> 50 + " aria-describedby=\"" <> string.join(ids, " ") <> "\"" 51 + } 52 + } 53 + 54 + fn step_size_attr(step_size: String) -> String { 55 + case step_size { 56 + "" -> "" 57 + _ -> " step=\"" <> step_size <> "\"" 58 + } 59 + } 60 + 42 61 fn type_attr(type_: String) -> String { 43 62 " type=\"" <> type_ <> "\"" 44 63 } ··· 54 73 } 55 74 } 56 75 76 + fn disabled_attr(disabled: Bool) -> String { 77 + case disabled { 78 + True -> " disabled" 79 + False -> "" 80 + } 81 + } 82 + 83 + fn required_attr(required: Bool) -> String { 84 + case required { 85 + True -> " required" 86 + False -> "" 87 + } 88 + } 89 + 90 + fn checked_attr(value: String) -> String { 91 + case value { 92 + "on" -> " checked" 93 + _ -> "" 94 + } 95 + } 96 + 97 + // Create a checkbox widget (`<input type="checkbox">`). The checkbox is checked 98 + // if the value is "on" (the browser default). 57 99 pub fn checkbox_widget() { 58 - fn(field: Field, args: widget.Args) -> String { 59 - let checked_attr = case field.value { 60 - "on" -> " checked" 61 - _ -> "" 62 - } 100 + fn(field: Field, args: widget.Args) { 101 + do_input_widget(field |> field.set_raw_value(""), args, "checkbox", [ 102 + checked_attr(field.value), 103 + ]) 104 + } 105 + } 63 106 64 - "<input" 65 - <> type_attr("checkbox") 66 - <> name_attr(field.name) 67 - <> id_attr(args.id) 68 - <> checked_attr 69 - <> aria_label_attr(args.labelled_by, field.label) 70 - <> ">" 107 + pub fn number_widget(step_size: String) { 108 + fn(field: Field, args: widget.Args) { 109 + do_input_widget(field, args, "number", [step_size_attr(step_size)]) 71 110 } 72 111 } 73 112 74 113 pub fn password_widget() { 75 - fn(field: Field, args: widget.Args) -> String { 76 - "<input" 77 - <> type_attr("password") 78 - <> name_attr(field.name) 79 - <> id_attr(args.id) 80 - // <> value_attr(field.value) 81 - <> aria_label_attr(args.labelled_by, field.label) 82 - <> ">" 114 + fn(field: Field, args: widget.Args) { 115 + do_input_widget(field |> field.set_raw_value(""), args, "password", []) 83 116 } 84 117 } 85 118 86 119 pub fn text_like_widget(type_: String) { 87 - fn(field: Field, args: widget.Args) -> String { 88 - "<input" 89 - <> type_attr(type_) 90 - <> name_attr(field.name) 91 - <> id_attr(args.id) 92 - <> value_attr(field.value) 93 - <> aria_label_attr(args.labelled_by, field.label) 94 - <> ">" 120 + fn(field: Field, args: widget.Args) { 121 + do_input_widget(field, args, type_, []) 95 122 } 96 123 } 97 124 125 + fn do_input_widget( 126 + field: Field, 127 + args: widget.Args, 128 + type_: String, 129 + extra_attrs: List(String), 130 + ) { 131 + "<input" 132 + <> type_attr(type_) 133 + <> name_attr(field.name) 134 + <> id_attr(args.id) 135 + <> required_attr(field.required) 136 + <> disabled_attr(field.disabled) 137 + <> value_attr(field.value) 138 + <> aria_label_attr(args.labelled_by, field.label) 139 + <> aria_describedby_attr(args.described_by) 140 + <> extra_attrs |> string.join("") 141 + <> ">" 142 + } 143 + 98 144 pub fn textarea_widget() { 99 145 fn(field: Field, args: widget.Args) -> String { 100 146 // https://chriscoyier.net/2023/09/29/css-solves-auto-expanding-textareas-probably-eventually/ ··· 102 148 "<textarea" 103 149 <> name_attr(field.name) 104 150 <> id_attr(args.id) 151 + <> required_attr(field.required) 152 + <> disabled_attr(field.disabled) 105 153 <> aria_label_attr(args.labelled_by, field.label) 154 + <> aria_describedby_attr(args.described_by) 106 155 <> ">" 107 156 <> field.value 108 157 <> "</textarea>" ··· 138 187 "<select" 139 188 <> name_attr(field.name) 140 189 <> id_attr(args.id) 190 + <> required_attr(field.required) 191 + <> disabled_attr(field.disabled) 141 192 <> aria_label_attr(args.labelled_by, field.label) 193 + <> aria_describedby_attr(args.described_by) 142 194 <> ">" 143 195 } 144 196 // TODO make this placeholder option not selectable? with disabled selected attributes
+221
formz_string/test/formz_string/widgets_test.gleam
··· 1 + import birdie 2 + import formz/field 3 + import formz/widget 4 + import formz_string/widgets 5 + import gleeunit 6 + 7 + pub fn main() { 8 + gleeunit.main() 9 + } 10 + 11 + pub fn text_like_labelled_by_field_value_test() { 12 + widgets.text_like_widget("text")( 13 + field.Valid( 14 + name: "name", 15 + label: "Label", 16 + help_text: "", 17 + value: "hello", 18 + disabled: False, 19 + required: False, 20 + hidden: False, 21 + ), 22 + widget.Args("", widget.LabelledByFieldValue, widget.DescribedByNone), 23 + ) 24 + |> birdie.snap(title: "text like labelled by field value") 25 + } 26 + 27 + pub fn text_like_labelled_by_element_with_id_test() { 28 + widgets.text_like_widget("text")( 29 + field.Valid( 30 + name: "name", 31 + label: "Label", 32 + help_text: "", 33 + value: "hello", 34 + disabled: False, 35 + required: False, 36 + hidden: False, 37 + ), 38 + widget.Args( 39 + "", 40 + widget.LabelledByElementsWithIds(["id"]), 41 + widget.DescribedByNone, 42 + ), 43 + ) 44 + |> birdie.snap(title: "text like labelled by element with id") 45 + } 46 + 47 + pub fn text_like_labelled_by_label_for_test() { 48 + widgets.text_like_widget("text")( 49 + field.Valid( 50 + name: "name", 51 + label: "Label", 52 + help_text: "", 53 + value: "hello", 54 + disabled: False, 55 + required: False, 56 + hidden: False, 57 + ), 58 + widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 59 + ) 60 + |> birdie.snap(title: "text like labelled by label/for") 61 + } 62 + 63 + pub fn text_like_required_test() { 64 + widgets.text_like_widget("text")( 65 + field.Valid( 66 + name: "name", 67 + label: "Label", 68 + help_text: "", 69 + value: "hello", 70 + disabled: False, 71 + required: True, 72 + hidden: False, 73 + ), 74 + widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 75 + ) 76 + |> birdie.snap(title: "text like required") 77 + } 78 + 79 + pub fn text_like_disabled_test() { 80 + widgets.text_like_widget("text")( 81 + field.Valid( 82 + name: "name", 83 + label: "Label", 84 + help_text: "", 85 + value: "hello", 86 + disabled: True, 87 + required: False, 88 + hidden: False, 89 + ), 90 + widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 91 + ) 92 + |> birdie.snap(title: "text like disabled") 93 + } 94 + 95 + pub fn checkbox_checked_test() { 96 + widgets.checkbox_widget()( 97 + field.Valid( 98 + name: "name", 99 + label: "Label", 100 + help_text: "", 101 + value: "on", 102 + disabled: False, 103 + required: False, 104 + hidden: False, 105 + ), 106 + widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 107 + ) 108 + |> birdie.snap(title: "checkbox checked") 109 + } 110 + 111 + pub fn checkbox_unchecked_test() { 112 + widgets.checkbox_widget()( 113 + field.Valid( 114 + name: "name", 115 + label: "Label", 116 + help_text: "", 117 + value: "", 118 + disabled: False, 119 + required: False, 120 + hidden: False, 121 + ), 122 + widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 123 + ) 124 + |> birdie.snap(title: "checkbox unchecked") 125 + } 126 + 127 + pub fn password_test() { 128 + widgets.password_widget()( 129 + field.Valid( 130 + name: "name", 131 + label: "Label", 132 + help_text: "", 133 + value: "pass", 134 + disabled: False, 135 + required: False, 136 + hidden: False, 137 + ), 138 + widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 139 + ) 140 + |> birdie.snap(title: "password ignores input") 141 + } 142 + 143 + pub fn numeric_no_step_test() { 144 + widgets.number_widget("")( 145 + field.Valid( 146 + name: "name", 147 + label: "Label", 148 + help_text: "", 149 + value: "1", 150 + disabled: False, 151 + required: False, 152 + hidden: False, 153 + ), 154 + widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 155 + ) 156 + |> birdie.snap(title: "number input with no step") 157 + } 158 + 159 + pub fn numeric_step_test() { 160 + widgets.number_widget("0.1")( 161 + field.Valid( 162 + name: "name", 163 + label: "Label", 164 + help_text: "", 165 + value: "1.0", 166 + disabled: False, 167 + required: False, 168 + hidden: False, 169 + ), 170 + widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 171 + ) 172 + |> birdie.snap(title: "number input with step") 173 + } 174 + 175 + pub fn select_test() { 176 + widgets.select_widget([#("One", "0"), #("Two", "1"), #("Three", "2")])( 177 + field.Valid( 178 + name: "name", 179 + label: "Label", 180 + help_text: "", 181 + value: "", 182 + disabled: False, 183 + required: False, 184 + hidden: False, 185 + ), 186 + widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 187 + ) 188 + |> birdie.snap(title: "basic select") 189 + } 190 + 191 + pub fn select_selected_test() { 192 + widgets.select_widget([#("One", "0"), #("Two", "1"), #("Three", "2")])( 193 + field.Valid( 194 + name: "name", 195 + label: "Label", 196 + help_text: "", 197 + value: "1", 198 + disabled: False, 199 + required: False, 200 + hidden: False, 201 + ), 202 + widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 203 + ) 204 + |> birdie.snap(title: "select selected") 205 + } 206 + 207 + pub fn select_required_test() { 208 + widgets.select_widget([#("One", "0"), #("Two", "1"), #("Three", "2")])( 209 + field.Valid( 210 + name: "name", 211 + label: "Label", 212 + help_text: "", 213 + value: "", 214 + disabled: False, 215 + required: True, 216 + hidden: False, 217 + ), 218 + widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), 219 + ) 220 + |> birdie.snap(title: "required select") 221 + }