radix_leptos_primitives/components/form_validation/
fields.rs

1use crate::utils::{merge_classes, generate_id};
2use leptos::callback::Callback;
3use leptos::prelude::*;
4
5use super::validation::{ValidationRule, FieldValidationResult};
6
7/// Form Field with Validation
8#[component]
9pub fn FormField(
10    #[prop(optional)] class: Option<String>,
11    #[prop(optional)] style: Option<String>,
12    #[prop(optional)] children: Option<Children>,
13    #[prop(optional)] name: Option<String>,
14    #[prop(optional)] label: Option<String>,
15    #[prop(optional)] required: Option<bool>,
16    #[prop(optional)] validation_rules: Option<Vec<ValidationRule>>,
17    #[prop(optional)] on_validation: Option<Callback<FieldValidationResult>>,
18) -> impl IntoView {
19    let name = name.unwrap_or_default();
20    let label = label.unwrap_or_default();
21    let required = required.unwrap_or(false);
22    let validation_rules = validation_rules.unwrap_or_default();
23
24    let class = merge_classes(vec![
25        "form-field",
26        class.as_deref().unwrap_or(""),
27    ]);
28
29    view! {
30        <div
31            class=class
32            style=style
33            data-field-name=name
34            data-required=required
35        >
36            {if !label.is_empty() {
37                view! {
38                    <FormLabel for_id=name.clone()>
39                        {label}
40                            {if required {
41                                view! { <span class="required-indicator">"*"</span> }
42                            } else {
43                                view! { <span class="required-indicator">""</span> }
44                            }}
45                    </FormLabel>
46                }.into_any()
47            } else {
48                view! { <div></div> }.into_any()
49            }}
50            {children.map(|c| c())}
51            <FormFieldError name=name.clone() />
52        </div>
53    }
54}
55
56/// Form Label component
57#[component]
58pub fn FormLabel(
59    #[prop(optional)] class: Option<String>,
60    #[prop(optional)] style: Option<String>,
61    #[prop(optional)] children: Option<Children>,
62    #[prop(optional)] for_id: Option<String>,
63) -> impl IntoView {
64    let class = merge_classes(vec![
65        "form-label",
66        class.as_deref().unwrap_or(""),
67    ]);
68
69    view! {
70        <label
71            class=class
72            style=style
73            for=for_id
74        >
75            {children.map(|c| c())}
76        </label>
77    }
78}
79
80/// Form Field Error component
81#[component]
82pub fn FormFieldError(
83    #[prop(optional)] class: Option<String>,
84    #[prop(optional)] style: Option<String>,
85    #[prop(optional)] name: Option<String>,
86) -> impl IntoView {
87    let name = name.unwrap_or_default();
88
89    let class = merge_classes(vec![
90        "form-field-error",
91        class.as_deref().unwrap_or(""),
92    ]);
93
94    view! {
95        <div
96            class=class
97            style=style
98            role="alert"
99            aria-live="polite"
100            data-field-name=name
101        >
102            // Error message will be displayed here
103        </div>
104    }
105}
106
107#[cfg(test)]
108mod fields_tests {
109    use super::*;
110
111    #[test]
112    fn test_form_field_creation() {
113        // Test component creation without runtime
114        let name = "email".to_string();
115        let label = "Email".to_string();
116        let required = true;
117        assert!(!name.is_empty());
118        assert!(!label.is_empty());
119        assert!(required);
120    }
121
122    #[test]
123    fn test_form_label_creation() {
124        // Test component creation without runtime
125        let for_id = "email".to_string();
126        assert!(!for_id.is_empty());
127    }
128
129    #[test]
130    fn test_form_field_error_creation() {
131        // Test component creation without runtime
132        let name = "email".to_string();
133        assert!(!name.is_empty());
134    }
135}