this repo has no description
4
fork

Configure Feed

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

swap input and field names, required and optional fields

reorg a bit

+416 -379
+87 -60
forma/src/forma/field.gleam
··· 1 - pub type Field(format) { 1 + import forma/input.{type Input, Input} 2 + import forma/validation 3 + import gleam/option 4 + import gleam/result 5 + import justin 6 + 7 + pub fn text_field(widget: fn(Input(format)) -> format) -> Field(format, String) { 8 + Field(input.empty_field(widget), "", validation.string) 9 + } 10 + 11 + pub fn email_field(widget: fn(Input(format)) -> format) -> Field(format, String) { 12 + Field(input.empty_field(widget), "", validation.email) 13 + } 14 + 15 + pub fn integer_field(widget: fn(Input(format)) -> format) -> Field(format, Int) { 16 + let transform = validation.int 17 + Field(input.empty_field(widget), 0, transform) 18 + } 19 + 20 + pub fn number_field(widget: fn(Input(format)) -> format) -> Field(format, Float) { 21 + let transform = validation.number 22 + Field(input.empty_field(widget), 0.0, transform) 23 + } 24 + 25 + pub type Field(format, output) { 2 26 Field( 3 - name: String, 4 - label: String, 5 - help_text: String, 6 - render: fn(Field(format)) -> format, 7 - value: String, 27 + input: Input(format), 28 + default: output, 29 + transform: fn(String) -> Result(output, String), 8 30 ) 9 - InvalidField( 10 - name: String, 11 - label: String, 12 - help_text: String, 13 - render: fn(Field(format)) -> format, 14 - value: String, 15 - error: String, 31 + } 32 + 33 + pub fn field( 34 + name: String, 35 + field: Field(format, output), 36 + ) -> Field(format, output) { 37 + Field( 38 + Input(name, justin.sentence_case(name), "", field.input.render, ""), 39 + field.default, 40 + field.transform, 16 41 ) 17 42 } 18 43 19 - pub fn empty_field(render: fn(Field(format)) -> format) -> Field(format) { 20 - Field("", "", "", render, "") 44 + pub fn full( 45 + name: String, 46 + label: String, 47 + help_text: String, 48 + field: Field(format, output), 49 + ) -> Field(format, output) { 50 + Field( 51 + Input(name, label, help_text, field.input.render, ""), 52 + field.default, 53 + field.transform, 54 + ) 21 55 } 22 56 23 - pub fn set_name(field: Field(format), name: String) -> Field(format) { 24 - case field { 25 - Field(_, label, help_text, render, value) -> 26 - Field(name, label, help_text, render, value) 27 - InvalidField(_, label, help_text, render, value, error) -> 28 - InvalidField(name, label, help_text, render, value, error) 29 - } 57 + pub fn name(field: Field(format, b), name: String) -> Field(format, b) { 58 + Field(..field, input: input.set_name(field.input, name)) 30 59 } 31 60 32 - pub fn set_label(field: Field(format), label: String) -> Field(format) { 33 - case field { 34 - Field(name, _, help_text, render, value) -> 35 - Field(name, label, help_text, render, value) 36 - InvalidField(name, _, help_text, render, value, error) -> 37 - InvalidField(name, label, help_text, render, value, error) 38 - } 61 + pub fn label(field: Field(format, b), label: String) -> Field(format, b) { 62 + Field(..field, input: input.set_label(field.input, label)) 39 63 } 40 64 41 - pub fn set_help_text(field: Field(format), help_text: String) -> Field(format) { 42 - case field { 43 - Field(name, label, _, render, value) -> 44 - Field(name, label, help_text, render, value) 45 - InvalidField(name, label, _, render, value, error) -> 46 - InvalidField(name, label, help_text, render, value, error) 47 - } 65 + pub fn help_text(field: Field(format, b), help_text: String) -> Field(format, b) { 66 + Field(..field, input: input.set_help_text(field.input, help_text)) 48 67 } 49 68 50 - pub fn set_render( 51 - field: Field(format), 52 - render: fn(Field(format)) -> format, 53 - ) -> Field(format) { 54 - case field { 55 - Field(name, label, help_text, _, value) -> 56 - Field(name, label, help_text, render, value) 57 - InvalidField(name, label, help_text, _, value, error) -> 58 - InvalidField(name, label, help_text, render, value, error) 59 - } 69 + pub fn optional(field: Field(format, b)) -> Field(format, option.Option(b)) { 70 + Field(input: field.input, default: option.None, transform: fn(str) { 71 + case str { 72 + "" -> Ok(option.None) 73 + _ -> result.map(field.transform(str), option.Some) 74 + } 75 + }) 60 76 } 61 77 62 - pub fn set_value(field: Field(format), value: String) -> Field(format) { 63 - case field { 64 - Field(name, label, help_text, render, _) -> 65 - Field(name, label, help_text, render, value) 66 - InvalidField(name, label, help_text, render, _, error) -> 67 - InvalidField(name, label, help_text, render, value, error) 68 - } 78 + pub fn validate( 79 + field: Field(format, b), 80 + next: fn(b) -> Result(b, String), 81 + ) -> Field(format, b) { 82 + let Field(field, default, previous_transform) = field 83 + 84 + Field(field, default, fn(str) { 85 + case previous_transform(str) { 86 + Ok(value) -> next(value) 87 + Error(error) -> Error(error) 88 + } 89 + }) 69 90 } 70 91 71 - pub fn set_error(field: Field(format), error: String) -> Field(format) { 72 - case field { 73 - Field(name, label, help_text, render, value) -> 74 - InvalidField(name, label, help_text, render, value, error) 75 - InvalidField(name, label, help_text, render, value, _) -> 76 - InvalidField(name, label, help_text, render, value, error) 77 - } 92 + pub fn transform( 93 + field: Field(format, b), 94 + next: fn(b) -> Result(c, String), 95 + default: c, 96 + ) -> Field(format, c) { 97 + let Field(field, _, previous_transform) = field 98 + 99 + Field(field, default, fn(str) { 100 + case previous_transform(str) { 101 + Ok(value) -> next(value) 102 + Error(error) -> Error(error) 103 + } 104 + }) 78 105 }
+23 -43
forma/src/forma/forma_pipes/forma.gleam
··· 6 6 // - list fields 7 7 // - form sets 8 8 // - csrf token 9 - // - required/option 10 9 11 - import forma/field.{type Field, Field} 12 - import forma/input.{type Input} 10 + import forma/field.{type Field} 11 + import forma/input.{type Input, Input} 13 12 import gleam/list 14 13 import gleam/option.{type Option, None, Some} 15 14 import gleam/result ··· 18 17 19 18 pub type NoDecoder 20 19 21 - pub type FieldsWithErrors(format) = 22 - List(Field(format)) 23 - 24 20 pub opaque type Form(format, output, decoder, has_decoder) { 25 21 Form( 26 - fields: List(Field(format)), 27 - parse_with: fn(List(Field(format)), decoder) -> 28 - Result(output, FieldsWithErrors(format)), 22 + fields: List(Input(format)), 23 + parse_with: fn(List(Input(format)), decoder) -> 24 + Result(output, List(Input(format))), 29 25 decoder: Option(decoder), 30 26 ) 31 27 } ··· 41 37 form_output, 42 38 has_decoder, 43 39 ), 44 - definition: Input(format, decoder_step_input), 40 + definition: Field(format, decoder_step_input), 45 41 ) -> Form(format, decoder_step_output, form_output, has_decoder) { 46 - let Form(fields, parse_with, decoder) = form 42 + let Form(inputs, parse_with, decoder) = form 47 43 48 44 // create new form with the new field and update the parse 49 45 // function to handle the new details from the type of the 50 46 // field 51 47 Form( 52 - fields: [definition.field, ..fields], 53 - parse_with: fn(fields, decoder: form_output) { 48 + fields: [definition.input, ..inputs], 49 + parse_with: fn(inputs, decoder: form_output) { 54 50 // can do let assert because we know there's at least one field since 55 51 // we just added one 56 - let assert [field, ..rest] = fields 57 - case parse_with(rest, decoder), definition.transform(field.value) { 52 + let assert [input, ..rest] = inputs 53 + case parse_with(rest, decoder), definition.transform(input.value) { 58 54 // the form we've already parsed has no errors and the field 59 55 // we just parsed has no errors. so we can move on to the next 60 56 Ok(next), Ok(value) -> Ok(next(value)) 61 57 62 58 // the form already has errors even though this one succeeded. 63 59 // so add this to the list and stop anymore parsing 64 - Error(fields), Ok(_value) -> Error([field, ..fields]) 60 + Error(inputs), Ok(_value) -> Error([input, ..inputs]) 65 61 66 62 // form was good so far, but this field errored, so need to 67 63 // mark this field as invalid and return all the fields we've got 68 64 // so far 69 - Ok(_), Error(error) -> Error([field.set_error(field, error), ..rest]) 65 + Ok(_), Error(error) -> Error([input.set_error(input, error), ..rest]) 70 66 71 67 // form already has errors and this field errored, so add this field 72 68 // to the list 73 69 Error(fields), Error(error) -> 74 - Error([field.set_error(field, error), ..fields]) 70 + Error([input.set_error(input, error), ..fields]) 75 71 } 76 72 }, 77 73 decoder:, ··· 80 76 81 77 pub fn data( 82 78 form: Form(a, b, format, has_decoder), 83 - input: List(#(String, String)), 79 + field: List(#(String, String)), 84 80 ) -> Form(a, b, format, has_decoder) { 85 81 case form { 86 82 Form(fields, parse_with, decoder) -> { ··· 88 84 // we always prepend fields, so reverse to get correct order 89 85 // TODO I think we're going to make it so order doesn't matter 90 86 |> list.reverse 91 - |> do_add_input_data(input, []) 87 + |> do_add_input_data(field, []) 92 88 |> Form(parse_with, decoder) 93 89 } 94 90 // FormWithErrors(..) -> form ··· 96 92 } 97 93 98 94 fn do_add_input_data( 99 - fields: List(Field(format)), 95 + fields: List(Input(format)), 100 96 data: List(#(String, String)), 101 - acc: List(Field(format)), 97 + acc: List(Input(format)), 102 98 ) { 103 99 case fields, data { 104 100 // no more fields, we've return all the fields with data we have accumulated ··· 106 102 // no more data! return all the fields we have left plus the ones we accumulated 107 103 _, [] -> list.append(fields, acc) 108 104 // we have a field and data, and the names match. update field to have data 109 - [Field(name: field_name, ..) as field, ..fields_rest], 105 + [Input(name: field_name, ..) as field, ..fields_rest], 110 106 [#(data_name, value), ..data_rest] 111 107 if field_name == data_name 112 108 -> 113 109 do_add_input_data(fields_rest, data_rest, [ 114 - field.set_value(field, value), 110 + input.set_value(field, value), 115 111 ..acc 116 112 ]) 117 113 // at this point we still have fields and data left, but the first ··· 151 147 parse(form) |> result.try(fun(_, form)) 152 148 } 153 149 154 - pub fn get_fields(form: Form(format, a, b, has_decoder)) -> List(Field(format)) { 150 + pub fn get_inputs(form: Form(format, a, b, has_decoder)) -> List(Input(format)) { 155 151 form.fields |> list.reverse 156 152 } 157 153 158 - pub fn set_field_error( 154 + pub fn update_input( 159 155 form: Form(format, output, decoder, has_decoder), 160 156 name: String, 161 - error: String, 162 - ) -> Form(format, output, decoder, has_decoder) { 163 - let updated = 164 - form.fields 165 - |> list.map(fn(field) { 166 - case field.name == name { 167 - True -> field.set_error(field, error) 168 - False -> field 169 - } 170 - }) 171 - Form(updated, form.parse_with, form.decoder) 172 - } 173 - 174 - pub fn field_update( 175 - form: Form(format, output, decoder, has_decoder), 176 - name: String, 177 - fun: fn(Field(format)) -> Field(format), 157 + fun: fn(Input(format)) -> Input(format), 178 158 ) -> Form(format, output, decoder, has_decoder) { 179 159 form.fields 180 160 |> list.map(fn(field) {
+49 -25
forma/src/forma/forma_use/forma.gleam
··· 1 1 /////// field related functions 2 2 3 - import forma/field.{type Field, Field} 4 - import forma/input.{type Input} 3 + import forma/field.{type Field} 4 + import forma/input.{type Input, Input} 5 5 import gleam/dict 6 6 import gleam/list 7 7 import gleam/result 8 8 9 9 pub opaque type Form(format, output) { 10 10 Form( 11 - fields: List(Field(format)), 12 - parse: fn(List(Field(format))) -> Result(output, List(Field(format))), 11 + inputs: List(Input(format)), 12 + parse: fn(List(Input(format))) -> Result(output, List(Input(format))), 13 13 ) 14 14 } 15 15 16 16 pub fn with( 17 - definition: Input(format, input_output), 17 + field: Field(format, input_output), 18 18 fun: fn(input_output) -> Form(format, form_output), 19 19 ) -> Form(format, form_output) { 20 - let field = definition.field 21 - let next = fun(definition.default) 22 - Form([field, ..next.fields], parse: fn(fields) { 23 - // pull out the latest version of this field to get latest data 24 - let assert [field, ..next_fields] = fields 20 + // we pass in our default value, and we're going to throw away the 21 + // decoded result here, we just care about pulling out the fields 22 + // from the form. 23 + let next = fun(field.default) 24 + 25 + // prepend the new input to the inputs from the form we got in the 26 + // previous step. 27 + let updated_inputs = [field.input, ..next.inputs] 25 28 26 - let input_output = definition.transform(field.value) 29 + // now create the parse function. parse function accepts most recent 30 + // version of input list, since data can be added to it. the list 31 + // above we just needed for the initial setup. 32 + let parse = fn(inputs: List(Input(format))) { 33 + // pull out the latest version of this field to get latest input data 34 + let assert [input, ..next_inputs] = inputs 27 35 28 - let next_form = fun(input_output |> result.unwrap(definition.default)) 29 - let form_output = next_form.parse(next_fields) 36 + // transform the input data using the transform/validate/decode/etc function 37 + let input_output = field.transform(input.value) 30 38 39 + // pass our transformed input data to the next function/form. if 40 + // we errored we still do this with our default so we can continue 41 + // processing all the fields in the form. we will return a form 42 + // with an error, so if we're on the error track we'll throw away 43 + // the "output" made with this and just keep the errors. 44 + let next_form = fun(input_output |> result.unwrap(field.default)) 45 + let form_output = next_form.parse(next_inputs) 46 + 47 + // ok, check which track we're on 31 48 case form_output, input_output { 32 49 // everything is good! pass along the output 33 50 Ok(_), Ok(_) -> form_output 34 51 35 52 // form has errors, but this field was good, so add it to the list 36 53 // of fields as is. 37 - Error(fields), Ok(_value) -> Error([field, ..fields]) 54 + Error(inputs), Ok(_value) -> Error([input, ..inputs]) 38 55 39 56 // form was good so far, but this field errored, so need to 40 57 // mark this field as invalid and return all the fields we've got 41 58 // so far 42 59 Ok(_), Error(error) -> 43 - field |> field.set_error(error) |> list.prepend(next_fields, _) |> Error 60 + input 61 + |> input.set_error(error) 62 + |> list.prepend(next_inputs, _) 63 + |> Error 44 64 45 65 // form already has errors and this field errored, so add this field 46 66 // to the list of errors 47 67 Error(fields), Error(error) -> 48 - field |> field.set_error(error) |> list.prepend(fields, _) |> Error 68 + input 69 + |> input.set_error(error) 70 + |> list.prepend(fields, _) 71 + |> Error 49 72 } 50 - }) 73 + } 74 + Form(updated_inputs, parse: parse) 51 75 } 52 76 53 77 pub fn data( 54 78 form: Form(format, output), 55 - input: List(#(String, String)), 79 + input_data: List(#(String, String)), 56 80 ) -> Form(format, output) { 57 - let data = dict.from_list(input) 81 + let data = dict.from_list(input_data) 58 82 let Form(fields, parse) = form 59 83 fields 60 84 |> list.map(fn(field) { 61 85 case dict.get(data, field.name) { 62 86 Ok(value) -> 63 - Field( 87 + Input( 64 88 name: field.name, 65 89 label: field.label, 66 90 help_text: field.help_text, ··· 94 118 parse(form) |> result.try(fun(_, form)) 95 119 } 96 120 97 - pub fn get_fields(form: Form(format, ouput)) -> List(Field(format)) { 98 - form.fields 121 + pub fn get_inputs(form: Form(format, ouput)) -> List(Input(format)) { 122 + form.inputs 99 123 } 100 124 101 - pub fn field_update( 125 + pub fn update_input( 102 126 form: Form(format, output), 103 127 name: String, 104 - fun: fn(Field(format)) -> Field(format), 128 + fun: fn(Input(format)) -> Input(format), 105 129 ) -> Form(format, output) { 106 - form.fields 130 + form.inputs 107 131 |> list.map(fn(field) { 108 132 case field.name == name { 109 133 True -> fun(field)
-18
forma/src/forma/generator/string_input.gleam
··· 1 - import forma/generator/string_widget 2 - import forma/input 3 - 4 - pub fn text_input() { 5 - input.text_input(string_widget.text_widget) 6 - } 7 - 8 - pub fn email_input() { 9 - input.email_input(string_widget.text_widget) 10 - } 11 - 12 - pub fn integer_input() { 13 - input.integer_input(string_widget.text_widget) 14 - } 15 - 16 - pub fn number_input() { 17 - input.number_input(string_widget.text_widget) 18 - }
-28
forma/src/forma/generator/string_widget.gleam
··· 1 - import forma/field.{type Field} 2 - 3 - pub fn checkbox_widget(_f, _env) -> String { 4 - "<input type=\"checkbox\">" 5 - } 6 - 7 - pub fn password_widget(_f, _env) -> String { 8 - "<input type=\"password\">" 9 - } 10 - 11 - pub fn text_widget(f: Field(String)) -> String { 12 - let placeholder = "" 13 - 14 - "<input name=\"" 15 - <> f.name 16 - <> "\" placeholder=\"" 17 - <> placeholder 18 - <> "\" type=\"text\" value=\"" 19 - <> f.value 20 - <> "\">" 21 - } 22 - 23 - // https://chriscoyier.net/2023/09/29/css-solves-auto-expanding-textareas-probably-eventually/ 24 - // https://til.simonwillison.net/css/resizing-textarea 25 - 26 - pub fn textarea_widget(_f, _env) -> String { 27 - "<textarea></textarea>" 28 - }
+61 -77
forma/src/forma/input.gleam
··· 1 - import forma/field.{type Field, Field} 2 - import forma/validation 3 - import justin 4 - 5 - pub fn text_input(widget: fn(Field(format)) -> format) -> Input(format, String) { 6 - Input(field.empty_field(widget), "", validation.string) 7 - } 8 - 9 - pub fn email_input(widget: fn(Field(format)) -> format) -> Input(format, String) { 10 - Input(field.empty_field(widget), "", validation.email) 11 - } 12 - 13 - pub fn integer_input(widget: fn(Field(format)) -> format) -> Input(format, Int) { 14 - let transform = validation.trim |> validation.and(validation.int) 15 - Input(field.empty_field(widget), 0, transform) 16 - } 17 - 18 - pub fn number_input(widget: fn(Field(format)) -> format) -> Input(format, Float) { 19 - let transform = validation.trim |> validation.and(validation.number) 20 - Input(field.empty_field(widget), 0.0, transform) 21 - } 22 - 23 - pub type Input(format, output) { 1 + pub type Input(format) { 24 2 Input( 25 - field: Field(format), 26 - default: output, 27 - transform: fn(String) -> Result(output, String), 3 + name: String, 4 + label: String, 5 + help_text: String, 6 + render: fn(Input(format)) -> format, 7 + value: String, 28 8 ) 29 - } 30 - 31 - pub fn input( 32 - name: String, 33 - input: Input(format, output), 34 - ) -> Input(format, output) { 35 - Input( 36 - Field(name, justin.sentence_case(name), "", input.field.render, ""), 37 - input.default, 38 - input.transform, 9 + InvalidInput( 10 + name: String, 11 + label: String, 12 + help_text: String, 13 + render: fn(Input(format)) -> format, 14 + value: String, 15 + error: String, 39 16 ) 40 17 } 41 18 42 - pub fn full( 43 - name: String, 44 - label: String, 45 - help_text: String, 46 - input: Input(format, output), 47 - ) -> Input(format, output) { 48 - Input( 49 - Field(name, label, help_text, input.field.render, ""), 50 - input.default, 51 - input.transform, 52 - ) 19 + pub fn empty_field(render: fn(Input(format)) -> format) -> Input(format) { 20 + Input("", "", "", render, "") 53 21 } 54 22 55 - pub fn name(input: Input(format, b), name: String) -> Input(format, b) { 56 - Input(..input, field: field.set_name(input.field, name)) 23 + pub fn set_name(field: Input(format), name: String) -> Input(format) { 24 + case field { 25 + Input(_, label, help_text, render, value) -> 26 + Input(name, label, help_text, render, value) 27 + InvalidInput(_, label, help_text, render, value, error) -> 28 + InvalidInput(name, label, help_text, render, value, error) 29 + } 57 30 } 58 31 59 - pub fn label(input: Input(format, b), label: String) -> Input(format, b) { 60 - Input(..input, field: field.set_label(input.field, label)) 32 + pub fn set_label(field: Input(format), label: String) -> Input(format) { 33 + case field { 34 + Input(name, _, help_text, render, value) -> 35 + Input(name, label, help_text, render, value) 36 + InvalidInput(name, _, help_text, render, value, error) -> 37 + InvalidInput(name, label, help_text, render, value, error) 38 + } 61 39 } 62 40 63 - pub fn help_text(input: Input(format, b), help_text: String) -> Input(format, b) { 64 - Input(..input, field: field.set_help_text(input.field, help_text)) 41 + pub fn set_help_text(field: Input(format), help_text: String) -> Input(format) { 42 + case field { 43 + Input(name, label, _, render, value) -> 44 + Input(name, label, help_text, render, value) 45 + InvalidInput(name, label, _, render, value, error) -> 46 + InvalidInput(name, label, help_text, render, value, error) 47 + } 65 48 } 66 49 67 - pub fn validate( 68 - input: Input(format, b), 69 - next: fn(b) -> Result(b, String), 70 - ) -> Input(format, b) { 71 - let Input(field, default, previous_transform) = input 50 + pub fn set_render( 51 + field: Input(format), 52 + render: fn(Input(format)) -> format, 53 + ) -> Input(format) { 54 + case field { 55 + Input(name, label, help_text, _, value) -> 56 + Input(name, label, help_text, render, value) 57 + InvalidInput(name, label, help_text, _, value, error) -> 58 + InvalidInput(name, label, help_text, render, value, error) 59 + } 60 + } 72 61 73 - Input(field, default, fn(str) { 74 - case previous_transform(str) { 75 - Ok(value) -> next(value) 76 - Error(error) -> Error(error) 77 - } 78 - }) 62 + pub fn set_value(field: Input(format), value: String) -> Input(format) { 63 + case field { 64 + Input(name, label, help_text, render, _) -> 65 + Input(name, label, help_text, render, value) 66 + InvalidInput(name, label, help_text, render, _, error) -> 67 + InvalidInput(name, label, help_text, render, value, error) 68 + } 79 69 } 80 70 81 - pub fn transform( 82 - input: Input(format, b), 83 - next: fn(b) -> Result(c, String), 84 - default: c, 85 - ) -> Input(format, c) { 86 - let Input(field, _, previous_transform) = input 87 - 88 - Input(field, default, fn(str) { 89 - case previous_transform(str) { 90 - Ok(value) -> next(value) 91 - Error(error) -> Error(error) 92 - } 93 - }) 71 + pub fn set_error(field: Input(format), error: String) -> Input(format) { 72 + case field { 73 + Input(name, label, help_text, render, value) -> 74 + InvalidInput(name, label, help_text, render, value, error) 75 + InvalidInput(name, label, help_text, render, value, _) -> 76 + InvalidInput(name, label, help_text, render, value, error) 77 + } 94 78 }
+44
forma/src/forma/string_fields.gleam
··· 1 + import forma/field 2 + import forma/input.{type Input} 3 + 4 + pub fn checkbox_widget(_f, _env) -> String { 5 + "<input type=\"checkbox\">" 6 + } 7 + 8 + pub fn password_widget(_f, _env) -> String { 9 + "<input type=\"password\">" 10 + } 11 + 12 + pub fn text_widget(f: Input(String)) -> String { 13 + let placeholder = "" 14 + 15 + "<input name=\"" 16 + <> f.name 17 + <> "\" placeholder=\"" 18 + <> placeholder 19 + <> "\" type=\"text\" value=\"" 20 + <> f.value 21 + <> "\">" 22 + } 23 + 24 + pub fn textarea_widget(_f, _env) -> String { 25 + // https://chriscoyier.net/2023/09/29/css-solves-auto-expanding-textareas-probably-eventually/ 26 + // https://til.simonwillison.net/css/resizing-textarea 27 + "<textarea></textarea>" 28 + } 29 + 30 + pub fn text_field() { 31 + field.text_field(text_widget) 32 + } 33 + 34 + pub fn email_field() { 35 + field.email_field(text_widget) 36 + } 37 + 38 + pub fn integer_field() { 39 + field.integer_field(text_widget) 40 + } 41 + 42 + pub fn number_field() { 43 + field.number_field(text_widget) 44 + }
+14 -8
forma/src/forma/validation.gleam
··· 4 4 import gleam/string 5 5 6 6 pub fn string(str: String) -> Result(String, String) { 7 - Ok(str) 7 + case string.trim(str) { 8 + "" -> Error("Must not be empty") 9 + trimmed -> Ok(trimmed) 10 + } 8 11 } 9 12 10 13 pub fn email(input: String) -> Result(String, String) { ··· 25 28 } 26 29 } 27 30 28 - pub fn trim(str: String) -> Result(String, String) { 29 - Ok(string.trim(str)) 30 - } 31 - 32 31 pub fn int(str: String) -> Result(Int, String) { 33 - str |> int.parse |> result.replace_error("Must be a whole number") 32 + str 33 + |> string.trim 34 + |> int.parse 35 + |> result.replace_error("Must be a whole number") 34 36 } 35 37 36 38 pub fn number(str: String) -> Result(Float, String) { 37 - // TODO accept whole numbers too 38 - str |> float.parse |> result.replace_error("Must be a number") 39 + let str = string.trim(str) 40 + case float.parse(str) { 41 + Ok(value) -> Ok(value) 42 + Error(_) -> int.parse(str) |> result.map(int.to_float) 43 + } 44 + |> result.replace_error("Must be a number") 39 45 } 40 46 41 47 pub fn and(
+38 -71
forma/test/forma/forma_pipes/forma_test.gleam
··· 1 - import forma/field 1 + import forma/field.{field} 2 2 import forma/forma_pipes/forma 3 - import forma/generator/string_input 4 - import forma/input.{input} 3 + import forma/input 4 + import forma/string_fields 5 5 import gleam/list 6 6 import gleeunit 7 7 import gleeunit/should ··· 12 12 13 13 pub fn empty_form_test() { 14 14 forma.new() 15 - |> forma.get_fields 15 + |> forma.get_inputs 16 16 |> list.length 17 17 |> should.equal(0) 18 18 } ··· 33 33 34 34 pub fn parse_single_field_form_test() { 35 35 forma.new() 36 - |> forma.add(input("first", string_input.text_input())) 36 + |> forma.add(field("first", string_fields.text_field())) 37 37 |> forma.data([#("first", "world")]) 38 38 |> forma.decodes(fn(str) { "hello " <> str }) 39 39 |> forma.parse 40 40 |> should.equal(Ok("hello world")) 41 - 42 - forma.new() 43 - |> forma.add(input("first", string_input.text_input())) 44 - |> forma.data([]) 45 - |> forma.decodes(fn(str) { "hello " <> str }) 46 - |> forma.parse 47 - |> should.equal(Ok("hello ")) 48 41 } 49 42 50 43 pub fn parse_double_field_form_test() { 51 44 forma.new() 52 - |> forma.add(input("first", string_input.text_input())) 53 - |> forma.add(input("second", string_input.text_input())) 45 + |> forma.add(field("first", string_fields.text_field())) 46 + |> forma.add(field("second", string_fields.text_field())) 54 47 |> forma.data([#("first", "hello"), #("second", "world")]) 55 48 |> forma.decodes(fn(a) { fn(b) { a <> " " <> b } }) 56 49 |> forma.parse 57 50 |> should.equal(Ok("hello world")) 58 - 59 - forma.new() 60 - |> forma.add(input("first", string_input.text_input())) 61 - |> forma.add(input("second", string_input.text_input())) 62 - |> forma.data([#("first", "hello")]) 63 - |> forma.decodes(fn(a) { fn(b) { a <> " " <> b } }) 64 - |> forma.parse 65 - |> should.equal(Ok("hello ")) 66 51 } 67 52 68 53 pub fn parse_double_field_form_extra_data_test() { 69 54 forma.new() 70 - |> forma.add(input("first", string_input.text_input())) 71 - |> forma.add(input("second", string_input.text_input())) 55 + |> forma.add(field("first", string_fields.text_field())) 56 + |> forma.add(field("second", string_fields.text_field())) 72 57 |> forma.data([#("first", "1"), #("second", "2")]) 73 58 |> forma.decodes(fn(a) { fn(b) { a <> " " <> b } }) 74 59 |> forma.parse 75 60 |> should.equal(Ok("1 2")) 76 61 77 62 forma.new() 78 - |> forma.add(input("first", string_input.text_input())) 79 - |> forma.add(input("second", string_input.text_input())) 63 + |> forma.add(field("first", string_fields.text_field())) 64 + |> forma.add(field("second", string_fields.text_field())) 80 65 |> forma.data([#("first", "1"), #("second", "2"), #("second", "3")]) 81 66 |> forma.decodes(fn(a) { fn(b) { a <> " " <> b } }) 82 67 |> forma.parse 83 68 |> should.equal(Ok("1 2")) 84 - } 85 - 86 - pub fn parse_double_field_form_missing_data_test() { 87 - forma.new() 88 - |> forma.add(input("first", string_input.text_input())) 89 - |> forma.add(input("second", string_input.text_input())) 90 - |> forma.data([#("second", "1")]) 91 - |> forma.decodes(fn(a) { fn(b) { a <> " " <> b } }) 92 - |> forma.parse 93 - |> should.equal(Ok(" 1")) 94 - 95 - forma.new() 96 - |> forma.add(input("first", string_input.text_input())) 97 - |> forma.add(input("second", string_input.text_input())) 98 - |> forma.data([#("first", "1"), #("first", "2")]) 99 - |> forma.decodes(fn(a) { fn(b) { a <> " " <> b } }) 100 - |> forma.parse 101 - |> should.equal(Ok("1 ")) 102 69 } 103 70 104 71 pub fn integer_field_test() { 105 72 forma.new() 106 - |> forma.add(input("first", string_input.integer_input())) 73 + |> forma.add(field("first", string_fields.integer_field())) 107 74 |> forma.data([#("first", " 1 ")]) 108 75 |> forma.decodes(fn(i) { i }) 109 76 |> forma.parse ··· 113 80 pub fn can_decodes_in_any_order_test() { 114 81 forma.new() 115 82 |> forma.decodes(fn(str) { "hello " <> str }) 116 - |> forma.add(input("first", string_input.text_input())) 83 + |> forma.add(field("first", string_fields.text_field())) 117 84 |> forma.data([#("first", "world")]) 118 85 |> forma.parse 119 86 |> should.equal(Ok("hello world")) 120 87 121 88 forma.new() 122 - |> forma.add(input("first", string_input.text_input())) 89 + |> forma.add(field("first", string_fields.text_field())) 123 90 |> forma.data([#("first", "world")]) 124 91 |> forma.decodes(fn(str) { "hello " <> str }) 125 92 |> forma.parse ··· 129 96 pub fn parse_single_field_form_with_error_test() { 130 97 let assert Error(f) = 131 98 forma.new() 132 - |> forma.add(input("first", string_input.integer_input())) 99 + |> forma.add(field("first", string_fields.integer_field())) 133 100 |> forma.data([#("first", "world")]) 134 101 |> forma.decodes(fn(_) { 1 }) 135 102 |> forma.parse 136 103 137 - let assert [field] = forma.get_fields(f) 104 + let assert [field] = forma.get_inputs(f) 138 105 field |> should_be_field_with_error("Must be a whole number") 139 106 } 140 107 141 108 pub fn parse_double_field_form_with_error_test() { 142 109 let form = 143 110 forma.new() 144 - |> forma.add(input("a", string_input.integer_input())) 145 - |> forma.add(input("b", string_input.integer_input())) 111 + |> forma.add(field("a", string_fields.integer_field())) 112 + |> forma.add(field("b", string_fields.integer_field())) 146 113 |> forma.decodes(fn(_) { fn(_) { 1 } }) 147 114 148 115 let assert Error(f) = ··· 150 117 |> forma.data([#("a", "not a number"), #("b", "2")]) 151 118 |> forma.parse 152 119 153 - let assert [fielda, fieldb] = forma.get_fields(f) 120 + let assert [fielda, fieldb] = forma.get_inputs(f) 154 121 fielda |> should_be_field_with_error("Must be a whole number") 155 122 fieldb |> should_be_field_no_error 156 123 ··· 159 126 |> forma.data([#("a", "1"), #("b", "string")]) 160 127 |> forma.parse 161 128 162 - let assert [fielda, fieldb] = forma.get_fields(f) 129 + let assert [fielda, fieldb] = forma.get_inputs(f) 163 130 fielda |> should_be_field_no_error 164 131 fieldb |> should_be_field_with_error("Must be a whole number") 165 132 ··· 168 135 |> forma.data([#("a", "string"), #("b", "string")]) 169 136 |> forma.parse 170 137 171 - let assert [fielda, fieldb] = forma.get_fields(f) 138 + let assert [fielda, fieldb] = forma.get_inputs(f) 172 139 fielda |> should_be_field_with_error("Must be a whole number") 173 140 fieldb |> should_be_field_with_error("Must be a whole number") 174 141 } ··· 176 143 pub fn parse_triple_field_form_with_error_test() { 177 144 let form = 178 145 forma.new() 179 - |> forma.add(input("a", string_input.integer_input())) 180 - |> forma.add(input("b", string_input.integer_input())) 181 - |> forma.add(input("c", string_input.integer_input())) 146 + |> forma.add(field("a", string_fields.integer_field())) 147 + |> forma.add(field("b", string_fields.integer_field())) 148 + |> forma.add(field("c", string_fields.integer_field())) 182 149 |> forma.decodes(fn(_) { fn(_) { fn(_) { 1 } } }) 183 150 184 151 let assert Error(f) = 185 152 form 186 153 |> forma.data([#("a", "1"), #("b", "2"), #("c", "string")]) 187 154 |> forma.parse 188 - let assert [fielda, fieldb, fieldc] = forma.get_fields(f) 155 + let assert [fielda, fieldb, fieldc] = forma.get_inputs(f) 189 156 fielda |> should_be_field_no_error 190 157 fieldb |> should_be_field_no_error 191 158 fieldc |> should_be_field_with_error("Must be a whole number") ··· 194 161 form 195 162 |> forma.data([#("a", "1"), #("b", "string"), #("c", "string")]) 196 163 |> forma.parse 197 - let assert [fielda, fieldb, fieldc] = forma.get_fields(f) 164 + let assert [fielda, fieldb, fieldc] = forma.get_inputs(f) 198 165 fielda |> should_be_field_no_error 199 166 fieldb |> should_be_field_with_error("Must be a whole number") 200 167 fieldc |> should_be_field_with_error("Must be a whole number") ··· 203 170 form 204 171 |> forma.data([#("a", "1"), #("b", "string"), #("c", "3")]) 205 172 |> forma.parse 206 - let assert [fielda, fieldb, fieldc] = forma.get_fields(f) 173 + let assert [fielda, fieldb, fieldc] = forma.get_inputs(f) 207 174 fielda |> should_be_field_no_error 208 175 fieldb |> should_be_field_with_error("Must be a whole number") 209 176 fieldc |> should_be_field_no_error ··· 212 179 form 213 180 |> forma.data([#("a", "string"), #("b", "string"), #("c", "3")]) 214 181 |> forma.parse 215 - let assert [fielda, fieldb, fieldc] = forma.get_fields(f) 182 + let assert [fielda, fieldb, fieldc] = forma.get_inputs(f) 216 183 fielda |> should_be_field_with_error("Must be a whole number") 217 184 fieldb |> should_be_field_with_error("Must be a whole number") 218 185 fieldc |> should_be_field_no_error ··· 221 188 form 222 189 |> forma.data([#("a", "string"), #("b", "2"), #("c", "3")]) 223 190 |> forma.parse 224 - let assert [fielda, fieldb, fieldc] = forma.get_fields(f) 191 + let assert [fielda, fieldb, fieldc] = forma.get_inputs(f) 225 192 fielda |> should_be_field_with_error("Must be a whole number") 226 193 fieldb |> should_be_field_no_error 227 194 fieldc |> should_be_field_no_error 228 195 } 229 196 230 - fn should_be_field_no_error(field: field.Field(String)) { 197 + fn should_be_field_no_error(field: input.Input(String)) { 231 198 should.equal( 232 199 field, 233 - field.Field( 200 + input.Input( 234 201 name: field.name, 235 202 label: field.label, 236 203 help_text: field.help_text, ··· 240 207 ) 241 208 } 242 209 243 - fn should_be_field_with_error(field: field.Field(String), str: String) { 210 + fn should_be_field_with_error(field: input.Input(String), str: String) { 244 211 should.equal( 245 212 field, 246 - field.InvalidField( 213 + input.InvalidInput( 247 214 name: field.name, 248 215 label: field.label, 249 216 help_text: field.help_text, ··· 257 224 pub fn parse_and_try_test() { 258 225 let f = 259 226 forma.new() 260 - |> forma.add(input("a", string_input.integer_input())) 261 - |> forma.add(input("b", string_input.integer_input())) 262 - |> forma.add(input("c", string_input.integer_input())) 227 + |> forma.add(field("a", string_fields.integer_field())) 228 + |> forma.add(field("b", string_fields.integer_field())) 229 + |> forma.add(field("c", string_fields.integer_field())) 263 230 |> forma.decodes(fn(_) { fn(_) { fn(_) { 1 } } }) 264 231 |> forma.data([#("a", "1"), #("b", "2"), #("c", "3")]) 265 232 ··· 278 245 // can change field 279 246 let assert Error(form) = 280 247 forma.parse_and_try(f, fn(_, form) { 281 - Error(forma.set_field_error(form, "a", "woops")) 248 + Error(forma.update_input(form, "a", input.set_error(_, "woops"))) 282 249 }) 283 - let assert [fielda, fieldb, fieldc] = forma.get_fields(form) 250 + let assert [fielda, fieldb, fieldc] = forma.get_inputs(form) 284 251 fielda |> should_be_field_with_error("woops") 285 252 fieldb |> should_be_field_no_error 286 253 fieldc |> should_be_field_no_error
+70 -49
forma/test/forma/forma_use/forma_test.gleam
··· 1 - import forma/field 1 + import forma/field.{field} 2 2 import forma/forma_use/forma 3 - import forma/generator/string_input 4 - import forma/input.{input} 3 + import forma/input 4 + import forma/string_fields 5 5 import forma/validation 6 + import gleam/option 6 7 import gleeunit 7 8 import gleeunit/should 8 9 9 - fn should_be_field_no_error(field: field.Field(String)) { 10 + fn should_be_field_no_error(field: input.Input(String)) { 10 11 should.equal( 11 12 field, 12 - field.Field( 13 + input.Input( 13 14 name: field.name, 14 15 label: field.label, 15 16 help_text: field.help_text, ··· 19 20 ) 20 21 } 21 22 22 - fn should_be_field_with_error(field: field.Field(String), str: String) { 23 + fn should_be_field_with_error(field: input.Input(String), str: String) { 23 24 should.equal( 24 25 field, 25 - field.InvalidField( 26 + input.InvalidInput( 26 27 name: field.name, 27 28 label: field.label, 28 29 help_text: field.help_text, ··· 49 50 } 50 51 51 52 fn one_field_form() { 52 - use a <- forma.with(input("a", string_input.text_input())) 53 + use a <- forma.with(field("a", string_fields.text_field())) 53 54 forma.create_form("hello " <> a) 54 55 } 55 56 56 57 fn two_field_form() { 57 58 { 58 - use a <- forma.with(input("a", string_input.text_input())) 59 - use b <- forma.with(input("b", string_input.text_input())) 59 + use a <- forma.with(field("a", string_fields.text_field())) 60 + use b <- forma.with(field("b", string_fields.text_field())) 60 61 61 62 forma.create_form(#(a, b)) 62 63 } ··· 64 65 65 66 fn three_field_form() { 66 67 use a <- forma.with( 67 - input("a", string_input.text_input()) 68 - |> input.validate(validation.must_be_longer_than(3)), 68 + field("x", string_fields.text_field()) 69 + |> field.name("a") 70 + |> field.label("A") 71 + |> field.validate(validation.must_be_longer_than(3)), 69 72 ) 70 - use b <- forma.with(input("b", string_input.integer_input())) 71 - use c <- forma.with(input.full("c", "C", "help!", string_input.number_input())) 73 + use b <- forma.with(field("b", string_fields.integer_field())) 74 + use c <- forma.with(field.full( 75 + "c", 76 + "C", 77 + "help!", 78 + string_fields.number_field(), 79 + )) 72 80 73 81 forma.create_form(#(a, b, c)) 74 82 } 75 83 76 84 pub fn empty_form_test() { 77 85 empty_form(1) 78 - |> forma.get_fields 86 + |> forma.get_inputs 79 87 |> should.equal([]) 80 88 } 81 89 ··· 98 106 |> should.equal(Ok("hello world")) 99 107 100 108 one_field_form() 101 - |> forma.data([]) 102 - |> forma.parse 103 - |> should.equal(Ok("hello ")) 104 - 105 - one_field_form() 106 109 |> forma.data([#("a", "ignored"), #("a", "world")]) 107 110 |> forma.parse 108 111 |> should.equal(Ok("hello world")) ··· 114 117 |> forma.parse 115 118 |> should.equal(Ok(#("hello", "world"))) 116 119 117 - // missing second 118 - two_field_form() 119 - |> forma.data([#("a", "hello")]) 120 - |> forma.parse 121 - |> should.equal(Ok(#("hello", ""))) 122 - 123 - // missing first 124 - two_field_form() 125 - |> forma.data([#("b", "world")]) 126 - |> forma.parse 127 - |> should.equal(Ok(#("", "world"))) 128 - 129 - // missing both 130 - two_field_form() 131 - |> forma.data([]) 132 - |> forma.parse 133 - |> should.equal(Ok(#("", ""))) 134 - 135 120 // wrong order 136 121 two_field_form() 137 122 |> forma.data([#("b", "world"), #("a", "hello")]) ··· 150 135 |> should.equal(Ok(#("hello", "world"))) 151 136 } 152 137 138 + pub fn parse_double_optional_field_form_test() { 139 + let f = { 140 + use a <- forma.with( 141 + field("a", string_fields.text_field()) |> field.optional, 142 + ) 143 + use b <- forma.with( 144 + field("b", string_fields.text_field()) |> field.optional, 145 + ) 146 + 147 + forma.create_form(#(a, b)) 148 + } 149 + 150 + f 151 + |> forma.data([#("a", "hello"), #("b", "world")]) 152 + |> forma.parse 153 + |> should.equal(Ok(#(option.Some("hello"), option.Some("world")))) 154 + 155 + // missing second 156 + f 157 + |> forma.data([#("a", "hello")]) 158 + |> forma.parse 159 + |> should.equal(Ok(#(option.Some("hello"), option.None))) 160 + 161 + // missing first 162 + f 163 + |> forma.data([#("b", "world")]) 164 + |> forma.parse 165 + |> should.equal(Ok(#(option.None, option.Some("world")))) 166 + 167 + // missing both 168 + f 169 + |> forma.data([]) 170 + |> forma.parse 171 + |> should.equal(Ok(#(option.None, option.None))) 172 + } 173 + 153 174 pub fn parse_single_field_form_with_error_test() { 154 175 let assert Error(f) = 155 176 { 156 - use a <- forma.with(input("a", string_input.integer_input())) 177 + use a <- forma.with(field("a", string_fields.integer_field())) 157 178 forma.create_form(a) 158 179 } 159 180 |> forma.data([#("first", "world")]) 160 181 |> forma.parse 161 182 162 - let assert [field] = forma.get_fields(f) 183 + let assert [field] = forma.get_inputs(f) 163 184 field |> should_be_field_with_error("Must be a whole number") 164 185 } 165 186 ··· 169 190 |> forma.data([#("a", "string"), #("b", "1"), #("c", "string")]) 170 191 |> forma.parse 171 192 |> get_form_from_error_result 172 - |> forma.get_fields 193 + |> forma.get_inputs 173 194 174 195 fielda |> should_be_field_no_error 175 196 fieldb |> should_be_field_no_error ··· 180 201 |> forma.data([#("a", "string"), #("b", "string"), #("c", "string")]) 181 202 |> forma.parse 182 203 |> get_form_from_error_result 183 - |> forma.get_fields 204 + |> forma.get_inputs 184 205 fielda |> should_be_field_no_error 185 206 fieldb |> should_be_field_with_error("Must be a whole number") 186 207 fieldc |> should_be_field_with_error("Must be a number") ··· 190 211 |> forma.data([#("a", "string"), #("b", "string"), #("c", "3.4")]) 191 212 |> forma.parse 192 213 |> get_form_from_error_result 193 - |> forma.get_fields 214 + |> forma.get_inputs 194 215 fielda |> should_be_field_no_error 195 216 fieldb |> should_be_field_with_error("Must be a whole number") 196 217 fieldc |> should_be_field_no_error 197 218 198 219 let assert [fielda, fieldb, fieldc] = 199 220 three_field_form() 200 - |> forma.data([#("a", ""), #("b", "string"), #("c", "3.4")]) 221 + |> forma.data([#("a", "."), #("b", "string"), #("c", "3.4")]) 201 222 |> forma.parse 202 223 |> get_form_from_error_result 203 - |> forma.get_fields 224 + |> forma.get_inputs 204 225 fielda |> should_be_field_with_error("Must be longer than 3 characters") 205 226 fieldb |> should_be_field_with_error("Must be a whole number") 206 227 fieldc |> should_be_field_no_error 207 228 208 229 let assert [fielda, fieldb, fieldc] = 209 230 three_field_form() 210 - |> forma.data([#("a", ""), #("b", "1"), #("c", "3.4")]) 231 + |> forma.data([#("a", "."), #("b", "1"), #("c", "3.4")]) 211 232 |> forma.parse 212 233 |> get_form_from_error_result 213 - |> forma.get_fields 234 + |> forma.get_inputs 214 235 fielda |> should_be_field_with_error("Must be longer than 3 characters") 215 236 fieldb |> should_be_field_no_error 216 237 fieldc |> should_be_field_no_error ··· 237 258 let assert Error(form) = 238 259 forma.parse_and_try(f, fn(_, form) { 239 260 form 240 - |> forma.field_update("a", fn(field) { field.set_error(field, "woops") }) 261 + |> forma.update_input("a", input.set_error(_, "woops")) 241 262 |> Error 242 263 }) 243 - let assert [fielda, fieldb, fieldc] = forma.get_fields(form) 264 + let assert [fielda, fieldb, fieldc] = forma.get_inputs(form) 244 265 fielda |> should_be_field_with_error("woops") 245 266 fieldb |> should_be_field_no_error 246 267 fieldc |> should_be_field_no_error
+30
forma/test/forma/validation_test.gleam
··· 1 + import forma/validation 2 + import gleeunit 3 + import gleeunit/should 4 + 5 + pub fn main() { 6 + gleeunit.main() 7 + } 8 + 9 + pub fn string_test() { 10 + "" |> validation.string |> should.equal(Error("Must not be empty")) 11 + " " |> validation.string |> should.equal(Error("Must not be empty")) 12 + "a" |> validation.string |> should.equal(Ok("a")) 13 + "b " |> validation.string |> should.equal(Ok("b")) 14 + " c" |> validation.string |> should.equal(Ok("c")) 15 + " d " |> validation.string |> should.equal(Ok("d")) 16 + } 17 + 18 + pub fn integer_test() { 19 + "" |> validation.int |> should.equal(Error("Must be a whole number")) 20 + "a" |> validation.int |> should.equal(Error("Must be a whole number")) 21 + "1.0" |> validation.int |> should.equal(Error("Must be a whole number")) 22 + "1" |> validation.int |> should.equal(Ok(1)) 23 + } 24 + 25 + pub fn number_test() { 26 + "" |> validation.number |> should.equal(Error("Must be a number")) 27 + "a" |> validation.number |> should.equal(Error("Must be a number")) 28 + "1.0" |> validation.number |> should.equal(Ok(1.0)) 29 + "1" |> validation.number |> should.equal(Ok(1.0)) 30 + }