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}