adui_dioxus/components/
form.rs

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/// Required mark configuration for form labels.
39///
40/// In Ant Design TypeScript, this can be:
41/// - `boolean` (true/false)
42/// - `'optional'` (string literal)
43/// - `(labelNode, info) => ReactNode` (function)
44///
45/// In Rust, we use an enum with a custom render function option.
46#[derive(Clone)]
47pub enum RequiredMark {
48    /// Hide required mark (equivalent to `false` in TypeScript)
49    None,
50    /// Show "optional" text (equivalent to `'optional'` in TypeScript)
51    Optional,
52    /// Show default required mark (equivalent to `true` in TypeScript)
53    Default,
54    /// Custom render function: `(label_node, required) -> Element`
55    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                // Function pointers cannot be compared for equality
72                // In practice, this means Custom variants are never equal
73                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/// Configuration for feedback icons displayed in form items.
99///
100/// When enabled, shows success/error/warning/validating icons in form items.
101#[derive(Clone, Default, PartialEq)]
102pub struct FeedbackIcons {
103    /// Custom success icon element.
104    pub success: Option<Element>,
105    /// Custom error icon element.
106    pub error: Option<Element>,
107    /// Custom warning icon element.
108    pub warning: Option<Element>,
109    /// Custom validating icon element.
110    pub validating: Option<Element>,
111}
112
113impl FeedbackIcons {
114    /// Create feedback icons with default icons.
115    pub fn default_icons() -> Self {
116        Self {
117            success: None, // Will use default Icon component
118            error: None,
119            warning: None,
120            validating: None,
121        }
122    }
123}
124
125/// Configuration for scroll behavior when validation fails.
126#[derive(Clone, Debug, Default, PartialEq)]
127pub struct ScrollToFirstErrorConfig {
128    /// Block position for scrollIntoView.
129    pub block: Option<String>,
130    /// Inline position for scrollIntoView.
131    pub inline: Option<String>,
132    /// Behavior for scrollIntoView (smooth/instant/auto).
133    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/// Event payload describing value changes at the Form level.
169#[derive(Clone, Debug, Default, PartialEq)]
170pub struct ValuesChangeEvent {
171    /// This update中实际发生变化的字段集合(通常只包含当前字段)。
172    pub changed_values: FormValues,
173    /// 变更后的完整字段快照。
174    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    /// Optional metadata describing which prop should be treated as value
349    /// when integrating with custom controls (similar to AntD's valuePropName).
350    pub fn value_prop_name(&self) -> Option<&str> {
351        self.value_prop_name.as_deref()
352    }
353
354    /// Apply a raw `Value` coming from a custom event, passing it through
355    /// `get_value_from_event` if configured, then writing it into FormStore.
356    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/// Context exposed by `FormList`, providing access to the parent list field
399/// and the underlying `FormHandle`.
400#[derive(Clone)]
401pub struct FormListContext {
402    pub name: String,
403    pub handle: FormHandle,
404}
405
406impl FormListContext {
407    /// Current length of the list field.
408    pub fn len(&self) -> usize {
409        form_list_len(&self.handle, &self.name)
410    }
411
412    /// Whether the list field is currently empty.
413    pub fn is_empty(&self) -> bool {
414        self.len() == 0
415    }
416
417    /// Insert an item at the given index.
418    pub fn insert(&self, index: usize, item: Value) {
419        form_list_insert(&self.handle, &self.name, index, item);
420    }
421
422    /// Remove an item at the given index.
423    pub fn remove(&self, index: usize) {
424        form_list_remove(&self.handle, &self.name, index);
425    }
426}
427
428/// Access the nearest `FormListContext`, if any.
429pub 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        // Register the current scope as a listener for this field so that
436        // changes in the FormStore (set_field_value/reset_fields) will
437        // trigger a re-render of the component using this context.
438        let scope = current_scope_id();
439        ctx.handle.register_listener(ctx.name(), scope);
440        Some(ctx)
441    } else {
442        None
443    }
444}
445
446/// Create a standalone form handle (类似 `Form.useForm`).
447pub 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    /// Whether to wrap label text when it's too long.
464    #[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    /// Visual variant for form controls (outlined/filled/borderless).
473    #[props(optional)]
474    pub variant: Option<Variant>,
475    /// Whether to scroll to the first field with error on submit failure.
476    /// Can be true (default scroll behavior) or a config object.
477    #[props(default)]
478    pub scroll_to_first_error: bool,
479    /// Custom scroll config for scroll_to_first_error.
480    #[props(optional)]
481    pub scroll_to_first_error_config: Option<ScrollToFirstErrorConfig>,
482    /// Feedback icons configuration for form items.
483    #[props(optional)]
484    pub feedback_icons: Option<FeedbackIcons>,
485    /// Form name, used for form identification and grouping.
486    #[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    /// Semantic class names for sub-parts.
497    #[props(optional)]
498    pub class_names: Option<FormClassNames>,
499    /// Semantic styles for sub-parts.
500    #[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    /// 表单字段值变更时的回调,主要用于字段联动与调试。
507    #[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    // Registry 需要在整个 Form 生命周期内保持稳定的引用,不能每次渲染都重新创建。
551    // 使用 signal 保证只在首次渲染时构建一次 Rc<RefCell<..>>。
552    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    // Build form classes
580    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    // Store scroll config for use in submit handler
595    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                    // Scroll to first error if enabled
611                    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/// Scroll to the first field with an error (WASM only).
628#[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    // Get field names in registration order
639    let field_names: Vec<String> = match registry.try_borrow() {
640        Ok(map) => map.keys().cloned().collect(),
641        Err(_) => return,
642    };
643
644    // Find first field with error
645    for name in field_names {
646        if errors.contains_key(&name) {
647            // Try to find and scroll to the form item
648            if let Some(window) = web_sys::window() {
649                if let Some(document) = window.document() {
650                    // Try to find by name attribute or data-field attribute
651                    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    // Snapshot 所有字段名,若无法读取 registry(理论上不应该发生),则保守返回失败。
685    let names: Vec<String> = match registry.try_borrow() {
686        Ok(map) => map.keys().cloned().collect(),
687        Err(_) => {
688            // 不冒险在无法读取规则时放行提交。
689            return false;
690        }
691    };
692
693    // 如果存在 required 规则且当前表单值整体为空,
694    // 对所有带 required 的字段应用校验,并直接视为失败。
695    // 这是专门覆盖 reset 后尚未输入任何值就立刻提交的场景。
696    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            // 无法读取规则时也不放行提交。
709            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    /// 完整字段名,例如 "users[0]",供嵌套 FormItem 使用。
734    pub name: String,
735}
736
737#[derive(Props, Clone, PartialEq)]
738pub struct FormListProps {
739    /// 列表字段名,如 "users"。
740    pub name: String,
741    /// 可选的初始项数量,仅在 Form 中当前没有该字段值时生效。
742    #[props(optional)]
743    pub initial_count: Option<usize>,
744    /// 列表内部渲染的子节点,由使用方自行遍历索引并渲染 FormItem。
745    pub children: Element,
746}
747
748/// 占位实现:当前仅作为 API 声明,行为等同于普通 div 包裹 children。
749/// 后续 B3 步骤会补充动态增删项逻辑。
750#[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    // 若配置了 initial_count,且当前列表为空,则初始化指定数量的空元素。
760    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// Function pointer only used for props equality in diffing; address-based
783// comparison is acceptable for this narrow use case.
784#[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    /// 简化版 dependencies:当前 FormItem 依赖的其他字段名列表,用于渲染联动。
800    #[props(optional)]
801    pub dependencies: Option<Vec<String>>,
802    /// 自定义值映射:指定控件使用哪个 prop 作为值(类似 AntD 的 valuePropName)。
803    #[props(optional)]
804    pub value_prop_name: Option<String>,
805    /// 自定义值映射:从原始 Value 映射到写入 FormStore 的 Value,供自定义控件配合 `apply_mapped_value` 使用。
806    #[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, &registry, &field_name);
863            });
864        }
865    }
866
867    // 简化版 dependencies:当依赖字段更新时,当前 FormItem scope 会被重新调度渲染。
868    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    // Build label content based on required_mark configuration
889    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        // 布尔值视 false 为“空”,便于在 Checkbox / Switch 等场景下用 required 表示“必须为 true”
1081        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
1112// ---- Shared helpers for mapping serde_json::Value to primitive types ----
1113
1114/// Convert an optional `Value` into a `String` suitable for text inputs.
1115///
1116/// - `Null`/`None` → empty string
1117/// - `String` → itself
1118/// - `Number`/`Bool` → `to_string()`
1119/// - `Array`/`Object` → empty string
1120pub 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
1136/// Convert an optional `Value` into a boolean, falling back to `default` when
1137/// no useful information is present.
1138pub 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
1151/// Convert an optional `Value` into a vector of strings (used by CheckboxGroup).
1152/// Non-string entries in the array are ignored.
1153pub 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
1163/// Convert an optional `Value` into a radio key string.
1164/// Accepts strings, numbers and booleans and normalises them to `String`.
1165pub 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
1175/// Helper: read the list value for a given field as a `Vec<Value>`.
1176///
1177/// If the field is missing or not an array, returns an empty vector.
1178pub 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
1185/// Helper: set the list value for a given field from a `Vec<Value>`.
1186pub fn form_list_set(handle: &FormHandle, name: &str, items: Vec<Value>) {
1187    handle.set_field_value(name, Value::Array(items));
1188}
1189
1190/// Helper: return the current length of a list field.
1191pub fn form_list_len(handle: &FormHandle, name: &str) -> usize {
1192    form_list_get(handle, name).len()
1193}
1194
1195/// Helper: insert an item into a list field at the given index.
1196///
1197/// If the index is greater than the current length, the item is appended.
1198pub 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
1205/// Helper: remove an item from a list field at the given index.
1206///
1207/// If the index is out of bounds, this is a no-op.
1208pub 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    // This helper exercises list operations on top of FormStore. It requires
1280    // the Dioxus runtime because `set_field_value` schedules updates, so we
1281    // keep it ignored in the default test run.
1282    #[test]
1283    #[ignore]
1284    fn form_list_helpers_operate_on_value_array() {
1285        let handle = FormHandle::new();
1286
1287        // Initially there is no list value.
1288        assert_eq!(form_list_len(&handle, "emails"), 0);
1289        assert!(form_list_get(&handle, "emails").is_empty());
1290
1291        // Insert two items.
1292        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        // Remove first item.
1301        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        // 初始状态:没有任何表单值,但存在 required 规则,应当校验失败。
1323        let ok = validate_all(&handle, &registry);
1324        assert!(!ok);
1325        let errors = handle.errors();
1326        assert_eq!(errors.get("username"), Some(&"请输入用户名".to_string()));
1327    }
1328
1329    // This scenario depends on the Dioxus runtime because `set_field_value` and
1330    // `reset_fields` will schedule view updates. In the library tests we don't
1331    // bootstrap a full runtime, so we keep this test ignored to avoid panics.
1332    #[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        // 填写并第一次提交:应当通过。
1349        handle.set_field_value("username", Value::String("alice".into()));
1350        let ok_first = validate_all(&handle, &registry);
1351        assert!(ok_first);
1352
1353        // 重置后再次提交:应当失败。
1354        handle.reset_fields();
1355        let ok_after_reset = validate_all(&handle, &registry);
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}