diff --git a/cot-core/src/html.rs b/cot-core/src/html.rs
index 10d5c2dda..a73709408 100644
--- a/cot-core/src/html.rs
+++ b/cot-core/src/html.rs
@@ -191,7 +191,7 @@ impl HtmlTag {
}
}
- /// Creates a new `HtmlTag` instance for an input element.
+ /// Creates a new `HtmlTag` instance for an [input] element.
///
/// # Examples
///
@@ -201,6 +201,8 @@ impl HtmlTag {
/// let input = HtmlTag::input("text");
/// assert_eq!(input.render().as_str(), "");
/// ```
+ ///
+ /// [input]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
#[must_use]
pub fn input(input_type: &str) -> Self {
let mut input = Self::new("input");
@@ -208,6 +210,38 @@ impl HtmlTag {
input
}
+ /// Creates a new `HtmlTag` instance for a [datalist] element.
+ ///
+ /// # Examples
+ /// ```
+ /// use cot::html::HtmlTag;
+ /// let data_list = HtmlTag::data_list(vec!["Option 1", "Option 2"], "my-datalist");
+ /// let rendered = data_list.render();
+ /// ```
+ ///
+ /// [datalist]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/datalist
+ #[must_use]
+ pub fn data_list(list: I, id: &str) -> Self
+ where
+ I: IntoIterator- ,
+ S: AsRef,
+ {
+ let mut data_list = Self::new("datalist");
+ data_list.attr("id", id);
+
+ let mut options: Vec = Vec::new();
+
+ for item in list {
+ let l = item.as_ref();
+ let mut option = HtmlTag::new("option");
+ option.attr("value", l);
+ options.push(HtmlNode::Tag(option));
+ }
+
+ data_list.children = options;
+ data_list
+ }
+
/// Adds an attribute to the HTML tag.
///
/// # Safety
diff --git a/cot/src/form/fields.rs b/cot/src/form/fields.rs
index 1fcb4f14f..fea8882f1 100644
--- a/cot/src/form/fields.rs
+++ b/cot/src/form/fields.rs
@@ -10,16 +10,18 @@ use std::num::{
};
use askama::filters::HtmlSafe;
-pub use attrs::Step;
+pub use attrs::{AutoCapitalize, AutoComplete, Capture, Dir, List, Step};
pub use chrono::{
- DateField, DateFieldOptions, DateTimeField, DateTimeFieldOptions, DateTimeWithTimezoneField,
- DateTimeWithTimezoneFieldOptions, TimeField, TimeFieldOptions,
+ DateField, DateFieldOptions, DateFieldOptionsBuilder, DateTimeField, DateTimeFieldOptions,
+ DateTimeFieldOptionsBuilder, DateTimeWithTimezoneField, DateTimeWithTimezoneFieldOptions,
+ DateTimeWithTimezoneFieldOptionsBuilder, TimeField, TimeFieldOptions, TimeFieldOptionsBuilder,
};
-pub use files::{FileField, FileFieldOptions, InMemoryUploadedFile};
+use derive_builder::Builder;
+pub use files::{FileField, FileFieldOptions, FileFieldOptionsBuilder, InMemoryUploadedFile};
pub(crate) use select::check_required_multiple;
pub use select::{
- SelectAsFormField, SelectChoice, SelectField, SelectFieldOptions, SelectMultipleField,
- SelectMultipleFieldOptions,
+ SelectAsFormField, SelectChoice, SelectField, SelectFieldOptions, SelectFieldOptionsBuilder,
+ SelectMultipleField, SelectMultipleFieldOptions, SelectMultipleFieldOptionsBuilder,
};
use crate::auth::PasswordHash;
@@ -68,16 +70,127 @@ macro_rules! impl_form_field {
}
};
}
+
+macro_rules! impl_field_options_builder {
+ (
+ $ty:ident < $($gen:ident),+ >,
+ $builder:ident < $($gen2:ident),+ >
+ { $($field:ident),+ $(,)? }
+ ) => {
+ impl<$($gen: Clone),+> $ty<$($gen),+> {
+ #[doc = concat!(
+ "Creates a new [`", stringify!($builder), "`] to build a ",
+ "[`", stringify!($ty), "`].\n\n",
+ "# Examples\n\n",
+ "```\n",
+ "# use cot::form::fields::", stringify!($ty), ";\n",
+ "let options = ", stringify!($ty), "::::builder().build();\n",
+ "```",
+ )]
+ #[must_use]
+ pub fn builder() -> $builder<$($gen),+> {
+ $builder::default()
+ }
+ }
+
+ impl<$($gen2: Clone),+> $builder<$($gen2),+> {
+ #[doc = concat!(
+ "Builds the [`", stringify!($ty), "`], falling back to ",
+ "defaults for any field that wasn't explicitly set.\n\n",
+ "# Examples\n\n",
+ "```\n",
+ "# use cot::form::fields::", stringify!($ty), ";\n",
+ "let options = ", stringify!($ty), "::::builder().build();\n",
+ "```",
+ )]
+ #[must_use]
+ pub fn build(&self) -> $ty<$($gen2),+> {
+ $ty {
+ $( $field: self.$field.clone().unwrap_or_default(), )+
+ }
+ }
+ }
+ };
+
+ ($ty:ident, $builder:ident { $($field:ident),+ $(,)? }) => {
+ impl $ty {
+ #[doc = concat!(
+ "Creates a new [`", stringify!($builder), "`] to build a ",
+ "[`", stringify!($ty), "`].\n\n",
+ "# Examples\n\n",
+ "```\n",
+ "# use cot::form::fields::", stringify!($ty), ";\n",
+ "let options = ", stringify!($ty), "::builder().build();\n",
+ "```",
+ )]
+ #[must_use]
+ pub fn builder() -> $builder {
+ $builder::default()
+ }
+ }
+
+ impl $builder {
+ #[doc = concat!(
+ "Builds the [`", stringify!($ty), "`], falling back to ",
+ "defaults for any field that wasn't explicitly set.\n\n",
+ "# Examples\n\n",
+ "```\n",
+ "# use cot::form::fields::", stringify!($ty), ";\n",
+ "let options = ", stringify!($ty), "::builder().build();\n",
+ "```",
+ )]
+ #[must_use]
+ pub fn build(&self) -> $ty {
+ $ty {
+ $( $field: self.$field.clone().unwrap_or_default(), )+
+ }
+ }
+ }
+ };
+}
+
+pub(crate) use impl_field_options_builder;
pub(crate) use impl_form_field;
impl_form_field!(StringField, StringFieldOptions, "a string");
/// Custom options for a [`StringField`].
-#[derive(Debug, Default, Copy, Clone)]
+#[derive(Debug, Default, Clone, Builder)]
+#[builder(build_fn(skip, error = std::convert::Infallible))]
+#[builder(setter(strip_option))]
+#[non_exhaustive]
pub struct StringFieldOptions {
/// The maximum length of the field. Used to set the `maxlength` attribute
/// in the HTML input element.
pub max_length: Option,
+ /// The minimum length of the field. Used to set the `minlength` attribute
+ /// in the HTML input element.
+ pub min_length: Option,
+ /// The size of the field. Used to set the `size` attribute in the HTML
+ /// input element.
+ pub size: Option,
+ /// Corresponds to the [`AutoCapitalize`] attribute in the HTML input
+ /// element.
+ pub autocapitalize: Option,
+ /// Corresponds to the [`AutoComplete`] attribute in the HTML input element.
+ pub autocomplete: Option,
+ /// The direction of the text input, which can be set to `ltr`
+ /// (left-to-right) or `rtl` (right-to-left). This corresponds to the
+ /// [`Dir`] attribute in the HTML input element.
+ pub dir: Option,
+ /// The [`dirname`] attribute in the HTML input element, which is used to
+ /// specify the direction of the text input.
+ ///
+ /// [`dirname`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#dirname
+ pub dirname: Option,
+ /// A [`List`] of options for the `datalist` element, which can be used to
+ /// provide predefined options for the input.
+ pub list: Option
,
+ /// The placeholder text for the input field, which is displayed when the
+ /// field is empty.
+ pub placeholder: Option,
+ /// If `true`, the field is read-only and cannot be modified by the user.
+ pub readonly: Option,
}
impl Display for StringField {
@@ -91,6 +204,48 @@ impl Display for StringField {
if let Some(max_length) = self.custom_options.max_length {
tag.attr("maxlength", max_length.to_string());
}
+ if let Some(min_length) = self.custom_options.min_length {
+ tag.attr("minlength", min_length.to_string());
+ }
+
+ if let Some(size) = self.custom_options.size {
+ tag.attr("size", size.to_string());
+ }
+
+ if let Some(autocapitalize) = &self.custom_options.autocapitalize {
+ tag.attr("autocapitalize", autocapitalize.to_string());
+ }
+
+ if let Some(autocomplete) = &self.custom_options.autocomplete {
+ tag.attr("autocomplete", autocomplete.to_string());
+ }
+
+ if let Some(dir) = &self.custom_options.dir {
+ tag.attr("dir", dir.as_str());
+ }
+
+ if let Some(dirname) = &self.custom_options.dirname {
+ tag.attr("dirname", dirname);
+ }
+
+ if let Some(list) = &self.custom_options.list {
+ let list_id = format!("__{}_datalist", self.id());
+ tag.attr("list", &list_id);
+
+ let data_list = HtmlTag::data_list(list.clone(), &list_id);
+ tag.push_tag(data_list);
+ }
+
+ if let Some(placeholder) = &self.custom_options.placeholder {
+ tag.attr("placeholder", placeholder);
+ }
+
+ if let Some(readonly) = self.custom_options.readonly
+ && readonly
+ {
+ tag.bool_attr("readonly");
+ }
+
if let Some(value) = &self.value {
tag.attr("value", value);
}
@@ -143,11 +298,29 @@ impl AsFormField for LimitedString {
impl_form_field!(PasswordField, PasswordFieldOptions, "a password");
/// Custom options for a [`PasswordField`].
-#[derive(Debug, Default, Copy, Clone)]
+#[derive(Debug, Default, Clone, Builder)]
+#[builder(build_fn(skip, error = std::convert::Infallible))]
+#[builder(setter(strip_option))]
+#[non_exhaustive]
pub struct PasswordFieldOptions {
/// The maximum length of the field. Used to set the `maxlength` attribute
/// in the HTML input element.
pub max_length: Option,
+ /// The minimum length of the field. Used to set the `minlength` attribute
+ /// in the HTML input element.
+ pub min_length: Option,
+ /// The size of the field. Used to set the [`size`] attribute in the HTML
+ /// input element.
+ ///
+ /// [`size`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#size
+ pub size: Option,
+ /// Corresponds to the [`AutoComplete`] attribute in the HTML input element.
+ pub autocomplete: Option,
+ /// The placeholder text for the input field, which is displayed when the
+ /// field is empty.
+ pub placeholder: Option,
+ /// If `true`, the field is read-only and cannot be modified by the user.
+ pub readonly: Option,
}
impl Display for PasswordField {
@@ -161,6 +334,28 @@ impl Display for PasswordField {
if let Some(max_length) = self.custom_options.max_length {
tag.attr("maxlength", max_length.to_string());
}
+
+ if let Some(min_length) = self.custom_options.min_length {
+ tag.attr("minlength", min_length.to_string());
+ }
+
+ if let Some(size) = self.custom_options.size {
+ tag.attr("size", size.to_string());
+ }
+
+ if let Some(autocomplete) = &self.custom_options.autocomplete {
+ tag.attr("autocomplete", autocomplete.to_string());
+ }
+
+ if let Some(placeholder) = &self.custom_options.placeholder {
+ tag.attr("placeholder", placeholder);
+ }
+ if let Some(readonly) = self.custom_options.readonly
+ && readonly
+ {
+ tag.bool_attr("readonly");
+ }
+
// we don't set the value attribute for password fields
// to avoid leaking the password in the HTML
@@ -218,7 +413,10 @@ impl AsFormField for PasswordHash {
impl_form_field!(EmailField, EmailFieldOptions, "an email");
/// Custom options for [`EmailField`]
-#[derive(Debug, Default, Copy, Clone)]
+#[derive(Debug, Default, Clone, Builder)]
+#[builder(build_fn(skip, error = std::convert::Infallible))]
+#[builder(setter(strip_option))]
+#[non_exhaustive]
pub struct EmailFieldOptions {
/// The maximum length of the field used to set the `maxlength` attribute
/// in the HTML input element.
@@ -226,11 +424,38 @@ pub struct EmailFieldOptions {
/// The minimum length of the field used to set the `minlength` attribute
/// in the HTML input element.
pub min_length: Option,
+ /// The size of the field used to set the [`size`] attribute in the HTML
+ /// input element.
+ ///
+ /// [`size`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#size
+ pub size: Option,
+ /// Corresponds to the [`AutoCapitalize`] attribute in the HTML input
+ /// element.
+ pub autocomplete: Option,
+ /// The direction of the text input, which can be set to `ltr`
+ /// (left-to-right) or `rtl` (right-to-left). This corresponds to the
+ /// [`Dir`] attribute in the HTML input element.
+ pub dir: Option,
+ /// The [`dirname`] attribute in the HTML input element, which is used to
+ /// specify the direction of the text input.
+ ///
+ /// [`dirname`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#dirname
+ pub dirname: Option,
+ /// A [`List`] of options for the `datalist` element, which can be used to
+ /// provide predefined options for the input.
+ pub list: Option,
+ /// The placeholder text for the input field, which is displayed when the
+ /// field is empty.
+ pub placeholder: Option,
+ /// If `true`, the field is read-only and cannot be modified by the user.
+ pub readonly: Option,
}
impl Display for EmailField {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut tag = HtmlTag::input("email");
+ let mut data_list: Option = None;
+
tag.attr("name", self.id());
tag.attr("id", self.id());
if self.options.required {
@@ -242,10 +467,51 @@ impl Display for EmailField {
if let Some(min_length) = self.custom_options.min_length {
tag.attr("minlength", min_length.to_string());
}
+
+ if let Some(size) = self.custom_options.size {
+ tag.attr("size", size.to_string());
+ }
+
+ if let Some(autocomplete) = &self.custom_options.autocomplete {
+ tag.attr("autocomplete", autocomplete.to_string());
+ }
+
+ if let Some(dir) = &self.custom_options.dir {
+ tag.attr("dir", dir.as_str());
+ }
+
+ if let Some(dirname) = &self.custom_options.dirname {
+ tag.attr("dirname", dirname);
+ }
+
+ if let Some(list) = &self.custom_options.list {
+ let list_id = format!("__{}_datalist", self.id());
+ tag.attr("list", &list_id);
+
+ data_list = Some(HtmlTag::data_list(list.clone(), &list_id));
+ }
+
+ if let Some(placeholder) = &self.custom_options.placeholder {
+ tag.attr("placeholder", placeholder);
+ }
+ if let Some(readonly) = self.custom_options.readonly
+ && readonly
+ {
+ tag.bool_attr("readonly");
+ }
+
if let Some(value) = &self.value {
tag.attr("value", value);
}
+ if let Some(data_list) = data_list {
+ let mut wrapper = HtmlTag::new("div");
+ wrapper
+ .attr("id", format!("__{}_datalist_wrapper", self.id()))
+ .push_tag(tag)
+ .push_tag(data_list);
+ return write!(f, "{}", wrapper.render());
+ }
write!(f, "{}", tag.render())
}
}
@@ -290,10 +556,13 @@ impl AsFormField for Email {
impl HtmlSafe for EmailField {}
-impl_form_field!(IntegerField, IntegerFieldOptions, "an integer", T: Integer);
+impl_form_field!(IntegerField, IntegerFieldOptions, "an integer", T: Integer + Display);
/// Custom options for a [`IntegerField`].
-#[derive(Debug, Copy, Clone)]
+#[derive(Debug, Clone, Builder)]
+#[builder(build_fn(skip, error = std::convert::Infallible))]
+#[builder(setter(strip_option))]
+#[non_exhaustive]
pub struct IntegerFieldOptions {
/// The minimum value of the field. Used to set the `min` attribute in the
/// HTML input element.
@@ -301,6 +570,14 @@ pub struct IntegerFieldOptions {
/// The maximum value of the field. Used to set the `max` attribute in the
/// HTML input element.
pub max: Option,
+ /// The placeholder text for the input field, which is displayed when the
+ /// field is empty.
+ pub placeholder: Option,
+ /// If `true`, the field is read-only and cannot be modified by the user.
+ pub readonly: Option,
+ /// The step size for the field. Used to set the [`Step`] attribute in the
+ /// HTML input element.
+ pub step: Option>,
}
impl Default for IntegerFieldOptions {
@@ -308,11 +585,14 @@ impl Default for IntegerFieldOptions {
Self {
min: T::MIN,
max: T::MAX,
+ placeholder: None,
+ readonly: None,
+ step: None,
}
}
}
-impl Display for IntegerField {
+impl Display for IntegerField {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut tag = HtmlTag::input("number");
tag.attr("name", self.id());
@@ -326,6 +606,19 @@ impl Display for IntegerField {
if let Some(max) = &self.custom_options.max {
tag.attr("max", max.to_string());
}
+
+ if let Some(placeholder) = &self.custom_options.placeholder {
+ tag.attr("placeholder", placeholder);
+ }
+ if let Some(readonly) = self.custom_options.readonly
+ && readonly
+ {
+ tag.bool_attr("readonly");
+ }
+ if let Some(step) = &self.custom_options.step {
+ tag.attr("step", step.to_string());
+ }
+
if let Some(value) = &self.value {
tag.attr("value", value);
}
@@ -334,7 +627,7 @@ impl Display for IntegerField {
}
}
-impl HtmlSafe for IntegerField {}
+impl HtmlSafe for IntegerField {}
/// A trait for numerical types that optionally have minimum and maximum values.
///
@@ -465,7 +758,10 @@ impl_integer_as_form_field!(NonZeroUsize);
impl_form_field!(BoolField, BoolFieldOptions, "a boolean");
/// Custom options for a [`BoolField`].
-#[derive(Debug, Default, Copy, Clone)]
+#[derive(Debug, Default, Copy, Clone, Builder)]
+#[builder(build_fn(skip, error = std::convert::Infallible))]
+#[builder(setter(strip_option))]
+#[non_exhaustive]
pub struct BoolFieldOptions {
/// If `true`, the field must be checked to be considered valid.
pub must_be_true: Option,
@@ -647,10 +943,13 @@ pub(crate) fn check_required(field: &T) -> Result<&str, FormFieldV
}
}
-impl_form_field!(FloatField, FloatFieldOptions, "a float", T: Float);
+impl_form_field!(FloatField, FloatFieldOptions, "a float", T: Float + Display);
/// Custom options for a [`FloatField`].
-#[derive(Debug, Copy, Clone)]
+#[derive(Debug, Clone, Builder)]
+#[builder(build_fn(skip, error = std::convert::Infallible))]
+#[builder(setter(strip_option))]
+#[non_exhaustive]
pub struct FloatFieldOptions {
/// The minimum value of the field. Used to set the `min` attribute in the
/// HTML input element.
@@ -658,6 +957,14 @@ pub struct FloatFieldOptions {
/// The maximum value of the field. Used to set the `max` attribute in the
/// HTML input element.
pub max: Option,
+ /// The placeholder text for the input field, which is displayed when the
+ /// field is empty.
+ pub placeholder: Option,
+ /// If `true`, the field is read-only and cannot be modified by the user.
+ pub readonly: Option,
+ /// The step size for the field. Used to set the [`Step`] attribute in the
+ /// HTML input element.
+ pub step: Option>,
}
impl Default for FloatFieldOptions {
@@ -665,11 +972,14 @@ impl Default for FloatFieldOptions {
Self {
min: T::MIN,
max: T::MAX,
+ placeholder: None,
+ readonly: None,
+ step: None,
}
}
}
-impl Display for FloatField {
+impl Display for FloatField {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut tag: HtmlTag = HtmlTag::input("number");
tag.attr("name", self.id());
@@ -684,6 +994,17 @@ impl Display for FloatField {
if let Some(max) = &self.custom_options.max {
tag.attr("max", max.to_string());
}
+ if let Some(placeholder) = &self.custom_options.placeholder {
+ tag.attr("placeholder", placeholder);
+ }
+ if let Some(readonly) = self.custom_options.readonly
+ && readonly
+ {
+ tag.bool_attr("readonly");
+ }
+ if let Some(step) = &self.custom_options.step {
+ tag.attr("step", step.to_string());
+ }
if let Some(value) = &self.value {
tag.attr("value", value);
}
@@ -692,7 +1013,7 @@ impl Display for FloatField {
}
}
-impl HtmlSafe for FloatField {}
+impl HtmlSafe for FloatField {}
/// A trait for types that can be represented as a float.
///
@@ -777,19 +1098,92 @@ impl_float_as_form_field!(f64);
impl_form_field!(UrlField, UrlFieldOptions, "a URL");
/// Custom options for a [`UrlField`].
-#[derive(Debug, Default, Copy, Clone)]
-pub struct UrlFieldOptions;
+#[derive(Debug, Default, Clone, Builder)]
+#[builder(build_fn(skip, error = std::convert::Infallible))]
+#[builder(setter(strip_option))]
+#[non_exhaustive]
+pub struct UrlFieldOptions {
+ /// The maximum length of the field. Used to set the `maxlength` attribute
+ /// in the HTML input element.
+ pub max_length: Option,
+ /// The minimum length of the field. Used to set the `minlength` attribute
+ /// in the HTML input element.
+ pub min_length: Option,
+ /// The size of the field. Used to set the [`size`]attribute in the HTML
+ /// input element.
+ ///
+ /// [`size`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#size
+ pub size: Option,
+ /// The [`AutoComplete`] attribute in the HTML input element, which is used
+ /// to specify how the browser should handle autocomplete for the input.
+ pub autocomplete: Option,
+ /// The direction of the text input, which can be set to `ltr`
+ /// (left-to-right) or `rtl` (right-to-left). This corresponds to the
+ /// [`Dir`] attribute in the HTML input element.
+ pub dir: Option,
+ /// The [`dirname`] attribute in the HTML input element, which is used to
+ /// specify the direction of the text input.
+ ///
+ /// [`dirname`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#dirname
+ pub dirname: Option,
+ /// The [`List`] of options for the `datalist` element, which can be used to
+ /// provide predefined options for the input.
+ pub list: Option,
+ /// The placeholder text for the input field, which is displayed when the
+ /// field is empty.
+ pub placeholder: Option,
+ /// If `true`, the field is read-only and cannot be modified by the user.
+ pub readonly: Option,
+}
impl Display for UrlField {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- // no custom options
- let _ = self.custom_options;
let mut tag = HtmlTag::input("url");
tag.attr("name", self.id());
tag.attr("id", self.id());
if self.options.required {
tag.bool_attr("required");
}
+ if let Some(max_length) = self.custom_options.max_length {
+ tag.attr("maxlength", max_length.to_string());
+ }
+ if let Some(min_length) = self.custom_options.min_length {
+ tag.attr("minlength", min_length.to_string());
+ }
+
+ if let Some(size) = self.custom_options.size {
+ tag.attr("size", size.to_string());
+ }
+
+ if let Some(autocomplete) = &self.custom_options.autocomplete {
+ tag.attr("autocomplete", autocomplete.to_string());
+ }
+
+ if let Some(dir) = &self.custom_options.dir {
+ tag.attr("dir", dir.as_str());
+ }
+
+ if let Some(dirname) = &self.custom_options.dirname {
+ tag.attr("dirname", dirname);
+ }
+
+ if let Some(list) = &self.custom_options.list {
+ let list_id = format!("__{}_datalist", self.id());
+ tag.attr("list", &list_id);
+
+ let data_list = HtmlTag::data_list(list, &list_id);
+ tag.push_tag(data_list);
+ }
+
+ if let Some(placeholder) = &self.custom_options.placeholder {
+ tag.attr("placeholder", placeholder);
+ }
+ if let Some(readonly) = self.custom_options.readonly
+ && readonly
+ {
+ tag.bool_attr("readonly");
+ }
+
if let Some(value) = &self.value {
tag.attr("value", value);
}
@@ -817,6 +1211,64 @@ impl AsFormField for Url {
}
}
+impl_field_options_builder!(
+ StringFieldOptions,
+ StringFieldOptionsBuilder {
+ max_length,
+ min_length,
+ size,
+ autocapitalize,
+ autocomplete,
+ dir,
+ dirname,
+ list,
+ placeholder,
+ readonly
+ }
+);
+impl_field_options_builder!(
+ PasswordFieldOptions,
+ PasswordFieldOptionsBuilder {
+ max_length,
+ min_length,
+ size,
+ autocomplete,
+ placeholder,
+ readonly
+ }
+);
+impl_field_options_builder!(
+ EmailFieldOptions,
+ EmailFieldOptionsBuilder {
+ max_length,
+ min_length,
+ size,
+ autocomplete,
+ dir,
+ dirname,
+ list,
+ placeholder,
+ readonly
+ }
+);
+impl_field_options_builder!(IntegerFieldOptions, IntegerFieldOptionsBuilder{min, max, placeholder, readonly, step});
+impl_field_options_builder!(FloatFieldOptions, FloatFieldOptionsBuilder{min, max, placeholder, readonly, step});
+impl_field_options_builder!(
+ UrlFieldOptions,
+ UrlFieldOptionsBuilder {
+ max_length,
+ min_length,
+ size,
+ autocomplete,
+ dir,
+ dirname,
+ list,
+ placeholder,
+ readonly
+ }
+);
+impl_field_options_builder!(BoolFieldOptions, BoolFieldOptionsBuilder { must_be_true });
+
#[cfg(test)]
mod tests {
use super::*;
@@ -830,14 +1282,33 @@ mod tests {
name: "test".to_owned(),
required: true,
},
- StringFieldOptions {
- max_length: Some(10),
- },
+ StringFieldOptions::builder()
+ .max_length(10)
+ .min_length(5)
+ .size(15)
+ .autocapitalize(AutoCapitalize::Words)
+ .autocomplete(AutoComplete::Value("foo bar".to_string()))
+ .dir(Dir::Ltr)
+ .dirname("dir".to_string())
+ .list(List::new(["bar", "baz"]))
+ .placeholder("Enter text".to_string())
+ .readonly(true)
+ .build(),
);
let html = field.to_string();
assert!(html.contains("type=\"text\""));
+ assert!(html.contains("name=\"test\""));
+ assert!(html.contains("id=\"test\""));
assert!(html.contains("required"));
assert!(html.contains("maxlength=\"10\""));
+ assert!(html.contains("minlength=\"5\""));
+ assert!(html.contains("size=\"15\""));
+ assert!(html.contains("autocomplete=\"foo bar\""));
+ assert!(html.contains("dir=\"ltr\""));
+ assert!(html.contains("dirname=\"dir\""));
+ assert!(html.contains("placeholder=\"Enter text\""));
+ assert!(html.contains("readonly"));
+ assert!(html.contains(""));
}
#[cot::test]
@@ -848,9 +1319,7 @@ mod tests {
name: "test".to_owned(),
required: true,
},
- StringFieldOptions {
- max_length: Some(10),
- },
+ StringFieldOptions::builder().max_length(10).build(),
);
field
.set_value(FormFieldValue::new_text("test"))
@@ -868,9 +1337,7 @@ mod tests {
name: "test".to_owned(),
required: true,
},
- StringFieldOptions {
- max_length: Some(10),
- },
+ StringFieldOptions::builder().max_length(10).build(),
);
field.set_value(FormFieldValue::new_text("")).await.unwrap();
let value = String::clean_value(&field);
@@ -885,15 +1352,24 @@ mod tests {
name: "test".to_owned(),
required: true,
},
- PasswordFieldOptions {
- max_length: Some(10),
- },
+ PasswordFieldOptions::builder()
+ .max_length(10)
+ .min_length(5)
+ .size(15)
+ .autocomplete(AutoComplete::Value("foo bar".to_string()))
+ .placeholder("Enter password".to_string())
+ .readonly(false)
+ .build(),
);
let html = field.to_string();
assert!(html.contains("type=\"password\""));
assert!(html.contains("required"));
assert!(html.contains("maxlength=\"10\""));
+ assert!(html.contains("minlength=\"5\""));
+ assert!(html.contains("autocomplete=\"foo bar\""));
+ assert!(html.contains("placeholder=\"Enter password\""));
}
+
#[cot::test]
async fn password_field_clean_value() {
let mut field = PasswordField::with_options(
@@ -902,9 +1378,7 @@ mod tests {
name: "test".to_owned(),
required: true,
},
- PasswordFieldOptions {
- max_length: Some(10),
- },
+ PasswordFieldOptions::builder().max_length(10).build(),
);
field
.set_value(FormFieldValue::new_text("password"))
@@ -922,19 +1396,34 @@ mod tests {
name: "test_name".to_owned(),
required: true,
},
- EmailFieldOptions {
- min_length: Some(10),
- max_length: Some(50),
- },
+ EmailFieldOptions::builder()
+ .max_length(10)
+ .min_length(5)
+ .size(15)
+ .autocomplete(AutoComplete::Value("foo bar".to_string()))
+ .dir(Dir::Ltr)
+ .dirname("dir".to_string())
+ .list(List::new(["foo@example.com", "baz@example.com"]))
+ .placeholder("Enter text".to_string())
+ .readonly(true)
+ .build(),
);
let html = field.to_string();
+
assert!(html.contains("type=\"email\""));
assert!(html.contains("required"));
- assert!(html.contains("minlength=\"10\""));
- assert!(html.contains("maxlength=\"50\""));
+ assert!(html.contains("minlength=\"5\""));
+ assert!(html.contains("maxlength=\"10\""));
assert!(html.contains("name=\"test_id\""));
assert!(html.contains("id=\"test_id\""));
+ assert!(html.contains("placeholder=\"Enter text\""));
+ assert!(html.contains("readonly"));
+ assert!(html.contains("autocomplete=\"foo bar\""));
+ assert!(html.contains("dir=\"ltr\""));
+ assert!(html.contains("dirname=\"dir\""));
+ assert!(html.contains("list=\"__test_id_datalist\""));
+ assert!(html.contains(r#""#));
}
#[cot::test]
@@ -945,10 +1434,10 @@ mod tests {
name: "email_test".to_owned(),
required: true,
},
- EmailFieldOptions {
- min_length: Some(10),
- max_length: Some(50),
- },
+ EmailFieldOptions::builder()
+ .max_length(50)
+ .min_length(10)
+ .build(),
);
field
@@ -968,10 +1457,10 @@ mod tests {
name: "email_test".to_owned(),
required: true,
},
- EmailFieldOptions {
- min_length: Some(10),
- max_length: Some(50),
- },
+ EmailFieldOptions::builder()
+ .max_length(50)
+ .min_length(10)
+ .build(),
);
field
@@ -991,10 +1480,10 @@ mod tests {
name: "email_test".to_owned(),
required: true,
},
- EmailFieldOptions {
- min_length: Some(5),
- max_length: Some(10),
- },
+ EmailFieldOptions::builder()
+ .max_length(10)
+ .min_length(5)
+ .build(),
);
field
@@ -1017,10 +1506,10 @@ mod tests {
name: "email_test".to_owned(),
required: true,
},
- EmailFieldOptions {
- min_length: Some(5),
- max_length: Some(10),
- },
+ EmailFieldOptions::builder()
+ .max_length(50)
+ .min_length(10)
+ .build(),
);
field
@@ -1043,10 +1532,10 @@ mod tests {
name: "email_test".to_owned(),
required: true,
},
- EmailFieldOptions {
- min_length: Some(50),
- max_length: Some(10),
- },
+ EmailFieldOptions::builder()
+ .max_length(10)
+ .min_length(50)
+ .build(),
);
field
@@ -1070,16 +1559,25 @@ mod tests {
name: "test".to_owned(),
required: true,
},
- IntegerFieldOptions {
- min: Some(1),
- max: Some(10),
- },
+ IntegerFieldOptions::builder()
+ .max(10)
+ .min(1)
+ .placeholder("Enter text".to_string())
+ .readonly(false)
+ .step(Step::Value(10))
+ .build(),
);
let html = field.to_string();
+
assert!(html.contains("type=\"number\""));
assert!(html.contains("required"));
assert!(html.contains("min=\"1\""));
assert!(html.contains("max=\"10\""));
+ assert!(html.contains("step=\"10\""));
+ assert!(html.contains("placeholder=\"Enter text\""));
+ assert!(html.contains("name=\"test\""));
+ assert!(html.contains("id=\"test\""));
+ assert!(!html.contains("readonly"));
}
#[cot::test]
@@ -1090,10 +1588,7 @@ mod tests {
name: "test".to_owned(),
required: true,
},
- IntegerFieldOptions {
- min: Some(1),
- max: Some(10),
- },
+ IntegerFieldOptions::builder().max(10).min(1).build(),
);
field
.set_value(FormFieldValue::new_text("5"))
@@ -1111,10 +1606,7 @@ mod tests {
name: "test".to_owned(),
required: true,
},
- IntegerFieldOptions {
- min: Some(10),
- max: Some(50),
- },
+ IntegerFieldOptions::builder().min(10).max(50).build(),
);
field
.set_value(FormFieldValue::new_text("5"))
@@ -1135,10 +1627,7 @@ mod tests {
name: "test".to_owned(),
required: true,
},
- IntegerFieldOptions {
- min: Some(10),
- max: Some(50),
- },
+ IntegerFieldOptions::builder().min(10).max(50).build(),
);
field
.set_value(FormFieldValue::new_text("100"))
@@ -1159,9 +1648,7 @@ mod tests {
name: "test".to_owned(),
required: true,
},
- BoolFieldOptions {
- must_be_true: Some(false),
- },
+ BoolFieldOptions::builder().must_be_true(false).build(),
);
let html = field.to_string();
assert!(html.contains("type=\"checkbox\""));
@@ -1177,9 +1664,7 @@ mod tests {
name: "test".to_owned(),
required: true,
},
- BoolFieldOptions {
- must_be_true: Some(true),
- },
+ BoolFieldOptions::builder().must_be_true(true).build(),
);
let html = field.to_string();
assert!(html.contains("type=\"checkbox\""));
@@ -1195,9 +1680,7 @@ mod tests {
name: "test".to_owned(),
required: true,
},
- BoolFieldOptions {
- must_be_true: Some(true),
- },
+ BoolFieldOptions::builder().must_be_true(false).build(),
);
field
.set_value(FormFieldValue::new_text("true"))
@@ -1215,16 +1698,25 @@ mod tests {
name: "test".to_owned(),
required: true,
},
- FloatFieldOptions {
- min: Some(1.5),
- max: Some(10.7),
- },
+ FloatFieldOptions::builder()
+ .min(1.5)
+ .max(10.7)
+ .step(Step::Any)
+ .placeholder("Enter text".to_string())
+ .readonly(true)
+ .build(),
);
let html = field.to_string();
+
assert!(html.contains("type=\"number\""));
assert!(html.contains("required"));
assert!(html.contains("min=\"1.5\""));
assert!(html.contains("max=\"10.7\""));
+ assert!(html.contains("step=\"any\""));
+ assert!(html.contains("placeholder=\"Enter text\""));
+ assert!(html.contains("name=\"test\""));
+ assert!(html.contains("id=\"test\""));
+ assert!(html.contains("readonly"));
}
#[cot::test]
@@ -1236,10 +1728,7 @@ mod tests {
name: "test".to_owned(),
required: true,
},
- FloatFieldOptions {
- min: Some(1.0),
- max: Some(10.0),
- },
+ FloatFieldOptions::builder().min(1.0).max(10.0).build(),
);
field
.set_value(FormFieldValue::new_text("5.0"))
@@ -1257,10 +1746,7 @@ mod tests {
name: "test".to_owned(),
required: true,
},
- FloatFieldOptions {
- min: Some(5.0),
- max: Some(10.0),
- },
+ FloatFieldOptions::builder().min(5.0).max(10.0).build(),
);
field
.set_value(FormFieldValue::new_text("2.0"))
@@ -1281,10 +1767,7 @@ mod tests {
name: "test".to_owned(),
required: true,
},
- FloatFieldOptions {
- min: Some(5.0),
- max: Some(10.0),
- },
+ FloatFieldOptions::builder().min(5.0).max(10.0).build(),
);
field
.set_value(FormFieldValue::new_text("20.0"))
@@ -1305,10 +1788,7 @@ mod tests {
name: "test".to_owned(),
required: true,
},
- FloatFieldOptions {
- min: Some(1.0),
- max: Some(10.0),
- },
+ FloatFieldOptions::builder().min(1.0).max(10.0).build(),
);
let bad_inputs = ["NaN", "inf"];
@@ -1335,10 +1815,7 @@ mod tests {
name: "test".to_owned(),
required: true,
},
- FloatFieldOptions {
- min: Some(1.0),
- max: Some(10.0),
- },
+ FloatFieldOptions::builder().min(1.0).max(10.0).build(),
);
field.set_value(FormFieldValue::new_text("")).await.unwrap();
let value = f32::clean_value(&field);
@@ -1353,12 +1830,24 @@ mod tests {
name: "test".to_owned(),
required: true,
},
- UrlFieldOptions,
+ UrlFieldOptions::builder()
+ .max_length(100)
+ .min_length(5)
+ .size(30)
+ .autocomplete(AutoComplete::Value("url".to_string()))
+ .dir(Dir::Ltr)
+ .dirname("dir".to_string())
+ .list(List::new(["http:://example.com"]))
+ .placeholder("Enter URL".to_string())
+ .readonly(true)
+ .build(),
);
+
field
.set_value(FormFieldValue::new_text("https://example.com"))
.await
.unwrap();
+
let value = Url::clean_value(&field).unwrap();
assert_eq!(
value.as_str(),
@@ -1374,16 +1863,40 @@ mod tests {
name: "url".to_owned(),
required: true,
},
- UrlFieldOptions,
+ UrlFieldOptions::builder()
+ .max_length(120)
+ .min_length(10)
+ .size(40)
+ .list(List::new(["http://example.com", "https://example.org"]))
+ .dir(Dir::Ltr)
+ .dirname("lang".to_owned())
+ .autocomplete(AutoComplete::Value("url".to_owned()))
+ .placeholder("Paste link".to_owned())
+ .readonly(true)
+ .build(),
);
+
field
.set_value(FormFieldValue::new_text("http://example.com"))
.await
.unwrap();
+
let html = field.to_string();
+
assert!(html.contains("type=\"url\""));
+ assert!(html.contains("id=\"id_url\""));
+ assert!(html.contains("name=\"id_url\""));
assert!(html.contains("required"));
assert!(html.contains("value=\"http://example.com\""));
+ assert!(html.contains("maxlength=\"120\""));
+ assert!(html.contains("minlength=\"10\""));
+ assert!(html.contains("size=\"40\""));
+ assert!(html.contains("placeholder=\"Paste link\""));
+ assert!(html.contains("autocomplete=\"url\""));
+ assert!(html.contains("dir=\"ltr\""));
+ assert!(html.contains("dirname=\"lang\""));
+ assert!(html.contains("readonly"));
+ assert!(html.contains("