this repo has no description
4
fork

Configure Feed

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

Document formz_x widgets and definitions.

At least there’s something now.

+224 -56
+1 -1
formz/README.md
··· 105 105 The second role is to parse the data from the field. There are a two parts 106 106 to this, as how you parse a field's value depends on if it is optional or 107 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 108 + an optional checkbox might be `False`, and an optional select might 109 109 be `option.None`. So you need to provide two parse functions, one for when 110 110 a field is required, and a second for when it's optional (and it uses the first 111 111 one).
+123 -13
formz/src/formz/definition.gleam
··· 1 + //// A `Definition` is the second argument needed to add a field to a form. It 2 + //// is what describes how a field works, e.g. how it looks and how it's parsed. 3 + //// It is the heavy compared to the lightness of a [Field](https://hexdocs.pm/formz/formz/field.html); 4 + //// they take a bit more work to make as they are intended to be reusable. 5 + //// 6 + //// The first role of a `Definition` is to generate the HTML widget for the field. 7 + //// This library is format-agnostic and you can generate HTML widgets as raw 8 + //// strings, Lustre elements, Nakai nodes, something else, etc, etc. There are 9 + //// currently three formz libraries that provide common field definitions in 10 + //// different formats. 11 + //// 12 + //// - [formz_string](https://hexdocs.pm/formz_string/) 13 + //// - [formz_nakai](https://hexdocs.pm/formz_nakai/) 14 + //// - [formz_lustre](https://hexdocs.pm/formz_lustre/) (untested in a browser, 15 + //// would it be useful there??) 16 + //// 17 + //// The second role of a `Definition` is to parse the data from the field. 18 + //// There are a two parts 19 + //// to this, as how you parse a field's value depends on if it is optional or 20 + //// required. Not all scenarios can be cookie-cutter placed into an `Option`. 21 + //// So you need to provide two parse functions, one for when 22 + //// a field is required, and a second for when it's optional. 23 + //// 24 + //// ### Example password field definition 25 + //// 26 + //// ```gleam 27 + //// /// you won't often need to do this directly (I think??). The idea is that 28 + //// /// there'd be libs with the definitions you need. 29 + //// 30 + //// import formz/definition.{Definition} 31 + //// import formz/field 32 + //// import formz/validation 33 + //// import formz/widget 34 + //// import lustre/attribute 35 + //// import lustre/element 36 + //// import lustre/element/html 37 + //// 38 + //// fn password_widget( 39 + //// field: field.Field, 40 + //// args: widget.Args, 41 + //// ) -> element.Element(msg) { 42 + //// html.input([ 43 + //// attribute.type_("password"), 44 + //// attribute.name(field.name), 45 + //// attribute.id(args.id), 46 + //// attribute.attribute("aria-labelledby", field.label), 47 + //// ]) 48 + //// } 49 + //// 50 + //// pub fn password_field() { 51 + //// Definition( 52 + //// widget: password_widget, 53 + //// parse: validation.string, 54 + //// optional_parse: fn(parse, str) { 55 + //// case str { 56 + //// "" -> Ok(option.None) 57 + //// _ -> parse(str) 58 + //// } 59 + //// }, 60 + //// // We need to have a stub value for each parser that's used 61 + //// // when building the decoder and parse functions for the form as the fields 62 + //// // are being added 63 + //// stub: "", 64 + //// optional_stub: option.None, 65 + //// ) 66 + //// } 67 + //// ``` 68 + 1 69 import formz/widget 2 70 import gleam/option 3 71 import gleam/result 4 72 5 73 pub type Definition(format, required, optional) { 6 74 Definition( 75 + /// The widget generates the HTML for the field. 7 76 widget: widget.Widget(format), 77 + /// This parse function takes the raw string from the parsed POST data 78 + /// and converts it to a Gleam type. This `parse` is for when a value 79 + /// is required, so it should return an error if the field is empty. 8 80 parse: fn(String) -> Result(required, String), 81 + /// The `use`/callbacks pattern for generating a form requires a stub 82 + /// value for each field, because the actual decode function is called 83 + /// step by step as the fields are added to the form and `formz` learns 84 + /// the form's details as it goes. This stub value is purely used 85 + /// for navigating the decode function, and just needs to match the type 86 + /// of the real value that can be parsed. 9 87 stub: required, 88 + /// If a field is marked as optional, this function is called, with the 89 + /// above parse as an argument. The idea is that this function will 90 + /// call out to the parse function if the field is not empty, and 91 + /// this should only handle the case where the raw input value is empty. 92 + /// This function is necessary because not all fields should just be parsed 93 + /// into an `Option` when they aren't provided. 94 + /// For example, an optional text field might be an empty string, 95 + /// an optional checkbox might be `False`, and an optional select might 96 + /// be `option.None`. 10 97 optional_parse: fn(fn(String) -> Result(required, String), String) -> 11 98 Result(optional, String), 99 + /// stub for the optional_parse return value 12 100 optional_stub: optional, 13 101 ) 14 102 } 15 103 104 + /// A convenience function to make the simple optional parse function where 105 + /// if a value isn't provided, just return `option.None`, otherwise call out 106 + /// to the parse function and put it's value in `option.Some`. 107 + pub fn make_simple_optional_parse() -> fn( 108 + fn(String) -> Result(required, String), 109 + String, 110 + ) -> 111 + Result(option.Option(required), String) { 112 + fn(fun, str) { 113 + case str { 114 + "" -> Ok(option.None) 115 + _ -> fun(str) |> result.map(option.Some) 116 + } 117 + } 118 + } 119 + 120 + /// Replace the widget that this `Definition` uses for rendering the field. Most 121 + /// HTML inputs can be interchangeable, they all generate a `String` after all, 122 + /// but not all are the best UX. This allows you to choose the one that is the 123 + /// most appropriate for your field. 16 124 pub fn set_widget( 17 125 definition: Definition(format, a, b), 18 126 widget: widget.Widget(format), ··· 20 128 Definition(..definition, widget:) 21 129 } 22 130 131 + /// Chain additional validation onto the `parse` function. This is 132 + /// useful if you don't need to change the returned type, but might have 133 + /// additional constraints. Like say, requiring a `String` to be at least 134 + /// a certain length, or that an Int must be positive. 135 + /// 136 + /// ### Example 137 + /// ```gleam 138 + /// field 139 + /// |> validate(fn(i) { 140 + /// case i > 0 { 141 + /// True -> Ok(i) 142 + /// False -> Error("must be positive") 143 + /// } 144 + /// }), 145 + /// ``` 23 146 pub fn validate( 24 147 def: Definition(format, a, b), 25 148 fun: fn(a) -> Result(a, String), 26 149 ) -> Definition(format, a, b) { 27 150 Definition(..def, parse: fn(val) { val |> def.parse |> result.try(fun) }) 28 151 } 29 - 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 - } 41 - }
+4
formz/src/formz/field.gleam
··· 1 + //// A `Field` is the first argument needed to add a field to a form. It contains 2 + //// information about this specific field, like it's name, label, or (optional) 3 + //// help_text. 4 + 1 5 import justin 2 6 3 7 pub type Field {
+2 -2
formz_lustre/src/formz_lustre/definitions.gleam
··· 15 15 /// Create a basic form input. Parsed as a String. 16 16 pub fn text_field() { 17 17 Definition( 18 - widgets.text_like_widget("text"), 18 + widgets.input_widget("text"), 19 19 validation.non_empty_string, 20 20 "", 21 21 fn(fun, str) { ··· 32 32 /// look like an email address, i.e. the string has an `@`. 33 33 pub fn email_field() { 34 34 Definition( 35 - widgets.text_like_widget("email"), 35 + widgets.input_widget("email"), 36 36 validation.email, 37 37 "", 38 38 definition.make_simple_optional_parse(),
+20 -3
formz_lustre/src/formz_lustre/widgets.gleam
··· 82 82 } 83 83 } 84 84 85 - // Create a checkbox widget (`<input type="checkbox">`). The checkbox is checked 86 - // if the value is "on" (the browser default). 85 + /// Create an `<input type="checkbox">`. The checkbox is checked 86 + /// if the value is "on" (the browser default). 87 87 pub fn checkbox_widget() { 88 88 fn(field: Field, args: widget.Args) { 89 89 do_input_widget(field |> field.set_raw_value(""), args, "checkbox", [ ··· 92 92 } 93 93 } 94 94 95 + /// Create a `<input type="number">`. Normally browsers only allow whole numbers, 96 + /// unless a decimal step size is provided. The step size here is a string that 97 + /// will be put straight into the `step-size` attribute. Doing non-whole numbers 98 + /// this way does mean that a user can only input numbers up to the precision of 99 + /// the step size. If you truly need any float, then a `type="text"` input might be a 100 + /// better choice. 95 101 pub fn number_widget(step_size: String) { 96 102 fn(field: Field, args: widget.Args) { 97 103 do_input_widget(field, args, "number", [step_size_attr(step_size)]) 98 104 } 99 105 } 100 106 107 + /// Create an `<input type="password">`. This will not output the value in the 108 + /// generated HTML for privacy/security concerns. 101 109 pub fn password_widget() { 102 110 fn(field: Field, args: widget.Args) { 103 111 do_input_widget(field |> field.set_raw_value(""), args, "password", []) 104 112 } 105 113 } 106 114 107 - pub fn text_like_widget(type_: String) { 115 + /// Generate any `<input>` like `type="text"`, `type="email"` or 116 + /// `type="url"`. 117 + pub fn input_widget(type_: String) { 108 118 fn(field: Field, args: widget.Args) { 109 119 do_input_widget(field, args, type_, []) 110 120 } ··· 133 143 ) 134 144 } 135 145 146 + /// Create a `<textarea></textarea>`. 136 147 pub fn textarea_widget() { 137 148 fn(field: Field, args: widget.Args) -> element.Element(msg) { 138 149 html.textarea( ··· 148 159 } 149 160 } 150 161 162 + /// Create a `<input type="hidden">`. This is useful for if a field is just 163 + /// passing data around and you don't want it to be visible to the user. Like 164 + /// say, the ID of a record being edited. 151 165 pub fn hidden_widget() { 152 166 fn(field: Field, _args: widget.Args) -> element.Element(msg) { 153 167 html.input([ ··· 158 172 } 159 173 } 160 174 175 + /// Create a `<select></select>` with `<option>`s for each variant. The list 176 + /// of variants is a two-tuple, where the first item is the text to display and 177 + /// the second item is the value. 161 178 pub fn select_widget(variants: List(#(String, String))) { 162 179 fn(field: Field, args: widget.Args) -> element.Element(msg) { 163 180 html.select(
+8 -8
formz_lustre/test/formz_lustre/widgets_test.gleam
··· 56 56 57 57 pub fn text_widget_test() { 58 58 test_inputs( 59 - string_widgets.text_like_widget("text"), 60 - widgets.text_like_widget("text"), 59 + string_widgets.input_widget("text"), 60 + widgets.input_widget("text"), 61 61 name: "a", 62 62 label: "A", 63 63 help: "help", ··· 73 73 ) 74 74 75 75 test_inputs( 76 - string_widgets.text_like_widget("text"), 77 - widgets.text_like_widget("text"), 76 + string_widgets.input_widget("text"), 77 + widgets.input_widget("text"), 78 78 name: "", 79 79 label: "A", 80 80 help: "help", ··· 90 90 ) 91 91 92 92 test_inputs( 93 - string_widgets.text_like_widget("text"), 94 - widgets.text_like_widget("text"), 93 + string_widgets.input_widget("text"), 94 + widgets.input_widget("text"), 95 95 name: "a", 96 96 label: "A", 97 97 help: "help", ··· 107 107 ) 108 108 109 109 test_inputs( 110 - string_widgets.text_like_widget("text"), 111 - widgets.text_like_widget("text"), 110 + string_widgets.input_widget("text"), 111 + widgets.input_widget("text"), 112 112 name: "a", 113 113 label: "A", 114 114 help: "help",
+2 -2
formz_nakai/src/formz_nakai/definitions.gleam
··· 15 15 /// Create a basic form input. Parsed as a String. 16 16 pub fn text_field() { 17 17 Definition( 18 - widgets.text_like_widget("text"), 18 + widgets.input_widget("text"), 19 19 validation.non_empty_string, 20 20 "", 21 21 fn(fun, str) { ··· 32 32 /// look like an email address, i.e. the string has an `@`. 33 33 pub fn email_field() { 34 34 Definition( 35 - widgets.text_like_widget("email"), 35 + widgets.input_widget("email"), 36 36 validation.email, 37 37 "", 38 38 definition.make_simple_optional_parse(),
+20 -3
formz_nakai/src/formz_nakai/widgets.gleam
··· 85 85 } 86 86 } 87 87 88 - // Create a checkbox widget (`<input type="checkbox">`). The checkbox is checked 89 - // if the value is "on" (the browser default). 88 + /// Create an `<input type="checkbox">`. The checkbox is checked 89 + /// if the value is "on" (the browser default). 90 90 pub fn checkbox_widget() { 91 91 fn(field: Field, args: widget.Args) { 92 92 do_input_widget(field |> field.set_raw_value(""), args, "checkbox", [ ··· 95 95 } 96 96 } 97 97 98 + /// Create a `<input type="number">`. Normally browsers only allow whole numbers, 99 + /// unless a decimal step size is provided. The step size here is a string that 100 + /// will be put straight into the `step-size` attribute. Doing non-whole numbers 101 + /// this way does mean that a user can only input numbers up to the precision of 102 + /// the step size. If you truly need any float, then a `type="text"` input might be a 103 + /// better choice. 98 104 pub fn number_widget(step_size: String) { 99 105 fn(field: Field, args: widget.Args) { 100 106 do_input_widget(field, args, "number", [step_size_attr(step_size)]) 101 107 } 102 108 } 103 109 110 + /// Create an `<input type="password">`. This will not output the value in the 111 + /// generated HTML for privacy/security concerns. 104 112 pub fn password_widget() { 105 113 fn(field: Field, args: widget.Args) { 106 114 do_input_widget(field |> field.set_raw_value(""), args, "password", []) 107 115 } 108 116 } 109 117 110 - pub fn text_like_widget(type_: String) { 118 + /// Generate any `<input>` like `type="text"`, `type="email"` or 119 + /// `type="url"`. 120 + pub fn input_widget(type_: String) { 111 121 fn(field: Field, args: widget.Args) { 112 122 do_input_widget(field, args, type_, []) 113 123 } ··· 134 144 ) 135 145 } 136 146 147 + /// Create a `<textarea></textarea>`. 137 148 pub fn textarea_widget() { 138 149 fn(field: Field, args: widget.Args) -> html.Node { 139 150 html.textarea( ··· 148 159 } 149 160 } 150 161 162 + /// Create a `<input type="hidden">`. This is useful for if a field is just 163 + /// passing data around and you don't want it to be visible to the user. Like 164 + /// say, the ID of a record being edited. 151 165 pub fn hidden_widget() { 152 166 fn(field: Field, _) -> html.Node { 153 167 html.input( ··· 160 174 } 161 175 } 162 176 177 + /// Create a `<select></select>` with `<option>`s for each variant. The list 178 + /// of variants is a two-tuple, where the first item is the text to display and 179 + /// the second item is the value. 163 180 pub fn select_widget(variants: List(#(String, String))) { 164 181 fn(field: Field, args: widget.Args) -> html.Node { 165 182 html.select(
+10 -10
formz_nakai/test/formz_nakai/widgets_test.gleam
··· 74 74 75 75 pub fn text_widget_test() { 76 76 test_inputs( 77 - string_widgets.text_like_widget("text"), 78 - widgets.text_like_widget("text"), 77 + string_widgets.input_widget("text"), 78 + widgets.input_widget("text"), 79 79 name: "a", 80 80 label: "A", 81 81 help: "help", ··· 91 91 ) 92 92 93 93 test_inputs( 94 - string_widgets.text_like_widget("text"), 95 - widgets.text_like_widget("text"), 94 + string_widgets.input_widget("text"), 95 + widgets.input_widget("text"), 96 96 name: "", 97 97 label: "A", 98 98 help: "help", ··· 108 108 ) 109 109 110 110 test_inputs( 111 - string_widgets.text_like_widget("text"), 112 - widgets.text_like_widget("text"), 111 + string_widgets.input_widget("text"), 112 + widgets.input_widget("text"), 113 113 name: "a", 114 114 label: "A", 115 115 help: "help", ··· 125 125 ) 126 126 127 127 test_inputs( 128 - string_widgets.text_like_widget("text"), 129 - widgets.text_like_widget("text"), 128 + string_widgets.input_widget("text"), 129 + widgets.input_widget("text"), 130 130 name: "a", 131 131 label: "A", 132 132 help: "help", ··· 142 142 ) 143 143 144 144 test_inputs( 145 - string_widgets.text_like_widget("text"), 146 - widgets.text_like_widget("text"), 145 + string_widgets.input_widget("text"), 146 + widgets.input_widget("text"), 147 147 name: "a", 148 148 label: "A", 149 149 help: "help",
+9 -6
formz_string/src/formz_string/definitions.gleam
··· 15 15 /// Create a basic form input. Parsed as a String. 16 16 pub fn text_field() { 17 17 Definition( 18 - widgets.text_like_widget("text"), 18 + widgets.input_widget("text"), 19 19 validation.non_empty_string, 20 20 "", 21 21 fn(fun, str) { ··· 32 32 /// look like an email address, i.e. the string has an `@`. 33 33 pub fn email_field() { 34 34 Definition( 35 - widgets.text_like_widget("email"), 35 + widgets.input_widget("email"), 36 36 validation.email, 37 37 "", 38 38 definition.make_simple_optional_parse(), ··· 62 62 ) 63 63 } 64 64 65 - /// Create a checkbox form input. Parsed as a Boolean. 65 + /// Create a checkbox form input. Parsed as a `Bool`. If required, the parsed 66 + /// `Bool` must be `True`. 66 67 pub fn boolean_field() { 67 68 definition.Definition( 68 69 widget: widgets.checkbox_widget(), ··· 78 79 ) 79 80 } 80 81 81 - /// Create a password form input, which hides the input value. Parsed as a String 82 + /// Create a password form input, which hides the input value. Parsed as a String. 82 83 pub fn password_field() { 83 84 Definition( 84 85 widgets.password_widget(), ··· 91 92 92 93 /// Creates a `<select>` input. Takes a tuple of #(String, String) where the first 93 94 /// item in the tuple is the label, and the second item can be any Gleam type and 94 - /// is the value that would be parsed for a given selection. 95 + /// is the value that would be parsed for a given selection. The actual values 96 + /// rendered in the `<option>` tags are the numeric indeces of the items in the 97 + /// list. 95 98 /// 96 - /// Because of how you build `formz` forms, you need to provide a placeholder of 99 + /// Because of how you build `formz` forms, you need to provide a stub of 97 100 /// the value type. Is this annoying? Would it be more or less annoying if I 98 101 /// required a non-empty list for the variants instead? I'm not sure. Let me know! 99 102 pub fn choices_field(variants: List(#(String, enum)), stub stub: enum) {
+20 -3
formz_string/src/formz_string/widgets.gleam
··· 94 94 } 95 95 } 96 96 97 - // Create a checkbox widget (`<input type="checkbox">`). The checkbox is checked 98 - // if the value is "on" (the browser default). 97 + /// Create an `<input type="checkbox">`. The checkbox is checked 98 + /// if the value is "on" (the browser default). 99 99 pub fn checkbox_widget() { 100 100 fn(field: Field, args: widget.Args) { 101 101 do_input_widget(field |> field.set_raw_value(""), args, "checkbox", [ ··· 104 104 } 105 105 } 106 106 107 + /// Create a `<input type="number">`. Normally browsers only allow whole numbers, 108 + /// unless a decimal step size is provided. The step size here is a string that 109 + /// will be put straight into the `step-size` attribute. Doing non-whole numbers 110 + /// this way does mean that a user can only input numbers up to the precision of 111 + /// the step size. If you truly need any float, then a `type="text"` input might be a 112 + /// better choice. 107 113 pub fn number_widget(step_size: String) { 108 114 fn(field: Field, args: widget.Args) { 109 115 do_input_widget(field, args, "number", [step_size_attr(step_size)]) 110 116 } 111 117 } 112 118 119 + /// Create an `<input type="password">`. This will not output the value in the 120 + /// generated HTML for privacy/security concerns. 113 121 pub fn password_widget() { 114 122 fn(field: Field, args: widget.Args) { 115 123 do_input_widget(field |> field.set_raw_value(""), args, "password", []) 116 124 } 117 125 } 118 126 119 - pub fn text_like_widget(type_: String) { 127 + /// Generate any `<input>` like `type="text"`, `type="email"` or 128 + /// `type="url"`. 129 + pub fn input_widget(type_: String) { 120 130 fn(field: Field, args: widget.Args) { 121 131 do_input_widget(field, args, type_, []) 122 132 } ··· 141 151 <> ">" 142 152 } 143 153 154 + /// Create a `<textarea></textarea>`. 144 155 pub fn textarea_widget() { 145 156 fn(field: Field, args: widget.Args) -> String { 146 157 // https://chriscoyier.net/2023/09/29/css-solves-auto-expanding-textareas-probably-eventually/ ··· 158 169 } 159 170 } 160 171 172 + /// Create a `<input type="hidden">`. This is useful for if a field is just 173 + /// passing data around and you don't want it to be visible to the user. Like 174 + /// say, the ID of a record being edited. 161 175 pub fn hidden_widget() { 162 176 fn(field: Field, _args: widget.Args) -> String { 163 177 "<input" ··· 168 182 } 169 183 } 170 184 185 + /// Create a `<select></select>` with `<option>`s for each variant. The list 186 + /// of variants is a two-tuple, where the first item is the text to display and 187 + /// the second item is the value. 171 188 pub fn select_widget(variants: List(#(String, String))) { 172 189 fn(field: Field, args: widget.Args) { 173 190 let choices =
+5 -5
formz_string/test/formz_string/widgets_test.gleam
··· 9 9 } 10 10 11 11 pub fn text_like_labelled_by_field_value_test() { 12 - widgets.text_like_widget("text")( 12 + widgets.input_widget("text")( 13 13 field.Valid( 14 14 name: "name", 15 15 label: "Label", ··· 25 25 } 26 26 27 27 pub fn text_like_labelled_by_element_with_id_test() { 28 - widgets.text_like_widget("text")( 28 + widgets.input_widget("text")( 29 29 field.Valid( 30 30 name: "name", 31 31 label: "Label", ··· 45 45 } 46 46 47 47 pub fn text_like_labelled_by_label_for_test() { 48 - widgets.text_like_widget("text")( 48 + widgets.input_widget("text")( 49 49 field.Valid( 50 50 name: "name", 51 51 label: "Label", ··· 61 61 } 62 62 63 63 pub fn text_like_required_test() { 64 - widgets.text_like_widget("text")( 64 + widgets.input_widget("text")( 65 65 field.Valid( 66 66 name: "name", 67 67 label: "Label", ··· 77 77 } 78 78 79 79 pub fn text_like_disabled_test() { 80 - widgets.text_like_widget("text")( 80 + widgets.input_widget("text")( 81 81 field.Valid( 82 82 name: "name", 83 83 label: "Label",