Skip to main content

orbital_base_components/form/rules/
mod.rs

1mod date_picker;
2mod input;
3mod numeric_stepper;
4mod radio_group;
5mod rating;
6mod select;
7mod slider;
8mod switch;
9mod textarea;
10
11#[cfg(test)]
12mod tests;
13
14pub use date_picker::{DatePickerRule, DatePickerRuleTrigger};
15pub use input::{InputRule, InputRuleTrigger};
16pub use numeric_stepper::{NumericStepperRule, NumericStepperRuleTrigger};
17pub use radio_group::{RadioGroupRule, RadioGroupRuleTrigger};
18pub use rating::{RatingRule, RatingRuleTrigger};
19pub use select::{SelectRule, SelectRuleTrigger};
20pub use slider::{SliderRule, SliderRuleTrigger};
21pub use switch::{SwitchRule, SwitchRuleTrigger};
22pub use textarea::{TextareaRule, TextareaRuleTrigger};
23
24use std::ops::Deref;
25
26use leptos::prelude::*;
27
28use super::bind::FormBind;
29use super::field_injection::FieldInjection;
30use super::field_validation::FieldValidationState;
31use super::option_bind::OptionBind;
32
33type RuleValidator<T> =
34    Box<dyn Fn(&T, Signal<Option<String>>) -> Result<(), FieldValidationState> + Send + Sync>;
35
36pub struct Rule<T, Trigger> {
37    pub(crate) validator: RuleValidator<T>,
38    pub(crate) trigger: Trigger,
39}
40
41impl<T, Trigger> Rule<T, Trigger> {
42    pub fn validator(
43        f: impl Fn(&T, Signal<Option<String>>) -> Result<(), FieldValidationState>
44            + Send
45            + Sync
46            + 'static,
47    ) -> Self
48    where
49        Trigger: Default,
50    {
51        Self {
52            trigger: Default::default(),
53            validator: Box::new(f),
54        }
55    }
56
57    pub fn with_trigger(mut self, trigger: Trigger) -> Self {
58        self.trigger = trigger;
59        self
60    }
61
62    pub fn validate<V, R>(
63        rules: Vec<R>,
64        value: V,
65        name: Signal<Option<String>>,
66    ) -> Callback<Option<Trigger>, bool>
67    where
68        V: RuleValueWithUntracked<T>,
69        V: Send + Sync + Copy + 'static,
70        R: Deref<Target = Rule<T, Trigger>> + Send + Sync + 'static,
71        Trigger: PartialEq + 'static,
72    {
73        if rules.is_empty() {
74            return Callback::new(move |_trigger: Option<Trigger>| true);
75        }
76
77        let field_injection = FieldInjection::use_context();
78        Callback::new(move |trigger: Option<Trigger>| {
79            let state = {
80                let mut rules_iter = rules.iter();
81                let mut call_count = 0;
82                loop {
83                    let Some(rule) = rules_iter.next() else {
84                        break if call_count == 0 { None } else { Some(Ok(())) };
85                    };
86
87                    if let Some(trigger) = trigger.as_ref() {
88                        if &rule.trigger != trigger {
89                            continue;
90                        }
91                    }
92                    call_count += 1;
93
94                    let state = value.value_with_untracked(|value| (rule.validator)(value, name));
95                    if state.is_err() {
96                        break Some(state);
97                    }
98                }
99            };
100
101            let Some(state) = state else {
102                return true;
103            };
104
105            let rt = state.is_ok();
106            if let Some(field_injection) = field_injection.as_ref() {
107                field_injection.update_validation_state(state);
108            }
109            rt
110        })
111    }
112}
113
114pub trait RuleValueWithUntracked<T> {
115    fn value_with_untracked(
116        &self,
117        f: impl FnOnce(&T) -> Result<(), FieldValidationState>,
118    ) -> Result<(), FieldValidationState>;
119}
120
121impl<T: Clone + Send + Sync + 'static> RuleValueWithUntracked<T> for FormBind<T> {
122    fn value_with_untracked(
123        &self,
124        f: impl FnOnce(&T) -> Result<(), FieldValidationState>,
125    ) -> Result<(), FieldValidationState> {
126        f(&self.get_untracked())
127    }
128}
129
130impl<T: Clone + Send + Sync + 'static> RuleValueWithUntracked<Option<T>> for OptionBind<T> {
131    fn value_with_untracked(
132        &self,
133        f: impl FnOnce(&Option<T>) -> Result<(), FieldValidationState>,
134    ) -> Result<(), FieldValidationState> {
135        f(&self.get_untracked())
136    }
137}
138
139impl<T: Clone + Send + Sync + 'static> RuleValueWithUntracked<T> for StoredValue<FormBind<T>> {
140    fn value_with_untracked(
141        &self,
142        f: impl FnOnce(&T) -> Result<(), FieldValidationState>,
143    ) -> Result<(), FieldValidationState> {
144        f(&self.get_value().get_untracked())
145    }
146}
147
148impl<T: Clone + Send + Sync + 'static> RuleValueWithUntracked<Option<T>>
149    for StoredValue<OptionBind<T>>
150{
151    fn value_with_untracked(
152        &self,
153        f: impl FnOnce(&Option<T>) -> Result<(), FieldValidationState>,
154    ) -> Result<(), FieldValidationState> {
155        f(&self.get_value().get_untracked())
156    }
157}