this repo has no description
4
fork

Configure Feed

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

singular module names

after a comment by louis: https://discord.com/channels/768594524158427167/768594524158427170/1315297603184885830

+882 -921
+12 -12
formz_demo/src/formz_demo/examples/all_the_inputs.gleam
··· 1 1 import formz 2 2 import formz/field.{field} 3 - import formz_string/definitions 4 - import formz_string/widgets 3 + import formz_string/definition 4 + import formz_string/widget 5 5 6 6 pub fn make_form() { 7 - use a <- formz.optional(field("text"), definitions.text_field()) 8 - use b <- formz.optional(field("int"), definitions.integer_field()) 9 - use c <- formz.optional(field("number"), definitions.number_field()) 10 - use d <- formz.optional(field("bool"), definitions.boolean_field()) 11 - use e <- formz.optional(field("email"), definitions.email_field()) 12 - use f <- formz.optional(field("password"), definitions.password_field()) 7 + use a <- formz.optional(field("text"), definition.text_field()) 8 + use b <- formz.optional(field("int"), definition.integer_field()) 9 + use c <- formz.optional(field("number"), definition.number_field()) 10 + use d <- formz.optional(field("bool"), definition.boolean_field()) 11 + use e <- formz.optional(field("email"), definition.email_field()) 12 + use f <- formz.optional(field("password"), definition.password_field()) 13 13 use g <- formz.optional( 14 14 field("choices"), 15 - definitions.choices_field(letters(), A), 15 + definition.choices_field(letters(), A), 16 16 ) 17 17 use h <- formz.optional( 18 18 field("list"), 19 - definitions.list_field(["Dog", "Cat", "Ant"]), 19 + definition.list_field(["Dog", "Cat", "Ant"]), 20 20 ) 21 21 use i <- formz.optional( 22 22 field("textarea_widget"), 23 - definitions.text_field() 24 - |> formz.widget(widgets.textarea_widget()), 23 + definition.text_field() 24 + |> formz.widget(widget.textarea_widget()), 25 25 ) 26 26 27 27 formz.create_form(#(a, b, c, d, e, f, g, h, i))
+3 -3
formz_demo/src/formz_demo/examples/custom_output.gleam
··· 1 1 import formz.{Field} 2 2 import formz/field.{field} 3 - import formz_lustre/definitions 3 + import formz_lustre/definition 4 4 import formz_lustre/widget 5 5 import lustre/attribute 6 6 import lustre/element/html 7 7 8 8 pub fn make_form() { 9 - use username <- formz.require(field("username"), definitions.text_field()) 10 - use password <- formz.require(field("password"), definitions.password_field()) 9 + use username <- formz.require(field("username"), definition.text_field()) 10 + use password <- formz.require(field("password"), definition.password_field()) 11 11 12 12 formz.create_form(#(username, password)) 13 13 }
+2 -2
formz_demo/src/formz_demo/examples/hello_world.gleam
··· 1 1 import formz 2 2 import formz/field.{field} 3 - import formz_string/definitions 3 + import formz_string/definition 4 4 5 5 pub fn make_form() { 6 - use name <- formz.require(field("name"), definitions.text_field()) 6 + use name <- formz.require(field("name"), definition.text_field()) 7 7 formz.create_form("Hello " <> name) 8 8 }
+5 -5
formz_demo/src/formz_demo/examples/hidden_inputs.gleam
··· 1 1 import formz 2 2 import formz/field.{field} 3 - import formz_string/definitions 4 - import formz_string/widgets 3 + import formz_string/definition 4 + import formz_string/widget 5 5 6 6 pub fn make_form() { 7 7 use id1 <- formz.require( 8 8 field("id_1"), 9 - definitions.make_hidden(definitions.integer_field()), 9 + definition.make_hidden(definition.integer_field()), 10 10 ) 11 11 use id2 <- formz.require( 12 12 field("id_2"), 13 - definitions.integer_field() |> definitions.make_hidden, 13 + definition.integer_field() |> definition.make_hidden, 14 14 ) 15 15 use id3 <- formz.require( 16 16 field("id_3"), 17 - definitions.integer_field() |> formz.widget(widgets.hidden_widget()), 17 + definition.integer_field() |> formz.widget(widget.hidden_widget()), 18 18 ) 19 19 formz.create_form(#(id1, id2, id3)) 20 20 }
+4 -4
formz_demo/src/formz_demo/examples/labels.gleam
··· 1 1 import formz 2 2 import formz/field.{field} 3 - import formz_string/definitions 3 + import formz_string/definition 4 4 5 5 pub fn make_form() { 6 - use name <- formz.require(field(named: "name"), is: definitions.text_field()) 6 + use name <- formz.require(field(named: "name"), is: definition.text_field()) 7 7 use age <- formz.require( 8 8 field("age") |> field.set_label("Age"), 9 - is: definitions.integer_field(), 9 + is: definition.integer_field(), 10 10 ) 11 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"), 15 - is: definitions.integer_field(), 15 + is: definition.integer_field(), 16 16 ) 17 17 18 18 formz.create_form(#(name, age, height))
+5 -5
formz_demo/src/formz_demo/examples/list_fields.gleam
··· 1 1 import formz 2 2 import formz/field.{field} 3 - import formz_string/definitions 3 + import formz_string/definition 4 4 import gleam/string 5 5 6 6 pub fn make_form() { 7 7 use cats <- formz.list( 8 8 field("cats") |> field.set_help_text("Any number of cats"), 9 - definitions.text_field() 9 + definition.text_field() 10 10 |> formz.verify(fn(value) { 11 11 case string.length(value) > 3 { 12 12 True -> Ok(value) ··· 17 17 use dogs <- formz.limited_list( 18 18 formz.limit_at_least(1), 19 19 field("dogs") |> field.set_help_text("At least 1 dog"), 20 - definitions.text_field(), 20 + definition.text_field(), 21 21 ) 22 22 use fish <- formz.limited_list( 23 23 formz.limit_at_most(2), 24 24 field("fish") |> field.set_help_text("At most 2 fish"), 25 - definitions.text_field(), 25 + definition.text_field(), 26 26 ) 27 27 use hamsters <- formz.limited_list( 28 28 formz.limit_between(2, 4), 29 29 field("hamsters") |> field.set_help_text("Between 2 and 4 hamsters"), 30 - definitions.text_field(), 30 + definition.text_field(), 31 31 ) 32 32 33 33 formz.create_form(#(cats, dogs, fish, hamsters))
+3 -3
formz_demo/src/formz_demo/examples/login.gleam
··· 1 1 import formz 2 2 import formz/field.{field} 3 - import formz_string/definitions 3 + import formz_string/definition 4 4 import wisp 5 5 6 6 pub type Credentials { ··· 12 12 } 13 13 14 14 pub fn make_form() { 15 - use username <- formz.require(field("username"), definitions.text_field()) 16 - use password <- formz.require(field("password"), definitions.password_field()) 15 + use username <- formz.require(field("username"), definition.text_field()) 16 + use password <- formz.require(field("password"), definition.password_field()) 17 17 18 18 formz.create_form(Credentials(username, password)) 19 19 }
+12 -12
formz_demo/src/formz_demo/examples/require_all_the_inputs.gleam
··· 1 1 import formz 2 2 import formz/field.{field} 3 - import formz_string/definitions 4 - import formz_string/widgets 3 + import formz_string/definition 4 + import formz_string/widget 5 5 6 6 pub fn make_form() { 7 - use a <- formz.require(field("text"), definitions.text_field()) 8 - use b <- formz.require(field("int"), definitions.integer_field()) 9 - use c <- formz.require(field("number"), definitions.number_field()) 10 - use d <- formz.require(field("bool"), definitions.boolean_field()) 11 - use e <- formz.require(field("email"), definitions.email_field()) 12 - use f <- formz.require(field("password"), definitions.password_field()) 7 + use a <- formz.require(field("text"), definition.text_field()) 8 + use b <- formz.require(field("int"), definition.integer_field()) 9 + use c <- formz.require(field("number"), definition.number_field()) 10 + use d <- formz.require(field("bool"), definition.boolean_field()) 11 + use e <- formz.require(field("email"), definition.email_field()) 12 + use f <- formz.require(field("password"), definition.password_field()) 13 13 use g <- formz.require( 14 14 field("choices"), 15 - definitions.choices_field(letters(), stub: A), 15 + definition.choices_field(letters(), stub: A), 16 16 ) 17 17 use h <- formz.require( 18 18 field("list"), 19 - definitions.list_field(["Dog", "Cat", "Ant"]), 19 + definition.list_field(["Dog", "Cat", "Ant"]), 20 20 ) 21 21 use i <- formz.require( 22 22 field("textarea_widget"), 23 - definitions.text_field() 24 - |> formz.widget(widgets.textarea_widget()), 23 + definition.text_field() 24 + |> formz.widget(widget.textarea_widget()), 25 25 ) 26 26 27 27 formz.create_form(#(a, b, c, d, e, f, g, h, i))
+5 -5
formz_demo/src/formz_demo/examples/sub_form.gleam
··· 1 1 import formz 2 2 import formz/field.{field} 3 3 import formz/subform.{subform} 4 - import formz_string/definitions 4 + import formz_string/definition 5 5 6 6 pub fn make_form() { 7 7 use billing_address <- formz.subform(subform("billing"), address_form()) ··· 11 11 } 12 12 13 13 fn address_form() { 14 - use street <- formz.require(field("street"), definitions.text_field()) 15 - use city <- formz.require(field("city"), definitions.text_field()) 14 + use street <- formz.require(field("street"), definition.text_field()) 15 + use city <- formz.require(field("city"), definition.text_field()) 16 16 use state <- formz.require( 17 17 field("state"), 18 - definitions.list_field(states_list()), 18 + definition.list_field(states_list()), 19 19 ) 20 20 use postal_code <- formz.require( 21 21 field.field("postal_code"), 22 - definitions.text_field(), 22 + definition.text_field(), 23 23 ) 24 24 25 25 formz.create_form(Address(street:, city:, state:, postal_code:))
+8 -8
formz_lustre/src/formz_lustre/definitions.gleam formz_lustre/src/formz_lustre/definition.gleam
··· 6 6 7 7 import formz 8 8 import formz/validation 9 - import formz_lustre/widgets 9 + import formz_lustre/widget 10 10 import gleam/int 11 11 import gleam/list 12 12 13 13 /// Create a basic form input. Parsed as a String. 14 14 pub fn text_field() { 15 15 formz.definition_with_custom_optional( 16 - widgets.input_widget("text"), 16 + widget.input_widget("text"), 17 17 validation.non_empty_string, 18 18 "", 19 19 fn(fun, str) { ··· 29 29 /// Create an email form input. Parsed as a String but must 30 30 /// look like an email address, i.e. the string has an `@`. 31 31 pub fn email_field() { 32 - formz.definition(widgets.input_widget("email"), validation.email, "") 32 + formz.definition(widget.input_widget("email"), validation.email, "") 33 33 } 34 34 35 35 /// Create a whole number form input. Parsed as an Int. 36 36 pub fn integer_field() { 37 - formz.definition(widgets.number_widget(""), validation.int, 0) 37 + formz.definition(widget.number_widget(""), validation.int, 0) 38 38 } 39 39 40 40 /// Create a number form input. Parsed as a Float. 41 41 pub fn number_field() { 42 - formz.definition(widgets.number_widget("0.01"), validation.number, 0.0) 42 + formz.definition(widget.number_widget("0.01"), validation.number, 0.0) 43 43 } 44 44 45 45 /// Create a checkbox form input. Parsed as a Boolean. 46 46 pub fn boolean_field() { 47 47 formz.definition_with_custom_optional( 48 - widget: widgets.checkbox_widget(), 48 + widget: widget.checkbox_widget(), 49 49 parse: validation.on, 50 50 stub: False, 51 51 optional_parse: fn(parse, str) { ··· 60 60 61 61 /// Create a password form input, which hides the input value. Parsed as a String 62 62 pub fn password_field() { 63 - formz.definition(widgets.password_widget(), validation.non_empty_string, "") 63 + formz.definition(widget.password_widget(), validation.non_empty_string, "") 64 64 } 65 65 66 66 /// Creates a `<select>` input. Takes a tuple of `#(String, String)` where the first ··· 77 77 let values = variants |> list.map(fn(t) { t.1 }) 78 78 79 79 formz.definition( 80 - widgets.select_widget(keys_indexed), 80 + widget.select_widget(keys_indexed), 81 81 validation.list_item_by_index(values) 82 82 |> validation.replace_error("is required"), 83 83 stub,
+215 -1
formz_lustre/src/formz_lustre/widget.gleam
··· 9 9 //// generators, and it's use is optional if you have different needs. 10 10 11 11 import formz 12 - import formz/field 12 + import formz/field.{type Field} 13 + import gleam/list 14 + import gleam/string 15 + import lustre/attribute 13 16 import lustre/element 17 + import lustre/element/html 14 18 15 19 pub type Widget(msg) { 16 20 Widget(fn(field.Field, formz.InputState, Args) -> element.Element(msg)) ··· 45 49 DescribedByElementsWithIds(ids: List(String)) 46 50 DescribedByNone 47 51 } 52 + 53 + fn id_attr(id: String) -> attribute.Attribute(msg) { 54 + case id { 55 + "" -> attribute.none() 56 + _ -> attribute.id(id) 57 + } 58 + } 59 + 60 + fn name_attr(name: String) -> attribute.Attribute(msg) { 61 + case name { 62 + "" -> attribute.none() 63 + _ -> attribute.name(name) 64 + } 65 + } 66 + 67 + fn aria_label_attr( 68 + labelled_by: LabelledBy, 69 + label: String, 70 + ) -> attribute.Attribute(msg) { 71 + case labelled_by { 72 + LabelledByLabelFor -> attribute.none() 73 + LabelledByElementsWithIds(ids) -> 74 + attribute.attribute("aria-labelledby", string.join(ids, " ")) 75 + LabelledByFieldValue -> 76 + case label { 77 + "" -> attribute.none() 78 + _ -> attribute.attribute("aria-label", label) 79 + } 80 + } 81 + } 82 + 83 + fn aria_describedby_attr(described_by: DescribedBy) -> attribute.Attribute(msg) { 84 + case described_by { 85 + DescribedByNone -> attribute.none() 86 + DescribedByElementsWithIds(ids) -> { 87 + case ids |> list.filter(fn(x) { !string.is_empty(x) }) { 88 + [] -> attribute.none() 89 + non_empty_ids -> 90 + attribute.attribute( 91 + "aria-describedby", 92 + string.join(non_empty_ids, " "), 93 + ) 94 + } 95 + } 96 + } 97 + } 98 + 99 + fn value_attr(value: String) -> attribute.Attribute(msg) { 100 + case value { 101 + "" -> attribute.none() 102 + _ -> attribute.value(value) 103 + } 104 + } 105 + 106 + fn required_attr(requirement: formz.Requirement) -> attribute.Attribute(msg) { 107 + case requirement { 108 + formz.Required -> attribute.required(True) 109 + formz.Optional -> attribute.none() 110 + } 111 + } 112 + 113 + fn step_size_attr(step_size: String) -> attribute.Attribute(msg) { 114 + case step_size { 115 + "" -> attribute.none() 116 + _ -> attribute.attribute("step", step_size) 117 + } 118 + } 119 + 120 + fn checked_attr(value: String) -> attribute.Attribute(msg) { 121 + case value { 122 + "on" -> attribute.checked(True) 123 + _ -> attribute.none() 124 + } 125 + } 126 + 127 + fn disabled_attr(disabled: Bool) -> attribute.Attribute(msg) { 128 + case disabled { 129 + True -> attribute.disabled(True) 130 + False -> attribute.none() 131 + } 132 + } 133 + 134 + /// Create an `<input type="checkbox">`. The checkbox is checked 135 + /// if the value is "on" (the browser default). 136 + pub fn checkbox_widget() { 137 + Widget(fn(field: Field, state: formz.InputState, args: Args) { 138 + let value = state.value 139 + let state = case state { 140 + formz.Unvalidated(_, requirement) -> formz.Unvalidated("", requirement) 141 + formz.Valid(_, requirement) -> formz.Valid("", requirement) 142 + formz.Invalid(_, requirement, e) -> formz.Invalid("", requirement, e) 143 + } 144 + do_input_widget(field, state, args, "checkbox", [checked_attr(value)]) 145 + }) 146 + } 147 + 148 + /// Create a `<input type="number">`. Normally browsers only allow whole numbers, 149 + /// unless a decimal step size is provided. The step size here is a string that 150 + /// will be put straight into the `step-size` attribute. Doing non-whole numbers 151 + /// this way does mean that a user can only input numbers up to the precision of 152 + /// the step size. If you truly need any float, then a `type="text"` input might be a 153 + /// better choice. 154 + pub fn number_widget(step_size: String) { 155 + Widget(fn(field: Field, state: formz.InputState, args: Args) { 156 + do_input_widget(field, state, args, "number", [step_size_attr(step_size)]) 157 + }) 158 + } 159 + 160 + /// Create an `<input type="password">`. This will not output the value in the 161 + /// generated HTML for privacy/security concerns. 162 + pub fn password_widget() { 163 + Widget(fn(field: Field, state: formz.InputState, args: Args) { 164 + let state = case state { 165 + formz.Unvalidated(_, requirement) -> formz.Unvalidated("", requirement) 166 + formz.Valid(_, requirement) -> formz.Valid("", requirement) 167 + formz.Invalid(_, requirement, e) -> formz.Invalid("", requirement, e) 168 + } 169 + do_input_widget(field, state, args, "password", []) 170 + }) 171 + } 172 + 173 + /// Generate any `<input>` like `type="text"`, `type="email"` or 174 + /// `type="url"`. 175 + pub fn input_widget(type_: String) { 176 + Widget(fn(field: Field, state: formz.InputState, args: Args) { 177 + do_input_widget(field, state, args, type_, []) 178 + }) 179 + } 180 + 181 + fn do_input_widget( 182 + field: Field, 183 + state: formz.InputState, 184 + args: Args, 185 + type_: String, 186 + extra_attrs: List(attribute.Attribute(msg)), 187 + ) { 188 + html.input( 189 + list.flatten([ 190 + [ 191 + attribute.type_(type_), 192 + name_attr(field.name), 193 + id_attr(args.id), 194 + required_attr(state.requirement), 195 + disabled_attr(field.disabled), 196 + value_attr(state.value), 197 + aria_label_attr(args.labelled_by, field.label), 198 + aria_describedby_attr(args.described_by), 199 + ], 200 + extra_attrs, 201 + ]), 202 + ) 203 + } 204 + 205 + /// Create a `<textarea></textarea>`. 206 + pub fn textarea_widget() { 207 + Widget( 208 + fn(field: Field, state: formz.InputState, args: Args) -> element.Element( 209 + msg, 210 + ) { 211 + html.textarea( 212 + [ 213 + name_attr(field.name), 214 + id_attr(args.id), 215 + required_attr(state.requirement), 216 + aria_label_attr(args.labelled_by, field.label), 217 + aria_describedby_attr(args.described_by), 218 + ], 219 + state.value, 220 + ) 221 + }, 222 + ) 223 + } 224 + 225 + /// Create a `<input type="hidden">`. This is useful for if a field is just 226 + /// passing data around and you don't want it to be visible to the user. Like 227 + /// say, the ID of a record being edited. 228 + pub fn hidden_widget() { 229 + Hidden 230 + } 231 + 232 + /// Create a `<select></select>` with `<option>`s for each variant. The list 233 + /// of variants is a two-tuple, where the first item is the text to display and 234 + /// the second item is the value. 235 + pub fn select_widget(variants: List(#(String, String))) { 236 + Widget( 237 + fn(field: Field, state: formz.InputState, args: Args) -> element.Element( 238 + msg, 239 + ) { 240 + html.select( 241 + [ 242 + name_attr(field.name), 243 + id_attr(args.id), 244 + required_attr(state.requirement), 245 + aria_label_attr(args.labelled_by, field.label), 246 + aria_describedby_attr(args.described_by), 247 + ], 248 + list.flatten([ 249 + [html.option([attribute.value("")], "Select..."), html.hr([])], 250 + list.map(variants, fn(variant) { 251 + let val = variant.1 252 + html.option( 253 + [attribute.value(val), attribute.selected(state.value == val)], 254 + variant.0, 255 + ) 256 + }), 257 + ]), 258 + ) 259 + }, 260 + ) 261 + }
-220
formz_lustre/src/formz_lustre/widgets.gleam
··· 1 - import formz 2 - import formz/field.{type Field} 3 - import formz_lustre/widget 4 - import gleam/list 5 - import gleam/string 6 - import lustre/attribute 7 - import lustre/element 8 - import lustre/element/html 9 - 10 - fn id_attr(id: String) -> attribute.Attribute(msg) { 11 - case id { 12 - "" -> attribute.none() 13 - _ -> attribute.id(id) 14 - } 15 - } 16 - 17 - fn name_attr(name: String) -> attribute.Attribute(msg) { 18 - case name { 19 - "" -> attribute.none() 20 - _ -> attribute.name(name) 21 - } 22 - } 23 - 24 - fn aria_label_attr( 25 - labelled_by: widget.LabelledBy, 26 - label: String, 27 - ) -> attribute.Attribute(msg) { 28 - case labelled_by { 29 - widget.LabelledByLabelFor -> attribute.none() 30 - widget.LabelledByElementsWithIds(ids) -> 31 - attribute.attribute("aria-labelledby", string.join(ids, " ")) 32 - widget.LabelledByFieldValue -> 33 - case label { 34 - "" -> attribute.none() 35 - _ -> attribute.attribute("aria-label", label) 36 - } 37 - } 38 - } 39 - 40 - fn aria_describedby_attr( 41 - described_by: widget.DescribedBy, 42 - ) -> attribute.Attribute(msg) { 43 - case described_by { 44 - widget.DescribedByNone -> attribute.none() 45 - widget.DescribedByElementsWithIds(ids) -> { 46 - case ids |> list.filter(fn(x) { !string.is_empty(x) }) { 47 - [] -> attribute.none() 48 - non_empty_ids -> 49 - attribute.attribute( 50 - "aria-describedby", 51 - string.join(non_empty_ids, " "), 52 - ) 53 - } 54 - } 55 - } 56 - } 57 - 58 - fn value_attr(value: String) -> attribute.Attribute(msg) { 59 - case value { 60 - "" -> attribute.none() 61 - _ -> attribute.value(value) 62 - } 63 - } 64 - 65 - fn required_attr(requirement: formz.Requirement) -> attribute.Attribute(msg) { 66 - case requirement { 67 - formz.Required -> attribute.required(True) 68 - formz.Optional -> attribute.none() 69 - } 70 - } 71 - 72 - fn step_size_attr(step_size: String) -> attribute.Attribute(msg) { 73 - case step_size { 74 - "" -> attribute.none() 75 - _ -> attribute.attribute("step", step_size) 76 - } 77 - } 78 - 79 - fn checked_attr(value: String) -> attribute.Attribute(msg) { 80 - case value { 81 - "on" -> attribute.checked(True) 82 - _ -> attribute.none() 83 - } 84 - } 85 - 86 - fn disabled_attr(disabled: Bool) -> attribute.Attribute(msg) { 87 - case disabled { 88 - True -> attribute.disabled(True) 89 - False -> attribute.none() 90 - } 91 - } 92 - 93 - /// Create an `<input type="checkbox">`. The checkbox is checked 94 - /// if the value is "on" (the browser default). 95 - pub fn checkbox_widget() { 96 - widget.Widget(fn(field: Field, state: formz.InputState, args: widget.Args) { 97 - let value = state.value 98 - let state = case state { 99 - formz.Unvalidated(_, requirement) -> formz.Unvalidated("", requirement) 100 - formz.Valid(_, requirement) -> formz.Valid("", requirement) 101 - formz.Invalid(_, requirement, e) -> formz.Invalid("", requirement, e) 102 - } 103 - do_input_widget(field, state, args, "checkbox", [checked_attr(value)]) 104 - }) 105 - } 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. 113 - pub fn number_widget(step_size: String) { 114 - widget.Widget(fn(field: Field, state: formz.InputState, args: widget.Args) { 115 - do_input_widget(field, state, args, "number", [step_size_attr(step_size)]) 116 - }) 117 - } 118 - 119 - /// Create an `<input type="password">`. This will not output the value in the 120 - /// generated HTML for privacy/security concerns. 121 - pub fn password_widget() { 122 - widget.Widget(fn(field: Field, state: formz.InputState, args: widget.Args) { 123 - let state = case state { 124 - formz.Unvalidated(_, requirement) -> formz.Unvalidated("", requirement) 125 - formz.Valid(_, requirement) -> formz.Valid("", requirement) 126 - formz.Invalid(_, requirement, e) -> formz.Invalid("", requirement, e) 127 - } 128 - do_input_widget(field, state, args, "password", []) 129 - }) 130 - } 131 - 132 - /// Generate any `<input>` like `type="text"`, `type="email"` or 133 - /// `type="url"`. 134 - pub fn input_widget(type_: String) { 135 - widget.Widget(fn(field: Field, state: formz.InputState, args: widget.Args) { 136 - do_input_widget(field, state, args, type_, []) 137 - }) 138 - } 139 - 140 - fn do_input_widget( 141 - field: Field, 142 - state: formz.InputState, 143 - args: widget.Args, 144 - type_: String, 145 - extra_attrs: List(attribute.Attribute(msg)), 146 - ) { 147 - html.input( 148 - list.flatten([ 149 - [ 150 - attribute.type_(type_), 151 - name_attr(field.name), 152 - id_attr(args.id), 153 - required_attr(state.requirement), 154 - disabled_attr(field.disabled), 155 - value_attr(state.value), 156 - aria_label_attr(args.labelled_by, field.label), 157 - aria_describedby_attr(args.described_by), 158 - ], 159 - extra_attrs, 160 - ]), 161 - ) 162 - } 163 - 164 - /// Create a `<textarea></textarea>`. 165 - pub fn textarea_widget() { 166 - widget.Widget( 167 - fn(field: Field, state: formz.InputState, args: widget.Args) -> element.Element( 168 - msg, 169 - ) { 170 - html.textarea( 171 - [ 172 - name_attr(field.name), 173 - id_attr(args.id), 174 - required_attr(state.requirement), 175 - aria_label_attr(args.labelled_by, field.label), 176 - aria_describedby_attr(args.described_by), 177 - ], 178 - state.value, 179 - ) 180 - }, 181 - ) 182 - } 183 - 184 - /// Create a `<input type="hidden">`. This is useful for if a field is just 185 - /// passing data around and you don't want it to be visible to the user. Like 186 - /// say, the ID of a record being edited. 187 - pub fn hidden_widget() { 188 - widget.Hidden 189 - } 190 - 191 - /// Create a `<select></select>` with `<option>`s for each variant. The list 192 - /// of variants is a two-tuple, where the first item is the text to display and 193 - /// the second item is the value. 194 - pub fn select_widget(variants: List(#(String, String))) { 195 - widget.Widget( 196 - fn(field: Field, state: formz.InputState, args: widget.Args) -> element.Element( 197 - msg, 198 - ) { 199 - html.select( 200 - [ 201 - name_attr(field.name), 202 - id_attr(args.id), 203 - required_attr(state.requirement), 204 - aria_label_attr(args.labelled_by, field.label), 205 - aria_describedby_attr(args.described_by), 206 - ], 207 - list.flatten([ 208 - [html.option([attribute.value("")], "Select..."), html.hr([])], 209 - list.map(variants, fn(variant) { 210 - let val = variant.1 211 - html.option( 212 - [attribute.value(val), attribute.selected(state.value == val)], 213 - variant.0, 214 - ) 215 - }), 216 - ]), 217 - ) 218 - }, 219 - ) 220 - }
+16 -16
formz_lustre/test/formz_lustre/fields_test.gleam
··· 1 - import formz_lustre/definitions 1 + import formz_lustre/definition 2 2 3 3 import formz.{type Definition} 4 - import formz_string/definitions as string_definitions 4 + import formz_string/definition as string_definition 5 5 import gleeunit 6 6 import gleeunit/should 7 7 ··· 16 16 } 17 17 18 18 pub fn text_field_test() { 19 - let string_definition = string_definitions.text_field() 20 - let definition = definitions.text_field() 19 + let string_definition = string_definition.text_field() 20 + let definition = definition.text_field() 21 21 22 22 compare_parse_fns(definition, string_definition, "") 23 23 compare_parse_fns(definition, string_definition, "a") 24 24 } 25 25 26 26 pub fn email_field_test() { 27 - let string_definition = string_definitions.email_field() 28 - let definition = definitions.email_field() 27 + let string_definition = string_definition.email_field() 28 + let definition = definition.email_field() 29 29 30 30 compare_parse_fns(definition, string_definition, "") 31 31 compare_parse_fns(definition, string_definition, "a") 32 32 } 33 33 34 34 pub fn number_field_test() { 35 - let string_definition = string_definitions.number_field() 36 - let definition = definitions.number_field() 35 + let string_definition = string_definition.number_field() 36 + let definition = definition.number_field() 37 37 38 38 compare_parse_fns(definition, string_definition, "") 39 39 compare_parse_fns(definition, string_definition, "a") 40 40 } 41 41 42 42 pub fn integer_field_test() { 43 - let string_definition = string_definitions.integer_field() 44 - let definition = definitions.integer_field() 43 + let string_definition = string_definition.integer_field() 44 + let definition = definition.integer_field() 45 45 46 46 compare_parse_fns(definition, string_definition, "") 47 47 compare_parse_fns(definition, string_definition, "a") 48 48 } 49 49 50 50 pub fn boolean_field_test() { 51 - let string_definition = string_definitions.boolean_field() 52 - let definition = definitions.boolean_field() 51 + let string_definition = string_definition.boolean_field() 52 + let definition = definition.boolean_field() 53 53 54 54 compare_parse_fns(definition, string_definition, "") 55 55 compare_parse_fns(definition, string_definition, "a") ··· 57 57 58 58 pub fn choices_field_test() { 59 59 let string_definition = 60 - string_definitions.choices_field([#("a", "A"), #("b", "B")], "") 61 - let definition = definitions.choices_field([#("a", "A"), #("b", "B")], "") 60 + string_definition.choices_field([#("a", "A"), #("b", "B")], "") 61 + let definition = definition.choices_field([#("a", "A"), #("b", "B")], "") 62 62 63 63 compare_parse_fns(definition, string_definition, "") 64 64 compare_parse_fns(definition, string_definition, "a") 65 65 } 66 66 67 67 pub fn indexed_enum_field_test() { 68 - let string_definition = string_definitions.list_field(["A", "B"]) 69 - let definition = definitions.list_field(["A", "B"]) 68 + let string_definition = string_definition.list_field(["A", "B"]) 69 + let definition = definition.list_field(["A", "B"]) 70 70 71 71 compare_parse_fns(definition, string_definition, "") 72 72 compare_parse_fns(definition, string_definition, "a")
+10 -10
formz_lustre/test/formz_lustre/simple_test.gleam
··· 1 1 import formz 2 2 import formz/field.{field} 3 3 import formz/subform 4 - import formz_lustre/definitions 4 + import formz_lustre/definition 5 5 import formz_lustre/simple 6 - import formz_string/definitions as string_definitions 6 + import formz_string/definition as string_definition 7 7 import formz_string/simple as string_simple 8 8 import gleeunit 9 9 import gleeunit/should ··· 19 19 } 20 20 21 21 pub fn three_field_form() { 22 - use a <- formz.require(field("a"), definitions.integer_field()) 23 - use b <- formz.require(field("b"), definitions.integer_field()) 24 - use c <- formz.optional(field("c"), definitions.integer_field()) 22 + use a <- formz.require(field("a"), definition.integer_field()) 23 + use b <- formz.require(field("b"), definition.integer_field()) 24 + use c <- formz.optional(field("c"), definition.integer_field()) 25 25 26 26 formz.create_form(#(a, b, c)) 27 27 } 28 28 29 29 pub fn three_field_string_form() { 30 - use a <- formz.require(field("a"), string_definitions.integer_field()) 31 - use b <- formz.require(field("b"), string_definitions.integer_field()) 32 - use c <- formz.optional(field("c"), string_definitions.integer_field()) 30 + use a <- formz.require(field("a"), string_definition.integer_field()) 31 + use b <- formz.require(field("b"), string_definition.integer_field()) 32 + use c <- formz.optional(field("c"), string_definition.integer_field()) 33 33 34 34 formz.create_form(#(a, b, c)) 35 35 } 36 36 37 37 pub fn one_field_and_subform_form() { 38 - use a <- formz.require(field("a"), definitions.integer_field()) 38 + use a <- formz.require(field("a"), definition.integer_field()) 39 39 use b <- formz.subform(subform.subform("b"), three_field_form()) 40 40 41 41 formz.create_form(#(a, b)) 42 42 } 43 43 44 44 pub fn one_field_and_subform_string_form() { 45 - use a <- formz.require(field("a"), string_definitions.integer_field()) 45 + use a <- formz.require(field("a"), string_definition.integer_field()) 46 46 use b <- formz.subform(subform.subform("b"), three_field_string_form()) 47 47 48 48 formz.create_form(#(a, b))
+38 -40
formz_lustre/test/formz_lustre/widgets_test.gleam
··· 1 1 import formz 2 2 import formz/field 3 3 import formz_lustre/widget 4 - import formz_lustre/widgets 5 4 import formz_string/widget as string_widget 6 - import formz_string/widgets as string_widgets 7 5 import gleeunit 8 6 import gleeunit/should 9 7 import lustre/element ··· 70 68 71 69 pub fn text_widget_test() { 72 70 test_inputs( 73 - string_widgets.input_widget("text"), 74 - widgets.input_widget("text"), 71 + string_widget.input_widget("text"), 72 + widget.input_widget("text"), 75 73 name: "a", 76 74 label: "A", 77 75 help: "help", ··· 86 84 ) 87 85 88 86 test_inputs( 89 - string_widgets.input_widget("text"), 90 - widgets.input_widget("text"), 87 + string_widget.input_widget("text"), 88 + widget.input_widget("text"), 91 89 name: "", 92 90 label: "A", 93 91 help: "help", ··· 102 100 ) 103 101 104 102 test_inputs( 105 - string_widgets.input_widget("text"), 106 - widgets.input_widget("text"), 103 + string_widget.input_widget("text"), 104 + widget.input_widget("text"), 107 105 name: "a", 108 106 label: "A", 109 107 help: "help", ··· 118 116 ) 119 117 120 118 test_inputs( 121 - string_widgets.input_widget("text"), 122 - widgets.input_widget("text"), 119 + string_widget.input_widget("text"), 120 + widget.input_widget("text"), 123 121 name: "a", 124 122 label: "A", 125 123 help: "help", ··· 136 134 137 135 pub fn checkbox_widget_test() { 138 136 test_inputs( 139 - string_widgets.checkbox_widget(), 140 - widgets.checkbox_widget(), 137 + string_widget.checkbox_widget(), 138 + widget.checkbox_widget(), 141 139 name: "a", 142 140 label: "A", 143 141 help: "help", ··· 151 149 ), 152 150 ) 153 151 test_inputs( 154 - string_widgets.checkbox_widget(), 155 - widgets.checkbox_widget(), 152 + string_widget.checkbox_widget(), 153 + widget.checkbox_widget(), 156 154 name: "a", 157 155 label: "A", 158 156 help: "help", ··· 167 165 ) 168 166 169 167 test_inputs( 170 - string_widgets.checkbox_widget(), 171 - widgets.checkbox_widget(), 168 + string_widget.checkbox_widget(), 169 + widget.checkbox_widget(), 172 170 name: "a", 173 171 label: "A", 174 172 help: "help", ··· 185 183 186 184 pub fn password_widget_test() { 187 185 test_inputs( 188 - string_widgets.password_widget(), 189 - widgets.password_widget(), 186 + string_widget.password_widget(), 187 + widget.password_widget(), 190 188 name: "a", 191 189 label: "A", 192 190 help: "help", ··· 200 198 ), 201 199 ) 202 200 test_inputs( 203 - string_widgets.password_widget(), 204 - widgets.password_widget(), 201 + string_widget.password_widget(), 202 + widget.password_widget(), 205 203 name: "a", 206 204 label: "A", 207 205 help: "help", ··· 216 214 ) 217 215 218 216 test_inputs( 219 - string_widgets.password_widget(), 220 - widgets.password_widget(), 217 + string_widget.password_widget(), 218 + widget.password_widget(), 221 219 name: "a", 222 220 label: "A", 223 221 help: "help", ··· 234 232 235 233 pub fn textarea_widget_test() { 236 234 test_inputs( 237 - string_widgets.textarea_widget(), 238 - widgets.textarea_widget(), 235 + string_widget.textarea_widget(), 236 + widget.textarea_widget(), 239 237 name: "a", 240 238 label: "A", 241 239 help: "help", ··· 249 247 ), 250 248 ) 251 249 test_inputs( 252 - string_widgets.textarea_widget(), 253 - widgets.textarea_widget(), 250 + string_widget.textarea_widget(), 251 + widget.textarea_widget(), 254 252 name: "a", 255 253 label: "A", 256 254 help: "help", ··· 265 263 ) 266 264 267 265 test_inputs( 268 - string_widgets.textarea_widget(), 269 - widgets.textarea_widget(), 266 + string_widget.textarea_widget(), 267 + widget.textarea_widget(), 270 268 name: "a", 271 269 label: "A", 272 270 help: "help", ··· 283 281 284 282 pub fn hidden_widget_test() { 285 283 test_inputs( 286 - string_widgets.hidden_widget(), 287 - widgets.hidden_widget(), 284 + string_widget.hidden_widget(), 285 + widget.hidden_widget(), 288 286 name: "a", 289 287 label: "A", 290 288 help: "help", ··· 298 296 ), 299 297 ) 300 298 test_inputs( 301 - string_widgets.hidden_widget(), 302 - widgets.hidden_widget(), 299 + string_widget.hidden_widget(), 300 + widget.hidden_widget(), 303 301 name: "a", 304 302 label: "A", 305 303 help: "help", ··· 314 312 ) 315 313 316 314 test_inputs( 317 - string_widgets.hidden_widget(), 318 - widgets.hidden_widget(), 315 + string_widget.hidden_widget(), 316 + widget.hidden_widget(), 319 317 name: "a", 320 318 label: "A", 321 319 help: "help", ··· 333 331 pub fn select_widget_test() { 334 332 let list = [#("One", "a"), #("Two", "b"), #("Three", "c")] 335 333 test_inputs( 336 - string_widgets.select_widget(list), 337 - widgets.select_widget(list), 334 + string_widget.select_widget(list), 335 + widget.select_widget(list), 338 336 name: "a", 339 337 label: "A", 340 338 help: "help", ··· 348 346 ), 349 347 ) 350 348 test_inputs( 351 - string_widgets.select_widget(list), 352 - widgets.select_widget(list), 349 + string_widget.select_widget(list), 350 + widget.select_widget(list), 353 351 name: "a", 354 352 label: "A", 355 353 help: "help", ··· 364 362 ) 365 363 366 364 test_inputs( 367 - string_widgets.select_widget(list), 368 - widgets.select_widget(list), 365 + string_widget.select_widget(list), 366 + widget.select_widget(list), 369 367 name: "a", 370 368 label: "A", 371 369 help: "help",
+8 -8
formz_nakai/src/formz_nakai/definitions.gleam formz_nakai/src/formz_nakai/definition.gleam
··· 6 6 7 7 import formz 8 8 import formz/validation 9 - import formz_nakai/widgets 9 + import formz_nakai/widget 10 10 import gleam/int 11 11 import gleam/list 12 12 13 13 /// Create a basic form input. Parsed as a String. 14 14 pub fn text_field() { 15 15 formz.definition_with_custom_optional( 16 - widgets.input_widget("text"), 16 + widget.input_widget("text"), 17 17 validation.non_empty_string, 18 18 "", 19 19 fn(parse, str) { ··· 29 29 /// Create an email form input. Parsed as a String but must 30 30 /// look like an email address, i.e. the string has an `@`. 31 31 pub fn email_field() { 32 - formz.definition(widgets.input_widget("email"), validation.email, "") 32 + formz.definition(widget.input_widget("email"), validation.email, "") 33 33 } 34 34 35 35 /// Create a whole number form input. Parsed as an Int. 36 36 pub fn integer_field() { 37 - formz.definition(widgets.number_widget(""), validation.int, 0) 37 + formz.definition(widget.number_widget(""), validation.int, 0) 38 38 } 39 39 40 40 /// Create a number form input. Parsed as a Float. 41 41 pub fn number_field() { 42 - formz.definition(widgets.number_widget("0.01"), validation.number, 0.0) 42 + formz.definition(widget.number_widget("0.01"), validation.number, 0.0) 43 43 } 44 44 45 45 /// Create a checkbox form input. Parsed as a Boolean. 46 46 pub fn boolean_field() { 47 47 formz.definition_with_custom_optional( 48 - widget: widgets.checkbox_widget(), 48 + widget: widget.checkbox_widget(), 49 49 parse: validation.on, 50 50 stub: False, 51 51 optional_parse: fn(parse, str) { ··· 60 60 61 61 /// Create a password form input, which hides the input value. Parsed as a String 62 62 pub fn password_field() { 63 - formz.definition(widgets.password_widget(), validation.non_empty_string, "") 63 + formz.definition(widget.password_widget(), validation.non_empty_string, "") 64 64 } 65 65 66 66 /// Creates a `<select>` input. Takes a tuple of `#(String, String)` where the first ··· 77 77 let values = variants |> list.map(fn(t) { t.1 }) 78 78 79 79 formz.definition( 80 - widgets.select_widget(keys_indexed), 80 + widget.select_widget(keys_indexed), 81 81 validation.list_item_by_index(values) 82 82 |> validation.replace_error("is required"), 83 83 stub,
+204 -1
formz_nakai/src/formz_nakai/widget.gleam
··· 9 9 //// generators, and it's use is optional if you have different needs. 10 10 11 11 import formz 12 - import formz/field 12 + import formz/field.{type Field, Field} 13 + import gleam/list 14 + import gleam/string 15 + import nakai/attr 13 16 import nakai/html 14 17 15 18 pub type Widget { ··· 45 48 DescribedByElementsWithIds(ids: List(String)) 46 49 DescribedByNone 47 50 } 51 + 52 + fn id_attr(id: String) -> List(attr.Attr) { 53 + case id { 54 + "" -> [] 55 + _ -> [attr.id(id)] 56 + } 57 + } 58 + 59 + fn name_attr(name: String) -> List(attr.Attr) { 60 + case name { 61 + "" -> [] 62 + _ -> [attr.name(name)] 63 + } 64 + } 65 + 66 + fn aria_label_attr(labelled_by: LabelledBy, label: String) -> List(attr.Attr) { 67 + case labelled_by { 68 + LabelledByLabelFor -> [] 69 + LabelledByElementsWithIds(ids) -> [ 70 + attr.aria_labelledby(string.join(ids, " ")), 71 + ] 72 + LabelledByFieldValue -> 73 + case label { 74 + "" -> [] 75 + _ -> [attr.aria_label(label)] 76 + } 77 + } 78 + } 79 + 80 + fn aria_describedby_attr(described_by: DescribedBy) -> List(attr.Attr) { 81 + case described_by { 82 + DescribedByNone -> [] 83 + DescribedByElementsWithIds(ids) -> 84 + case ids |> list.filter(fn(x) { !string.is_empty(x) }) { 85 + [] -> [] 86 + non_empty_ids -> [ 87 + attr.Attr("aria-describedby", string.join(non_empty_ids, " ")), 88 + ] 89 + } 90 + } 91 + } 92 + 93 + fn type_attr(type_: String) -> List(attr.Attr) { 94 + [attr.type_(type_)] 95 + } 96 + 97 + fn value_attr(value: String) -> List(attr.Attr) { 98 + case value { 99 + "" -> [] 100 + _ -> [attr.value(value)] 101 + } 102 + } 103 + 104 + fn required_attr(requirement: formz.Requirement) -> List(attr.Attr) { 105 + case requirement { 106 + formz.Required -> [attr.required("")] 107 + formz.Optional -> [] 108 + } 109 + } 110 + 111 + fn checked_attr(value: String) -> List(attr.Attr) { 112 + case value { 113 + "on" -> [attr.checked()] 114 + _ -> [] 115 + } 116 + } 117 + 118 + fn disabled_attr(disabled: Bool) -> List(attr.Attr) { 119 + case disabled { 120 + True -> [attr.disabled()] 121 + False -> [] 122 + } 123 + } 124 + 125 + fn step_size_attr(step_size: String) -> List(attr.Attr) { 126 + case step_size { 127 + "" -> [] 128 + _ -> [attr.Attr("step", step_size)] 129 + } 130 + } 131 + 132 + /// Create an `<input type="checkbox">`. The checkbox is checked 133 + /// if the value is "on" (the browser default). 134 + pub fn checkbox_widget() -> Widget { 135 + Widget(fn(field: Field, state: formz.InputState, args: Args) { 136 + let value = state.value 137 + let state = case state { 138 + formz.Unvalidated(_, requirement) -> formz.Unvalidated("", requirement) 139 + formz.Valid(_, requirement) -> formz.Valid("", requirement) 140 + formz.Invalid(_, requirement, e) -> formz.Invalid("", requirement, e) 141 + } 142 + do_input_widget(field, state, args, "checkbox", [checked_attr(value)]) 143 + }) 144 + } 145 + 146 + /// Create a `<input type="number">`. Normally browsers only allow whole numbers, 147 + /// unless a decimal step size is provided. The step size here is a string that 148 + /// will be put straight into the `step-size` attribute. Doing non-whole numbers 149 + /// this way does mean that a user can only input numbers up to the precision of 150 + /// the step size. If you truly need any float, then a `type="text"` input might be a 151 + /// better choice. 152 + pub fn number_widget(step_size: String) { 153 + Widget(fn(field: Field, state: formz.InputState, args: Args) { 154 + do_input_widget(field, state, args, "number", [step_size_attr(step_size)]) 155 + }) 156 + } 157 + 158 + /// Create an `<input type="password">`. This will not output the value in the 159 + /// generated HTML for privacy/security concerns. 160 + pub fn password_widget() { 161 + Widget(fn(field: Field, state: formz.InputState, args: Args) { 162 + let state = case state { 163 + formz.Unvalidated(_, requirement) -> formz.Unvalidated("", requirement) 164 + formz.Valid(_, requirement) -> formz.Valid("", requirement) 165 + formz.Invalid(_, requirement, e) -> formz.Invalid("", requirement, e) 166 + } 167 + do_input_widget(field, state, args, "password", []) 168 + }) 169 + } 170 + 171 + /// Generate any `<input>` like `type="text"`, `type="email"` or 172 + /// `type="url"`. 173 + pub fn input_widget(type_: String) { 174 + Widget(fn(field: Field, state: formz.InputState, args: Args) { 175 + do_input_widget(field, state, args, type_, []) 176 + }) 177 + } 178 + 179 + fn do_input_widget( 180 + field: Field, 181 + state: formz.InputState, 182 + args: Args, 183 + type_: String, 184 + extra_attrs: List(List(attr.Attr)), 185 + ) { 186 + html.input( 187 + list.flatten([ 188 + type_attr(type_), 189 + name_attr(field.name), 190 + id_attr(args.id), 191 + required_attr(state.requirement), 192 + value_attr(state.value), 193 + disabled_attr(field.disabled), 194 + aria_describedby_attr(args.described_by), 195 + aria_label_attr(args.labelled_by, field.label), 196 + extra_attrs |> list.flatten, 197 + ]), 198 + ) 199 + } 200 + 201 + /// Create a `<textarea></textarea>`. 202 + pub fn textarea_widget() { 203 + Widget(fn(field: Field, state: formz.InputState, args: Args) -> html.Node { 204 + html.textarea( 205 + list.flatten([ 206 + name_attr(field.name), 207 + id_attr(args.id), 208 + required_attr(state.requirement), 209 + aria_label_attr(args.labelled_by, field.label), 210 + ]), 211 + [html.Text(state.value)], 212 + ) 213 + }) 214 + } 215 + 216 + /// Create a `<input type="hidden">`. This is useful for if a field is just 217 + /// passing data around and you don't want it to be visible to the user. Like 218 + /// say, the ID of a record being edited. 219 + pub fn hidden_widget() { 220 + Hidden 221 + } 222 + 223 + /// Create a `<select></select>` with `<option>`s for each variant. The list 224 + /// of variants is a two-tuple, where the first item is the text to display and 225 + /// the second item is the value. 226 + pub fn select_widget(variants: List(#(String, String))) { 227 + Widget(fn(field: Field, state: formz.InputState, args: Args) -> html.Node { 228 + html.select( 229 + list.flatten([ 230 + name_attr(field.name), 231 + id_attr(args.id), 232 + required_attr(state.requirement), 233 + aria_label_attr(args.labelled_by, field.label), 234 + ]), 235 + list.flatten([ 236 + [html.option([attr.value("")], [html.Text("Select...")]), html.hr([])], 237 + list.map(variants, fn(variant) { 238 + let val = variant.1 239 + let selected_attr = case state.value == val { 240 + True -> [attr.selected()] 241 + _ -> [] 242 + } 243 + html.option(list.flatten([value_attr(val), selected_attr]), [ 244 + html.Text(variant.0), 245 + ]) 246 + }), 247 + ]), 248 + ) 249 + }) 250 + }
-215
formz_nakai/src/formz_nakai/widgets.gleam
··· 1 - import formz 2 - import formz/field.{type Field} 3 - import formz_nakai/widget 4 - import gleam/string 5 - 6 - import gleam/list 7 - import nakai/attr 8 - import nakai/html 9 - 10 - fn id_attr(id: String) -> List(attr.Attr) { 11 - case id { 12 - "" -> [] 13 - _ -> [attr.id(id)] 14 - } 15 - } 16 - 17 - fn name_attr(name: String) -> List(attr.Attr) { 18 - case name { 19 - "" -> [] 20 - _ -> [attr.name(name)] 21 - } 22 - } 23 - 24 - fn aria_label_attr( 25 - labelled_by: widget.LabelledBy, 26 - label: String, 27 - ) -> List(attr.Attr) { 28 - case labelled_by { 29 - widget.LabelledByLabelFor -> [] 30 - widget.LabelledByElementsWithIds(ids) -> [ 31 - attr.aria_labelledby(string.join(ids, " ")), 32 - ] 33 - widget.LabelledByFieldValue -> 34 - case label { 35 - "" -> [] 36 - _ -> [attr.aria_label(label)] 37 - } 38 - } 39 - } 40 - 41 - fn aria_describedby_attr(described_by: widget.DescribedBy) -> List(attr.Attr) { 42 - case described_by { 43 - widget.DescribedByNone -> [] 44 - widget.DescribedByElementsWithIds(ids) -> 45 - case ids |> list.filter(fn(x) { !string.is_empty(x) }) { 46 - [] -> [] 47 - non_empty_ids -> [ 48 - attr.Attr("aria-describedby", string.join(non_empty_ids, " ")), 49 - ] 50 - } 51 - } 52 - } 53 - 54 - fn type_attr(type_: String) -> List(attr.Attr) { 55 - [attr.type_(type_)] 56 - } 57 - 58 - fn value_attr(value: String) -> List(attr.Attr) { 59 - case value { 60 - "" -> [] 61 - _ -> [attr.value(value)] 62 - } 63 - } 64 - 65 - fn required_attr(requirement: formz.Requirement) -> List(attr.Attr) { 66 - case requirement { 67 - formz.Required -> [attr.required("")] 68 - formz.Optional -> [] 69 - } 70 - } 71 - 72 - fn checked_attr(value: String) -> List(attr.Attr) { 73 - case value { 74 - "on" -> [attr.checked()] 75 - _ -> [] 76 - } 77 - } 78 - 79 - fn disabled_attr(disabled: Bool) -> List(attr.Attr) { 80 - case disabled { 81 - True -> [attr.disabled()] 82 - False -> [] 83 - } 84 - } 85 - 86 - fn step_size_attr(step_size: String) -> List(attr.Attr) { 87 - case step_size { 88 - "" -> [] 89 - _ -> [attr.Attr("step", step_size)] 90 - } 91 - } 92 - 93 - /// Create an `<input type="checkbox">`. The checkbox is checked 94 - /// if the value is "on" (the browser default). 95 - pub fn checkbox_widget() -> widget.Widget { 96 - widget.Widget(fn(field: Field, state: formz.InputState, args: widget.Args) { 97 - let value = state.value 98 - let state = case state { 99 - formz.Unvalidated(_, requirement) -> formz.Unvalidated("", requirement) 100 - formz.Valid(_, requirement) -> formz.Valid("", requirement) 101 - formz.Invalid(_, requirement, e) -> formz.Invalid("", requirement, e) 102 - } 103 - do_input_widget(field, state, args, "checkbox", [checked_attr(value)]) 104 - }) 105 - } 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. 113 - pub fn number_widget(step_size: String) { 114 - widget.Widget(fn(field: Field, state: formz.InputState, args: widget.Args) { 115 - do_input_widget(field, state, args, "number", [step_size_attr(step_size)]) 116 - }) 117 - } 118 - 119 - /// Create an `<input type="password">`. This will not output the value in the 120 - /// generated HTML for privacy/security concerns. 121 - pub fn password_widget() { 122 - widget.Widget(fn(field: Field, state: formz.InputState, args: widget.Args) { 123 - let state = case state { 124 - formz.Unvalidated(_, requirement) -> formz.Unvalidated("", requirement) 125 - formz.Valid(_, requirement) -> formz.Valid("", requirement) 126 - formz.Invalid(_, requirement, e) -> formz.Invalid("", requirement, e) 127 - } 128 - do_input_widget(field, state, args, "password", []) 129 - }) 130 - } 131 - 132 - /// Generate any `<input>` like `type="text"`, `type="email"` or 133 - /// `type="url"`. 134 - pub fn input_widget(type_: String) { 135 - widget.Widget(fn(field: Field, state: formz.InputState, args: widget.Args) { 136 - do_input_widget(field, state, args, type_, []) 137 - }) 138 - } 139 - 140 - fn do_input_widget( 141 - field: Field, 142 - state: formz.InputState, 143 - args: widget.Args, 144 - type_: String, 145 - extra_attrs: List(List(attr.Attr)), 146 - ) { 147 - html.input( 148 - list.flatten([ 149 - type_attr(type_), 150 - name_attr(field.name), 151 - id_attr(args.id), 152 - required_attr(state.requirement), 153 - value_attr(state.value), 154 - disabled_attr(field.disabled), 155 - aria_describedby_attr(args.described_by), 156 - aria_label_attr(args.labelled_by, field.label), 157 - extra_attrs |> list.flatten, 158 - ]), 159 - ) 160 - } 161 - 162 - /// Create a `<textarea></textarea>`. 163 - pub fn textarea_widget() { 164 - widget.Widget( 165 - fn(field: Field, state: formz.InputState, args: widget.Args) -> html.Node { 166 - html.textarea( 167 - list.flatten([ 168 - name_attr(field.name), 169 - id_attr(args.id), 170 - required_attr(state.requirement), 171 - aria_label_attr(args.labelled_by, field.label), 172 - ]), 173 - [html.Text(state.value)], 174 - ) 175 - }, 176 - ) 177 - } 178 - 179 - /// Create a `<input type="hidden">`. This is useful for if a field is just 180 - /// passing data around and you don't want it to be visible to the user. Like 181 - /// say, the ID of a record being edited. 182 - pub fn hidden_widget() { 183 - widget.Hidden 184 - } 185 - 186 - /// Create a `<select></select>` with `<option>`s for each variant. The list 187 - /// of variants is a two-tuple, where the first item is the text to display and 188 - /// the second item is the value. 189 - pub fn select_widget(variants: List(#(String, String))) { 190 - widget.Widget( 191 - fn(field: Field, state: formz.InputState, args: widget.Args) -> html.Node { 192 - html.select( 193 - list.flatten([ 194 - name_attr(field.name), 195 - id_attr(args.id), 196 - required_attr(state.requirement), 197 - aria_label_attr(args.labelled_by, field.label), 198 - ]), 199 - list.flatten([ 200 - [html.option([attr.value("")], [html.Text("Select...")]), html.hr([])], 201 - list.map(variants, fn(variant) { 202 - let val = variant.1 203 - let selected_attr = case state.value == val { 204 - True -> [attr.selected()] 205 - _ -> [] 206 - } 207 - html.option(list.flatten([value_attr(val), selected_attr]), [ 208 - html.Text(variant.0), 209 - ]) 210 - }), 211 - ]), 212 - ) 213 - }, 214 - ) 215 - }
+16 -16
formz_nakai/test/formz_nakai/fields_test.gleam
··· 1 - import formz_nakai/definitions 1 + import formz_nakai/definition 2 2 3 3 import formz.{type Definition} 4 - import formz_string/definitions as string_definitions 4 + import formz_string/definition as string_definition 5 5 import gleeunit 6 6 import gleeunit/should 7 7 ··· 16 16 } 17 17 18 18 pub fn text_field_test() { 19 - let string_definition = string_definitions.text_field() 20 - let definition = definitions.text_field() 19 + let string_definition = string_definition.text_field() 20 + let definition = definition.text_field() 21 21 22 22 compare_parse_fns(definition, string_definition, "") 23 23 compare_parse_fns(definition, string_definition, "a") 24 24 } 25 25 26 26 pub fn email_field_test() { 27 - let string_definition = string_definitions.email_field() 28 - let definition = definitions.email_field() 27 + let string_definition = string_definition.email_field() 28 + let definition = definition.email_field() 29 29 30 30 compare_parse_fns(definition, string_definition, "") 31 31 compare_parse_fns(definition, string_definition, "a") 32 32 } 33 33 34 34 pub fn number_field_test() { 35 - let string_definition = string_definitions.number_field() 36 - let definition = definitions.number_field() 35 + let string_definition = string_definition.number_field() 36 + let definition = definition.number_field() 37 37 38 38 compare_parse_fns(definition, string_definition, "") 39 39 compare_parse_fns(definition, string_definition, "a") 40 40 } 41 41 42 42 pub fn integer_field_test() { 43 - let string_definition = string_definitions.integer_field() 44 - let definition = definitions.integer_field() 43 + let string_definition = string_definition.integer_field() 44 + let definition = definition.integer_field() 45 45 46 46 compare_parse_fns(definition, string_definition, "") 47 47 compare_parse_fns(definition, string_definition, "a") 48 48 } 49 49 50 50 pub fn boolean_field_test() { 51 - let string_definition = string_definitions.boolean_field() 52 - let definition = definitions.boolean_field() 51 + let string_definition = string_definition.boolean_field() 52 + let definition = definition.boolean_field() 53 53 54 54 compare_parse_fns(definition, string_definition, "") 55 55 compare_parse_fns(definition, string_definition, "a") ··· 57 57 58 58 pub fn choices_field_test() { 59 59 let string_definition = 60 - string_definitions.choices_field([#("a", "A"), #("b", "B")], "") 61 - let definition = definitions.choices_field([#("a", "A"), #("b", "B")], "") 60 + string_definition.choices_field([#("a", "A"), #("b", "B")], "") 61 + let definition = definition.choices_field([#("a", "A"), #("b", "B")], "") 62 62 63 63 compare_parse_fns(definition, string_definition, "") 64 64 compare_parse_fns(definition, string_definition, "a") 65 65 } 66 66 67 67 pub fn indexed_enum_field_test() { 68 - let string_definition = string_definitions.list_field(["A", "B"]) 69 - let definition = definitions.list_field(["A", "B"]) 68 + let string_definition = string_definition.list_field(["A", "B"]) 69 + let definition = definition.list_field(["A", "B"]) 70 70 71 71 compare_parse_fns(definition, string_definition, "") 72 72 compare_parse_fns(definition, string_definition, "a")
+10 -10
formz_nakai/test/formz_nakai/simple_test.gleam
··· 1 1 import formz 2 2 import formz/field.{field} 3 3 import formz/subform 4 - import formz_nakai/definitions 4 + import formz_nakai/definition 5 5 import formz_nakai/simple 6 - import formz_string/definitions as string_definitions 6 + import formz_string/definition as string_definition 7 7 import formz_string/simple as string_simple 8 8 import gleam/string 9 9 import gleeunit ··· 37 37 } 38 38 39 39 pub fn three_field_form() { 40 - use a <- formz.require(field("a"), definitions.integer_field()) 41 - use b <- formz.require(field("b"), definitions.integer_field()) 42 - use c <- formz.optional(field("c"), definitions.integer_field()) 40 + use a <- formz.require(field("a"), definition.integer_field()) 41 + use b <- formz.require(field("b"), definition.integer_field()) 42 + use c <- formz.optional(field("c"), definition.integer_field()) 43 43 44 44 formz.create_form(#(a, b, c)) 45 45 } 46 46 47 47 pub fn three_field_string_form() { 48 - use a <- formz.require(field("a"), string_definitions.integer_field()) 49 - use b <- formz.require(field("b"), string_definitions.integer_field()) 50 - use c <- formz.optional(field("c"), string_definitions.integer_field()) 48 + use a <- formz.require(field("a"), string_definition.integer_field()) 49 + use b <- formz.require(field("b"), string_definition.integer_field()) 50 + use c <- formz.optional(field("c"), string_definition.integer_field()) 51 51 52 52 formz.create_form(#(a, b, c)) 53 53 } 54 54 55 55 pub fn one_field_and_subform_form() { 56 - use a <- formz.require(field("a"), definitions.integer_field()) 56 + use a <- formz.require(field("a"), definition.integer_field()) 57 57 use b <- formz.subform(subform.subform("b"), three_field_form()) 58 58 59 59 formz.create_form(#(a, b)) 60 60 } 61 61 62 62 pub fn one_field_and_subform_string_form() { 63 - use a <- formz.require(field("a"), string_definitions.integer_field()) 63 + use a <- formz.require(field("a"), string_definition.integer_field()) 64 64 use b <- formz.subform(subform.subform("b"), three_field_string_form()) 65 65 66 66 formz.create_form(#(a, b))
+38 -40
formz_nakai/test/formz_nakai/widgets_test.gleam
··· 1 1 import formz 2 2 import formz/field 3 3 import formz_nakai/widget 4 - import formz_nakai/widgets 5 4 import formz_string/widget as string_widget 6 - import formz_string/widgets as string_widgets 7 5 import gleam/string 8 6 import gleeunit 9 7 import gleeunit/should ··· 88 86 89 87 pub fn text_widget_test() { 90 88 test_inputs( 91 - string_widgets.input_widget("text"), 92 - widgets.input_widget("text"), 89 + string_widget.input_widget("text"), 90 + widget.input_widget("text"), 93 91 name: "a", 94 92 label: "A", 95 93 help: "help", ··· 104 102 ) 105 103 106 104 test_inputs( 107 - string_widgets.input_widget("text"), 108 - widgets.input_widget("text"), 105 + string_widget.input_widget("text"), 106 + widget.input_widget("text"), 109 107 name: "", 110 108 label: "A", 111 109 help: "help", ··· 120 118 ) 121 119 122 120 test_inputs( 123 - string_widgets.input_widget("text"), 124 - widgets.input_widget("text"), 121 + string_widget.input_widget("text"), 122 + widget.input_widget("text"), 125 123 name: "a", 126 124 label: "A", 127 125 help: "help", ··· 136 134 ) 137 135 138 136 test_inputs( 139 - string_widgets.input_widget("text"), 140 - widgets.input_widget("text"), 137 + string_widget.input_widget("text"), 138 + widget.input_widget("text"), 141 139 name: "a", 142 140 label: "A", 143 141 help: "help", ··· 154 152 155 153 pub fn checkbox_widget_test() { 156 154 test_inputs( 157 - string_widgets.checkbox_widget(), 158 - widgets.checkbox_widget(), 155 + string_widget.checkbox_widget(), 156 + widget.checkbox_widget(), 159 157 name: "a", 160 158 label: "A", 161 159 help: "help", ··· 169 167 ), 170 168 ) 171 169 test_inputs( 172 - string_widgets.checkbox_widget(), 173 - widgets.checkbox_widget(), 170 + string_widget.checkbox_widget(), 171 + widget.checkbox_widget(), 174 172 name: "a", 175 173 label: "A", 176 174 help: "help", ··· 185 183 ) 186 184 187 185 test_inputs( 188 - string_widgets.checkbox_widget(), 189 - widgets.checkbox_widget(), 186 + string_widget.checkbox_widget(), 187 + widget.checkbox_widget(), 190 188 name: "a", 191 189 label: "A", 192 190 help: "help", ··· 203 201 204 202 pub fn password_widget_test() { 205 203 test_inputs( 206 - string_widgets.password_widget(), 207 - widgets.password_widget(), 204 + string_widget.password_widget(), 205 + widget.password_widget(), 208 206 name: "a", 209 207 label: "A", 210 208 help: "help", ··· 218 216 ), 219 217 ) 220 218 test_inputs( 221 - string_widgets.password_widget(), 222 - widgets.password_widget(), 219 + string_widget.password_widget(), 220 + widget.password_widget(), 223 221 name: "a", 224 222 label: "A", 225 223 help: "help", ··· 234 232 ) 235 233 236 234 test_inputs( 237 - string_widgets.password_widget(), 238 - widgets.password_widget(), 235 + string_widget.password_widget(), 236 + widget.password_widget(), 239 237 name: "a", 240 238 label: "A", 241 239 help: "help", ··· 252 250 253 251 pub fn textarea_widget_test() { 254 252 test_inputs( 255 - string_widgets.textarea_widget(), 256 - widgets.textarea_widget(), 253 + string_widget.textarea_widget(), 254 + widget.textarea_widget(), 257 255 name: "a", 258 256 label: "A", 259 257 help: "help", ··· 267 265 ), 268 266 ) 269 267 test_inputs( 270 - string_widgets.textarea_widget(), 271 - widgets.textarea_widget(), 268 + string_widget.textarea_widget(), 269 + widget.textarea_widget(), 272 270 name: "a", 273 271 label: "A", 274 272 help: "help", ··· 283 281 ) 284 282 285 283 test_inputs( 286 - string_widgets.textarea_widget(), 287 - widgets.textarea_widget(), 284 + string_widget.textarea_widget(), 285 + widget.textarea_widget(), 288 286 name: "a", 289 287 label: "A", 290 288 help: "help", ··· 301 299 302 300 pub fn hidden_widget_test() { 303 301 test_inputs( 304 - string_widgets.hidden_widget(), 305 - widgets.hidden_widget(), 302 + string_widget.hidden_widget(), 303 + widget.hidden_widget(), 306 304 name: "a", 307 305 label: "A", 308 306 help: "help", ··· 316 314 ), 317 315 ) 318 316 test_inputs( 319 - string_widgets.hidden_widget(), 320 - widgets.hidden_widget(), 317 + string_widget.hidden_widget(), 318 + widget.hidden_widget(), 321 319 name: "a", 322 320 label: "A", 323 321 help: "help", ··· 332 330 ) 333 331 334 332 test_inputs( 335 - string_widgets.hidden_widget(), 336 - widgets.hidden_widget(), 333 + string_widget.hidden_widget(), 334 + widget.hidden_widget(), 337 335 name: "a", 338 336 label: "A", 339 337 help: "help", ··· 351 349 pub fn select_widget_test() { 352 350 let list = [#("One", "a"), #("Two", "b"), #("Three", "c")] 353 351 test_inputs( 354 - string_widgets.select_widget(list), 355 - widgets.select_widget(list), 352 + string_widget.select_widget(list), 353 + widget.select_widget(list), 356 354 name: "a", 357 355 label: "A", 358 356 help: "help", ··· 366 364 ), 367 365 ) 368 366 test_inputs( 369 - string_widgets.select_widget(list), 370 - widgets.select_widget(list), 367 + string_widget.select_widget(list), 368 + widget.select_widget(list), 371 369 name: "a", 372 370 label: "A", 373 371 help: "help", ··· 382 380 ) 383 381 384 382 test_inputs( 385 - string_widgets.select_widget(list), 386 - widgets.select_widget(list), 383 + string_widget.select_widget(list), 384 + widget.select_widget(list), 387 385 name: "a", 388 386 label: "A", 389 387 help: "help",
+8 -9
formz_string/src/formz_string/definitions.gleam formz_string/src/formz_string/definition.gleam
··· 7 7 import formz 8 8 import formz/validation 9 9 import formz_string/widget 10 - import formz_string/widgets 11 10 import gleam/int 12 11 import gleam/list 13 12 import gleam/option ··· 15 14 /// Create a basic form input. Parsed as a String. 16 15 pub fn text_field() -> formz.Definition(widget.Widget, String, String) { 17 16 formz.definition_with_custom_optional( 18 - widgets.input_widget("text"), 17 + widget.input_widget("text"), 19 18 validation.non_empty_string, 20 19 "", 21 20 fn(fun, str) { ··· 35 34 String, 36 35 option.Option(String), 37 36 ) { 38 - formz.definition(widgets.input_widget("email"), validation.email, "") 37 + formz.definition(widget.input_widget("email"), validation.email, "") 39 38 } 40 39 41 40 /// Create a whole number form input. Parsed as an Int. ··· 44 43 Int, 45 44 option.Option(Int), 46 45 ) { 47 - formz.definition(widgets.number_widget(""), validation.int, 0) 46 + formz.definition(widget.number_widget(""), validation.int, 0) 48 47 } 49 48 50 49 /// Create a number form input. Parsed as a Float. ··· 53 52 Float, 54 53 option.Option(Float), 55 54 ) { 56 - formz.definition(widgets.number_widget("0.01"), validation.number, 0.0) 55 + formz.definition(widget.number_widget("0.01"), validation.number, 0.0) 57 56 } 58 57 59 58 /// Create a checkbox form input. Parsed as a `Bool`. If required, the parsed 60 59 /// `Bool` must be `True`. 61 60 pub fn boolean_field() -> formz.Definition(widget.Widget, Bool, Bool) { 62 61 formz.definition_with_custom_optional( 63 - widget: widgets.checkbox_widget(), 62 + widget: widget.checkbox_widget(), 64 63 parse: validation.on, 65 64 stub: False, 66 65 optional_parse: fn(fun, str) { ··· 79 78 String, 80 79 option.Option(String), 81 80 ) { 82 - formz.definition(widgets.password_widget(), validation.non_empty_string, "") 81 + formz.definition(widget.password_widget(), validation.non_empty_string, "") 83 82 } 84 83 85 84 /// Creates a `<select>` input. Takes a tuple of `#(String, String)` where the first ··· 101 100 let values = variants |> list.map(fn(t) { t.1 }) 102 101 103 102 formz.definition( 104 - widgets.select_widget(keys_indexed), 103 + widget.select_widget(keys_indexed), 105 104 validation.list_item_by_index(values) 106 105 |> validation.replace_error("is required"), 107 106 stub, ··· 120 119 pub fn make_hidden( 121 120 def: formz.Definition(widget.Widget, a, b), 122 121 ) -> formz.Definition(widget.Widget, a, b) { 123 - def |> formz.widget(widgets.hidden_widget()) 122 + def |> formz.widget(widget.hidden_widget()) 124 123 }
+238
formz_string/src/formz_string/widget.gleam
··· 10 10 11 11 import formz 12 12 import formz/field 13 + import gleam/list 14 + import gleam/string 13 15 14 16 pub type Widget { 15 17 Widget(fn(field.Field, formz.InputState, Args) -> String) ··· 44 46 DescribedByElementsWithIds(ids: List(String)) 45 47 DescribedByNone 46 48 } 49 + 50 + fn sanitize_attr(str: String) -> String { 51 + str 52 + |> string.replace("\"", "&quot;") 53 + |> string.replace(">", "&gt;") 54 + } 55 + 56 + fn with_non_empty_strs( 57 + list: List(String), 58 + empty: j, 59 + fun: fn(List(String)) -> j, 60 + ) -> j { 61 + case list |> list.filter(fn(x) { !string.is_empty(x) }) { 62 + [] -> empty 63 + non_empty_ids -> fun(non_empty_ids) 64 + } 65 + } 66 + 67 + fn id_attr(id: String) -> String { 68 + case id { 69 + "" -> "" 70 + _ -> " id=\"" <> sanitize_attr(id) <> "\"" 71 + } 72 + } 73 + 74 + fn name_attr(name: String) -> String { 75 + case name { 76 + "" -> "" 77 + _ -> " name=\"" <> sanitize_attr(name) <> "\"" 78 + } 79 + } 80 + 81 + fn aria_label_attr(labelled_by: LabelledBy, label: String) -> String { 82 + // https://www.w3.org/WAI/tutorials/forms/labels/ 83 + // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-labelledby 84 + case labelled_by { 85 + // there should be a label with a for attribute pointing to this id 86 + LabelledByLabelFor -> "" 87 + 88 + // we have the id of the element that labels this input 89 + LabelledByElementsWithIds(ids) -> 90 + with_non_empty_strs(ids, "", fn(ids) { 91 + " aria-labelledby=\"" <> sanitize_attr(string.join(ids, " ")) <> "\"" 92 + }) 93 + 94 + // we'll use the label value as the aria-label 95 + LabelledByFieldValue -> { 96 + case label { 97 + "" -> "" 98 + _ -> " aria-label=\"" <> sanitize_attr(label) <> "\"" 99 + } 100 + } 101 + } 102 + } 103 + 104 + fn aria_describedby_attr(described_by: DescribedBy) -> String { 105 + // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-describedby 106 + case described_by { 107 + // there should be a label with a for attribute pointing to this id 108 + DescribedByNone -> "" 109 + 110 + // we have the id of the element that labels this input 111 + DescribedByElementsWithIds(ids) -> 112 + with_non_empty_strs(ids, "", fn(ids) { 113 + " aria-describedby=\"" <> sanitize_attr(string.join(ids, " ")) <> "\"" 114 + }) 115 + } 116 + } 117 + 118 + fn step_size_attr(step_size: String) -> String { 119 + case step_size { 120 + "" -> "" 121 + _ -> " step=\"" <> step_size <> "\"" 122 + } 123 + } 124 + 125 + fn type_attr(type_: String) -> String { 126 + " type=\"" <> type_ <> "\"" 127 + } 128 + 129 + fn value_attr(value: String) -> String { 130 + case value { 131 + "" -> "" 132 + _ -> " value=\"" <> sanitize_attr(value) <> "\"" 133 + } 134 + } 135 + 136 + fn disabled_attr(disabled: Bool) -> String { 137 + case disabled { 138 + True -> " disabled" 139 + False -> "" 140 + } 141 + } 142 + 143 + fn required_attr(requirement: formz.Requirement) -> String { 144 + case requirement { 145 + formz.Required -> " required" 146 + formz.Optional -> "" 147 + } 148 + } 149 + 150 + fn checked_attr(value: String) -> String { 151 + case value { 152 + "on" -> " checked" 153 + _ -> "" 154 + } 155 + } 156 + 157 + /// Create an `<input type="checkbox">`. The checkbox is checked 158 + /// if the value is "on" (the browser default). 159 + pub fn checkbox_widget() -> Widget { 160 + Widget(fn(field: field.Field, state: formz.InputState, args: Args) { 161 + let value = state.value 162 + let state = case state { 163 + formz.Unvalidated(_, requirement) -> formz.Unvalidated("", requirement) 164 + formz.Valid(_, requirement) -> formz.Valid("", requirement) 165 + formz.Invalid(_, requirement, e) -> formz.Invalid("", requirement, e) 166 + } 167 + do_input_widget(field, state, args, "checkbox", [checked_attr(value)]) 168 + }) 169 + } 170 + 171 + /// Create a `<input type="number">`. Normally browsers only allow whole numbers, 172 + /// unless a decimal step size is provided. The step size here is a string that 173 + /// will be put straight into the `step-size` attribute. Doing non-whole numbers 174 + /// this way does mean that a user can only input numbers up to the precision of 175 + /// the step size. If you truly need any float, then a `type="text"` input might be a 176 + /// better choice. 177 + pub fn number_widget(step_size: String) -> Widget { 178 + Widget(fn(field: field.Field, state: formz.InputState, args: Args) { 179 + do_input_widget(field, state, args, "number", [step_size_attr(step_size)]) 180 + }) 181 + } 182 + 183 + /// Create an `<input type="password">`. This will not output the value in the 184 + /// generated HTML for privacy/security concerns. 185 + pub fn password_widget() -> Widget { 186 + Widget(fn(field: field.Field, state: formz.InputState, args: Args) { 187 + let state = case state { 188 + formz.Unvalidated(_, requirement) -> formz.Unvalidated("", requirement) 189 + formz.Valid(_, requirement) -> formz.Valid("", requirement) 190 + formz.Invalid(_, requirement, e) -> formz.Invalid("", requirement, e) 191 + } 192 + do_input_widget(field, state, args, "password", []) 193 + }) 194 + } 195 + 196 + /// Generate any `<input>` like `type="text"`, `type="email"` or 197 + /// `type="url"`. 198 + pub fn input_widget(type_: String) -> Widget { 199 + Widget(fn(field: field.Field, state: formz.InputState, args: Args) { 200 + do_input_widget(field, state, args, type_, []) 201 + }) 202 + } 203 + 204 + fn do_input_widget( 205 + field: field.Field, 206 + state: formz.InputState, 207 + args: Args, 208 + type_: String, 209 + extra_attrs: List(String), 210 + ) -> String { 211 + "<input" 212 + <> type_attr(type_) 213 + <> name_attr(field.name) 214 + <> id_attr(args.id) 215 + <> required_attr(state.requirement) 216 + <> disabled_attr(field.disabled) 217 + <> value_attr(state.value) 218 + <> aria_label_attr(args.labelled_by, field.label) 219 + <> aria_describedby_attr(args.described_by) 220 + <> extra_attrs |> string.join("") 221 + <> ">" 222 + } 223 + 224 + /// Create a `<textarea></textarea>`. 225 + pub fn textarea_widget() -> Widget { 226 + Widget(fn(field: field.Field, state: formz.InputState, args: Args) -> String { 227 + // https://chriscoyier.net/2023/09/29/css-solves-auto-expanding-textareas-probably-eventually/ 228 + // https://til.simonwillison.net/css/resizing-textarea 229 + "<textarea" 230 + <> name_attr(field.name) 231 + <> id_attr(args.id) 232 + <> required_attr(state.requirement) 233 + <> disabled_attr(field.disabled) 234 + <> aria_label_attr(args.labelled_by, field.label) 235 + <> aria_describedby_attr(args.described_by) 236 + <> ">" 237 + <> state.value 238 + <> "</textarea>" 239 + }) 240 + } 241 + 242 + /// Create a `<input type="hidden">`. This is useful for if a field is just 243 + /// passing data around and you don't want it to be visible to the user. Like 244 + /// say, the ID of a record being edited. 245 + pub fn hidden_widget() -> Widget { 246 + Hidden 247 + } 248 + 249 + /// Create a `<select></select>` with `<option>`s for each variant. The list 250 + /// of variants is a two-tuple, where the first item is the text to display and 251 + /// the second item is the value. 252 + pub fn select_widget(variants: List(#(String, String))) -> Widget { 253 + Widget(fn(field: field.Field, state: formz.InputState, args: Args) { 254 + let choices = 255 + list.map(variants, fn(variant) { 256 + let val = variant.1 257 + let selected_attr = case state.value == val { 258 + True -> " selected" 259 + _ -> "" 260 + } 261 + { "<option" <> value_attr(val) <> selected_attr <> ">" } 262 + <> variant.0 263 + <> "</option>" 264 + }) 265 + |> string.join("") 266 + 267 + { 268 + "<select" 269 + <> name_attr(field.name) 270 + <> id_attr(args.id) 271 + <> required_attr(state.requirement) 272 + <> disabled_attr(field.disabled) 273 + <> aria_label_attr(args.labelled_by, field.label) 274 + <> aria_describedby_attr(args.described_by) 275 + <> ">" 276 + } 277 + // TODO make this placeholder option not selectable? with disabled selected attributes 278 + // https://stackoverflow.com/questions/5805059/how-do-i-make-a-placeholder-for-a-select-box 279 + <> { "<option value>Select...</option>" } 280 + <> { "<hr>" } 281 + <> choices 282 + <> { "</select>" } 283 + }) 284 + }
-253
formz_string/src/formz_string/widgets.gleam
··· 1 - import formz 2 - import formz/field 3 - import formz_string/widget.{type Widget} 4 - import gleam/list 5 - import gleam/string 6 - 7 - fn sanitize_attr(str: String) -> String { 8 - str 9 - |> string.replace("\"", "&quot;") 10 - |> string.replace(">", "&gt;") 11 - } 12 - 13 - fn with_non_empty_strs( 14 - list: List(String), 15 - empty: j, 16 - fun: fn(List(String)) -> j, 17 - ) -> j { 18 - case list |> list.filter(fn(x) { !string.is_empty(x) }) { 19 - [] -> empty 20 - non_empty_ids -> fun(non_empty_ids) 21 - } 22 - } 23 - 24 - fn id_attr(id: String) -> String { 25 - case id { 26 - "" -> "" 27 - _ -> " id=\"" <> sanitize_attr(id) <> "\"" 28 - } 29 - } 30 - 31 - fn name_attr(name: String) -> String { 32 - case name { 33 - "" -> "" 34 - _ -> " name=\"" <> sanitize_attr(name) <> "\"" 35 - } 36 - } 37 - 38 - fn aria_label_attr(labelled_by: widget.LabelledBy, label: String) -> String { 39 - // https://www.w3.org/WAI/tutorials/forms/labels/ 40 - // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-labelledby 41 - case labelled_by { 42 - // there should be a label with a for attribute pointing to this id 43 - widget.LabelledByLabelFor -> "" 44 - 45 - // we have the id of the element that labels this input 46 - widget.LabelledByElementsWithIds(ids) -> 47 - with_non_empty_strs(ids, "", fn(ids) { 48 - " aria-labelledby=\"" <> sanitize_attr(string.join(ids, " ")) <> "\"" 49 - }) 50 - 51 - // we'll use the label value as the aria-label 52 - widget.LabelledByFieldValue -> { 53 - case label { 54 - "" -> "" 55 - _ -> " aria-label=\"" <> sanitize_attr(label) <> "\"" 56 - } 57 - } 58 - } 59 - } 60 - 61 - fn aria_describedby_attr(described_by: widget.DescribedBy) -> String { 62 - // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-describedby 63 - case described_by { 64 - // there should be a label with a for attribute pointing to this id 65 - widget.DescribedByNone -> "" 66 - 67 - // we have the id of the element that labels this input 68 - widget.DescribedByElementsWithIds(ids) -> 69 - with_non_empty_strs(ids, "", fn(ids) { 70 - " aria-describedby=\"" <> sanitize_attr(string.join(ids, " ")) <> "\"" 71 - }) 72 - } 73 - } 74 - 75 - fn step_size_attr(step_size: String) -> String { 76 - case step_size { 77 - "" -> "" 78 - _ -> " step=\"" <> step_size <> "\"" 79 - } 80 - } 81 - 82 - fn type_attr(type_: String) -> String { 83 - " type=\"" <> type_ <> "\"" 84 - } 85 - 86 - fn value_attr(value: String) -> String { 87 - case value { 88 - "" -> "" 89 - _ -> " value=\"" <> sanitize_attr(value) <> "\"" 90 - } 91 - } 92 - 93 - fn disabled_attr(disabled: Bool) -> String { 94 - case disabled { 95 - True -> " disabled" 96 - False -> "" 97 - } 98 - } 99 - 100 - fn required_attr(requirement: formz.Requirement) -> String { 101 - case requirement { 102 - formz.Required -> " required" 103 - formz.Optional -> "" 104 - } 105 - } 106 - 107 - fn checked_attr(value: String) -> String { 108 - case value { 109 - "on" -> " checked" 110 - _ -> "" 111 - } 112 - } 113 - 114 - /// Create an `<input type="checkbox">`. The checkbox is checked 115 - /// if the value is "on" (the browser default). 116 - pub fn checkbox_widget() -> Widget { 117 - widget.Widget( 118 - fn(field: field.Field, state: formz.InputState, args: widget.Args) { 119 - let value = state.value 120 - let state = case state { 121 - formz.Unvalidated(_, requirement) -> formz.Unvalidated("", requirement) 122 - formz.Valid(_, requirement) -> formz.Valid("", requirement) 123 - formz.Invalid(_, requirement, e) -> formz.Invalid("", requirement, e) 124 - } 125 - do_input_widget(field, state, args, "checkbox", [checked_attr(value)]) 126 - }, 127 - ) 128 - } 129 - 130 - /// Create a `<input type="number">`. Normally browsers only allow whole numbers, 131 - /// unless a decimal step size is provided. The step size here is a string that 132 - /// will be put straight into the `step-size` attribute. Doing non-whole numbers 133 - /// this way does mean that a user can only input numbers up to the precision of 134 - /// the step size. If you truly need any float, then a `type="text"` input might be a 135 - /// better choice. 136 - pub fn number_widget(step_size: String) -> Widget { 137 - widget.Widget( 138 - fn(field: field.Field, state: formz.InputState, args: widget.Args) { 139 - do_input_widget(field, state, args, "number", [step_size_attr(step_size)]) 140 - }, 141 - ) 142 - } 143 - 144 - /// Create an `<input type="password">`. This will not output the value in the 145 - /// generated HTML for privacy/security concerns. 146 - pub fn password_widget() -> Widget { 147 - widget.Widget( 148 - fn(field: field.Field, state: formz.InputState, args: widget.Args) { 149 - let state = case state { 150 - formz.Unvalidated(_, requirement) -> formz.Unvalidated("", requirement) 151 - formz.Valid(_, requirement) -> formz.Valid("", requirement) 152 - formz.Invalid(_, requirement, e) -> formz.Invalid("", requirement, e) 153 - } 154 - do_input_widget(field, state, args, "password", []) 155 - }, 156 - ) 157 - } 158 - 159 - /// Generate any `<input>` like `type="text"`, `type="email"` or 160 - /// `type="url"`. 161 - pub fn input_widget(type_: String) -> Widget { 162 - widget.Widget( 163 - fn(field: field.Field, state: formz.InputState, args: widget.Args) { 164 - do_input_widget(field, state, args, type_, []) 165 - }, 166 - ) 167 - } 168 - 169 - fn do_input_widget( 170 - field: field.Field, 171 - state: formz.InputState, 172 - args: widget.Args, 173 - type_: String, 174 - extra_attrs: List(String), 175 - ) -> String { 176 - "<input" 177 - <> type_attr(type_) 178 - <> name_attr(field.name) 179 - <> id_attr(args.id) 180 - <> required_attr(state.requirement) 181 - <> disabled_attr(field.disabled) 182 - <> value_attr(state.value) 183 - <> aria_label_attr(args.labelled_by, field.label) 184 - <> aria_describedby_attr(args.described_by) 185 - <> extra_attrs |> string.join("") 186 - <> ">" 187 - } 188 - 189 - /// Create a `<textarea></textarea>`. 190 - pub fn textarea_widget() -> Widget { 191 - widget.Widget( 192 - fn(field: field.Field, state: formz.InputState, args: widget.Args) -> String { 193 - // https://chriscoyier.net/2023/09/29/css-solves-auto-expanding-textareas-probably-eventually/ 194 - // https://til.simonwillison.net/css/resizing-textarea 195 - "<textarea" 196 - <> name_attr(field.name) 197 - <> id_attr(args.id) 198 - <> required_attr(state.requirement) 199 - <> disabled_attr(field.disabled) 200 - <> aria_label_attr(args.labelled_by, field.label) 201 - <> aria_describedby_attr(args.described_by) 202 - <> ">" 203 - <> state.value 204 - <> "</textarea>" 205 - }, 206 - ) 207 - } 208 - 209 - /// Create a `<input type="hidden">`. This is useful for if a field is just 210 - /// passing data around and you don't want it to be visible to the user. Like 211 - /// say, the ID of a record being edited. 212 - pub fn hidden_widget() -> Widget { 213 - widget.Hidden 214 - } 215 - 216 - /// Create a `<select></select>` with `<option>`s for each variant. The list 217 - /// of variants is a two-tuple, where the first item is the text to display and 218 - /// the second item is the value. 219 - pub fn select_widget(variants: List(#(String, String))) -> Widget { 220 - widget.Widget( 221 - fn(field: field.Field, state: formz.InputState, args: widget.Args) { 222 - let choices = 223 - list.map(variants, fn(variant) { 224 - let val = variant.1 225 - let selected_attr = case state.value == val { 226 - True -> " selected" 227 - _ -> "" 228 - } 229 - { "<option" <> value_attr(val) <> selected_attr <> ">" } 230 - <> variant.0 231 - <> "</option>" 232 - }) 233 - |> string.join("") 234 - 235 - { 236 - "<select" 237 - <> name_attr(field.name) 238 - <> id_attr(args.id) 239 - <> required_attr(state.requirement) 240 - <> disabled_attr(field.disabled) 241 - <> aria_label_attr(args.labelled_by, field.label) 242 - <> aria_describedby_attr(args.described_by) 243 - <> ">" 244 - } 245 - // TODO make this placeholder option not selectable? with disabled selected attributes 246 - // https://stackoverflow.com/questions/5805059/how-do-i-make-a-placeholder-for-a-select-box 247 - <> { "<option value>Select...</option>" } 248 - <> { "<hr>" } 249 - <> choices 250 - <> { "</select>" } 251 - }, 252 - ) 253 - }
+6 -6
formz_string/test/formz_string/simple_test.gleam
··· 2 2 import formz 3 3 import formz/field.{field} 4 4 import formz/subform 5 - import formz_string/definitions 5 + import formz_string/definition 6 6 import formz_string/simple 7 7 import gleeunit 8 8 ··· 11 11 } 12 12 13 13 pub fn three_field_form() { 14 - use a <- formz.require(field("a"), definitions.integer_field()) 15 - use b <- formz.require(field("b"), definitions.integer_field()) 16 - use c <- formz.optional(field("c"), definitions.integer_field()) 14 + use a <- formz.require(field("a"), definition.integer_field()) 15 + use b <- formz.require(field("b"), definition.integer_field()) 16 + use c <- formz.optional(field("c"), definition.integer_field()) 17 17 18 18 formz.create_form(#(a, b, c)) 19 19 } 20 20 21 21 pub fn one_field_and_subform_form() { 22 - use a <- formz.require(field("a"), definitions.integer_field()) 22 + use a <- formz.require(field("a"), definition.integer_field()) 23 23 use b <- formz.subform(subform.subform("b"), three_field_form()) 24 24 25 25 formz.create_form(#(a, b)) ··· 28 28 pub fn hidden_field_form() { 29 29 use a <- formz.require( 30 30 field("a"), 31 - definitions.integer_field() |> definitions.make_hidden, 31 + definition.integer_field() |> definition.make_hidden, 32 32 ) 33 33 34 34 formz.create_form(#(a))
+16 -17
formz_string/test/formz_string/widgets_test.gleam
··· 2 2 import formz 3 3 import formz/field 4 4 import formz_string/widget 5 - import formz_string/widgets 6 5 import gleeunit 7 6 8 7 pub fn main() { ··· 17 16 } 18 17 19 18 pub fn input_labelled_by_field_value_test() { 20 - get_make_fun(widgets.input_widget("text"))( 19 + get_make_fun(widget.input_widget("text"))( 21 20 field.Field(name: "name", label: "Label", help_text: "", disabled: False), 22 21 formz.Valid("hello", formz.Optional), 23 22 widget.Args("", widget.LabelledByFieldValue, widget.DescribedByNone), ··· 26 25 } 27 26 28 27 pub fn input_labelled_by_element_with_id_test() { 29 - get_make_fun(widgets.input_widget("text"))( 28 + get_make_fun(widget.input_widget("text"))( 30 29 field.Field(name: "name", label: "Label", help_text: "", disabled: False), 31 30 formz.Valid("hello", formz.Optional), 32 31 widget.Args( ··· 39 38 } 40 39 41 40 pub fn input_labelled_by_label_for_test() { 42 - get_make_fun(widgets.input_widget("text"))( 41 + get_make_fun(widget.input_widget("text"))( 43 42 field.Field(name: "name", label: "Label", help_text: "", disabled: False), 44 43 formz.Valid("hello", formz.Optional), 45 44 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), ··· 48 47 } 49 48 50 49 pub fn input_described_by_elements_with_ids_test() { 51 - get_make_fun(widgets.input_widget("text"))( 50 + get_make_fun(widget.input_widget("text"))( 52 51 field.Field(name: "name", label: "Label", help_text: "", disabled: False), 53 52 formz.Valid("hello", formz.Optional), 54 53 widget.Args( ··· 61 60 } 62 61 63 62 pub fn input_described_by_elements_with_ids_all_empty_test() { 64 - get_make_fun(widgets.input_widget("text"))( 63 + get_make_fun(widget.input_widget("text"))( 65 64 field.Field(name: "name", label: "Label", help_text: "", disabled: False), 66 65 formz.Valid("hello", formz.Optional), 67 66 widget.Args( ··· 74 73 } 75 74 76 75 pub fn input_required_test() { 77 - get_make_fun(widgets.input_widget("text"))( 76 + get_make_fun(widget.input_widget("text"))( 78 77 field.Field(name: "name", label: "Label", help_text: "", disabled: False), 79 78 formz.Valid("hello", formz.Required), 80 79 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), ··· 83 82 } 84 83 85 84 pub fn input_disabled_test() { 86 - get_make_fun(widgets.input_widget("text"))( 85 + get_make_fun(widget.input_widget("text"))( 87 86 field.Field(name: "name", label: "Label", help_text: "", disabled: True), 88 87 formz.Valid("hello", formz.Optional), 89 88 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), ··· 92 91 } 93 92 94 93 pub fn input_sanitized_value_test() { 95 - get_make_fun(widgets.input_widget("text"))( 94 + get_make_fun(widget.input_widget("text"))( 96 95 field.Field(name: "name", label: "Label", help_text: "", disabled: False), 97 96 formz.Valid("hello\"<-_=>", formz.Optional), 98 97 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), ··· 101 100 } 102 101 103 102 pub fn checkbox_checked_test() { 104 - get_make_fun(widgets.checkbox_widget())( 103 + get_make_fun(widget.checkbox_widget())( 105 104 field.Field(name: "name", label: "Label", help_text: "", disabled: False), 106 105 formz.Valid("on", formz.Optional), 107 106 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), ··· 110 109 } 111 110 112 111 pub fn checkbox_unchecked_test() { 113 - get_make_fun(widgets.checkbox_widget())( 112 + get_make_fun(widget.checkbox_widget())( 114 113 field.Field(name: "name", label: "Label", help_text: "", disabled: False), 115 114 formz.Valid("", formz.Optional), 116 115 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), ··· 119 118 } 120 119 121 120 pub fn password_test() { 122 - get_make_fun(widgets.password_widget())( 121 + get_make_fun(widget.password_widget())( 123 122 field.Field(name: "name", label: "Label", help_text: "", disabled: False), 124 123 formz.Valid("pass", formz.Optional), 125 124 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), ··· 128 127 } 129 128 130 129 pub fn numeric_no_step_test() { 131 - get_make_fun(widgets.number_widget(""))( 130 + get_make_fun(widget.number_widget(""))( 132 131 field.Field(name: "name", label: "Label", help_text: "", disabled: False), 133 132 formz.Valid("1", formz.Optional), 134 133 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), ··· 137 136 } 138 137 139 138 pub fn numeric_step_test() { 140 - get_make_fun(widgets.number_widget("0.1"))( 139 + get_make_fun(widget.number_widget("0.1"))( 141 140 field.Field(name: "name", label: "Label", help_text: "", disabled: False), 142 141 formz.Valid("1.0", formz.Optional), 143 142 widget.Args("", widget.LabelledByLabelFor, widget.DescribedByNone), ··· 147 146 148 147 pub fn select_test() { 149 148 get_make_fun( 150 - widgets.select_widget([#("One", "0"), #("Two", "1"), #("Three", "2")]), 149 + widget.select_widget([#("One", "0"), #("Two", "1"), #("Three", "2")]), 151 150 )( 152 151 field.Field(name: "name", label: "Label", help_text: "", disabled: False), 153 152 formz.Valid("", formz.Optional), ··· 158 157 159 158 pub fn select_selected_test() { 160 159 get_make_fun( 161 - widgets.select_widget([#("One", "0"), #("Two", "1"), #("Three", "2")]), 160 + widget.select_widget([#("One", "0"), #("Two", "1"), #("Three", "2")]), 162 161 )( 163 162 field.Field(name: "name", label: "Label", help_text: "", disabled: False), 164 163 formz.Valid("1", formz.Optional), ··· 169 168 170 169 pub fn select_required_test() { 171 170 get_make_fun( 172 - widgets.select_widget([#("One", "0"), #("Two", "1"), #("Three", "2")]), 171 + widget.select_widget([#("One", "0"), #("Two", "1"), #("Three", "2")]), 173 172 )( 174 173 field.Field(name: "name", label: "Label", help_text: "", disabled: False), 175 174 formz.Valid("", formz.Required),