stratum_components/forms/
form.rs1use crate::common::merge_classes;
4use stratum_core::aria::{AriaAttributes, AriaRole};
5use stratum_core::render::{AttrValue, ChildrenSpec, RenderOutput};
6
7#[derive(Debug, Clone, PartialEq, Default)]
10pub struct FormProps {
11 pub class: Option<String>,
12 pub id: Option<String>,
13}
14
15pub struct Form;
16
17impl Form {
18 pub fn classes(props: &FormProps) -> String {
19 merge_classes("space-y-6", &props.class)
20 }
21
22 pub fn render(props: &FormProps) -> RenderOutput {
23 let classes = Self::classes(props);
24 let mut output = RenderOutput::new()
25 .with_tag("form")
26 .with_class(classes)
27 .with_aria(AriaAttributes::new().with_role(AriaRole::Form));
28 if let Some(ref id) = props.id {
29 output = output.with_attr("id", AttrValue::String(id.clone()));
30 }
31 output
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Default)]
38pub struct FormFieldProps {
39 pub class: Option<String>,
40}
41
42pub struct FormField;
43
44impl FormField {
45 pub fn classes(props: &FormFieldProps) -> String {
46 merge_classes("space-y-2", &props.class)
47 }
48
49 pub fn render(props: &FormFieldProps) -> RenderOutput {
50 RenderOutput::new()
51 .with_tag("div")
52 .with_class(Self::classes(props))
53 }
54}
55
56#[derive(Debug, Clone, PartialEq, Default)]
59pub struct FormLabelProps {
60 pub html_for: Option<String>,
61 pub required: bool,
62 pub class: Option<String>,
63}
64
65pub struct FormLabel;
66
67impl FormLabel {
68 const BASE: &'static str = "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70";
69
70 pub fn classes(props: &FormLabelProps) -> String {
71 merge_classes(Self::BASE, &props.class)
72 }
73
74 pub fn render(props: &FormLabelProps) -> RenderOutput {
75 let mut output = RenderOutput::new()
76 .with_tag("label")
77 .with_class(Self::classes(props));
78 if let Some(ref f) = props.html_for {
79 output = output.with_attr("for", AttrValue::String(f.clone()));
80 }
81 output
82 }
83}
84
85#[derive(Debug, Clone, PartialEq)]
88pub struct FormErrorProps {
89 pub message: String,
90 pub class: Option<String>,
91 pub id: Option<String>,
92}
93
94pub struct FormError;
95
96impl FormError {
97 const BASE: &'static str = "text-[0.8rem] font-medium text-destructive";
98
99 pub fn classes(props: &FormErrorProps) -> String {
100 merge_classes(Self::BASE, &props.class)
101 }
102
103 pub fn render(props: &FormErrorProps) -> RenderOutput {
104 let mut output = RenderOutput::new()
105 .with_tag("p")
106 .with_class(Self::classes(props))
107 .with_aria(AriaAttributes::new().with_role(AriaRole::Alert))
108 .with_children(ChildrenSpec::Text(props.message.clone()));
109 if let Some(ref id) = props.id {
110 output = output.with_attr("id", AttrValue::String(id.clone()));
111 }
112 output
113 }
114}
115
116#[derive(Debug, Clone, PartialEq)]
119pub struct FormHelperTextProps {
120 pub text: String,
121 pub class: Option<String>,
122 pub id: Option<String>,
123}
124
125pub struct FormHelperText;
126
127impl FormHelperText {
128 const BASE: &'static str = "text-[0.8rem] text-muted-foreground";
129
130 pub fn classes(props: &FormHelperTextProps) -> String {
131 merge_classes(Self::BASE, &props.class)
132 }
133
134 pub fn render(props: &FormHelperTextProps) -> RenderOutput {
135 let mut output = RenderOutput::new()
136 .with_tag("p")
137 .with_class(Self::classes(props))
138 .with_children(ChildrenSpec::Text(props.text.clone()));
139 if let Some(ref id) = props.id {
140 output = output.with_attr("id", AttrValue::String(id.clone()));
141 }
142 output
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn form_render_tag() {
152 let props = FormProps::default();
153 let output = Form::render(&props);
154 assert_eq!(output.effective_tag(), "form");
155 assert_eq!(output.aria.role, Some(AriaRole::Form));
156 }
157
158 #[test]
159 fn form_field_classes() {
160 let props = FormFieldProps::default();
161 let classes = FormField::classes(&props);
162 assert!(classes.contains("space-y-2"));
163 }
164
165 #[test]
166 fn form_label_render() {
167 let props = FormLabelProps {
168 html_for: Some("email".to_string()),
169 ..Default::default()
170 };
171 let output = FormLabel::render(&props);
172 assert_eq!(output.effective_tag(), "label");
173 assert!(output.attrs.iter().any(|(k, _)| k == "for"));
174 }
175
176 #[test]
177 fn form_error_render() {
178 let props = FormErrorProps {
179 message: "Required field".to_string(),
180 class: None,
181 id: Some("email-error".to_string()),
182 };
183 let output = FormError::render(&props);
184 assert_eq!(output.effective_tag(), "p");
185 assert_eq!(output.aria.role, Some(AriaRole::Alert));
186 assert_eq!(
187 output.children,
188 ChildrenSpec::Text("Required field".to_string())
189 );
190 }
191
192 #[test]
193 fn form_helper_text_render() {
194 let props = FormHelperTextProps {
195 text: "Enter your email".to_string(),
196 class: None,
197 id: None,
198 };
199 let output = FormHelperText::render(&props);
200 assert_eq!(output.effective_tag(), "p");
201 assert!(output.class_string().contains("text-muted-foreground"));
202 }
203}