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 variant: Option<Variant>,
321 feedback_icons: Option<FeedbackIcons>,
322 registry: Rc<RefCell<HashMap<String, Vec<FormRule>>>>,
323 on_values_change: Option<EventHandler<ValuesChangeEvent>>,
324}
325
326#[derive(Clone)]
327pub struct FormItemControlContext {
328 name: String,
329 handle: FormHandle,
330 disabled: bool,
331 registry: Rc<RefCell<HashMap<String, Vec<FormRule>>>>,
332 on_values_change: Option<EventHandler<ValuesChangeEvent>>,
333 value_prop_name: Option<String>,
334 get_value_from_event: Option<GetValueFromEventFn>,
335}
336
337impl FormItemControlContext {
338 pub fn name(&self) -> &str {
339 &self.name
340 }
341
342 pub fn value(&self) -> Option<Value> {
343 self.handle.get_field_value(&self.name)
344 }
345
346 pub fn value_prop_name(&self) -> Option<&str> {
349 self.value_prop_name.as_deref()
350 }
351
352 pub fn apply_mapped_value(&self, raw: Value) {
355 let mapped = if let Some(mapper) = self.get_value_from_event {
356 mapper(raw)
357 } else {
358 raw
359 };
360 self.set_value(mapped);
361 }
362
363 pub fn set_value(&self, value: Value) {
364 if self.disabled {
365 return;
366 }
367 self.handle.set_field_value(&self.name, value);
368 self.run_validation();
369
370 if let Some(cb) = self.on_values_change.as_ref() {
371 let mut changed = FormValues::new();
372 if let Some(current) = self.handle.get_field_value(&self.name) {
373 changed.insert(self.name.clone(), current);
374 }
375 let all = self.handle.values();
376 cb.call(ValuesChangeEvent {
377 changed_values: changed,
378 all_values: all,
379 });
380 }
381 }
382
383 pub fn set_string(&self, value: impl Into<String>) {
384 self.set_value(Value::String(value.into()));
385 }
386
387 pub fn is_disabled(&self) -> bool {
388 self.disabled
389 }
390
391 fn run_validation(&self) {
392 apply_validation_result(&self.handle, &self.registry, &self.name);
393 }
394}
395
396#[derive(Clone)]
399pub struct FormListContext {
400 pub name: String,
401 pub handle: FormHandle,
402}
403
404impl FormListContext {
405 pub fn len(&self) -> usize {
407 form_list_len(&self.handle, &self.name)
408 }
409
410 pub fn is_empty(&self) -> bool {
412 self.len() == 0
413 }
414
415 pub fn insert(&self, index: usize, item: Value) {
417 form_list_insert(&self.handle, &self.name, index, item);
418 }
419
420 pub fn remove(&self, index: usize) {
422 form_list_remove(&self.handle, &self.name, index);
423 }
424}
425
426pub fn use_form_list() -> Option<FormListContext> {
428 try_use_context::<FormListContext>()
429}
430
431pub fn use_form_item_control() -> Option<FormItemControlContext> {
432 if let Some(ctx) = try_use_context::<FormItemControlContext>() {
433 let scope = current_scope_id();
437 ctx.handle.register_listener(ctx.name(), scope);
438 Some(ctx)
439 } else {
440 None
441 }
442}
443
444pub fn use_form() -> FormHandle {
446 FormHandle::new()
447}
448
449#[derive(Props, Clone, PartialEq)]
450pub struct FormProps {
451 #[props(default)]
452 pub layout: FormLayout,
453 #[props(default)]
454 pub size: ControlSize,
455 #[props(default)]
456 pub colon: bool,
457 #[props(default)]
458 pub required_mark: RequiredMark,
459 #[props(default)]
460 pub label_align: LabelAlign,
461 #[props(default)]
463 pub label_wrap: bool,
464 #[props(optional)]
465 pub label_col: Option<ColProps>,
466 #[props(optional)]
467 pub wrapper_col: Option<ColProps>,
468 #[props(default)]
469 pub disabled: bool,
470 #[props(optional)]
472 pub variant: Option<Variant>,
473 #[props(default)]
476 pub scroll_to_first_error: bool,
477 #[props(optional)]
479 pub scroll_to_first_error_config: Option<ScrollToFirstErrorConfig>,
480 #[props(optional)]
482 pub feedback_icons: Option<FeedbackIcons>,
483 #[props(optional)]
485 pub name: Option<String>,
486 #[props(optional)]
487 pub initial_values: Option<FormValues>,
488 #[props(optional)]
489 pub form: Option<FormHandle>,
490 #[props(optional)]
491 pub class: Option<String>,
492 #[props(optional)]
493 pub style: Option<String>,
494 #[props(optional)]
496 pub class_names: Option<FormClassNames>,
497 #[props(optional)]
499 pub styles: Option<FormStyles>,
500 #[props(optional)]
501 pub on_finish: Option<EventHandler<FormFinishEvent>>,
502 #[props(optional)]
503 pub on_finish_failed: Option<EventHandler<FormFinishFailedEvent>>,
504 #[props(optional)]
506 pub on_values_change: Option<EventHandler<ValuesChangeEvent>>,
507 pub children: Element,
508}
509
510#[component]
511pub fn Form(props: FormProps) -> Element {
512 let FormProps {
513 layout,
514 size,
515 colon,
516 required_mark,
517 label_align,
518 label_wrap,
519 label_col,
520 wrapper_col,
521 disabled,
522 variant,
523 scroll_to_first_error,
524 scroll_to_first_error_config,
525 feedback_icons,
526 name,
527 initial_values,
528 form,
529 class,
530 style,
531 class_names,
532 styles,
533 on_finish,
534 on_finish_failed,
535 on_values_change,
536 children,
537 } = props;
538
539 let handle = form.unwrap_or_else(FormHandle::new);
540 if let Some(initial) = initial_values
541 && handle.values().is_empty()
542 {
543 for (k, v) in initial.into_iter() {
544 handle.set_field_value(&k, v);
545 }
546 }
547
548 let registry_signal = use_signal::<Rc<RefCell<HashMap<String, Vec<FormRule>>>>>(|| {
551 Rc::new(RefCell::new(HashMap::new()))
552 });
553 let registry = registry_signal.read().clone();
554 let context = FormContext {
555 handle: handle.clone(),
556 _layout: layout,
557 _size: size,
558 colon,
559 required_mark,
560 _label_align: label_align,
561 label_wrap,
562 _label_col: label_col,
563 _wrapper_col: wrapper_col,
564 disabled,
565 variant,
566 feedback_icons,
567 registry: registry.clone(),
568 on_values_change,
569 };
570 use_context_provider(|| context);
571
572 let submit_handle = handle.clone();
573 let submit_registry = registry.clone();
574 let finish_cb = on_finish;
575 let failed_cb = on_finish_failed;
576
577 let mut form_classes = vec![form_class(layout, size)];
579 form_classes.push_semantic(&class_names, FormSemantic::Root);
580 if let Some(extra) = class {
581 form_classes.push(extra);
582 }
583 let form_class_attr = form_classes
584 .into_iter()
585 .filter(|s| !s.is_empty())
586 .collect::<Vec<_>>()
587 .join(" ");
588
589 let mut form_style_attr = style.unwrap_or_default();
590 form_style_attr.append_semantic(&styles, FormSemantic::Root);
591
592 let _scroll_config = scroll_to_first_error_config;
594
595 let name_attr = name.clone();
596 rsx! {
597 form {
598 class: "{form_class_attr}",
599 style: "{form_style_attr}",
600 name: name_attr,
601 onsubmit: move |evt| {
602 evt.prevent_default();
603 if validate_all(&submit_handle, &submit_registry) {
604 if let Some(cb) = finish_cb.as_ref() {
605 cb.call(FormFinishEvent { values: submit_handle.values() });
606 }
607 } else {
608 if scroll_to_first_error {
610 #[cfg(target_arch = "wasm32")]
611 {
612 scroll_to_first_error_field(&submit_handle, &submit_registry);
613 }
614 }
615 if let Some(cb) = failed_cb.as_ref() {
616 cb.call(FormFinishFailedEvent { errors: submit_handle.errors() });
617 }
618 }
619 },
620 {children}
621 }
622 }
623}
624
625#[cfg(target_arch = "wasm32")]
627fn scroll_to_first_error_field(
628 handle: &FormHandle,
629 registry: &Rc<RefCell<HashMap<String, Vec<FormRule>>>>,
630) {
631 let errors = handle.errors();
632 if errors.is_empty() {
633 return;
634 }
635
636 let field_names: Vec<String> = match registry.try_borrow() {
638 Ok(map) => map.keys().cloned().collect(),
639 Err(_) => return,
640 };
641
642 for name in field_names {
644 if errors.contains_key(&name) {
645 if let Some(window) = web_sys::window() {
647 if let Some(document) = window.document() {
648 if let Some(element) = document
650 .query_selector(&format!("[name=\"{}\"]", name))
651 .ok()
652 .flatten()
653 {
654 let _ = element.scroll_into_view();
655 }
656 }
657 }
658 break;
659 }
660 }
661}
662
663fn form_class(layout: FormLayout, size: ControlSize) -> String {
664 let mut classes = vec!["adui-form".to_string()];
665 match layout {
666 FormLayout::Horizontal => classes.push("adui-form-horizontal".into()),
667 FormLayout::Vertical => classes.push("adui-form-vertical".into()),
668 FormLayout::Inline => classes.push("adui-form-inline".into()),
669 }
670 match size {
671 ControlSize::Small => classes.push("adui-form-small".into()),
672 ControlSize::Large => classes.push("adui-form-large".into()),
673 ControlSize::Middle => {}
674 }
675 classes.join(" ")
676}
677
678fn validate_all(
679 handle: &FormHandle,
680 registry: &Rc<RefCell<HashMap<String, Vec<FormRule>>>>,
681) -> bool {
682 let names: Vec<String> = match registry.try_borrow() {
684 Ok(map) => map.keys().cloned().collect(),
685 Err(_) => {
686 return false;
688 }
689 };
690
691 if handle.values().is_empty() {
695 let mut required_names = Vec::new();
696 let mut has_required = false;
697
698 if let Ok(map) = registry.try_borrow() {
699 for (name, rules) in map.iter() {
700 if rules.iter().any(|rule| rule.required) {
701 has_required = true;
702 required_names.push(name.clone());
703 }
704 }
705 } else {
706 return false;
708 }
709
710 if has_required {
711 for name in &required_names {
712 apply_validation_result(handle, registry, name);
713 }
714 return false;
715 }
716 }
717
718 let mut ok = true;
719 for name in &names {
720 if !apply_validation_result(handle, registry, name) {
721 ok = false;
722 }
723 }
724
725 ok
726}
727
728#[derive(Clone, Debug, PartialEq, Eq)]
729pub struct FormListItemMeta {
730 pub index: usize,
731 pub name: String,
733}
734
735#[derive(Props, Clone, PartialEq)]
736pub struct FormListProps {
737 pub name: String,
739 #[props(optional)]
741 pub initial_count: Option<usize>,
742 pub children: Element,
744}
745
746#[component]
749pub fn FormList(props: FormListProps) -> Element {
750 let ctx = use_context::<FormContext>();
751 let FormListProps {
752 name,
753 initial_count,
754 children,
755 } = props;
756
757 if let Some(count) = initial_count
759 && count > 0
760 && form_list_len(&ctx.handle, &name) == 0
761 {
762 let mut items = Vec::with_capacity(count);
763 for _ in 0..count {
764 items.push(Value::Null);
765 }
766 form_list_set(&ctx.handle, &name, items);
767 }
768
769 let list_ctx = FormListContext {
770 name: name.clone(),
771 handle: ctx.handle.clone(),
772 };
773 use_context_provider(|| list_ctx);
774
775 rsx! { div { class: "adui-form-list", {children} } }
776}
777
778pub type GetValueFromEventFn = fn(Value) -> Value;
779
780#[allow(unpredictable_function_pointer_comparisons)]
783#[derive(Props, Clone, PartialEq)]
784pub struct FormItemProps {
785 #[props(optional)]
786 pub name: Option<String>,
787 #[props(optional)]
788 pub label: Option<String>,
789 #[props(optional)]
790 pub tooltip: Option<String>,
791 #[props(optional)]
792 pub help: Option<String>,
793 #[props(optional)]
794 pub extra: Option<String>,
795 #[props(optional)]
796 pub rules: Option<Vec<FormRule>>,
797 #[props(optional)]
799 pub dependencies: Option<Vec<String>>,
800 #[props(optional)]
802 pub value_prop_name: Option<String>,
803 #[props(optional)]
805 pub get_value_from_event: Option<GetValueFromEventFn>,
806 #[props(optional)]
807 pub class: Option<String>,
808 #[props(optional)]
809 pub style: Option<String>,
810 #[props(default)]
811 pub has_feedback: bool,
812 pub children: Element,
813}
814
815#[component]
816pub fn FormItem(props: FormItemProps) -> Element {
817 let ctx = use_context::<FormContext>();
818 let FormItemProps {
819 name,
820 label,
821 tooltip,
822 help,
823 extra,
824 rules,
825 dependencies,
826 value_prop_name,
827 get_value_from_event,
828 class,
829 style,
830 has_feedback,
831 children,
832 } = props;
833
834 let item_rules = rules.unwrap_or_default();
835 let is_required = item_rules.iter().any(|rule| rule.required);
836
837 let item_scope = current_scope_id();
838
839 if let Some(field_name) = name.clone() {
840 {
841 ctx.registry
842 .borrow_mut()
843 .insert(field_name.clone(), item_rules.clone());
844 }
845 ctx.handle.register_listener(&field_name, item_scope);
846 let control_ctx = FormItemControlContext {
847 name: field_name.clone(),
848 handle: ctx.handle.clone(),
849 disabled: ctx.disabled,
850 registry: ctx.registry.clone(),
851 on_values_change: ctx.on_values_change,
852 value_prop_name: value_prop_name.clone(),
853 get_value_from_event,
854 };
855 use_context_provider(|| control_ctx);
856 if !item_rules.is_empty() {
857 let handle = ctx.handle.clone();
858 let registry = ctx.registry.clone();
859 use_effect(move || {
860 apply_validation_result(&handle, ®istry, &field_name);
861 });
862 }
863 }
864
865 if let Some(deps) = dependencies.as_ref() {
867 for dep in deps {
868 ctx.handle.register_listener(dep, item_scope);
869 }
870 }
871
872 let error_message = name.as_ref().and_then(|field| ctx.handle.get_error(field));
873
874 let mut wrapper_class = vec!["adui-form-item".to_string()];
875 if let Some(extra) = class {
876 wrapper_class.push(extra);
877 }
878 if error_message.is_some() {
879 wrapper_class.push("adui-form-item-has-error".into());
880 } else if has_feedback {
881 wrapper_class.push("adui-form-item-has-feedback".into());
882 }
883
884 let tooltip_text = tooltip.clone().unwrap_or_default();
885
886 let label_content = if let Some(text) = label {
888 let label_text = if ctx.colon {
889 format!("{}:", text)
890 } else {
891 text.clone()
892 };
893 let label_node = rsx! { "{label_text}" };
894
895 let final_label = match &ctx.required_mark {
896 RequiredMark::None => label_node,
897 RequiredMark::Optional => {
898 rsx! {
899 {label_node}
900 span { class: "adui-form-item-optional", "(optional)" }
901 }
902 }
903 RequiredMark::Default => {
904 if is_required {
905 rsx! {
906 span { class: "adui-form-item-required", "*" }
907 {label_node}
908 }
909 } else {
910 label_node
911 }
912 }
913 RequiredMark::Custom(render_fn) => {
914 let custom_node = render_fn(label_node, is_required);
915 custom_node
916 }
917 };
918
919 Some(final_label)
920 } else {
921 None
922 };
923
924 rsx! {
925 div { class: wrapper_class.join(" "), style: style.unwrap_or_default(),
926 if let Some(label_el) = label_content {
927 label {
928 class: if ctx.label_wrap { "adui-form-item-label adui-form-item-label-wrap" } else { "adui-form-item-label" },
929 title: "{tooltip_text}",
930 {label_el}
931 }
932 }
933 div { class: "adui-form-item-control", {children} }
934 if let Some(help_text) = error_message.or(help) {
935 div { class: "adui-form-item-help", "{help_text}" }
936 }
937 if let Some(extra_text) = extra {
938 div { class: "adui-form-item-extra", "{extra_text}" }
939 }
940 }
941 }
942}
943
944fn apply_validation_result(
945 handle: &FormHandle,
946 registry: &Rc<RefCell<HashMap<String, Vec<FormRule>>>>,
947 name: &str,
948) -> bool {
949 if let Some(err) = validate_field(handle, registry, name) {
950 handle.set_error(name, Some(err));
951 false
952 } else {
953 handle.set_error(name, None);
954 true
955 }
956}
957
958fn validate_field(
959 handle: &FormHandle,
960 registry: &Rc<RefCell<HashMap<String, Vec<FormRule>>>>,
961 name: &str,
962) -> Option<String> {
963 let rules = registry
964 .try_borrow()
965 .ok()
966 .and_then(|map| map.get(name).cloned())
967 .unwrap_or_default();
968 run_rules(handle, name, &rules)
969}
970
971fn run_rules(handle: &FormHandle, name: &str, rules: &[FormRule]) -> Option<String> {
972 if rules.is_empty() {
973 return None;
974 }
975 let value = handle.get_field_value(name);
976 for rule in rules {
977 if let Some(msg) = evaluate_rule(rule, value.as_ref()) {
978 return Some(msg);
979 }
980 }
981 None
982}
983
984fn evaluate_rule(rule: &FormRule, value: Option<&Value>) -> Option<String> {
985 if rule.required && value_is_empty(value) {
986 return Some(
987 rule.message
988 .clone()
989 .unwrap_or_else(|| "必填项不能为空".into()),
990 );
991 }
992
993 if value_is_empty(value) {
994 return None;
995 }
996
997 if let Some(len_target) = rule.len
998 && value_length(value) != Some(len_target)
999 {
1000 return Some(
1001 rule.message
1002 .clone()
1003 .unwrap_or_else(|| format!("长度必须为 {}", len_target)),
1004 );
1005 }
1006
1007 if let Some(min) = rule.min {
1008 match (value_length(value), numeric_value(value)) {
1009 (Some(len), _) if len < min => {
1010 return Some(
1011 rule.message
1012 .clone()
1013 .unwrap_or_else(|| format!("长度不能小于 {}", min)),
1014 );
1015 }
1016 (_, Some(num)) if num < min as f64 => {
1017 return Some(
1018 rule.message
1019 .clone()
1020 .unwrap_or_else(|| format!("数值不能小于 {}", min)),
1021 );
1022 }
1023 _ => {}
1024 }
1025 }
1026
1027 if let Some(max) = rule.max {
1028 match (value_length(value), numeric_value(value)) {
1029 (Some(len), _) if len > max => {
1030 return Some(
1031 rule.message
1032 .clone()
1033 .unwrap_or_else(|| format!("长度不能大于 {}", max)),
1034 );
1035 }
1036 (_, Some(num)) if num > max as f64 => {
1037 return Some(
1038 rule.message
1039 .clone()
1040 .unwrap_or_else(|| format!("数值不能大于 {}", max)),
1041 );
1042 }
1043 _ => {}
1044 }
1045 }
1046
1047 if let Some(pattern) = &rule.pattern
1048 && let Some(text) = value_to_string(value)
1049 {
1050 match Regex::new(pattern) {
1051 Ok(re) => {
1052 if !re.is_match(&text) {
1053 return Some(rule.message.clone().unwrap_or_else(|| "格式不匹配".into()));
1054 }
1055 }
1056 Err(_) => {
1057 return Some(format!("无效的正则表达式: {}", pattern));
1058 }
1059 }
1060 }
1061
1062 if let Some(validator) = rule.validator
1063 && let Err(err) = validator(value)
1064 {
1065 return Some(err);
1066 }
1067
1068 None
1069}
1070
1071fn value_is_empty(value: Option<&Value>) -> bool {
1072 match value {
1073 None => true,
1074 Some(Value::Null) => true,
1075 Some(Value::String(text)) => text.trim().is_empty(),
1076 Some(Value::Array(list)) => list.is_empty(),
1077 Some(Value::Object(map)) => map.is_empty(),
1078 Some(Value::Bool(flag)) => !flag,
1080 _ => false,
1081 }
1082}
1083
1084fn value_length(value: Option<&Value>) -> Option<usize> {
1085 value.and_then(|val| match val {
1086 Value::String(text) => Some(text.chars().count()),
1087 Value::Array(list) => Some(list.len()),
1088 Value::Object(map) => Some(map.len()),
1089 _ => None,
1090 })
1091}
1092
1093fn numeric_value(value: Option<&Value>) -> Option<f64> {
1094 value.and_then(|val| match val {
1095 Value::Number(num) => num.as_f64(),
1096 _ => None,
1097 })
1098}
1099
1100fn value_to_string(value: Option<&Value>) -> Option<String> {
1101 value.and_then(|val| match val {
1102 Value::String(text) => Some(text.clone()),
1103 Value::Number(num) => Some(num.to_string()),
1104 Value::Bool(flag) => Some(flag.to_string()),
1105 Value::Null => None,
1106 Value::Array(_) | Value::Object(_) => None,
1107 })
1108}
1109
1110pub fn form_value_to_string(val: Option<Value>) -> String {
1119 match val {
1120 None | Some(Value::Null) => String::new(),
1121 Some(Value::String(s)) => s,
1122 Some(Value::Number(n)) => n.to_string(),
1123 Some(Value::Bool(b)) => {
1124 if b {
1125 "true".into()
1126 } else {
1127 "false".into()
1128 }
1129 }
1130 Some(Value::Array(_)) | Some(Value::Object(_)) => String::new(),
1131 }
1132}
1133
1134pub fn form_value_to_bool(val: Option<Value>, default: bool) -> bool {
1137 match val {
1138 Some(Value::Bool(b)) => b,
1139 Some(Value::Number(n)) => n.as_f64().map(|v| v != 0.0).unwrap_or(default),
1140 Some(Value::String(s)) => match s.as_str() {
1141 "true" | "1" => true,
1142 "false" | "0" => false,
1143 _ => default,
1144 },
1145 _ => default,
1146 }
1147}
1148
1149pub fn form_value_to_string_vec(val: Option<Value>) -> Vec<String> {
1152 match val {
1153 Some(Value::Array(items)) => items
1154 .into_iter()
1155 .filter_map(|v| v.as_str().map(|s| s.to_string()))
1156 .collect(),
1157 _ => Vec::new(),
1158 }
1159}
1160
1161pub fn form_value_to_radio_key(val: Option<Value>) -> Option<String> {
1164 match val {
1165 None | Some(Value::Null) => None,
1166 Some(Value::String(s)) => Some(s),
1167 Some(Value::Number(n)) => Some(n.to_string()),
1168 Some(Value::Bool(b)) => Some(if b { "true".into() } else { "false".into() }),
1169 Some(Value::Array(_)) | Some(Value::Object(_)) => None,
1170 }
1171}
1172
1173pub fn form_list_get(handle: &FormHandle, name: &str) -> Vec<Value> {
1177 match handle.get_field_value(name) {
1178 Some(Value::Array(items)) => items,
1179 _ => Vec::new(),
1180 }
1181}
1182
1183pub fn form_list_set(handle: &FormHandle, name: &str, items: Vec<Value>) {
1185 handle.set_field_value(name, Value::Array(items));
1186}
1187
1188pub fn form_list_len(handle: &FormHandle, name: &str) -> usize {
1190 form_list_get(handle, name).len()
1191}
1192
1193pub fn form_list_insert(handle: &FormHandle, name: &str, index: usize, item: Value) {
1197 let mut items = form_list_get(handle, name);
1198 let idx = index.min(items.len());
1199 items.insert(idx, item);
1200 form_list_set(handle, name, items);
1201}
1202
1203pub fn form_list_remove(handle: &FormHandle, name: &str, index: usize) {
1207 let mut items = form_list_get(handle, name);
1208 if index < items.len() {
1209 items.remove(index);
1210 form_list_set(handle, name, items);
1211 }
1212}
1213
1214#[cfg(test)]
1215mod tests {
1216 use super::*;
1217
1218 #[test]
1219 fn form_value_to_string_covers_common_variants() {
1220 assert_eq!(form_value_to_string(None), "");
1221 assert_eq!(form_value_to_string(Some(Value::Null)), "");
1222 assert_eq!(
1223 form_value_to_string(Some(Value::String("abc".into()))),
1224 "abc"
1225 );
1226 assert_eq!(form_value_to_string(Some(Value::Number(42.into()))), "42");
1227 assert_eq!(form_value_to_string(Some(Value::Bool(true))), "true");
1228 assert_eq!(form_value_to_string(Some(Value::Bool(false))), "false");
1229 }
1230
1231 #[test]
1232 fn form_value_to_bool_falls_back_to_default() {
1233 assert!(!form_value_to_bool(None, false));
1234 assert!(form_value_to_bool(None, true));
1235 assert!(form_value_to_bool(Some(Value::Bool(true)), false));
1236 assert!(!form_value_to_bool(Some(Value::Bool(false)), true));
1237 assert!(form_value_to_bool(Some(Value::Number(1.into())), false));
1238 assert!(!form_value_to_bool(Some(Value::Number(0.into())), true));
1239 assert!(form_value_to_bool(
1240 Some(Value::String("true".into())),
1241 false
1242 ));
1243 assert!(!form_value_to_bool(
1244 Some(Value::String("false".into())),
1245 true
1246 ));
1247 }
1248
1249 #[test]
1250 fn form_value_to_string_vec_extracts_strings() {
1251 let val = Value::Array(vec![
1252 Value::String("a".into()),
1253 Value::Number(1.into()),
1254 Value::String("b".into()),
1255 ]);
1256 let vec = form_value_to_string_vec(Some(val));
1257 assert_eq!(vec, vec!["a".to_string(), "b".to_string()]);
1258 }
1259
1260 #[test]
1261 fn form_value_to_radio_key_converts_basic_types() {
1262 assert_eq!(form_value_to_radio_key(None), None);
1263 assert_eq!(
1264 form_value_to_radio_key(Some(Value::String("x".into()))),
1265 Some("x".into())
1266 );
1267 assert_eq!(
1268 form_value_to_radio_key(Some(Value::Number(1.into()))),
1269 Some("1".into())
1270 );
1271 assert_eq!(
1272 form_value_to_radio_key(Some(Value::Bool(true))),
1273 Some("true".into())
1274 );
1275 }
1276
1277 #[test]
1281 #[ignore]
1282 fn form_list_helpers_operate_on_value_array() {
1283 let handle = FormHandle::new();
1284
1285 assert_eq!(form_list_len(&handle, "emails"), 0);
1287 assert!(form_list_get(&handle, "emails").is_empty());
1288
1289 form_list_insert(&handle, "emails", 0, Value::String("a@example.com".into()));
1291 form_list_insert(&handle, "emails", 1, Value::String("b@example.com".into()));
1292
1293 let items = form_list_get(&handle, "emails");
1294 assert_eq!(items.len(), 2);
1295 assert_eq!(items[0], Value::String("a@example.com".into()));
1296 assert_eq!(items[1], Value::String("b@example.com".into()));
1297
1298 form_list_remove(&handle, "emails", 0);
1300 let items_after_remove = form_list_get(&handle, "emails");
1301 assert_eq!(items_after_remove.len(), 1);
1302 assert_eq!(items_after_remove[0], Value::String("b@example.com".into()));
1303 }
1304
1305 #[test]
1306 fn validate_all_fails_for_required_when_values_empty() {
1307 let handle = FormHandle::new();
1308 let registry: Rc<RefCell<HashMap<String, Vec<FormRule>>>> =
1309 Rc::new(RefCell::new(HashMap::new()));
1310
1311 registry.borrow_mut().insert(
1312 "username".to_string(),
1313 vec![FormRule {
1314 required: true,
1315 message: Some("请输入用户名".into()),
1316 ..FormRule::default()
1317 }],
1318 );
1319
1320 let ok = validate_all(&handle, ®istry);
1322 assert!(!ok);
1323 let errors = handle.errors();
1324 assert_eq!(errors.get("username"), Some(&"请输入用户名".to_string()));
1325 }
1326
1327 #[test]
1331 #[ignore]
1332 fn validate_all_fails_again_after_reset() {
1333 let handle = FormHandle::new();
1334 let registry: Rc<RefCell<HashMap<String, Vec<FormRule>>>> =
1335 Rc::new(RefCell::new(HashMap::new()));
1336
1337 registry.borrow_mut().insert(
1338 "username".to_string(),
1339 vec![FormRule {
1340 required: true,
1341 message: Some("请输入用户名".into()),
1342 ..FormRule::default()
1343 }],
1344 );
1345
1346 handle.set_field_value("username", Value::String("alice".into()));
1348 let ok_first = validate_all(&handle, ®istry);
1349 assert!(ok_first);
1350
1351 handle.reset_fields();
1353 let ok_after_reset = validate_all(&handle, ®istry);
1354 assert!(!ok_after_reset);
1355 let errors_after_reset = handle.errors();
1356 assert_eq!(
1357 errors_after_reset.get("username"),
1358 Some(&"请输入用户名".to_string())
1359 );
1360 }
1361}