#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, Copy, Default)]
pub struct FieldFlags {
pub required: bool,
pub disabled: bool,
pub readonly: bool,
pub autofocus: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum InputType {
#[default]
Text,
Email,
Password,
Number,
Tel,
Url,
Search,
Date,
Time,
DateTimeLocal,
Month,
Week,
Color,
Range,
Hidden,
File,
}
impl InputType {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Text => "text",
Self::Email => "email",
Self::Password => "password",
Self::Number => "number",
Self::Tel => "tel",
Self::Url => "url",
Self::Search => "search",
Self::Date => "date",
Self::Time => "time",
Self::DateTimeLocal => "datetime-local",
Self::Month => "month",
Self::Week => "week",
Self::Color => "color",
Self::Range => "range",
Self::Hidden => "hidden",
Self::File => "file",
}
}
}
impl std::fmt::Display for InputType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SelectOption {
pub value: String,
pub label: String,
pub disabled: bool,
}
impl SelectOption {
#[must_use]
pub fn new(value: impl Into<String>, label: impl Into<String>) -> Self {
Self {
value: value.into(),
label: label.into(),
disabled: false,
}
}
#[must_use]
pub fn disabled(value: impl Into<String>, label: impl Into<String>) -> Self {
Self {
value: value.into(),
label: label.into(),
disabled: true,
}
}
}
#[derive(Debug, Clone)]
pub enum FieldKind {
Input(InputType),
Textarea {
rows: Option<u32>,
cols: Option<u32>,
},
Select {
options: Vec<SelectOption>,
multiple: bool,
},
Checkbox {
checked: bool,
},
Radio {
options: Vec<SelectOption>,
},
}
impl Default for FieldKind {
fn default() -> Self {
Self::Input(InputType::default())
}
}
#[derive(Debug, Clone)]
pub struct FormField {
pub name: String,
pub kind: FieldKind,
pub label: Option<String>,
pub placeholder: Option<String>,
pub value: Option<String>,
pub flags: FieldFlags,
pub autocomplete: Option<String>,
pub min_length: Option<usize>,
pub max_length: Option<usize>,
pub min: Option<String>,
pub max: Option<String>,
pub step: Option<String>,
pub pattern: Option<String>,
pub class: Option<String>,
pub id: Option<String>,
pub help_text: Option<String>,
pub htmx: HtmxFieldAttrs,
pub data_attrs: Vec<(String, String)>,
pub custom_attrs: Vec<(String, String)>,
pub file_attrs: FileFieldAttrs,
}
impl FormField {
#[must_use]
pub fn input(name: impl Into<String>, input_type: InputType) -> Self {
Self::new(name, FieldKind::Input(input_type))
}
#[must_use]
pub fn textarea(name: impl Into<String>) -> Self {
Self::new(
name,
FieldKind::Textarea {
rows: None,
cols: None,
},
)
}
#[must_use]
pub fn select(name: impl Into<String>) -> Self {
Self::new(
name,
FieldKind::Select {
options: Vec::new(),
multiple: false,
},
)
}
#[must_use]
pub fn checkbox(name: impl Into<String>) -> Self {
Self::new(name, FieldKind::Checkbox { checked: false })
}
#[must_use]
pub fn radio(name: impl Into<String>) -> Self {
Self::new(name, FieldKind::Radio { options: Vec::new() })
}
fn new(name: impl Into<String>, kind: FieldKind) -> Self {
Self {
name: name.into(),
kind,
label: None,
placeholder: None,
value: None,
flags: FieldFlags::default(),
autocomplete: None,
min_length: None,
max_length: None,
min: None,
max: None,
step: None,
pattern: None,
class: None,
id: None,
help_text: None,
htmx: HtmxFieldAttrs::default(),
data_attrs: Vec::new(),
custom_attrs: Vec::new(),
file_attrs: FileFieldAttrs::default(),
}
}
#[must_use]
pub fn effective_id(&self) -> &str {
self.id.as_deref().unwrap_or(&self.name)
}
#[must_use]
pub const fn is_input(&self) -> bool {
matches!(self.kind, FieldKind::Input(_))
}
#[must_use]
pub const fn is_textarea(&self) -> bool {
matches!(self.kind, FieldKind::Textarea { .. })
}
#[must_use]
pub const fn is_select(&self) -> bool {
matches!(self.kind, FieldKind::Select { .. })
}
#[must_use]
pub const fn is_checkbox(&self) -> bool {
matches!(self.kind, FieldKind::Checkbox { .. })
}
#[must_use]
pub const fn is_radio(&self) -> bool {
matches!(self.kind, FieldKind::Radio { .. })
}
}
#[derive(Debug, Clone, Default)]
pub struct HtmxFieldAttrs {
pub get: Option<String>,
pub post: Option<String>,
pub put: Option<String>,
pub delete: Option<String>,
pub patch: Option<String>,
pub target: Option<String>,
pub swap: Option<String>,
pub trigger: Option<String>,
pub indicator: Option<String>,
pub vals: Option<String>,
pub validate: bool,
}
impl HtmxFieldAttrs {
#[must_use]
pub const fn has_any(&self) -> bool {
self.get.is_some()
|| self.post.is_some()
|| self.put.is_some()
|| self.delete.is_some()
|| self.patch.is_some()
|| self.target.is_some()
|| self.swap.is_some()
|| self.trigger.is_some()
|| self.indicator.is_some()
|| self.vals.is_some()
|| self.validate
}
}
#[derive(Debug, Clone, Default)]
pub struct FileFieldAttrs {
pub accept: Option<String>,
pub multiple: bool,
pub max_size_mb: Option<u32>,
pub show_preview: bool,
pub drag_drop: bool,
pub progress_endpoint: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_input_type_as_str() {
assert_eq!(InputType::Email.as_str(), "email");
assert_eq!(InputType::Password.as_str(), "password");
assert_eq!(InputType::DateTimeLocal.as_str(), "datetime-local");
}
#[test]
fn test_select_option() {
let opt = SelectOption::new("us", "United States");
assert_eq!(opt.value, "us");
assert_eq!(opt.label, "United States");
assert!(!opt.disabled);
}
#[test]
fn test_select_option_disabled() {
let opt = SelectOption::disabled("", "Select a country...");
assert!(opt.disabled);
}
#[test]
fn test_form_field_input() {
let field = FormField::input("email", InputType::Email);
assert_eq!(field.name, "email");
assert!(field.is_input());
assert!(!field.is_textarea());
}
#[test]
fn test_form_field_effective_id() {
let mut field = FormField::input("email", InputType::Email);
assert_eq!(field.effective_id(), "email");
field.id = Some("custom-email-id".into());
assert_eq!(field.effective_id(), "custom-email-id");
}
#[test]
fn test_htmx_field_attrs() {
let attrs = HtmxFieldAttrs::default();
assert!(!attrs.has_any());
let attrs_with_get = HtmxFieldAttrs {
get: Some("/search".into()),
..Default::default()
};
assert!(attrs_with_get.has_any());
}
}