orbital_base_components/form/rules/
mod.rs1mod 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}