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    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    /// Optional metadata describing which prop should be treated as value
347    /// when integrating with custom controls (similar to AntD's valuePropName).
348    pub fn value_prop_name(&self) -> Option<&str> {
349        self.value_prop_name.as_deref()
350    }
351
352    /// Apply a raw `Value` coming from a custom event, passing it through
353    /// `get_value_from_event` if configured, then writing it into FormStore.
354    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/// Context exposed by `FormList`, providing access to the parent list field
397/// and the underlying `FormHandle`.
398#[derive(Clone)]
399pub struct FormListContext {
400    pub name: String,
401    pub handle: FormHandle,
402}
403
404impl FormListContext {
405    /// Current length of the list field.
406    pub fn len(&self) -> usize {
407        form_list_len(&self.handle, &self.name)
408    }
409
410    /// Whether the list field is currently empty.
411    pub fn is_empty(&self) -> bool {
412        self.len() == 0
413    }
414
415    /// Insert an item at the given index.
416    pub fn insert(&self, index: usize, item: Value) {
417        form_list_insert(&self.handle, &self.name, index, item);
418    }
419
420    /// Remove an item at the given index.
421    pub fn remove(&self, index: usize) {
422        form_list_remove(&self.handle, &self.name, index);
423    }
424}
425
426/// Access the nearest `FormListContext`, if any.
427pub 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        // Register the current scope as a listener for this field so that
434        // changes in the FormStore (set_field_value/reset_fields) will
435        // trigger a re-render of the component using this context.
436        let scope = current_scope_id();
437        ctx.handle.register_listener(ctx.name(), scope);
438        Some(ctx)
439    } else {
440        None
441    }
442}
443
444/// Create a standalone form handle (类似 `Form.useForm`).
445pub 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    /// Whether to wrap label text when it's too long.
462    #[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    /// Visual variant for form controls (outlined/filled/borderless).
471    #[props(optional)]
472    pub variant: Option<Variant>,
473    /// Whether to scroll to the first field with error on submit failure.
474    /// Can be true (default scroll behavior) or a config object.
475    #[props(default)]
476    pub scroll_to_first_error: bool,
477    /// Custom scroll config for scroll_to_first_error.
478    #[props(optional)]
479    pub scroll_to_first_error_config: Option<ScrollToFirstErrorConfig>,
480    /// Feedback icons configuration for form items.
481    #[props(optional)]
482    pub feedback_icons: Option<FeedbackIcons>,
483    /// Form name, used for form identification and grouping.
484    #[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    /// Semantic class names for sub-parts.
495    #[props(optional)]
496    pub class_names: Option<FormClassNames>,
497    /// Semantic styles for sub-parts.
498    #[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    /// 表单字段值变更时的回调,主要用于字段联动与调试。
505    #[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    // Registry 需要在整个 Form 生命周期内保持稳定的引用,不能每次渲染都重新创建。
549    // 使用 signal 保证只在首次渲染时构建一次 Rc<RefCell<..>>。
550    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    // Build form classes
578    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    // Store scroll config for use in submit handler
593    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                    // Scroll to first error if enabled
609                    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/// Scroll to the first field with an error (WASM only).
626#[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    // Get field names in registration order
637    let field_names: Vec<String> = match registry.try_borrow() {
638        Ok(map) => map.keys().cloned().collect(),
639        Err(_) => return,
640    };
641
642    // Find first field with error
643    for name in field_names {
644        if errors.contains_key(&name) {
645            // Try to find and scroll to the form item
646            if let Some(window) = web_sys::window() {
647                if let Some(document) = window.document() {
648                    // Try to find by name attribute or data-field attribute
649                    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    // Snapshot 所有字段名,若无法读取 registry(理论上不应该发生),则保守返回失败。
683    let names: Vec<String> = match registry.try_borrow() {
684        Ok(map) => map.keys().cloned().collect(),
685        Err(_) => {
686            // 不冒险在无法读取规则时放行提交。
687            return false;
688        }
689    };
690
691    // 如果存在 required 规则且当前表单值整体为空,
692    // 对所有带 required 的字段应用校验,并直接视为失败。
693    // 这是专门覆盖 reset 后尚未输入任何值就立刻提交的场景。
694    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            // 无法读取规则时也不放行提交。
707            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    /// 完整字段名,例如 "users[0]",供嵌套 FormItem 使用。
732    pub name: String,
733}
734
735#[derive(Props, Clone, PartialEq)]
736pub struct FormListProps {
737    /// 列表字段名,如 "users"。
738    pub name: String,
739    /// 可选的初始项数量,仅在 Form 中当前没有该字段值时生效。
740    #[props(optional)]
741    pub initial_count: Option<usize>,
742    /// 列表内部渲染的子节点,由使用方自行遍历索引并渲染 FormItem。
743    pub children: Element,
744}
745
746/// 占位实现:当前仅作为 API 声明,行为等同于普通 div 包裹 children。
747/// 后续 B3 步骤会补充动态增删项逻辑。
748#[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    // 若配置了 initial_count,且当前列表为空,则初始化指定数量的空元素。
758    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// Function pointer only used for props equality in diffing; address-based
781// comparison is acceptable for this narrow use case.
782#[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    /// 简化版 dependencies:当前 FormItem 依赖的其他字段名列表,用于渲染联动。
798    #[props(optional)]
799    pub dependencies: Option<Vec<String>>,
800    /// 自定义值映射:指定控件使用哪个 prop 作为值(类似 AntD 的 valuePropName)。
801    #[props(optional)]
802    pub value_prop_name: Option<String>,
803    /// 自定义值映射:从原始 Value 映射到写入 FormStore 的 Value,供自定义控件配合 `apply_mapped_value` 使用。
804    #[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, &registry, &field_name);
861            });
862        }
863    }
864
865    // 简化版 dependencies:当依赖字段更新时,当前 FormItem scope 会被重新调度渲染。
866    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    // Build label content based on required_mark configuration
887    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        // 布尔值视 false 为“空”,便于在 Checkbox / Switch 等场景下用 required 表示“必须为 true”
1079        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
1110// ---- Shared helpers for mapping serde_json::Value to primitive types ----
1111
1112/// Convert an optional `Value` into a `String` suitable for text inputs.
1113///
1114/// - `Null`/`None` → empty string
1115/// - `String` → itself
1116/// - `Number`/`Bool` → `to_string()`
1117/// - `Array`/`Object` → empty string
1118pub 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
1134/// Convert an optional `Value` into a boolean, falling back to `default` when
1135/// no useful information is present.
1136pub 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
1149/// Convert an optional `Value` into a vector of strings (used by CheckboxGroup).
1150/// Non-string entries in the array are ignored.
1151pub 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
1161/// Convert an optional `Value` into a radio key string.
1162/// Accepts strings, numbers and booleans and normalises them to `String`.
1163pub 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
1173/// Helper: read the list value for a given field as a `Vec<Value>`.
1174///
1175/// If the field is missing or not an array, returns an empty vector.
1176pub 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
1183/// Helper: set the list value for a given field from a `Vec<Value>`.
1184pub fn form_list_set(handle: &FormHandle, name: &str, items: Vec<Value>) {
1185    handle.set_field_value(name, Value::Array(items));
1186}
1187
1188/// Helper: return the current length of a list field.
1189pub fn form_list_len(handle: &FormHandle, name: &str) -> usize {
1190    form_list_get(handle, name).len()
1191}
1192
1193/// Helper: insert an item into a list field at the given index.
1194///
1195/// If the index is greater than the current length, the item is appended.
1196pub 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
1203/// Helper: remove an item from a list field at the given index.
1204///
1205/// If the index is out of bounds, this is a no-op.
1206pub 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    // This helper exercises list operations on top of FormStore. It requires
1278    // the Dioxus runtime because `set_field_value` schedules updates, so we
1279    // keep it ignored in the default test run.
1280    #[test]
1281    #[ignore]
1282    fn form_list_helpers_operate_on_value_array() {
1283        let handle = FormHandle::new();
1284
1285        // Initially there is no list value.
1286        assert_eq!(form_list_len(&handle, "emails"), 0);
1287        assert!(form_list_get(&handle, "emails").is_empty());
1288
1289        // Insert two items.
1290        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        // Remove first item.
1299        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        // 初始状态:没有任何表单值,但存在 required 规则,应当校验失败。
1321        let ok = validate_all(&handle, &registry);
1322        assert!(!ok);
1323        let errors = handle.errors();
1324        assert_eq!(errors.get("username"), Some(&"请输入用户名".to_string()));
1325    }
1326
1327    // This scenario depends on the Dioxus runtime because `set_field_value` and
1328    // `reset_fields` will schedule view updates. In the library tests we don't
1329    // bootstrap a full runtime, so we keep this test ignored to avoid panics.
1330    #[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        // 填写并第一次提交:应当通过。
1347        handle.set_field_value("username", Value::String("alice".into()));
1348        let ok_first = validate_all(&handle, &registry);
1349        assert!(ok_first);
1350
1351        // 重置后再次提交:应当失败。
1352        handle.reset_fields();
1353        let ok_after_reset = validate_all(&handle, &registry);
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}