radix_leptos_primitives/components/form_validation/
fields.rs1use crate::utils::{merge_classes, generate_id};
2use leptos::callback::Callback;
3use leptos::prelude::*;
4
5use super::validation::{ValidationRule, FieldValidationResult};
6
7#[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#[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#[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 </div>
104 }
105}
106
107#[cfg(test)]
108mod fields_tests {
109 use super::*;
110
111 #[test]
112 fn test_form_field_creation() {
113 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 let for_id = "email".to_string();
126 assert!(!for_id.is_empty());
127 }
128
129 #[test]
130 fn test_form_field_error_creation() {
131 let name = "email".to_string();
133 assert!(!name.is_empty());
134 }
135}