From 4a4e49870d80b32364758b54dc0895aa3eb36d98 Mon Sep 17 00:00:00 2001 From: Elijah Date: Wed, 18 Jun 2025 00:20:08 +0000 Subject: [PATCH 1/6] Add more form attributes --- cot/src/form/fields.rs | 301 ++++++++++++++++++++++++++++++++--- cot/src/form/fields/attrs.rs | 67 ++++++++ cot/src/html.rs | 16 ++ 3 files changed, 365 insertions(+), 19 deletions(-) diff --git a/cot/src/form/fields.rs b/cot/src/form/fields.rs index 920e19b1..32318ecf 100644 --- a/cot/src/form/fields.rs +++ b/cot/src/form/fields.rs @@ -10,7 +10,7 @@ use std::num::{ }; use askama::filters::HtmlSafe; -pub use attrs::Step; +pub use attrs::{AutoCapitalize, AutoComplete, List, Step}; pub use chrono::{ DateField, DateFieldOptions, DateTimeField, DateTimeFieldOptions, DateTimeWithTimezoneField, DateTimeWithTimezoneFieldOptions, TimeField, TimeFieldOptions, @@ -72,11 +72,19 @@ 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)] pub struct StringFieldOptions { /// The maximum length of the field. Used to set the `maxlength` attribute /// in the HTML input element. pub max_length: Option, + pub min_length: Option, + pub size: Option, + pub autocapitalize: Option, + pub autocomplete: Option, + pub dirname: Option, + pub list: Option, + pub placeholder: Option, + pub readonly: Option, } impl Display for StringField { @@ -90,6 +98,38 @@ 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(placeholder) = &self.custom_options.placeholder { + tag.attr("placeholder", placeholder); + } + if let Some(readonly) = self.custom_options.readonly { + if readonly { + tag.bool_attr("readonly"); + } + } + if let Some(autocomplete) = &self.custom_options.autocomplete { + tag.attr("autocomplete", autocomplete.to_string()); + } + + if let Some(size) = self.custom_options.size { + tag.attr("size", size.to_string()); + } + + 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(value) = &self.value { tag.attr("value", value); } @@ -142,11 +182,16 @@ 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)] pub struct PasswordFieldOptions { /// The maximum length of the field. Used to set the `maxlength` attribute /// in the HTML input element. pub max_length: Option, + pub min_length: Option, + pub size: Option, + pub autocomplete: Option, + pub placeholder: Option, + pub readonly: Option, } impl Display for PasswordField { @@ -160,6 +205,26 @@ 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(placeholder) = &self.custom_options.placeholder { + tag.attr("placeholder", placeholder); + } + if let Some(readonly) = self.custom_options.readonly { + if readonly { + tag.bool_attr("readonly"); + } + } + if let Some(autocomplete) = &self.custom_options.autocomplete { + tag.attr("autocomplete", autocomplete.to_string()); + } + + if let Some(size) = self.custom_options.size { + tag.attr("size", size.to_string()); + } + // we don't set the value attribute for password fields // to avoid leaking the password in the HTML @@ -217,7 +282,7 @@ impl AsFormField for PasswordHash { impl_form_field!(EmailField, EmailFieldOptions, "an email"); /// Custom options for [`EmailField`] -#[derive(Debug, Default, Copy, Clone)] +#[derive(Debug, Default, Clone)] pub struct EmailFieldOptions { /// The maximum length of the field used to set the `maxlength` attribute /// in the HTML input element. @@ -225,6 +290,13 @@ pub struct EmailFieldOptions { /// The minimum length of the field used to set the `minlength` attribute /// in the HTML input element. pub min_length: Option, + pub size: Option, + pub autocomplete: Option, + pub dirname: Option, + pub list: Option, + pub multiple: Option, + pub placeholder: Option, + pub readonly: Option, } impl Display for EmailField { @@ -241,6 +313,39 @@ impl Display for EmailField { if let Some(min_length) = self.custom_options.min_length { tag.attr("minlength", min_length.to_string()); } + if let Some(placeholder) = &self.custom_options.placeholder { + tag.attr("placeholder", placeholder); + } + if let Some(readonly) = self.custom_options.readonly { + if readonly { + tag.bool_attr("readonly"); + } + } + if let Some(autocomplete) = &self.custom_options.autocomplete { + tag.attr("autocomplete", autocomplete.to_string()); + } + + if let Some(size) = self.custom_options.size { + tag.attr("size", size.to_string()); + } + + 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(multiple) = self.custom_options.multiple { + if multiple { + tag.bool_attr("multiple"); + } + } + if let Some(value) = &self.value { tag.attr("value", value); } @@ -289,10 +394,10 @@ 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)] pub struct IntegerFieldOptions { /// The minimum value of the field. Used to set the `min` attribute in the /// HTML input element. @@ -300,6 +405,12 @@ pub struct IntegerFieldOptions { /// The maximum value of the field. Used to set the `max` attribute in the /// HTML input element. pub max: Option, + + pub placeholder: Option, + + pub readonly: Option, + + pub step: Option>, } impl Default for IntegerFieldOptions { @@ -307,11 +418,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()); @@ -325,6 +439,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 { + if 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); } @@ -333,7 +460,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. /// @@ -646,10 +773,10 @@ 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)] pub struct FloatFieldOptions { /// The minimum value of the field. Used to set the `min` attribute in the /// HTML input element. @@ -657,6 +784,12 @@ pub struct FloatFieldOptions { /// The maximum value of the field. Used to set the `max` attribute in the /// HTML input element. pub max: Option, + + pub placeholder: Option, + + pub readonly: Option, + + pub step: Option>, } impl Default for FloatFieldOptions { @@ -664,11 +797,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()); @@ -683,6 +819,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 { + if 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); } @@ -691,7 +838,7 @@ impl Display for FloatField { } } -impl HtmlSafe for FloatField {} +impl HtmlSafe for FloatField {} /// A trait for types that can be represented as a float. /// @@ -776,19 +923,59 @@ 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)] +pub struct UrlFieldOptions { + pub max_length: Option, + pub min_length: Option, + pub size: Option, + pub list: Option, + pub dirname: Option, + pub autocomplete: Option, + pub placeholder: Option, + 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(placeholder) = &self.custom_options.placeholder { + tag.attr("placeholder", placeholder); + } + if let Some(readonly) = self.custom_options.readonly { + if readonly { + tag.bool_attr("readonly"); + } + } + if let Some(autocomplete) = &self.custom_options.autocomplete { + tag.attr("autocomplete", autocomplete.to_string()); + } + + if let Some(size) = self.custom_options.size { + tag.attr("size", size.to_string()); + } + + 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(value) = &self.value { tag.attr("value", value); } @@ -831,12 +1018,28 @@ mod tests { }, StringFieldOptions { max_length: Some(10), + min_length: Some(5), + size: Some(15), + autocapitalize: Some(AutoCapitalize::Words), + autocomplete: Some(AutoComplete::Value("foo bar".to_string())), + dirname: Some("dir".to_string()), + list: Some(List::new(["bar", "baz"])), + placeholder: Some("Enter text".to_string()), + readonly: Some(true), }, ); 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("dirname=\"dir\"")); + assert!(html.contains("placeholder=\"Enter text\"")); + assert!(html.contains("readonly")); } #[cot::test] @@ -849,6 +1052,7 @@ mod tests { }, StringFieldOptions { max_length: Some(10), + ..Default::default() }, ); field @@ -869,6 +1073,7 @@ mod tests { }, StringFieldOptions { max_length: Some(10), + ..Default::default() }, ); field.set_value(FormFieldValue::new_text("")).await.unwrap(); @@ -903,6 +1108,7 @@ mod tests { }, PasswordFieldOptions { max_length: Some(10), + ..Default::default() }, ); field @@ -947,6 +1153,7 @@ mod tests { EmailFieldOptions { min_length: Some(10), max_length: Some(50), + ..Default::default() }, ); @@ -970,6 +1177,7 @@ mod tests { EmailFieldOptions { min_length: Some(10), max_length: Some(50), + ..Default::default() }, ); @@ -993,6 +1201,7 @@ mod tests { EmailFieldOptions { min_length: Some(5), max_length: Some(10), + ..Default::default() }, ); @@ -1019,6 +1228,7 @@ mod tests { EmailFieldOptions { min_length: Some(5), max_length: Some(10), + ..Default::default() }, ); @@ -1045,6 +1255,7 @@ mod tests { EmailFieldOptions { min_length: Some(50), max_length: Some(10), + ..Default::default() }, ); @@ -1092,6 +1303,7 @@ mod tests { IntegerFieldOptions { min: Some(1), max: Some(10), + ..Default::default() }, ); field @@ -1113,6 +1325,7 @@ mod tests { IntegerFieldOptions { min: Some(10), max: Some(50), + ..Default::default() }, ); field @@ -1137,6 +1350,7 @@ mod tests { IntegerFieldOptions { min: Some(10), max: Some(50), + ..Default::default() }, ); field @@ -1238,6 +1452,7 @@ mod tests { FloatFieldOptions { min: Some(1.0), max: Some(10.0), + ..Default::default() }, ); field @@ -1259,6 +1474,7 @@ mod tests { FloatFieldOptions { min: Some(5.0), max: Some(10.0), + ..Default::default() }, ); field @@ -1283,6 +1499,7 @@ mod tests { FloatFieldOptions { min: Some(5.0), max: Some(10.0), + ..Default::default() }, ); field @@ -1307,6 +1524,7 @@ mod tests { FloatFieldOptions { min: Some(1.0), max: Some(10.0), + ..Default::default() }, ); let bad_inputs = ["NaN", "inf"]; @@ -1337,6 +1555,7 @@ mod tests { FloatFieldOptions { min: Some(1.0), max: Some(10.0), + ..Default::default() }, ); field.set_value(FormFieldValue::new_text("")).await.unwrap(); @@ -1352,12 +1571,23 @@ mod tests { name: "test".to_owned(), required: true, }, - UrlFieldOptions, + UrlFieldOptions { + max_length: Some(100), + min_length: Some(5), + size: Some(30), + list: Some(List::new(["https://example.com"])), + dirname: Some("dir".to_owned()), + autocomplete: Some(AutoComplete::Value("url".to_owned())), + placeholder: Some("Enter URL".to_owned()), + readonly: Some(true), + }, ); + field .set_value(FormFieldValue::new_text("https://example.com")) .await .unwrap(); + let value = Url::clean_value(&field).unwrap(); assert_eq!( value.as_str(), @@ -1373,16 +1603,38 @@ mod tests { name: "url".to_owned(), required: true, }, - UrlFieldOptions, + UrlFieldOptions { + max_length: Some(120), + min_length: Some(10), + size: Some(40), + list: Some(List::new(["https://one.com", "https://two.com"])), + dirname: Some("lang".to_owned()), + autocomplete: Some(AutoComplete::Value("url".to_owned())), + placeholder: Some("Paste link".to_owned()), + readonly: Some(true), + }, ); + 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("dirname=\"lang\"")); + assert!(html.contains("readonly")); + assert!(html.contains(" Display for Step { } } } + +#[derive(Debug, Clone, Default)] +pub struct List(Vec); + +impl List { + pub fn new(iter: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + let v = iter.into_iter().map(|s| s.as_ref().to_string()).collect(); + Self(v) + } +} + +impl From for Vec { + fn from(value: List) -> Self { + value.0 + } +} + +#[derive(Debug, Clone)] +pub enum AutoComplete { + On, + Off, + Value(String), +} + +impl Display for AutoComplete { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Off => f.write_str("off"), + Self::On => f.write_str("on"), + Self::Value(value) => f.write_str(&value), + } + } +} +#[derive(Debug, Clone, Copy)] +pub enum AutoCapitalize { + Off, + On, + Words, + Characters, +} + +impl AutoCapitalize { + fn as_str(self) -> &'static str { + match self { + AutoCapitalize::Off => "off", + AutoCapitalize::On => "on", + AutoCapitalize::Words => "words", + AutoCapitalize::Characters => "characters", + } + } +} + +impl Display for AutoCapitalize { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +#[derive(Debug, Clone, Copy)] +pub enum Dir { + Rtl, + Ltr, +} diff --git a/cot/src/html.rs b/cot/src/html.rs index 00066911..c741783b 100644 --- a/cot/src/html.rs +++ b/cot/src/html.rs @@ -189,6 +189,22 @@ impl HtmlTag { input } + pub fn data_list>>(list: L, id: &str) -> Self { + let mut data_list = Self::new("datalist"); + data_list.attr("id", id); + + let mut options: Vec = Vec::new(); + + for l in list.into() { + 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 From 89708fa19955acf6b255cfc5f45db05c4de15792 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 00:22:56 +0000 Subject: [PATCH 2/6] chore(pre-commit.ci): auto fixes from pre-commit hooks --- cot/src/form/fields.rs | 4 ++-- cot/src/form/fields/attrs.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cot/src/form/fields.rs b/cot/src/form/fields.rs index 32318ecf..b21f1386 100644 --- a/cot/src/form/fields.rs +++ b/cot/src/form/fields.rs @@ -444,7 +444,7 @@ impl Display for IntegerField { tag.attr("placeholder", placeholder); } if let Some(readonly) = self.custom_options.readonly { - if readonly{ + if readonly { tag.bool_attr("readonly"); } } @@ -823,7 +823,7 @@ impl Display for FloatField { tag.attr("placeholder", placeholder); } if let Some(readonly) = self.custom_options.readonly { - if readonly{ + if readonly { tag.bool_attr("readonly"); } } diff --git a/cot/src/form/fields/attrs.rs b/cot/src/form/fields/attrs.rs index c1805a2e..0cd596d2 100644 --- a/cot/src/form/fields/attrs.rs +++ b/cot/src/form/fields/attrs.rs @@ -74,9 +74,9 @@ pub enum AutoCapitalize { impl AutoCapitalize { fn as_str(self) -> &'static str { match self { - AutoCapitalize::Off => "off", - AutoCapitalize::On => "on", - AutoCapitalize::Words => "words", + AutoCapitalize::Off => "off", + AutoCapitalize::On => "on", + AutoCapitalize::Words => "words", AutoCapitalize::Characters => "characters", } } From 3a2b7aeafb5cf10d20ae603db9de54643a0834d4 Mon Sep 17 00:00:00 2001 From: Elijah Date: Wed, 18 Jun 2025 01:11:31 +0000 Subject: [PATCH 3/6] wrap input and datalist in a div --- cot/src/form/fields.rs | 13 +++++++++++-- cot/src/html.rs | 3 ++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/cot/src/form/fields.rs b/cot/src/form/fields.rs index b21f1386..f048575b 100644 --- a/cot/src/form/fields.rs +++ b/cot/src/form/fields.rs @@ -302,6 +302,8 @@ pub struct EmailFieldOptions { 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 { @@ -337,8 +339,7 @@ impl Display for EmailField { 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); + data_list = Some(HtmlTag::data_list(list.clone(), &list_id)); } if let Some(multiple) = self.custom_options.multiple { if multiple { @@ -350,6 +351,14 @@ impl Display for EmailField { 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()) } } diff --git a/cot/src/html.rs b/cot/src/html.rs index c741783b..c77738a0 100644 --- a/cot/src/html.rs +++ b/cot/src/html.rs @@ -197,7 +197,8 @@ impl HtmlTag { for l in list.into() { let mut option = HtmlTag::new("option"); - option.attr("value", l); + option.attr("value", &l); + option.push_str(&l); options.push(HtmlNode::Tag(option)); } From ae395e2440c9893203190f391ae8075eb07f382b Mon Sep 17 00:00:00 2001 From: Elijah Date: Wed, 18 Jun 2025 19:01:23 +0000 Subject: [PATCH 4/6] remove multiple option --- cot/src/form/fields.rs | 6 ---- cot/src/form/fields/attrs.rs | 54 ++++++++++++++++++++++++++++++++---- cot/src/form/fields/files.rs | 22 +++++++++++++-- 3 files changed, 68 insertions(+), 14 deletions(-) diff --git a/cot/src/form/fields.rs b/cot/src/form/fields.rs index f048575b..6d168e7e 100644 --- a/cot/src/form/fields.rs +++ b/cot/src/form/fields.rs @@ -294,7 +294,6 @@ pub struct EmailFieldOptions { pub autocomplete: Option, pub dirname: Option, pub list: Option, - pub multiple: Option, pub placeholder: Option, pub readonly: Option, } @@ -341,11 +340,6 @@ impl Display for EmailField { data_list = Some(HtmlTag::data_list(list.clone(), &list_id)); } - if let Some(multiple) = self.custom_options.multiple { - if multiple { - tag.bool_attr("multiple"); - } - } if let Some(value) = &self.value { tag.attr("value", value); diff --git a/cot/src/form/fields/attrs.rs b/cot/src/form/fields/attrs.rs index 0cd596d2..f5a6d406 100644 --- a/cot/src/form/fields/attrs.rs +++ b/cot/src/form/fields/attrs.rs @@ -54,15 +54,20 @@ pub enum AutoComplete { Value(String), } -impl Display for AutoComplete { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { +impl AutoComplete { + pub fn as_str(&self) -> &str { match self { - Self::Off => f.write_str("off"), - Self::On => f.write_str("on"), - Self::Value(value) => f.write_str(&value), + Self::Off => "off", + Self::On => "on", + Self::Value(value) => value.as_str(), } } } +impl Display for AutoComplete { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} #[derive(Debug, Clone, Copy)] pub enum AutoCapitalize { Off, @@ -93,3 +98,42 @@ pub enum Dir { Rtl, Ltr, } + +impl Dir { + pub fn as_str(&self) -> &'static str { + match self{ + Self::Rtl => "rtl", + Self::Ltr => "ltr" + } + } +} + +impl Display for Dir { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + + + +#[derive(Debug, Clone, Copy)] +pub enum Capture { + User, + Environment +} + + +impl Capture{ + pub fn as_str(&self) -> &'static str { + match self { + Self::User => "user", + Self::Environment => "environment" + } + } +} + +impl Display for Capture { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} \ No newline at end of file diff --git a/cot/src/form/fields/files.rs b/cot/src/form/fields/files.rs index fb107280..01db6027 100644 --- a/cot/src/form/fields/files.rs +++ b/cot/src/form/fields/files.rs @@ -6,6 +6,7 @@ use cot::form::{AsFormField, FormFieldValidationError}; use cot::html::HtmlTag; use crate::form::{FormField, FormFieldOptions, FormFieldValue, FormFieldValueError}; +use crate::form::fields::attrs::Capture; #[derive(Debug)] /// A form field for a file. @@ -64,6 +65,8 @@ pub struct FileFieldOptions { /// /// [`accept` attribute]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/file#limiting_accepted_file_types pub accept: Option>, + + pub capture: Option } impl Display for FileField { @@ -78,6 +81,10 @@ impl Display for FileField { tag.attr("accept", accept.join(",")); } + if let Some(capture) = self.custom_options.capture { + tag.attr("capture", capture.to_string()); + } + write!(f, "{}", tag.render()) } } @@ -161,6 +168,7 @@ mod tests { }, FileFieldOptions { accept: Some(vec!["image/*".to_string(), ".pdf".to_string()]), + capture: Some(Capture::Environment), }, ); @@ -169,6 +177,7 @@ mod tests { assert!(html.contains("type=\"file\"")); assert!(html.contains("required")); assert!(html.contains("accept=\"image/*,.pdf\"")); + assert!(html.contains("capture=\"environment\"")) } #[test] @@ -179,7 +188,10 @@ mod tests { name: "test".to_owned(), required: true, }, - FileFieldOptions { accept: None }, + FileFieldOptions { + accept: None, + capture: Some(Capture::User), + }, ); let html = field.to_string(); @@ -187,6 +199,7 @@ mod tests { assert!(html.contains("type=\"file\"")); assert!(html.contains("required")); assert!(!html.contains("accept=")); + assert!(html.contains("capture=\"user\"")) } #[cot::test] @@ -197,7 +210,10 @@ mod tests { name: "test".to_owned(), required: true, }, - FileFieldOptions { accept: None }, + FileFieldOptions { + ..Default::default() + + }, ); let boundary = "boundary"; @@ -232,7 +248,7 @@ mod tests { name: "test".to_owned(), required: true, }, - FileFieldOptions { accept: None }, + FileFieldOptions { ..Default::default() }, ); let value = InMemoryUploadedFile::clean_value(&field); From e6b2f3dcad3dbf2cd04063c043f0c55eb99188f4 Mon Sep 17 00:00:00 2001 From: Elijah Date: Sat, 12 Jul 2025 18:20:21 +0000 Subject: [PATCH 5/6] rebase --- cot/src/form/fields.rs | 48 +++++++++++++++++++++++++++++++++--- cot/src/form/fields/attrs.rs | 15 +++++------ cot/src/form/fields/files.rs | 9 ++++--- 3 files changed, 55 insertions(+), 17 deletions(-) diff --git a/cot/src/form/fields.rs b/cot/src/form/fields.rs index 6d168e7e..68867877 100644 --- a/cot/src/form/fields.rs +++ b/cot/src/form/fields.rs @@ -1094,13 +1094,22 @@ mod tests { }, PasswordFieldOptions { max_length: Some(10), + min_length: Some(5), + size: Some(15), + autocomplete: Some(AutoComplete::Value("foo bar".to_string())), + placeholder: Some("Enter password".to_string()), + readonly: Some(false), }, ); 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( @@ -1131,18 +1140,31 @@ mod tests { required: true, }, EmailFieldOptions { - min_length: Some(10), - max_length: Some(50), + max_length: Some(10), + min_length: Some(5), + size: Some(15), + autocomplete: Some(AutoComplete::Value("foo bar".to_string())), + dirname: Some("dir".to_string()), + list: Some(List::new(["foo@example.com", "baz@example.com"])), + placeholder: Some("Enter text".to_string()), + readonly: Some(true), }, ); 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("dirname=\"dir\"")); + assert!(html.contains("list=\"__test_id_datalist\"")); + assert!(html.contains(r#""#)); } #[cot::test] @@ -1286,13 +1308,22 @@ mod tests { IntegerFieldOptions { min: Some(1), max: Some(10), + placeholder: Some("Enter text".to_string()), + readonly: Some(false), + step: Some(Step::Value(10)), }, ); 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] @@ -1434,13 +1465,22 @@ mod tests { FloatFieldOptions { min: Some(1.5), max: Some(10.7), + placeholder: Some("Enter text".to_string()), + readonly: Some(true), + step: Some(Step::Any), }, ); 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] diff --git a/cot/src/form/fields/attrs.rs b/cot/src/form/fields/attrs.rs index f5a6d406..8c2626ea 100644 --- a/cot/src/form/fields/attrs.rs +++ b/cot/src/form/fields/attrs.rs @@ -101,9 +101,9 @@ pub enum Dir { impl Dir { pub fn as_str(&self) -> &'static str { - match self{ + match self { Self::Rtl => "rtl", - Self::Ltr => "ltr" + Self::Ltr => "ltr", } } } @@ -114,20 +114,17 @@ impl Display for Dir { } } - - #[derive(Debug, Clone, Copy)] pub enum Capture { User, - Environment + Environment, } - -impl Capture{ +impl Capture { pub fn as_str(&self) -> &'static str { match self { Self::User => "user", - Self::Environment => "environment" + Self::Environment => "environment", } } } @@ -136,4 +133,4 @@ impl Display for Capture { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str(self.as_str()) } -} \ No newline at end of file +} diff --git a/cot/src/form/fields/files.rs b/cot/src/form/fields/files.rs index 01db6027..7a490544 100644 --- a/cot/src/form/fields/files.rs +++ b/cot/src/form/fields/files.rs @@ -5,8 +5,8 @@ use bytes::Bytes; use cot::form::{AsFormField, FormFieldValidationError}; use cot::html::HtmlTag; -use crate::form::{FormField, FormFieldOptions, FormFieldValue, FormFieldValueError}; use crate::form::fields::attrs::Capture; +use crate::form::{FormField, FormFieldOptions, FormFieldValue, FormFieldValueError}; #[derive(Debug)] /// A form field for a file. @@ -66,7 +66,7 @@ pub struct FileFieldOptions { /// [`accept` attribute]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/file#limiting_accepted_file_types pub accept: Option>, - pub capture: Option + pub capture: Option, } impl Display for FileField { @@ -212,7 +212,6 @@ mod tests { }, FileFieldOptions { ..Default::default() - }, ); @@ -248,7 +247,9 @@ mod tests { name: "test".to_owned(), required: true, }, - FileFieldOptions { ..Default::default() }, + FileFieldOptions { + ..Default::default() + }, ); let value = InMemoryUploadedFile::clean_value(&field); From e0277972756fa5dc97016dd8d629eaf9ebb47082 Mon Sep 17 00:00:00 2001 From: Elijah Date: Sat, 12 Jul 2025 20:14:04 +0000 Subject: [PATCH 6/6] add more docs --- cot/src/form/fields.rs | 110 ++++++++++++++++++++++++++++++++--- cot/src/form/fields/attrs.rs | 48 ++++++++++++++- cot/src/form/fields/files.rs | 6 +- cot/src/html.rs | 10 ++++ 4 files changed, 161 insertions(+), 13 deletions(-) diff --git a/cot/src/form/fields.rs b/cot/src/form/fields.rs index 68867877..771ae1b5 100644 --- a/cot/src/form/fields.rs +++ b/cot/src/form/fields.rs @@ -10,7 +10,7 @@ use std::num::{ }; use askama::filters::HtmlSafe; -pub use attrs::{AutoCapitalize, AutoComplete, List, Step}; +pub use attrs::{AutoCapitalize, AutoComplete, Dir, List, Step}; pub use chrono::{ DateField, DateFieldOptions, DateTimeField, DateTimeFieldOptions, DateTimeWithTimezoneField, DateTimeWithTimezoneFieldOptions, TimeField, TimeFieldOptions, @@ -77,13 +77,33 @@ 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, } @@ -118,6 +138,10 @@ impl Display for StringField { tag.attr("size", size.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); } @@ -187,10 +211,20 @@ 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, } @@ -290,11 +324,30 @@ 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, } @@ -330,6 +383,10 @@ impl Display for EmailField { tag.attr("size", size.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); } @@ -408,11 +465,13 @@ 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>, } @@ -787,11 +846,13 @@ 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>, } @@ -928,13 +989,36 @@ impl_form_field!(UrlField, UrlFieldOptions, "a URL"); /// Custom options for a [`UrlField`]. #[derive(Debug, Default, Clone)] 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 [`List`] of options for the `datalist` element, which can be used to + /// provide predefined options for the input. pub list: 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 [`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 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, } @@ -968,6 +1052,10 @@ impl Display for UrlField { tag.attr("size", size.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); } @@ -1025,6 +1113,7 @@ mod tests { size: Some(15), autocapitalize: Some(AutoCapitalize::Words), autocomplete: Some(AutoComplete::Value("foo bar".to_string())), + dir: Some(Dir::Ltr), dirname: Some("dir".to_string()), list: Some(List::new(["bar", "baz"])), placeholder: Some("Enter text".to_string()), @@ -1040,6 +1129,7 @@ mod tests { 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")); @@ -1144,6 +1234,7 @@ mod tests { min_length: Some(5), size: Some(15), autocomplete: Some(AutoComplete::Value("foo bar".to_string())), + dir: Some(Dir::Ltr), dirname: Some("dir".to_string()), list: Some(List::new(["foo@example.com", "baz@example.com"])), placeholder: Some("Enter text".to_string()), @@ -1162,6 +1253,7 @@ mod tests { 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#""#)); @@ -1619,6 +1711,7 @@ mod tests { min_length: Some(5), size: Some(30), list: Some(List::new(["https://example.com"])), + dir: Some(Dir::Ltr), dirname: Some("dir".to_owned()), autocomplete: Some(AutoComplete::Value("url".to_owned())), placeholder: Some("Enter URL".to_owned()), @@ -1651,6 +1744,7 @@ mod tests { min_length: Some(10), size: Some(40), list: Some(List::new(["https://one.com", "https://two.com"])), + dir: Some(Dir::Ltr), dirname: Some("lang".to_owned()), autocomplete: Some(AutoComplete::Value("url".to_owned())), placeholder: Some("Paste link".to_owned()), @@ -1675,6 +1769,7 @@ mod tests { 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("` elements: +/// Represents the HTML [`step`] attribute for `` elements: /// - `Any` → `step="any"` /// - `Value(T)` → `step=""` where `T` is converted appropriately +/// +/// [`step`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/step #[derive(Debug, Copy, Clone)] pub enum Step { /// Indicates that the user may enter any value (no fixed “step” interval). @@ -27,10 +29,15 @@ impl Display for Step { } } +/// Represents the HTML [`list`] attribute for `` elements. +/// Used to provide a set of predefined options for the input. +/// +/// [`list`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#list #[derive(Debug, Clone, Default)] pub struct List(Vec); impl List { + /// Creates a new `List` from any iterator of string-like items. pub fn new(iter: I) -> Self where I: IntoIterator, @@ -47,14 +54,22 @@ impl From for Vec { } } +/// Represents the HTML [`autocomplete`] attribute for form fields. +/// +/// [`autocomplete`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/autocomplete #[derive(Debug, Clone)] pub enum AutoComplete { + /// Enables autocomplete. On, + /// Disables autocomplete. Off, + /// Custom autocomplete value. Value(String), } impl AutoComplete { + /// Returns the string representation for use in HTML. + #[must_use] pub fn as_str(&self) -> &str { match self { Self::Off => "off", @@ -63,20 +78,30 @@ impl AutoComplete { } } } + impl Display for AutoComplete { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str(self.as_str()) } } + +/// Represents the HTML [`autocapitalize`] attribute for form fields. +/// +/// [`autocapitalize`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/autocapitalize #[derive(Debug, Clone, Copy)] pub enum AutoCapitalize { + /// No capitalization. Off, + /// Capitalize all letters. On, + /// Capitalize the first letter of each word. Words, + /// Capitalize all characters. Characters, } impl AutoCapitalize { + /// Returns the string representation for use in HTML. fn as_str(self) -> &'static str { match self { AutoCapitalize::Off => "off", @@ -93,17 +118,27 @@ impl Display for AutoCapitalize { } } +/// Represents the HTML [`dir`] attribute for text direction. +/// +/// [`dir`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/dir #[derive(Debug, Clone, Copy)] pub enum Dir { + /// Right-to-left text direction. Rtl, + /// Left-to-right text direction. Ltr, + /// User agent auto-detects the text direction. + Auto, } impl Dir { - pub fn as_str(&self) -> &'static str { + /// Returns the string representation for use in HTML. + #[must_use] + pub fn as_str(self) -> &'static str { match self { Self::Rtl => "rtl", Self::Ltr => "ltr", + Self::Auto => "auto", } } } @@ -114,14 +149,21 @@ impl Display for Dir { } } +/// Represents the HTML [`capture`] attribute for file inputs. +/// Used to specify the preferred source for file capture. +/// +/// [`capture`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/file#capture #[derive(Debug, Clone, Copy)] pub enum Capture { + /// Use the user-facing camera or microphone. User, + /// Use the environment-facing camera or microphone. Environment, } impl Capture { - pub fn as_str(&self) -> &'static str { + /// Returns the string representation for use in HTML. + pub fn as_str(self) -> &'static str { match self { Self::User => "user", Self::Environment => "environment", diff --git a/cot/src/form/fields/files.rs b/cot/src/form/fields/files.rs index 7a490544..131080c4 100644 --- a/cot/src/form/fields/files.rs +++ b/cot/src/form/fields/files.rs @@ -65,7 +65,7 @@ pub struct FileFieldOptions { /// /// [`accept` attribute]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/file#limiting_accepted_file_types pub accept: Option>, - + /// The [`Capture`] attribute specifies the source of the file input. pub capture: Option, } @@ -177,7 +177,7 @@ mod tests { assert!(html.contains("type=\"file\"")); assert!(html.contains("required")); assert!(html.contains("accept=\"image/*,.pdf\"")); - assert!(html.contains("capture=\"environment\"")) + assert!(html.contains("capture=\"environment\"")); } #[test] @@ -199,7 +199,7 @@ mod tests { assert!(html.contains("type=\"file\"")); assert!(html.contains("required")); assert!(!html.contains("accept=")); - assert!(html.contains("capture=\"user\"")) + assert!(html.contains("capture=\"user\"")); } #[cot::test] diff --git a/cot/src/html.rs b/cot/src/html.rs index c77738a0..a07ec51a 100644 --- a/cot/src/html.rs +++ b/cot/src/html.rs @@ -189,6 +189,16 @@ 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(); + /// assert_eq!(rendered.as_str(), ""); + /// ``` + #[must_use] pub fn data_list>>(list: L, id: &str) -> Self { let mut data_list = Self::new("datalist"); data_list.attr("id", id);