//// A form is a list of fields and a decoder function. This module uses a //// series of callbacks to construct the decoder function as the fields are //// being added to the form. The idea is that you'd have a function that //// makes the form using the `use` syntax, and then be able to use the form //// later for parsing or rendering in different contexts. //// //// You can use this cheatsheet to navigate the module documentation: //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// //// ////
Creating a form //// create_form
//// required_field
//// field
//// list
//// limited_list
//// subform ////
Decoding and validating a form //// data
//// decode
//// decode_then_try
//// validate
//// validate_all ////
Creating a field definition //// definition
//// definition_with_custom_optional
//// verify
//// widget ////
Creating a limited list //// limit_at_least
//// limit_at_most
//// limit_between
//// simple_limit_check ////
Accessing and manipulating form items //// get
//// items
//// field_error
//// listfield_errors
//// update
////
Accessing and manipulating config for a form item //// update_config
//// set_name
//// set_label
//// set_help_text
//// set_disabled
//// make_disabled
////
//// //// ### Examples //// //// ```gleam //// fn make_form() { //// use name <- formz.required_field(field("name"), definitions.text_field()) //// //// formz.create_form(name) //// } //// //// fn process_form() { //// make_form() //// |> formz.data([#("name", "Louis"))]) //// |> formz.decode //// # -> Ok("Louis") //// } //// ``` //// //// ```gleam //// fn make_form() { //// use greeting <- field(field("greeting"), definitions.text_field()) //// use name <- field(field("name"), definitions.text_field()) //// //// formz.create_form(greeting <> " " <> name) //// } //// //// fn process_form() { //// make_form() //// |> data([#("greeting", "Hello"), #("name", "World")]) //// |> formz.decode //// # -> Ok("Hello World") //// } //// ``` import gleam/dict.{type Dict} import gleam/int import gleam/list import gleam/option.{None, Some} import gleam/result import gleam/string import justin /// You create this using the `create_form` function. /// /// The `widget` type is set by the `Definition`s used to add fields for /// this form, and has the details of how to turn given fields into HTML inputs. /// /// The `output` type is the type of the decoded data from the form. This is /// set directly by the `create_form` function, after all the fields have /// been added. pub opaque type Form(widget, output) { Form( items: List(Item(widget)), decode: fn(List(Item(widget))) -> Result(output, List(Item(widget))), stub: output, ) } /// You add an `Item` to a form using the `field`, `required_field`, `list`, /// `limited_list` and `subform` functions. A form is a list of `Item`s and /// each item is parsed to a single value, and then passed to the decode /// function. /// /// You primarily only use an `Item` directly when writing a form generator /// function to output your form to HTML. /// /// You can also manipulate an `Item` after a form has been created, to change /// things like labels, help text, etc with `update`. Each item has a `Config` /// that describes how it works, and this can be updated directly /// with `update_config`. /// /// ``` /// let form = make_form() /// formz.update_config("name", formz.set_label(_, "Full Name")) /// ``` pub type Item(widget) { /// A single field that (generally speaking) corresponds to a single /// HTML input Field(config: Config, state: InputState, widget: widget) /// A single field that a consumer can submit multiple values for. ListField( config: Config, states: List(InputState), limit_check: LimitCheck, widget: widget, ) /// A group of fields that are added as and then parsed to a single unit. SubForm(config: Config, items: List(Item(widget))) } /// The state of the an input for a field. This is used to track the current /// raw value, whether a value is required or not, if the value has been /// validated, and the outcome of that validation. pub type InputState { Unvalidated(value: String, requirement: Requirement) Valid(value: String, requirement: Requirement) Invalid(value: String, requirement: Requirement, error: String) } /// Whether an input value is required for an input field. pub type Requirement { Optional Required } /// Configuration for the form `Item`. The only required information here is /// `name` and there is a `named` function to make a config with a name. You /// can then chain the `set_x` functions to add the other information as needed. /// /// ### Example /// /// ``` /// let config = formz.named("name") |> formz.set_label("Full Name") /// ``` /// /// ``` /// let config = /// formz.named("name") /// |> formz.set_label("Full Name") /// |> formz.set_help_text("Enter your full name") /// ``` /// /// ``` /// let config = formz.named("name") |> formz.make_disabled /// ``` pub type Config { Config( /// The name of the field or subform. The only truly required information /// for a form `Item`. This is used to identify the field in the form. /// It should be unique for each form, and is untested with any values other /// than strings solely consisting of alphanumeric characters /// and underscores. name: String, /// This library thinks of a label as required, but will make one for you from /// the name if you don't provide one via the `field` function. For /// accessibility reasons, a field should always provide a label and all /// the maintained form generators will output one. label: String, /// Optional help text for the field. This is used to provide additional /// instructions or context for the field. It is up to the form generator /// to decide if and how to display this text. help_text: String, /// Whether the field is disabled. A disabled field is not editable in /// the browser. However, there is nothing stopping a user from changing /// the value or submitting a different value via other means, so (presently) /// this doesn't mean the value cannot be tampered with. disabled: Bool, ) } /// A `Definition` describes how an input works, e.g. how it looks and how it's /// parsed. Definitions are intended to be reusable. /// /// The first role of a `Defintion` is to generate the HTML input for the field. /// This library is format-agnostic and you can generate inputs as raw /// strings, Lustre elements, Nakai nodes, something else, etc. The second role /// of a `Definition` is to parse the raw string data from the input into a /// Gleam type. /// /// There are currently three `formz` libraries that provide common field /// definitions for the most common HTML inputs: /// /// - [formz_string](https://hexdocs.pm/formz_string/) /// - [formz_nakai](https://hexdocs.pm/formz_nakai/) /// - [formz_lustre](https://hexdocs.pm/formz_lustre/) /// /// How a definition parses an input value depends on whether a value is required /// for that input (i.e. whether `field`, `required_field`, `list`, or `limited_list` /// was used to add it to the form). If a value is required, the definition is /// expected to return a string error if the input is empty, or the `required` type /// if it isn't. You can use the `definition` function to create a simple /// definition that just parses to an `Option` if an input is empty. /// /// However, not all fields should be parsed into an `Option` when /// given an empty input value. For example, an optional text field might be an /// empty string or an optional checkbox might be False. For these cases, you /// can use the `definition_with_custom_optional` function to create a definition /// that can parse to any type when the input is empty. pub opaque type Definition(widget, required, optional) { Definition( widget: widget, parse: fn(String) -> Result(required, String), stub: required, /// If a field is marked as optional, this function is called, with the /// above parse as an argument. The idea is that this function will /// call out to the parse function if the field is not empty, and /// this should only handle the case where the raw input value is empty. /// This function is necessary because not all fields should just be parsed /// into an `Option` when they aren't provided. /// For example, an optional text field might be an empty string, /// an optional checkbox might be `False`, and an optional select might /// be `option.None`. optional_parse: fn(fn(String) -> Result(required, String), String) -> Result(optional, String), optional_stub: optional, ) } /// When adding a list field to a form with `limited_list`, you have to provide /// a `LimitCheck` function that checks the number of inputs (and associated /// values) for the field. This function can either say the number of inputs /// is Ok and optionally add more, or say the number of inputs was too high /// and return an error. For example, you are presenting a blank form to a /// consumer, and you want to show three initial fields for them to fill out, /// or you want to always show one more additional field than the number of /// values that have already belong to the form, etc. /// /// There are helper functions, `limit_at_least`, `limit_at_most`, and /// `limit_between` or more generally `simple_limit_check` to make a /// `LimitCheck` function for you. I would imagine that those will cover 99.9% /// of cases and almost no one will need to write their own `LimitCheck`. But /// if you do, look at the source for `simple_limit_check` for a better idea /// of how to write one. /// /// This function takes as its only argument, the number of fields that already /// have a value. It should return either `Ok` with a list of `Unvalidated` /// `InputState` items if it wants to offer more inputs to consumers of the /// form, or `Error` amount of inputs that were too many. /// /// This is used multiple times... when the form is created so we know how many /// initial inputs to present, when data is added so we know if we need to add /// more inputs so users can add more items, and when the form is decoded and /// we are checking if too many fields have been added. pub type LimitCheck = fn(Int) -> Result(List(InputState), Int) type ListParsingResult(input_output) { ListParsingResult( value: String, requirement: Requirement, output: Result(input_output, String), ) } /// Create an empty form that "decodes" directly to `thing`. This is /// intended to be the final return value of a chain of callbacks that adds /// the form's fields. /// /// ```gleam /// create_form(1) /// |> decode /// # -> Ok(1) /// ``` /// ```gleam /// fn make_form() { /// use field1 <- formz.required_field(field("field1"), definitions.text_field()) /// use field2 <- formz.required_field(field("field2"), definitions.text_field()) /// use field3 <- formz.required_field(field("field3"), definitions.text_field()) /// /// formz.create_form(#(field1, field2, field3)) /// } /// ``` pub fn create_form(thing: thing) -> Form(widget, thing) { Form([], fn(_) { Ok(thing) }, thing) } /// Add an optional field to a form. /// /// Ultimately whether a field is actually optional or not comes down to the /// details of the definition. The definition will receive the raw input string /// and is in charge of returning an error or an optional value. /// /// If multiple values are submitted for this field, the last one will be used. /// /// The final argument is a callback that will be called when the form /// is being... constructed to look for more fields; validated to check for /// errors; and decoded to parse the input data. **For this reason, the /// callback should be a function without side effects.** It can be called any /// number of times. Don't do anything but create the type with the data you /// need! If you need to do decoding that has side effects, you should use /// `decode_then_try`. pub fn field( config: Config, definition: Definition(widget, _, input_output), next: fn(input_output) -> Form(widget, form_output), ) -> Form(widget, form_output) { add_field( config, Optional, definition.widget, definition.optional_parse(definition.parse, _), definition.optional_stub, next, ) } /// Add a required field to a form. /// /// Ultimately whether a field is actually required or not comes down to the /// details of the definition. The definition will receive the raw input string /// and is in charge of returning an error or a value. /// /// If multiple values are submitted for this field, the last one will be used. /// /// The final argument is a callback that will be called when the form /// is being... constructed to look for more fields; validated to check for /// errors; and decoded to parse the input data. **For this reason, the /// callback should be a function without side effects.** It can be called any /// number of times. Don't do anything but create the type with the data you /// need! If you need to do decoding that has side effects, you should use /// `decode_then_try`. pub fn required_field( config: Config, is definition: Definition(widget, required_output, _), next next: fn(required_output) -> Form(widget, form_output), ) -> Form(widget, form_output) { add_field( config, Required, definition.widget, definition.parse, definition.stub, next, ) } fn add_field( config: Config, requirement: Requirement, widget: widget, parse_field: fn(String) -> Result(input_output, String), stub: input_output, next: fn(input_output) -> Form(widget, form_output), ) -> Form(widget, form_output) { // we pass in our stub value, and we're going to throw away the // decoded result here, we just care about pulling out the fields // from the form. let next_form = next(stub) // prepend the new field to the items from the form we got in the previous step. let updated_items = [ Field(config, Unvalidated("", requirement), widget), ..next_form.items ] // now create the decode function. decode function accepts most recent // version of input list, since data can be added to it. the list // above we just needed for the initial setup. let decode = fn(items: List(Item(widget))) { // pull out the latest version of this field to get latest input data let assert [Field(field, state, widget), ..next_items] = items // transform the input data using the decode function let item_output = parse_field(state.value) // pass our transformed input data to the next function/form. if // we errored we still do this with our stub so we can continue // processing all the fields in the form. if we're on the error track // we'll throw away the "output" made with this and just keep the error let next_form = next(item_output |> result.unwrap(stub)) let form_output = next_form.decode(next_items) // ok, check which track we're on case form_output, item_output { // everything is good! pass along the output Ok(_), Ok(_) -> form_output // form has errors, but this field was good, so add it to the list // of fields as is. Error(error_items), Ok(_) -> { let f = Field(field, Valid(state.value, requirement), widget) Error([f, ..error_items]) } // form was good so far, but this field errored, so need to // mark this field as invalid, mark all the existing fields as valid, // and return all the fields we've got so far Ok(_), Error(error) -> { let f = Field(field, Invalid(state.value, requirement, error), widget) Error([f, ..mark_all_fields_as_valid(next_items)]) } // form already has errors and this field errored, so mark this field // as invalid, and add it to the list of errors Error(error_items), Error(error) -> { let f = Field(field, Invalid(state.value, requirement, error), widget) Error([f, ..error_items]) } } } Form(items: updated_items, decode:, stub: next_form.stub) } /// Convenience function for creating a `LimitCheck` function. This takes /// the minimum number of required values, the maximum number of allowed values, /// and the number of "extra" blank inputs that should be offered to the user /// for filling out. /// /// If "extra" is 0, (say, to manage blank fields via javascript), then this /// will show 1 blank field initially. pub fn simple_limit_check(min: Int, max: Int, extra: Int) -> LimitCheck { fn(num_nonempty) { case max - num_nonempty { x if x < 0 -> Error(-x) _ -> { int.max(1 - num_nonempty, int.min(extra, max - num_nonempty)) list.fold([1, extra, min], 0, int.max) // if they've specified a minimum required, then start there let num_required = int.max(min - num_nonempty, 0) // at least one field needs to be present. don't want a form that is asking // for no input let num_base = case num_nonempty, num_required { x, y if x > 0 || y > 0 -> 0 _, _ -> 1 } // they've asked for extra fields, add those unless doing so would // exceed the max let num_extra = int.min(extra, max - num_nonempty) // take whatever's bigger for our optional ones, either the bare minimum // (base) or the extra if they've got room for it and they've asked for it let num_optional = int.max(num_base, num_extra) - num_required Ok(list.append( list.repeat(Unvalidated("", Required), num_required), list.repeat(Unvalidated("", Optional), num_optional), )) } } } } /// Convenience function for creating a `LimitCheck` with a minimum number /// of required values. This sets the maximum to `1,000,000`, effectively unlimited. pub fn limit_at_least(min: Int) -> LimitCheck { simple_limit_check(min, 1_000_000, 1) } /// Convenience function for creating a `LimitCheck` with a maximum number /// of accepted values. This sets the minimum to `0`. pub fn limit_at_most(max: Int) -> LimitCheck { simple_limit_check(0, max, 1) } /// Convenience function for creating a `LimitCheck` with a minimum and maximum /// number of values. pub fn limit_between(min: Int, max: Int) -> LimitCheck { simple_limit_check(min, max, 1) } /// Add a list field to a form, but with limits on the number of values that /// can be submitted. The `limit_check` function is used to impose those /// limits, and the `limit_at_least`, `limit_at_most`, and `limit_between` /// functions help you create this function for the most likely scenarios. /// /// The final argument is a callback that will be called when the form /// is being... constructed to look for more fields; validated to check for /// errors; and decoded to parse the input data. **For this reason, the /// callback should be a function without side effects.** It can be called any /// number of times. Don't do anything but create the type with the data you /// need! If you need to do decoding that has side effects, you should use /// `decode_then_try`. /// ///### Example /// /// ```gleam /// fn make_form() { /// use names <- formz.limited_list(formz.limit_at_most(4), field("name"), definitions.text_field()) /// // names is a List(String) /// formz.create_form(name) /// } pub fn limited_list( limit_check: fn(Int) -> Result(List(InputState), Int), config: Config, is definition: Definition(widget, required_output, _), next next: fn(List(required_output)) -> Form(widget, form_output), ) -> Form(widget, form_output) { // we pass in our stub value, and we're going to throw away the // decoded result here, we just care about pulling out the fields // from the form. let next_form = next([definition.stub]) let initial_fields = limit_check(0) |> result.unwrap([]) // prepend the new field to the items from the form we got in the previous step. let updated_items = [ ListField(config, initial_fields, limit_check, definition.widget), ..next_form.items ] // now create the decode function. decode function accepts most recent // version of input list, since data can be added to it. the list // above we just needed for the initial setup. let decode = fn(items: List(Item(widget))) { // pull out the latest version of this field to get latest input data let assert [ListField(field, states, limit_check, widget), ..next_items] = items // go through and parse all input values. these can have empty rows // that were offered to the consumer, but they didn't fill out. we need // to keep track of those so they can be displayed when generating the form // again on error. let item_results = states |> list.map(parse_list_state(_, definition.parse, definition.stub)) // then we need to check if too many values were submitted, and if so // mark the extra ones as having errors |> mark_above_max_values_as_invalid(limit_check) // but we don't want them for considering the output of this list field, // so remove the empty optional fields and just grab the outputs let item_outputs = item_results |> outputs_for_required_or_nonempty |> result.all // pass our transformed input data to the next function/form. if // we errored we still do this with our stub so we can continue // processing all the fields in the form. if we're on the error track // we'll throw away the "output" made with this and just keep the error let next_form = next(item_outputs |> result.unwrap([definition.stub])) let form_output = next_form.decode(next_items) // ok, check which track we're on case form_output, item_outputs { // everything is good! pass along the output Ok(_), Ok(_) -> form_output // form has errors, but this field was good, so mark all states as valid Error(error_items), Ok(_) -> { let states = states |> list.map(fn(s) { Valid(s.value, s.requirement) }) let f = ListField(field, states, limit_check, widget) Error([f, ..error_items]) } // form was good so far, but this field errored, so need to // mark the errored states as invalid, and mark the successful fields as // valid, and then return all these fields we've got so far Ok(_), Error(_) -> { let states = list.map(item_results, state_from_parse_result) let f = ListField(field, states, limit_check, widget) Error([f, ..mark_all_fields_as_valid(next_items)]) } // form already has errors and this field errored, so add this field // to the list of errors, but first marking the invalid states as... invalid Error(error_items), Error(_) -> { let states = list.map(item_results, state_from_parse_result) let f = ListField(field, states, limit_check, widget) Error([f, ..error_items]) } } } Form(items: updated_items, decode:, stub: next_form.stub) } fn state_from_parse_result( result: ListParsingResult(input_output), ) -> InputState { let ListParsingResult(value, requirement, output) = result case output { Ok(_) -> Valid(value, requirement) Error(error) -> Invalid(value, requirement, error) } } fn outputs_for_required_or_nonempty( states: List(ListParsingResult(output)), ) -> List(Result(output, String)) { list.filter_map(states, fn(result) { case result.value, result.requirement { "", Optional -> Error(Nil) _, _ -> Ok(result.output) } }) } fn parse_list_state( state: InputState, parse: fn(String) -> Result(a, String), stub: a, ) -> ListParsingResult(a) { case state.value, state.requirement { "", Optional -> Ok(stub) _, _ -> parse(state.value) } |> ListParsingResult(state.value, state.requirement, _) } fn mark_above_max_values_as_invalid( list: List(ListParsingResult(output)), limit_check: LimitCheck, ) -> List(ListParsingResult(output)) { let count = list.fold(list, 0, fn(acc, r) { case r.value, r.requirement { "", Optional -> acc _, _ -> acc + 1 } }) case limit_check(count) { Ok(_) -> list Error(num_too_many) -> { let max_index = count - num_too_many let #(mapped_results, _) = list.fold(list, #([], 0), fn(acc, r) { let #(results, i) = acc case r.value, r.requirement, i >= max_index { "", Optional, _ -> #([r, ..results], i) _, _, False -> #([r, ..results], i + 1) v, r, True -> #( [ ListParsingResult(v, r, Error("exceeds maximum allowed items")), ..results ], i + 1, ) } }) mapped_results |> list.reverse } } } /// Add a list field to a form, but with no limits on the number of values that /// can be submitted. A list field is like a normal field except a consumer can /// submit multiple values, and it will return a `List` of the parsed values. /// /// The final argument is a callback that will be called when the form /// is being... constructed to look for more fields; validated to check for /// errors; and decoded to parse the input data. **For this reason, the /// callback should be a function without side effects.** It can be called any /// number of times. Don't do anything but create the type with the data you /// need! If you need to do decoding that has side effects, you should use /// `decode_then_try`. pub fn list( config: Config, is definition: Definition(widget, required_output, _), next next: fn(List(required_output)) -> Form(widget, form_output), ) -> Form(widget, form_output) { limited_list(limit_at_most(1_000_000), config, definition, next) } fn add_prefix_to_item(item: Item(widget), prefix: String) -> Item(widget) { case item { Field(item_details, state, widget) -> { let new_name = prefix <> "-" <> item_details.name Field(item_details |> set_name(new_name), state, widget) } ListField(item_details, states, limit_check, widget) -> { let new_name = prefix <> "-" <> item_details.name ListField(item_details |> set_name(new_name), states, limit_check, widget) } SubForm(item_details, sub_items) -> { let new_name = prefix <> "-" <> item_details.name SubForm(item_details |> set_name(new_name), sub_items) } } } /// Add a form as a subform. This will essentially append the fields from the /// subform to the current form, prefixing their names with the name of the /// subform. Form generators will still see the fields as a set though, so they /// can be marked up as a group for accessibility reasons. /// /// The final argument is a callback that will be called when the form /// is being... constructed to look for more fields; validated to check for /// errors; and decoded to parse the input data. **For this reason, the /// callback should be a function without side effects.** It can be called any /// number of times. Don't do anything but create the type with the data you /// need! If you need to do decoding that has side effects, you should use /// `decode_then_try`. pub fn subform( config: Config, form: Form(widget, sub_output), next: fn(sub_output) -> Form(widget, form_output), ) -> Form(widget, form_output) { let next_form = next(form.stub) let sub_items = form.items |> list.map(add_prefix_to_item(_, config.name)) let subform = SubForm(config, sub_items) let updated_items = [subform, ..next_form.items] let decode = fn(items: List(Item(widget))) { // pull out the latest version of this field to get latest input data let assert [SubForm(details, sub_items), ..next_items] = items let item_output = form.decode(sub_items) let next_form = next(item_output |> result.unwrap(form.stub)) let form_output = next_form.decode(next_items) // ok, check which track we're on case form_output, item_output { // everything is good! pass along the output Ok(_), Ok(_) -> form_output // form has errors, but this sub form was good, so add it to the list // of items as is. Error(next_error_items), Ok(_) -> { let sub = SubForm(details, sub_items |> mark_all_fields_as_valid) Error([sub, ..next_error_items]) } // form was good so far, but this sub form errored, so need to // hop on error track Ok(_), Error(error_items) -> { let sub = SubForm(details, error_items) Error([sub, ..mark_all_fields_as_valid(next_items)]) } // form already has errors and this form errored, so add this field // to the list of errors Error(next_error_items), Error(error_items) -> { let sub = SubForm(details, error_items) Error([sub, ..next_error_items]) } } } Form(updated_items, decode:, stub: next_form.stub) } /// Add input data to this form. This will set the raw string value of the fields. /// It does not trigger any parsing or decoding, so you can also use this to set /// default values (if you do it in your form generator function) or initial values /// (if you do it before rendering a blank form). /// /// The input data is a list of tuples, where the first element is the name of the /// field and the second element is the value to set. If the field does not exist /// the data is ignored. /// /// This resets the validation state of the fields that have data, so you'll need to /// re-validate or decode the form after setting data. pub fn data( form: Form(widget, output), input_data: List(#(String, String)), ) -> Form(widget, output) { let data = to_dict(input_data) let Form(items, decode, stub) = form Form(do_data(items, data), decode, stub) } fn do_data( items: List(Item(widget)), data: Dict(String, List(String)), ) -> List(Item(widget)) { list.map(items, fn(item) { let values = dict.get(data, item.config.name) case item, values { Field(detail, state, widget), Ok([_, ..] as values) -> { let assert Ok(last) = list.last(values) Field(detail, Unvalidated(last, state.requirement), widget) } ListField(detail, states, limit_check, widget), Ok(values) -> { let nonempty = list.filter(values, fn(v) { !string.is_empty(v) }) let num_nonempty = list.length(nonempty) let additional_fields = limit_check(num_nonempty) |> result.unwrap([]) let items = list.map2(states, nonempty, fn(s, v) { Unvalidated(v, s.requirement) }) |> list.append( nonempty |> list.drop(list.length(states)) |> list.map(fn(v) { Unvalidated(v, Optional) }), ) |> list.append(additional_fields) ListField(detail, items, limit_check, widget) } SubForm(detail, items), _ -> SubForm(detail, do_data(items, data)) _, _ -> item } }) } fn to_dict(values: List(#(String, String))) -> Dict(String, List(String)) { list.fold_right(values, dict.new(), fn(acc, kv) { let #(key, value) = kv dict.upsert(acc, key, fn(opt) { case opt { None -> [value] Some(values) -> [value, ..values] } }) }) } /// Decode the form. This means step through the fields one by one, parsing /// them individually. If any field fails to parse, the whole form is considered /// invalid, however it will still continue parsing the rest of the fields to /// collect all errors. This is useful for showing all errors at once. If no /// fields fail to parse, the decoded value is returned, which is the value given /// to `create_form`. /// /// If you'd like to decode the form but not get the output, so you can give /// feedback to a user in response to input, you can use `validate` or `validate_all`. pub fn decode( form: Form(widget, output), ) -> Result(output, Form(widget, output)) { case form.decode(form.items) { Ok(output) -> Ok(output) Error(items) -> Error(Form(..form, items:)) } } /// Decode the form, then apply a function to the output if it was successful. /// This is a very thin wrapper around `decode` and `result.try`, but the /// difference being it will pass the form along to the function as a second /// argument in addition to the successful result. This allows you to easily /// update the form fields with errors or other information based on the output. /// /// This is useful for situations where you can have errors in the form that /// aren't easily checked in simple parsing functions. Like, say, hitting a /// db to check if a username is taken. /// /// As a reminder, parse functions will be called multiple times for each field /// when the form is being made, validated and parsed, and should not contain /// side effects. This function is the proper way to add errors to fields from /// functions that have side effects. /// /// ```gleam /// make_form() /// |> data(form_data) /// |> decode_then_try(fn(username, form) { /// case is_username_taken(username) { /// Ok(false) -> Ok(form) /// Ok(true) -> field_error(form, "username", "Username is taken") /// } /// } pub fn decode_then_try( form: Form(widget, output), apply fun: fn(Form(widget, output), output) -> Result(c, Form(widget, output)), ) -> Result(c, Form(widget, output)) { form |> decode |> result.try(fn(output) { let items = form.items |> mark_all_fields_as_valid fun(Form(..form, items:), output) }) } fn mark_all_fields_as_valid(items: List(Item(widget))) -> List(Item(widget)) { list.map(items, fn(item) { case item { Field(field, state, widget) -> Field(field, Valid(state.value, state.requirement), widget) ListField(field, states, limit_check, widget) -> { let new_states = states |> list.map(fn(state) { Valid(state.value, state.requirement) }) ListField(field, new_states, limit_check, widget) } SubForm(subform, items) -> SubForm(subform, mark_all_fields_as_valid(items)) } }) } /// Validate specific fields of the form. This is similar to `decode`, but /// instead of returning the decoded output if there are no errors, it returns /// the valid form. This is useful for if you want to be able to give feedback /// to the user about whether certain fields are valid or not. For example, you /// could just validate only fields that the user has interacted with. pub fn validate( form: Form(widget, output), names: List(String), ) -> Form(widget, output) { case form.decode(form.items) { Ok(_) -> { let items = list.map(form.items, fn(parsed_item) { let item_name = get_item_name(parsed_item) case list.find(names, fn(name) { item_name == name }) { Ok(_) -> { let assert Ok(item) = [parsed_item] |> mark_all_fields_as_valid |> list.first item } Error(_) -> { let assert Ok(original_item) = get(form, item_name) original_item } } }) Form(..form, items:) } Error(items) -> { let items = list.map(items, fn(parsed_item) { let item_name = get_item_name(parsed_item) case list.find(names, fn(name) { item_name == name }) { Ok(_) -> parsed_item Error(_) -> { let assert Ok(original_item) = get(form, item_name) original_item } } }) Form(..form, items:) } } } /// Validate all the fields in the form. This is similar to `decode`, but /// instead of returning the decoded output if there are no errors, it returns /// the valid form. This is useful for if you want to be able to give feedback /// to the user about whether certain fields are valid or not. pub fn validate_all(form: Form(widget, output)) -> Form(widget, output) { form.items |> list.map(get_item_name) |> validate(form, _) } /// Get each [`Item`](https://hexdocs.pm/formz/formz.html#Item) added /// to the form. Any time a field, list field, or subform are added, a `Item` /// is created. Use this to loop through all the fields of your form and /// generate HTML for them. pub fn items(form: Form(widget, output)) -> List(Item(widget)) { form.items } /// Get the [`Item`](https://hexdocs.pm/formz/formz.html#Item) with the /// given name. If multiple items have the same name, the first one is returned. pub fn get( form: Form(widget, output), name: String, ) -> Result(Item(widget), Nil) { list.find(form.items, fn(item) { name == get_item_name(item) }) } /// Update the [`Item`](https://hexdocs.pm/formz/formz.html#Item) with /// the given name using the provided function. If multiple items have the same /// name, it will be called on all of them. pub fn update( form: Form(widget, output), name: String, fun: fn(Item(widget)) -> Item(widget), ) { let items = do_update(form.items, name, fun) Form(..form, items:) } fn do_update( items: List(Item(widget)), name: String, fun: fn(Item(widget)) -> Item(widget), ) -> List(Item(widget)) { list.map(items, fn(item) { case item { Field(detail, _, _) if detail.name == name -> fun(item) ListField(detail, _, _, _) if detail.name == name -> fun(item) SubForm(detail, items) -> { let sub = SubForm(detail, do_update(items, name, fun)) case detail.name == name { True -> fun(sub) False -> sub } } _ -> item } }) } /// Update the `Field` [details](https://hexdocs.pm/formz/formz/field.html) with /// the given name using the provided function. If multiple items have the same /// name, it will be called on all of them. If no items have the given name, /// or an item with the given name exists but isn't a `Field`, this function /// will do nothing. /// /// ```gleam /// let form = make_form() /// update_field(form, "name", field.set_label(_, "Full Name")) /// ``` pub fn update_config( form: Form(widget, output), name: String, fun: fn(Config) -> Config, ) -> Form(widget, output) { update(form, name, fn(item) { case item { Field(details, ..) -> Field(..item, config: fun(details)) ListField(details, ..) -> ListField(..item, config: fun(details)) SubForm(details, ..) -> SubForm(..item, config: fun(details)) } }) } /// Convenience function for setting the `InputState` of a field to /// Invalid with a given error message. /// /// ### Example /// /// ``` /// field_error(form, "username", "Username is taken") /// ``` pub fn field_error( form: Form(widget, output), name: String, str: String, ) -> Form(widget, output) { update(form, name, fn(item) { case item { Field(field, state, widget) -> Field(field, Invalid(state.value, state.requirement, str), widget) _ -> item } }) } /// Convenience function for setting the `InputState`s of a list field. This /// takes a list of Results, where the Ok means the input is `Valid` and /// `Error` means the input is `Invalid` with the given error message. /// /// This does not clear any existing errors, it will just set the errors marked /// in the input list. If you want to clear errors you'll have to use the /// `update` function and do it manually. /// /// ### Example /// /// ``` /// listfield_errors(form, "pet_names", [Ok(Nil), Ok(Nil), Error("Must be a cat")]) /// ``` pub fn listfield_errors( form: Form(widget, output), name: String, errors: List(Result(Nil, String)), ) -> Form(widget, output) { update(form, name, fn(item) { case item { ListField(field, states, limit_check, widget) -> { let invalid_states = list.map2(states, errors, fn(state, err) { case err { Error(str) -> Invalid(state.value, state.requirement, str) Ok(Nil) -> state } }) ListField(field, invalid_states, limit_check, widget) } _ -> item } }) } fn get_item_name(item: Item(widget)) -> String { case item { Field(field, _, _) -> field.name ListField(field, _, _, _) -> field.name SubForm(subform, _) -> subform.name } } /// Create a simple `Definition` that is parsed as an `Option` if the field /// is empty. See [formz_string](https://hexdocs.pm/formz_string/formz_string/definitions.html) /// for more examples of making widgets and definitions. pub fn definition( widget widget: widget, parse parse: fn(String) -> Result(required, String), stub stub: required, ) { Definition( widget:, parse:, stub:, optional_parse: fn(fun, str) { case str { "" -> Ok(option.None) _ -> fun(str) |> result.map(option.Some) } }, optional_stub: option.None, ) } /// Create a `Definition` that can parse to any type if the field is optional. /// This takes two functions. The first, `parse`, is the "required" parse /// function, which takes the raw string value, and turns it into the required /// type. The second, `optional_parse`, is a function that takes the normal /// parse function and the raw string value, and it is supposed to check the /// input string: if it is empty, return an `Ok` with a value of the optional /// type; and if it's not empty use the normal parse function. /// /// See [formz_string](https://hexdocs.pm/formz_string/formz_string/definitions.html) /// for more examples of making widgets and definitions. pub fn definition_with_custom_optional( widget widget: widget, parse parse: fn(String) -> Result(required, String), stub stub: required, optional_parse optional_parse: fn( fn(String) -> Result(required, String), String, ) -> Result(optional, String), optional_stub optional_stub: optional, ) -> Definition(widget, required, optional) { Definition(widget:, parse:, stub:, optional_parse:, optional_stub:) } /// Chain additional validation onto the `parse` function. This is /// useful if you don't need to change the returned type, but might have /// additional constraints. Like say, requiring a `String` to be at least /// a certain length, or that an Int must be positive. /// /// ### Example /// ```gleam /// field /// |> validate(fn(i) { /// case i > 0 { /// True -> Ok(i) /// False -> Error("must be positive") /// } /// }), /// ``` pub fn verify( def: Definition(widget, a, b), fun: fn(a) -> Result(a, String), ) -> Definition(widget, a, b) { Definition(..def, parse: fn(val) { val |> def.parse |> result.try(fun) }) } // pub fn transform( // def: Definition(widget, old_required, old_optional), // transform: fn(old_required) -> Result(new_required, String), // stub: new_required, // ) -> Definition(widget, new_required, option.Option(new_required)) { // let Definition(widget, parse, _, _, _) = def // Definition( // widget:, // parse: fn(val) { val |> parse |> result.try(transform) }, // stub:, // optional_parse: fn(parse, str) { // case str { // "" -> Ok(option.None) // _ -> parse(str) |> result.map(option.Some) // } // }, // optional_stub: option.None, // ) // } // pub fn transform_with_custom_optional( // def: Definition(widget, old_required, old_optional), // transform: fn(old_required) -> Result(new_required, String), // stub: new_required, // optional_parse: fn(fn(String) -> Result(new_required, String), String) -> // Result(new_optional, String), // optional_stub: new_optional, // ) -> Definition(widget, new_required, new_optional) { // let Definition(widget, parse, _, _, _) = def // Definition( // widget:, // parse: fn(val) { val |> parse |> result.try(transform) }, // stub:, // optional_parse:, // optional_stub:, // ) // } /// Update the widget of a definition. pub fn widget( def: Definition(widget, a, b), widget: widget, ) -> Definition(widget, a, b) { Definition(..def, widget:) } @internal pub fn get_parse( def: Definition(widget, a, b), ) -> fn(String) -> Result(a, String) { def.parse } @internal pub fn get_optional_parse( def: Definition(widget, a, b), ) -> fn(fn(String) -> Result(a, String), String) -> Result(b, String) { def.optional_parse } @internal pub fn get_states(form: Form(widget, output)) -> List(InputState) { form.items |> do_get_states |> list.reverse } fn do_get_states(items: List(Item(widget))) -> List(InputState) { list.fold(items, [], fn(acc, item) { case item { Field(_, state, _) -> [state, ..acc] ListField(_, states, _, _) -> list.append(states |> list.reverse, acc) SubForm(_, sub_items) -> list.flatten([do_get_states(sub_items), acc]) } }) } /// Create a field with the given name. /// /// It uses /// [justin.sentence_case](https://hexdocs.pm/justin/justin.html#sentence_case) /// to create an initial label. You can override the label with the `set_label` /// function. I don't know if this is very english-centric, so let me know if /// this is a bad experience in other languages and I'll consider /// something else. /// /// ```gleam /// field("name") /// |> set_label("Full Name") /// ``` pub fn named(name: String) -> Config { Config( name: name, label: justin.sentence_case(name), help_text: "", disabled: False, ) } /// Set the name of the field. This is the key that will be used for the data. pub fn set_name(config: Config, name: String) -> Config { Config(..config, name:) } /// Set the label of the field. This is the primary text that will be displayed /// to the user about the field. pub fn set_label(config: Config, label: String) -> Config { Config(..config, label:) } /// Additional instructions or help text to display to the user about the field. pub fn set_help_text(config: Config, help_text: String) -> Config { Config(..config, help_text:) } /// Set the `disabled` flag directly. pub fn set_disabled(config: Config, disabled: Bool) -> Config { Config(..config, disabled:) } /// Mark the form `Item` as disabled. This will prevent the user from /// interacting with the field. If you do this for a subform, it will /// only work if the form generator renders the subform as a `fieldset`. /// For example, HTML does not allow you to mark inputs in a `
` /// disabled as group but you can do this with a `
`. pub fn make_disabled(config: Config) -> Config { set_disabled(config, True) }