Skip to main content

kozan_core/html/
form_control.rs

1//! Form control element trait — shared behavior for all form controls.
2//!
3//! Chrome equivalent: `HTMLFormControlElement`.
4//! Intermediate trait between `HtmlElement` and concrete form controls
5//! (button, input, select, textarea).
6//!
7//! # What it provides
8//!
9//! - `disabled` / `set_disabled` (all form controls can be disabled)
10//! - `name` / `set_name` (form submission key)
11//! - `form_id` (associated form element)
12//! - `validity` (constraint validation — future)
13//!
14//! # Chrome hierarchy
15//!
16//! ```text
17//! HTMLElement
18//!   └── HTMLFormControlElement      ← THIS TRAIT
19//!         ├── HTMLButtonElement
20//!         ├── HTMLSelectElement
21//!         └── HTMLTextControlElement ← Sub-trait for text editing
22//!               ├── HTMLInputElement
23//!               └── HTMLTextAreaElement
24//! ```
25
26use super::html_element::HtmlElement;
27
28/// Shared behavior for all form control elements.
29///
30/// Chrome equivalent: `HTMLFormControlElement`.
31/// Every form control (button, input, select, textarea) implements this.
32///
33/// All methods have default implementations that read/write attributes.
34pub trait FormControlElement: HtmlElement {
35    /// Whether this control is disabled.
36    ///
37    /// Disabled controls don't receive events and are excluded from
38    /// form submission. Chrome: `HTMLFormControlElement::IsDisabledFormControl()`.
39    fn disabled(&self) -> bool {
40        self.attribute("disabled").is_some()
41    }
42
43    fn set_disabled(&self, disabled: bool) {
44        if disabled {
45            self.set_attribute("disabled", "");
46        } else {
47            self.remove_attribute("disabled");
48        }
49    }
50
51    /// The control's name (used as the key in form submission).
52    fn name(&self) -> String {
53        self.attribute("name").unwrap_or_default()
54    }
55
56    fn set_name(&self, name: impl Into<String>) {
57        self.set_attribute("name", name);
58    }
59
60    /// The ID of the associated `<form>` element.
61    ///
62    /// Chrome: `HTMLFormControlElement::formOwner()` resolves this
63    /// to the actual form element. Currently returns the raw attribute;
64    /// form element resolution will be added with the form submission system.
65    fn form_id(&self) -> Option<String> {
66        self.attribute("form")
67    }
68
69    fn set_form_id(&self, form_id: impl Into<String>) {
70        self.set_attribute("form", form_id);
71    }
72
73    /// Whether this control is required for form submission.
74    fn required(&self) -> bool {
75        self.attribute("required").is_some()
76    }
77
78    fn set_required(&self, required: bool) {
79        if required {
80            self.set_attribute("required", "");
81        } else {
82            self.remove_attribute("required");
83        }
84    }
85
86    /// Whether this control's value satisfies its constraints.
87    ///
88    /// Chrome: `HTMLFormControlElement::checkValidity()`.
89    /// Future: constraint validation API.
90    fn check_validity(&self) -> bool {
91        // Default: always valid. Override in concrete elements.
92        true
93    }
94}
95
96/// Shared behavior for text-editing form controls (input, textarea).
97///
98/// Chrome equivalent: `TextControlElement`.
99/// Adds value, selection, and text editing capabilities.
100pub trait TextControlElement: FormControlElement {
101    /// The current text value.
102    fn value(&self) -> String {
103        self.attribute("value").unwrap_or_default()
104    }
105
106    fn set_value(&self, value: impl Into<String>) {
107        self.set_attribute("value", value);
108    }
109
110    /// The placeholder text shown when the control is empty.
111    fn placeholder(&self) -> String {
112        self.attribute("placeholder").unwrap_or_default()
113    }
114
115    fn set_placeholder(&self, placeholder: impl Into<String>) {
116        self.set_attribute("placeholder", placeholder);
117    }
118
119    /// Whether the control is read-only (value visible but not editable).
120    fn readonly(&self) -> bool {
121        self.attribute("readonly").is_some()
122    }
123
124    fn set_readonly(&self, readonly: bool) {
125        if readonly {
126            self.set_attribute("readonly", "");
127        } else {
128            self.remove_attribute("readonly");
129        }
130    }
131
132    /// Maximum number of characters allowed.
133    fn max_length(&self) -> Option<u32> {
134        self.attribute("maxlength").and_then(|v| v.parse().ok())
135    }
136
137    fn set_max_length(&self, max: u32) {
138        self.set_attribute("maxlength", max.to_string());
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use crate::dom::document::Document;
146    use crate::html::HtmlButtonElement;
147
148    #[test]
149    fn button_disabled() {
150        let doc = Document::new();
151        let btn = doc.create::<HtmlButtonElement>();
152        assert!(!btn.disabled());
153
154        btn.set_disabled(true);
155        assert!(btn.disabled());
156
157        btn.set_disabled(false);
158        assert!(!btn.disabled());
159    }
160
161    #[test]
162    fn button_name() {
163        let doc = Document::new();
164        let btn = doc.create::<HtmlButtonElement>();
165        assert_eq!(btn.name(), "");
166
167        btn.set_name("submit-btn");
168        assert_eq!(btn.name(), "submit-btn");
169    }
170
171    #[test]
172    fn button_required() {
173        let doc = Document::new();
174        let btn = doc.create::<HtmlButtonElement>();
175        assert!(!btn.required());
176
177        btn.set_required(true);
178        assert!(btn.required());
179    }
180}