1use crate::components::grid::ColProps;
2use crate::foundation::{
3 ClassListExt, FormClassNames, FormSemantic, FormStyles, StyleStringExt, Variant,
4};
5use dioxus::{
6 core::{current_scope_id, schedule_update_any},
7 prelude::*,
8};
9use regex::Regex;
10use serde_json::Value;
11use std::{
12 cell::RefCell,
13 collections::HashMap,
14 rc::Rc,
15 sync::atomic::{AtomicUsize, Ordering},
16};
17
18pub type FormValues = HashMap<String, Value>;
19pub type FormErrors = HashMap<String, String>;
20pub type FormValidator = fn(Option<&Value>) -> Result<(), String>;
21
22#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
23pub enum FormLayout {
24 #[default]
25 Horizontal,
26 Vertical,
27 Inline,
28}
29
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
31pub enum ControlSize {
32 Small,
33 #[default]
34 Middle,
35 Large,
36}
37
38#[derive(Clone)]
47pub enum RequiredMark {
48 None,
50 Optional,
52 Default,
54 Custom(Rc<dyn Fn(Element, bool) -> Element>),
56}
57
58impl Default for RequiredMark {
59 fn default() -> Self {
60 RequiredMark::Default
61 }
62}
63
64impl PartialEq for RequiredMark {
65 fn eq(&self, other: &Self) -> bool {
66 match (self, other) {
67 (RequiredMark::None, RequiredMark::None) => true,
68 (RequiredMark::Optional, RequiredMark::Optional) => true,
69 (RequiredMark::Default, RequiredMark::Default) => true,
70 (RequiredMark::Custom(_), RequiredMark::Custom(_)) => {
71 false
74 }
75 _ => false,
76 }
77 }
78}
79
80impl std::fmt::Debug for RequiredMark {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 match self {
83 RequiredMark::None => write!(f, "RequiredMark::None"),
84 RequiredMark::Optional => write!(f, "RequiredMark::Optional"),
85 RequiredMark::Default => write!(f, "RequiredMark::Default"),
86 RequiredMark::Custom(_) => write!(f, "RequiredMark::Custom(..)"),
87 }
88 }
89}
90
91#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
92pub enum LabelAlign {
93 Left,
94 #[default]
95 Right,
96}
97
98#[derive(Clone, Default, PartialEq)]
102pub struct FeedbackIcons {
103 pub success: Option<Element>,
105 pub error: Option<Element>,
107 pub warning: Option<Element>,
109 pub validating: Option<Element>,
111}
112
113impl FeedbackIcons {
114 pub fn default_icons() -> Self {
116 Self {
117 success: None, error: None,
119 warning: None,
120 validating: None,
121 }
122 }
123}
124
125#[derive(Clone, Debug, Default, PartialEq)]
127pub struct ScrollToFirstErrorConfig {
128 pub block: Option<String>,
130 pub inline: Option<String>,
132 pub behavior: Option<String>,
134}
135
136#[derive(Clone, Debug, Default)]
137pub struct FormRule {
138 pub required: bool,
139 pub min: Option<usize>,
140 pub max: Option<usize>,
141 pub len: Option<usize>,
142 pub pattern: Option<String>,
143 pub message: Option<String>,
144 pub validator: Option<FormValidator>,
145}
146
147impl PartialEq for FormRule {
148 fn eq(&self, other: &Self) -> bool {
149 self.required == other.required
150 && self.min == other.min
151 && self.max == other.max
152 && self.len == other.len
153 && self.pattern == other.pattern
154 && self.message == other.message
155 }
156}
157
158#[derive(Clone, Debug, Default, PartialEq)]
159pub struct FormFinishEvent {
160 pub values: FormValues,
161}
162
163#[derive(Clone, Debug, Default, PartialEq)]
164pub struct FormFinishFailedEvent {
165 pub errors: FormErrors,
166}
167
168#[derive(Clone, Debug, Default, PartialEq)]
170pub struct ValuesChangeEvent {
171 pub changed_values: FormValues,
173 pub all_values: FormValues,
175}
176
177#[derive(Clone)]
178struct FormStore {
179 values: FormValues,
180 errors: FormErrors,
181}
182
183impl FormStore {
184 fn new() -> Self {
185 Self {
186 values: HashMap::new(),
187 errors: HashMap::new(),
188 }
189 }
190}
191
192static FORM_HANDLE_ID: AtomicUsize = AtomicUsize::new(0);
193
194#[derive(Clone)]
195pub struct FormHandle {
196 store: Rc<RefCell<FormStore>>,
197 listeners: Rc<RefCell<HashMap<String, Vec<ScopeId>>>>,
198 id: usize,
199}
200
201impl PartialEq for FormHandle {
202 fn eq(&self, other: &Self) -> bool {
203 self.id == other.id
204 }
205}
206
207impl FormHandle {
208 pub fn new() -> Self {
209 let id = FORM_HANDLE_ID.fetch_add(1, Ordering::Relaxed);
210 Self {
211 store: Rc::new(RefCell::new(FormStore::new())),
212 listeners: Rc::new(RefCell::new(HashMap::new())),
213 id,
214 }
215 }
216
217 pub fn set_field_value(&self, name: &str, value: Value) {
218 if let Ok(mut store) = self.store.try_borrow_mut() {
219 store.values.insert(name.to_string(), value);
220 }
221 self.notify(name);
222 }
223
224 pub fn get_field_value(&self, name: &str) -> Option<Value> {
225 self.store
226 .try_borrow()
227 .ok()
228 .and_then(|store| store.values.get(name).cloned())
229 }
230
231 pub fn set_error(&self, name: &str, message: Option<String>) {
232 if let Ok(mut store) = self.store.try_borrow_mut() {
233 match message {
234 Some(msg) => {
235 store.errors.insert(name.to_string(), msg);
236 }
237 None => {
238 store.errors.remove(name);
239 }
240 }
241 }
242 }
243
244 pub fn get_error(&self, name: &str) -> Option<String> {
245 self.store
246 .try_borrow()
247 .ok()
248 .and_then(|store| store.errors.get(name).cloned())
249 }
250
251 pub fn values(&self) -> FormValues {
252 self.store
253 .try_borrow()
254 .map(|store| store.values.clone())
255 .unwrap_or_default()
256 }
257
258 pub fn errors(&self) -> FormErrors {
259 self.store
260 .try_borrow()
261 .map(|store| store.errors.clone())
262 .unwrap_or_default()
263 }
264
265 pub fn reset_fields(&self) {
266 if let Ok(mut store) = self.store.try_borrow_mut() {
267 store.values.clear();
268 store.errors.clear();
269 }
270 let names = self.listeners.borrow().keys().cloned().collect::<Vec<_>>();
271 self.notify_all(&names);
272 }
273
274 fn notify(&self, name: &str) {
275 let targets = self
276 .listeners
277 .borrow()
278 .get(name)
279 .cloned()
280 .unwrap_or_default();
281 let scheduler = schedule_update_any();
282 for scope in targets {
283 scheduler(scope);
284 }
285 }
286
287 fn notify_all(&self, names: &[String]) {
288 for name in names {
289 self.notify(name);
290 }
291 }
292
293 pub fn register_listener(&self, name: &str, scope_id: ScopeId) {
294 let mut map = self.listeners.borrow_mut();
295 let entry = map.entry(name.to_string()).or_default();
296 if !entry.contains(&scope_id) {
297 entry.push(scope_id);
298 }
299 }
300}
301
302impl Default for FormHandle {
303 fn default() -> Self {
304 FormHandle::new()
305 }
306}
307
308#[derive(Clone)]
309struct FormContext {
310 handle: FormHandle,
311 _layout: FormLayout,
312 _size: ControlSize,
313 colon: bool,
314 required_mark: RequiredMark,
315 _label_align: LabelAlign,
316 label_wrap: bool,
317 _label_col: Option<ColProps>,
318 _wrapper_col: Option<ColProps>,
319 disabled: bool,
320 #[allow(dead_code)]
321 variant: Option<Variant>,
322 #[allow(dead_code)]
323 feedback_icons: Option<FeedbackIcons>,
324 registry: Rc<RefCell<HashMap<String, Vec<FormRule>>>>,
325 on_values_change: Option<EventHandler<ValuesChangeEvent>>,
326}
327
328#[derive(Clone)]
329pub struct FormItemControlContext {
330 name: String,
331 handle: FormHandle,
332 disabled: bool,
333 registry: Rc<RefCell<HashMap<String, Vec<FormRule>>>>,
334 on_values_change: Option<EventHandler<ValuesChangeEvent>>,
335 value_prop_name: Option<String>,
336 get_value_from_event: Option<GetValueFromEventFn>,
337}
338
339impl FormItemControlContext {
340 pub fn name(&self) -> &str {
341 &self.name
342 }
343
344 pub fn value(&self) -> Option<Value> {
345 self.handle.get_field_value(&self.name)
346 }
347
348 pub fn value_prop_name(&self) -> Option<&str> {
351 self.value_prop_name.as_deref()
352 }
353
354 pub fn apply_mapped_value(&self, raw: Value) {
357 let mapped = if let Some(mapper) = self.get_value_from_event {
358 mapper(raw)
359 } else {
360 raw
361 };
362 self.set_value(mapped);
363 }
364
365 pub fn set_value(&self, value: Value) {
366 if self.disabled {
367 return;
368 }
369 self.handle.set_field_value(&self.name, value);
370 self.run_validation();
371
372 if let Some(cb) = self.on_values_change.as_ref() {
373 let mut changed = FormValues::new();
374 if let Some(current) = self.handle.get_field_value(&self.name) {
375 changed.insert(self.name.clone(), current);
376 }
377 let all = self.handle.values();
378 cb.call(ValuesChangeEvent {
379 changed_values: changed,
380 all_values: all,
381 });
382 }
383 }
384
385 pub fn set_string(&self, value: impl Into<String>) {
386 self.set_value(Value::String(value.into()));
387 }
388
389 pub fn is_disabled(&self) -> bool {
390 self.disabled
391 }
392
393 fn run_validation(&self) {
394 apply_validation_result(&self.handle, &self.registry, &self.name);
395 }
396}
397
398#[derive(Clone)]
401pub struct FormListContext {
402 pub name: String,
403 pub handle: FormHandle,
404}
405
406impl FormListContext {
407 pub fn len(&self) -> usize {
409 form_list_len(&self.handle, &self.name)
410 }
411
412 pub fn is_empty(&self) -> bool {
414 self.len() == 0
415 }
416
417 pub fn insert(&self, index: usize, item: Value) {
419 form_list_insert(&self.handle, &self.name, index, item);
420 }
421
422 pub fn remove(&self, index: usize) {
424 form_list_remove(&self.handle, &self.name, index);
425 }
426}
427
428pub fn use_form_list() -> Option<FormListContext> {
430 try_use_context::<FormListContext>()
431}
432
433pub fn use_form_item_control() -> Option<FormItemControlContext> {
434 if let Some(ctx) = try_use_context::<FormItemControlContext>() {
435 let scope = current_scope_id();
439 ctx.handle.register_listener(ctx.name(), scope);
440 Some(ctx)
441 } else {
442 None
443 }
444}
445
446pub fn use_form() -> FormHandle {
448 FormHandle::new()
449}
450
451#[derive(Props, Clone, PartialEq)]
452pub struct FormProps {
453 #[props(default)]
454 pub layout: FormLayout,
455 #[props(default)]
456 pub size: ControlSize,
457 #[props(default)]
458 pub colon: bool,
459 #[props(default)]
460 pub required_mark: RequiredMark,
461 #[props(default)]
462 pub label_align: LabelAlign,
463 #[props(default)]
465 pub label_wrap: bool,
466 #[props(optional)]
467 pub label_col: Option<ColProps>,
468 #[props(optional)]
469 pub wrapper_col: Option<ColProps>,
470 #[props(default)]
471 pub disabled: bool,
472 #[props(optional)]
474 pub variant: Option<Variant>,
475 #[props(default)]
478 pub scroll_to_first_error: bool,
479 #[props(optional)]
481 pub scroll_to_first_error_config: Option<ScrollToFirstErrorConfig>,
482 #[props(optional)]
484 pub feedback_icons: Option<FeedbackIcons>,
485 #[props(optional)]
487 pub name: Option<String>,
488 #[props(optional)]
489 pub initial_values: Option<FormValues>,
490 #[props(optional)]
491 pub form: Option<FormHandle>,
492 #[props(optional)]
493 pub class: Option<String>,
494 #[props(optional)]
495 pub style: Option<String>,
496 #[props(optional)]
498 pub class_names: Option<FormClassNames>,
499 #[props(optional)]
501 pub styles: Option<FormStyles>,
502 #[props(optional)]
503 pub on_finish: Option<EventHandler<FormFinishEvent>>,
504 #[props(optional)]
505 pub on_finish_failed: Option<EventHandler<FormFinishFailedEvent>>,
506 #[props(optional)]
508 pub on_values_change: Option<EventHandler<ValuesChangeEvent>>,
509 pub children: Element,
510}
511
512#[component]
513pub fn Form(props: FormProps) -> Element {
514 let FormProps {
515 layout,
516 size,
517 colon,
518 required_mark,
519 label_align,
520 label_wrap,
521 label_col,
522 wrapper_col,
523 disabled,
524 variant,
525 scroll_to_first_error,
526 scroll_to_first_error_config,
527 feedback_icons,
528 name,
529 initial_values,
530 form,
531 class,
532 style,
533 class_names,
534 styles,
535 on_finish,
536 on_finish_failed,
537 on_values_change,
538 children,
539 } = props;
540
541 let handle = form.unwrap_or_else(FormHandle::new);
542 if let Some(initial) = initial_values
543 && handle.values().is_empty()
544 {
545 for (k, v) in initial.into_iter() {
546 handle.set_field_value(&k, v);
547 }
548 }
549
550 let registry_signal = use_signal::<Rc<RefCell<HashMap<String, Vec<FormRule>>>>>(|| {
553 Rc::new(RefCell::new(HashMap::new()))
554 });
555 let registry = registry_signal.read().clone();
556 let context = FormContext {
557 handle: handle.clone(),
558 _layout: layout,
559 _size: size,
560 colon,
561 required_mark,
562 _label_align: label_align,
563 label_wrap,
564 _label_col: label_col,
565 _wrapper_col: wrapper_col,
566 disabled,
567 variant,
568 feedback_icons,
569 registry: registry.clone(),
570 on_values_change,
571 };
572 use_context_provider(|| context);
573
574 let submit_handle = handle.clone();
575 let submit_registry = registry.clone();
576 let finish_cb = on_finish;
577 let failed_cb = on_finish_failed;
578
579 let mut form_classes = vec![form_class(layout, size)];
581 form_classes.push_semantic(&class_names, FormSemantic::Root);
582 if let Some(extra) = class {
583 form_classes.push(extra);
584 }
585 let form_class_attr = form_classes
586 .into_iter()
587 .filter(|s| !s.is_empty())
588 .collect::<Vec<_>>()
589 .join(" ");
590
591 let mut form_style_attr = style.unwrap_or_default();
592 form_style_attr.append_semantic(&styles, FormSemantic::Root);
593
594 let _scroll_config = scroll_to_first_error_config;
596
597 let name_attr = name.clone();
598 rsx! {
599 form {
600 class: "{form_class_attr}",
601 style: "{form_style_attr}",
602 name: name_attr,
603 onsubmit: move |evt| {
604 evt.prevent_default();
605 if validate_all(&submit_handle, &submit_registry) {
606 if let Some(cb) = finish_cb.as_ref() {
607 cb.call(FormFinishEvent { values: submit_handle.values() });
608 }
609 } else {
610 if scroll_to_first_error {
612 #[cfg(target_arch = "wasm32")]
613 {
614 scroll_to_first_error_field(&submit_handle, &submit_registry);
615 }
616 }
617 if let Some(cb) = failed_cb.as_ref() {
618 cb.call(FormFinishFailedEvent { errors: submit_handle.errors() });
619 }
620 }
621 },
622 {children}
623 }
624 }
625}
626
627#[cfg(target_arch = "wasm32")]
629fn scroll_to_first_error_field(
630 handle: &FormHandle,
631 registry: &Rc<RefCell<HashMap<String, Vec<FormRule>>>>,
632) {
633 let errors = handle.errors();
634 if errors.is_empty() {
635 return;
636 }
637
638 let field_names: Vec<String> = match registry.try_borrow() {
640 Ok(map) => map.keys().cloned().collect(),
641 Err(_) => return,
642 };
643
644 for name in field_names {
646 if errors.contains_key(&name) {
647 if let Some(window) = web_sys::window() {
649 if let Some(document) = window.document() {
650 if let Some(element) = document
652 .query_selector(&format!("[name=\"{}\"]", name))
653 .ok()
654 .flatten()
655 {
656 let _ = element.scroll_into_view();
657 }
658 }
659 }
660 break;
661 }
662 }
663}
664
665fn form_class(layout: FormLayout, size: ControlSize) -> String {
666 let mut classes = vec!["adui-form".to_string()];
667 match layout {
668 FormLayout::Horizontal => classes.push("adui-form-horizontal".into()),
669 FormLayout::Vertical => classes.push("adui-form-vertical".into()),
670 FormLayout::Inline => classes.push("adui-form-inline".into()),
671 }
672 match size {
673 ControlSize::Small => classes.push("adui-form-small".into()),
674 ControlSize::Large => classes.push("adui-form-large".into()),
675 ControlSize::Middle => {}
676 }
677 classes.join(" ")
678}
679
680fn validate_all(
681 handle: &FormHandle,
682 registry: &Rc<RefCell<HashMap<String, Vec<FormRule>>>>,
683) -> bool {
684 let names: Vec<String> = match registry.try_borrow() {
686 Ok(map) => map.keys().cloned().collect(),
687 Err(_) => {
688 return false;
690 }
691 };
692
693 if handle.values().is_empty() {
697 let mut required_names = Vec::new();
698 let mut has_required = false;
699
700 if let Ok(map) = registry.try_borrow() {
701 for (name, rules) in map.iter() {
702 if rules.iter().any(|rule| rule.required) {
703 has_required = true;
704 required_names.push(name.clone());
705 }
706 }
707 } else {
708 return false;
710 }
711
712 if has_required {
713 for name in &required_names {
714 apply_validation_result(handle, registry, name);
715 }
716 return false;
717 }
718 }
719
720 let mut ok = true;
721 for name in &names {
722 if !apply_validation_result(handle, registry, name) {
723 ok = false;
724 }
725 }
726
727 ok
728}
729
730#[derive(Clone, Debug, PartialEq, Eq)]
731pub struct FormListItemMeta {
732 pub index: usize,
733 pub name: String,
735}
736
737#[derive(Props, Clone, PartialEq)]
738pub struct FormListProps {
739 pub name: String,
741 #[props(optional)]
743 pub initial_count: Option<usize>,
744 pub children: Element,
746}
747
748#[component]
751pub fn FormList(props: FormListProps) -> Element {
752 let ctx = use_context::<FormContext>();
753 let FormListProps {
754 name,
755 initial_count,
756 children,
757 } = props;
758
759 if let Some(count) = initial_count
761 && count > 0
762 && form_list_len(&ctx.handle, &name) == 0
763 {
764 let mut items = Vec::with_capacity(count);
765 for _ in 0..count {
766 items.push(Value::Null);
767 }
768 form_list_set(&ctx.handle, &name, items);
769 }
770
771 let list_ctx = FormListContext {
772 name: name.clone(),
773 handle: ctx.handle.clone(),
774 };
775 use_context_provider(|| list_ctx);
776
777 rsx! { div { class: "adui-form-list", {children} } }
778}
779
780pub type GetValueFromEventFn = fn(Value) -> Value;
781
782#[allow(unpredictable_function_pointer_comparisons)]
785#[derive(Props, Clone, PartialEq)]
786pub struct FormItemProps {
787 #[props(optional)]
788 pub name: Option<String>,
789 #[props(optional)]
790 pub label: Option<String>,
791 #[props(optional)]
792 pub tooltip: Option<String>,
793 #[props(optional)]
794 pub help: Option<String>,
795 #[props(optional)]
796 pub extra: Option<String>,
797 #[props(optional)]
798 pub rules: Option<Vec<FormRule>>,
799 #[props(optional)]
801 pub dependencies: Option<Vec<String>>,
802 #[props(optional)]
804 pub value_prop_name: Option<String>,
805 #[props(optional)]
807 pub get_value_from_event: Option<GetValueFromEventFn>,
808 #[props(optional)]
809 pub class: Option<String>,
810 #[props(optional)]
811 pub style: Option<String>,
812 #[props(default)]
813 pub has_feedback: bool,
814 pub children: Element,
815}
816
817#[component]
818pub fn FormItem(props: FormItemProps) -> Element {
819 let ctx = use_context::<FormContext>();
820 let FormItemProps {
821 name,
822 label,
823 tooltip,
824 help,
825 extra,
826 rules,
827 dependencies,
828 value_prop_name,
829 get_value_from_event,
830 class,
831 style,
832 has_feedback,
833 children,
834 } = props;
835
836 let item_rules = rules.unwrap_or_default();
837 let is_required = item_rules.iter().any(|rule| rule.required);
838
839 let item_scope = current_scope_id();
840
841 if let Some(field_name) = name.clone() {
842 {
843 ctx.registry
844 .borrow_mut()
845 .insert(field_name.clone(), item_rules.clone());
846 }
847 ctx.handle.register_listener(&field_name, item_scope);
848 let control_ctx = FormItemControlContext {
849 name: field_name.clone(),
850 handle: ctx.handle.clone(),
851 disabled: ctx.disabled,
852 registry: ctx.registry.clone(),
853 on_values_change: ctx.on_values_change,
854 value_prop_name: value_prop_name.clone(),
855 get_value_from_event,
856 };
857 use_context_provider(|| control_ctx);
858 if !item_rules.is_empty() {
859 let handle = ctx.handle.clone();
860 let registry = ctx.registry.clone();
861 use_effect(move || {
862 apply_validation_result(&handle, ®istry, &field_name);
863 });
864 }
865 }
866
867 if let Some(deps) = dependencies.as_ref() {
869 for dep in deps {
870 ctx.handle.register_listener(dep, item_scope);
871 }
872 }
873
874 let error_message = name.as_ref().and_then(|field| ctx.handle.get_error(field));
875
876 let mut wrapper_class = vec!["adui-form-item".to_string()];
877 if let Some(extra) = class {
878 wrapper_class.push(extra);
879 }
880 if error_message.is_some() {
881 wrapper_class.push("adui-form-item-has-error".into());
882 } else if has_feedback {
883 wrapper_class.push("adui-form-item-has-feedback".into());
884 }
885
886 let tooltip_text = tooltip.clone().unwrap_or_default();
887
888 let label_content = if let Some(text) = label {
890 let label_text = if ctx.colon {
891 format!("{}:", text)
892 } else {
893 text.clone()
894 };
895 let label_node = rsx! { "{label_text}" };
896
897 let final_label = match &ctx.required_mark {
898 RequiredMark::None => label_node,
899 RequiredMark::Optional => {
900 rsx! {
901 {label_node}
902 span { class: "adui-form-item-optional", "(optional)" }
903 }
904 }
905 RequiredMark::Default => {
906 if is_required {
907 rsx! {
908 span { class: "adui-form-item-required", "*" }
909 {label_node}
910 }
911 } else {
912 label_node
913 }
914 }
915 RequiredMark::Custom(render_fn) => {
916 let custom_node = render_fn(label_node, is_required);
917 custom_node
918 }
919 };
920
921 Some(final_label)
922 } else {
923 None
924 };
925
926 rsx! {
927 div { class: wrapper_class.join(" "), style: style.unwrap_or_default(),
928 if let Some(label_el) = label_content {
929 label {
930 class: if ctx.label_wrap { "adui-form-item-label adui-form-item-label-wrap" } else { "adui-form-item-label" },
931 title: "{tooltip_text}",
932 {label_el}
933 }
934 }
935 div { class: "adui-form-item-control", {children} }
936 if let Some(help_text) = error_message.or(help) {
937 div { class: "adui-form-item-help", "{help_text}" }
938 }
939 if let Some(extra_text) = extra {
940 div { class: "adui-form-item-extra", "{extra_text}" }
941 }
942 }
943 }
944}
945
946fn apply_validation_result(
947 handle: &FormHandle,
948 registry: &Rc<RefCell<HashMap<String, Vec<FormRule>>>>,
949 name: &str,
950) -> bool {
951 if let Some(err) = validate_field(handle, registry, name) {
952 handle.set_error(name, Some(err));
953 false
954 } else {
955 handle.set_error(name, None);
956 true
957 }
958}
959
960fn validate_field(
961 handle: &FormHandle,
962 registry: &Rc<RefCell<HashMap<String, Vec<FormRule>>>>,
963 name: &str,
964) -> Option<String> {
965 let rules = registry
966 .try_borrow()
967 .ok()
968 .and_then(|map| map.get(name).cloned())
969 .unwrap_or_default();
970 run_rules(handle, name, &rules)
971}
972
973fn run_rules(handle: &FormHandle, name: &str, rules: &[FormRule]) -> Option<String> {
974 if rules.is_empty() {
975 return None;
976 }
977 let value = handle.get_field_value(name);
978 for rule in rules {
979 if let Some(msg) = evaluate_rule(rule, value.as_ref()) {
980 return Some(msg);
981 }
982 }
983 None
984}
985
986fn evaluate_rule(rule: &FormRule, value: Option<&Value>) -> Option<String> {
987 if rule.required && value_is_empty(value) {
988 return Some(
989 rule.message
990 .clone()
991 .unwrap_or_else(|| "必填项不能为空".into()),
992 );
993 }
994
995 if value_is_empty(value) {
996 return None;
997 }
998
999 if let Some(len_target) = rule.len
1000 && value_length(value) != Some(len_target)
1001 {
1002 return Some(
1003 rule.message
1004 .clone()
1005 .unwrap_or_else(|| format!("长度必须为 {}", len_target)),
1006 );
1007 }
1008
1009 if let Some(min) = rule.min {
1010 match (value_length(value), numeric_value(value)) {
1011 (Some(len), _) if len < min => {
1012 return Some(
1013 rule.message
1014 .clone()
1015 .unwrap_or_else(|| format!("长度不能小于 {}", min)),
1016 );
1017 }
1018 (_, Some(num)) if num < min as f64 => {
1019 return Some(
1020 rule.message
1021 .clone()
1022 .unwrap_or_else(|| format!("数值不能小于 {}", min)),
1023 );
1024 }
1025 _ => {}
1026 }
1027 }
1028
1029 if let Some(max) = rule.max {
1030 match (value_length(value), numeric_value(value)) {
1031 (Some(len), _) if len > max => {
1032 return Some(
1033 rule.message
1034 .clone()
1035 .unwrap_or_else(|| format!("长度不能大于 {}", max)),
1036 );
1037 }
1038 (_, Some(num)) if num > max as f64 => {
1039 return Some(
1040 rule.message
1041 .clone()
1042 .unwrap_or_else(|| format!("数值不能大于 {}", max)),
1043 );
1044 }
1045 _ => {}
1046 }
1047 }
1048
1049 if let Some(pattern) = &rule.pattern
1050 && let Some(text) = value_to_string(value)
1051 {
1052 match Regex::new(pattern) {
1053 Ok(re) => {
1054 if !re.is_match(&text) {
1055 return Some(rule.message.clone().unwrap_or_else(|| "格式不匹配".into()));
1056 }
1057 }
1058 Err(_) => {
1059 return Some(format!("无效的正则表达式: {}", pattern));
1060 }
1061 }
1062 }
1063
1064 if let Some(validator) = rule.validator
1065 && let Err(err) = validator(value)
1066 {
1067 return Some(err);
1068 }
1069
1070 None
1071}
1072
1073fn value_is_empty(value: Option<&Value>) -> bool {
1074 match value {
1075 None => true,
1076 Some(Value::Null) => true,
1077 Some(Value::String(text)) => text.trim().is_empty(),
1078 Some(Value::Array(list)) => list.is_empty(),
1079 Some(Value::Object(map)) => map.is_empty(),
1080 Some(Value::Bool(flag)) => !flag,
1082 _ => false,
1083 }
1084}
1085
1086fn value_length(value: Option<&Value>) -> Option<usize> {
1087 value.and_then(|val| match val {
1088 Value::String(text) => Some(text.chars().count()),
1089 Value::Array(list) => Some(list.len()),
1090 Value::Object(map) => Some(map.len()),
1091 _ => None,
1092 })
1093}
1094
1095fn numeric_value(value: Option<&Value>) -> Option<f64> {
1096 value.and_then(|val| match val {
1097 Value::Number(num) => num.as_f64(),
1098 _ => None,
1099 })
1100}
1101
1102fn value_to_string(value: Option<&Value>) -> Option<String> {
1103 value.and_then(|val| match val {
1104 Value::String(text) => Some(text.clone()),
1105 Value::Number(num) => Some(num.to_string()),
1106 Value::Bool(flag) => Some(flag.to_string()),
1107 Value::Null => None,
1108 Value::Array(_) | Value::Object(_) => None,
1109 })
1110}
1111
1112pub fn form_value_to_string(val: Option<Value>) -> String {
1121 match val {
1122 None | Some(Value::Null) => String::new(),
1123 Some(Value::String(s)) => s,
1124 Some(Value::Number(n)) => n.to_string(),
1125 Some(Value::Bool(b)) => {
1126 if b {
1127 "true".into()
1128 } else {
1129 "false".into()
1130 }
1131 }
1132 Some(Value::Array(_)) | Some(Value::Object(_)) => String::new(),
1133 }
1134}
1135
1136pub fn form_value_to_bool(val: Option<Value>, default: bool) -> bool {
1139 match val {
1140 Some(Value::Bool(b)) => b,
1141 Some(Value::Number(n)) => n.as_f64().map(|v| v != 0.0).unwrap_or(default),
1142 Some(Value::String(s)) => match s.as_str() {
1143 "true" | "1" => true,
1144 "false" | "0" => false,
1145 _ => default,
1146 },
1147 _ => default,
1148 }
1149}
1150
1151pub fn form_value_to_string_vec(val: Option<Value>) -> Vec<String> {
1154 match val {
1155 Some(Value::Array(items)) => items
1156 .into_iter()
1157 .filter_map(|v| v.as_str().map(|s| s.to_string()))
1158 .collect(),
1159 _ => Vec::new(),
1160 }
1161}
1162
1163pub fn form_value_to_radio_key(val: Option<Value>) -> Option<String> {
1166 match val {
1167 None | Some(Value::Null) => None,
1168 Some(Value::String(s)) => Some(s),
1169 Some(Value::Number(n)) => Some(n.to_string()),
1170 Some(Value::Bool(b)) => Some(if b { "true".into() } else { "false".into() }),
1171 Some(Value::Array(_)) | Some(Value::Object(_)) => None,
1172 }
1173}
1174
1175pub fn form_list_get(handle: &FormHandle, name: &str) -> Vec<Value> {
1179 match handle.get_field_value(name) {
1180 Some(Value::Array(items)) => items,
1181 _ => Vec::new(),
1182 }
1183}
1184
1185pub fn form_list_set(handle: &FormHandle, name: &str, items: Vec<Value>) {
1187 handle.set_field_value(name, Value::Array(items));
1188}
1189
1190pub fn form_list_len(handle: &FormHandle, name: &str) -> usize {
1192 form_list_get(handle, name).len()
1193}
1194
1195pub fn form_list_insert(handle: &FormHandle, name: &str, index: usize, item: Value) {
1199 let mut items = form_list_get(handle, name);
1200 let idx = index.min(items.len());
1201 items.insert(idx, item);
1202 form_list_set(handle, name, items);
1203}
1204
1205pub fn form_list_remove(handle: &FormHandle, name: &str, index: usize) {
1209 let mut items = form_list_get(handle, name);
1210 if index < items.len() {
1211 items.remove(index);
1212 form_list_set(handle, name, items);
1213 }
1214}
1215
1216#[cfg(test)]
1217mod tests {
1218 use super::*;
1219
1220 #[test]
1221 fn form_value_to_string_covers_common_variants() {
1222 assert_eq!(form_value_to_string(None), "");
1223 assert_eq!(form_value_to_string(Some(Value::Null)), "");
1224 assert_eq!(
1225 form_value_to_string(Some(Value::String("abc".into()))),
1226 "abc"
1227 );
1228 assert_eq!(form_value_to_string(Some(Value::Number(42.into()))), "42");
1229 assert_eq!(form_value_to_string(Some(Value::Bool(true))), "true");
1230 assert_eq!(form_value_to_string(Some(Value::Bool(false))), "false");
1231 }
1232
1233 #[test]
1234 fn form_value_to_bool_falls_back_to_default() {
1235 assert!(!form_value_to_bool(None, false));
1236 assert!(form_value_to_bool(None, true));
1237 assert!(form_value_to_bool(Some(Value::Bool(true)), false));
1238 assert!(!form_value_to_bool(Some(Value::Bool(false)), true));
1239 assert!(form_value_to_bool(Some(Value::Number(1.into())), false));
1240 assert!(!form_value_to_bool(Some(Value::Number(0.into())), true));
1241 assert!(form_value_to_bool(
1242 Some(Value::String("true".into())),
1243 false
1244 ));
1245 assert!(!form_value_to_bool(
1246 Some(Value::String("false".into())),
1247 true
1248 ));
1249 }
1250
1251 #[test]
1252 fn form_value_to_string_vec_extracts_strings() {
1253 let val = Value::Array(vec![
1254 Value::String("a".into()),
1255 Value::Number(1.into()),
1256 Value::String("b".into()),
1257 ]);
1258 let vec = form_value_to_string_vec(Some(val));
1259 assert_eq!(vec, vec!["a".to_string(), "b".to_string()]);
1260 }
1261
1262 #[test]
1263 fn form_value_to_radio_key_converts_basic_types() {
1264 assert_eq!(form_value_to_radio_key(None), None);
1265 assert_eq!(
1266 form_value_to_radio_key(Some(Value::String("x".into()))),
1267 Some("x".into())
1268 );
1269 assert_eq!(
1270 form_value_to_radio_key(Some(Value::Number(1.into()))),
1271 Some("1".into())
1272 );
1273 assert_eq!(
1274 form_value_to_radio_key(Some(Value::Bool(true))),
1275 Some("true".into())
1276 );
1277 }
1278
1279 #[test]
1283 #[ignore]
1284 fn form_list_helpers_operate_on_value_array() {
1285 let handle = FormHandle::new();
1286
1287 assert_eq!(form_list_len(&handle, "emails"), 0);
1289 assert!(form_list_get(&handle, "emails").is_empty());
1290
1291 form_list_insert(&handle, "emails", 0, Value::String("a@example.com".into()));
1293 form_list_insert(&handle, "emails", 1, Value::String("b@example.com".into()));
1294
1295 let items = form_list_get(&handle, "emails");
1296 assert_eq!(items.len(), 2);
1297 assert_eq!(items[0], Value::String("a@example.com".into()));
1298 assert_eq!(items[1], Value::String("b@example.com".into()));
1299
1300 form_list_remove(&handle, "emails", 0);
1302 let items_after_remove = form_list_get(&handle, "emails");
1303 assert_eq!(items_after_remove.len(), 1);
1304 assert_eq!(items_after_remove[0], Value::String("b@example.com".into()));
1305 }
1306
1307 #[test]
1308 fn validate_all_fails_for_required_when_values_empty() {
1309 let handle = FormHandle::new();
1310 let registry: Rc<RefCell<HashMap<String, Vec<FormRule>>>> =
1311 Rc::new(RefCell::new(HashMap::new()));
1312
1313 registry.borrow_mut().insert(
1314 "username".to_string(),
1315 vec![FormRule {
1316 required: true,
1317 message: Some("请输入用户名".into()),
1318 ..FormRule::default()
1319 }],
1320 );
1321
1322 let ok = validate_all(&handle, ®istry);
1324 assert!(!ok);
1325 let errors = handle.errors();
1326 assert_eq!(errors.get("username"), Some(&"请输入用户名".to_string()));
1327 }
1328
1329 #[test]
1333 #[ignore]
1334 fn validate_all_fails_again_after_reset() {
1335 let handle = FormHandle::new();
1336 let registry: Rc<RefCell<HashMap<String, Vec<FormRule>>>> =
1337 Rc::new(RefCell::new(HashMap::new()));
1338
1339 registry.borrow_mut().insert(
1340 "username".to_string(),
1341 vec![FormRule {
1342 required: true,
1343 message: Some("请输入用户名".into()),
1344 ..FormRule::default()
1345 }],
1346 );
1347
1348 handle.set_field_value("username", Value::String("alice".into()));
1350 let ok_first = validate_all(&handle, ®istry);
1351 assert!(ok_first);
1352
1353 handle.reset_fields();
1355 let ok_after_reset = validate_all(&handle, ®istry);
1356 assert!(!ok_after_reset);
1357 let errors_after_reset = handle.errors();
1358 assert_eq!(
1359 errors_after_reset.get("username"),
1360 Some(&"请输入用户名".to_string())
1361 );
1362 }
1363}