Skip to main content

reratui_hooks/form/
types.rs

1//! Type definitions for form management
2
3use crate::state::{StateHandle, StateSetter};
4use std::{collections::HashMap, sync::Arc};
5
6use super::validation::Validator;
7
8/// Configuration for form initialization
9#[derive(Clone)]
10pub struct FormConfig {
11    /// Initial values for form fields
12    pub(crate) initial_values: HashMap<String, String>,
13
14    /// Validators for each field
15    pub(crate) validators: HashMap<String, Vec<Validator>>,
16    /// Callback when form is submitted
17    pub(crate) on_submit: Arc<dyn Fn(HashMap<String, String>) + Send + Sync>,
18}
19
20impl FormConfig {
21    /// Create a new FormConfig builder
22    ///
23    /// # Example
24    ///
25    /// ```rust,ignore
26    /// use reratui::prelude::*;
27    ///
28    /// let config = FormConfig::builder()
29    ///     .field("email", "")
30    ///     .field("password", "")
31    ///     .validate("email", vec![
32    ///         Validator::required("Email is required"),
33    ///         Validator::email("Invalid email"),
34    ///     ])
35    ///     .on_submit(|values| {
36    ///         println!("Submitted: {:?}", values);
37    ///     })
38    ///     .build();
39    /// ```
40    pub fn builder() -> FormConfigBuilder {
41        FormConfigBuilder::new()
42    }
43}
44
45/// Builder for creating FormConfig with a fluent API
46///
47/// Provides a clean, type-safe way to construct form configurations
48/// with method chaining.
49pub struct FormConfigBuilder {
50    initial_values: HashMap<String, String>,
51    validators: HashMap<String, Vec<Validator>>,
52    #[allow(clippy::type_complexity)]
53    on_submit: Option<Arc<dyn Fn(HashMap<String, String>) + Send + Sync>>,
54}
55
56impl FormConfigBuilder {
57    /// Create a new form configuration builder
58    pub fn new() -> Self {
59        Self {
60            initial_values: HashMap::new(),
61            validators: HashMap::new(),
62            on_submit: None,
63        }
64    }
65
66    /// Add a field with an initial value
67    ///
68    /// # Example
69    ///
70    /// ```rust,ignore
71    /// # use reratui::prelude::*;
72    /// FormConfig::builder()
73    ///     .field("username", "john_doe")
74    ///     .field("email", "john@example.com");
75    /// ```
76    pub fn field(mut self, name: impl Into<String>, initial_value: impl Into<String>) -> Self {
77        self.initial_values
78            .insert(name.into(), initial_value.into());
79        self
80    }
81
82    /// Add multiple fields at once
83    ///
84    /// # Example
85    ///
86    /// ```rust,ignore
87    /// # use reratui::prelude::*;
88    /// # use std::collections::HashMap;
89    /// FormConfig::builder()
90    ///     .fields(HashMap::from([
91    ///         ("username".to_string(), "".to_string()),
92    ///         ("email".to_string(), "".to_string()),
93    ///     ]));
94    /// ```
95    pub fn fields(mut self, fields: HashMap<String, String>) -> Self {
96        self.initial_values.extend(fields);
97        self
98    }
99
100    /// Add validators for a specific field
101    ///
102    /// # Example
103    ///
104    /// ```rust,ignore
105    /// # use reratui::prelude::*;
106    /// FormConfig::builder()
107    ///     .field("email", "")
108    ///     .validate("email", vec![
109    ///         Validator::required("Email is required"),
110    ///         Validator::email("Invalid email format"),
111    ///     ]);
112    /// ```
113    pub fn validate(mut self, field: impl Into<String>, validators: Vec<Validator>) -> Self {
114        self.validators.insert(field.into(), validators);
115        self
116    }
117
118    /// Add a single validator for a field
119    ///
120    /// # Example
121    ///
122    /// ```rust,ignore
123    /// # use reratui::prelude::*;
124    /// FormConfig::builder()
125    ///     .field("username", "")
126    ///     .validator("username", Validator::required("Username is required"))
127    ///     .validator("username", Validator::min_length(3, "Min 3 characters"));
128    /// ```
129    pub fn validator(mut self, field: impl Into<String>, validator: Validator) -> Self {
130        let field = field.into();
131        self.validators.entry(field).or_default().push(validator);
132        self
133    }
134
135    /// Set the submit handler
136    ///
137    /// # Example
138    ///
139    /// ```rust,ignore
140    /// # use reratui::prelude::*;
141    /// FormConfig::builder()
142    ///     .on_submit(|values| {
143    ///         println!("Form submitted with: {:?}", values);
144    ///     });
145    /// ```
146    pub fn on_submit<F>(mut self, handler: F) -> Self
147    where
148        F: Fn(HashMap<String, String>) + Send + Sync + 'static,
149    {
150        self.on_submit = Some(Arc::new(handler));
151        self
152    }
153
154    /// Build the final FormConfig
155    ///
156    /// # Panics
157    ///
158    /// Panics if `on_submit` was not set. Use `build_with_default_submit()` if you
159    /// want a no-op submit handler.
160    pub fn build(self) -> FormConfig {
161        FormConfig {
162            initial_values: self.initial_values,
163            validators: self.validators,
164            on_submit: self
165                .on_submit
166                .expect("on_submit handler must be set. Use build_with_default_submit() for a no-op handler."),
167        }
168    }
169
170    /// Build the FormConfig with a default no-op submit handler
171    ///
172    /// Useful for testing or when submit handling is not needed.
173    pub fn build_with_default_submit(self) -> FormConfig {
174        FormConfig {
175            initial_values: self.initial_values,
176            validators: self.validators,
177            on_submit: self.on_submit.unwrap_or_else(|| Arc::new(|_| {})),
178        }
179    }
180}
181
182impl Default for FormConfigBuilder {
183    fn default() -> Self {
184        Self::new()
185    }
186}
187
188/// Handle for interacting with form state
189#[derive(Clone)]
190pub struct FormHandle {
191    pub(crate) values: StateHandle<HashMap<String, String>>,
192    pub(crate) set_values: StateSetter<HashMap<String, String>>,
193    pub(crate) errors: StateHandle<HashMap<String, String>>,
194    pub(crate) set_errors: StateSetter<HashMap<String, String>>,
195    pub(crate) touched: StateHandle<HashMap<String, bool>>,
196    pub(crate) set_touched: StateSetter<HashMap<String, bool>>,
197    pub(crate) is_submitting: StateHandle<bool>,
198    pub(crate) set_is_submitting: StateSetter<bool>,
199    pub(crate) is_valid: StateHandle<bool>,
200    pub(crate) set_is_valid: StateSetter<bool>,
201    pub(crate) validators: HashMap<String, Vec<Validator>>,
202    pub(crate) on_submit: Arc<dyn Fn(HashMap<String, String>) + Send + Sync>,
203}
204
205impl Default for FormHandle {
206    fn default() -> Self {
207        Self {
208            values: StateHandle::default(),
209            set_values: StateSetter::default(),
210            errors: StateHandle::default(),
211            set_errors: StateSetter::default(),
212            touched: StateHandle::default(),
213            set_touched: StateSetter::default(),
214            is_submitting: StateHandle::default(),
215            set_is_submitting: StateSetter::default(),
216            is_valid: StateHandle::default(),
217            set_is_valid: StateSetter::default(),
218            validators: HashMap::new(),
219            on_submit: Arc::new(|_| {}),
220        }
221    }
222}
223
224impl FormHandle {
225    /// Register a field and get its registration info
226    pub fn register(&self, name: &str) -> FieldRegistration {
227        FieldRegistration {
228            name: name.to_string(),
229            value: self.get_value(name).unwrap_or_default(),
230            error: self.get_error(name),
231            touched: self.is_touched(name),
232        }
233    }
234
235    /// Get the current value of a field
236    pub fn get_value(&self, name: &str) -> Option<String> {
237        self.values.get().get(name).cloned()
238    }
239
240    /// Set the value of a field
241    pub fn set_value(&self, name: &str, value: String) {
242        let mut values = self.values.get();
243        values.insert(name.to_string(), value.clone());
244        self.set_values.set(values);
245
246        // Validate field if it has been touched
247        if self.is_touched(name) {
248            self.validate_field(name, &value);
249        }
250    }
251
252    /// Get the error message for a field
253    pub fn get_error(&self, name: &str) -> Option<String> {
254        self.errors.get().get(name).cloned()
255    }
256
257    /// Set the error for a field
258    pub fn set_error(&self, name: &str, error: Option<String>) {
259        let mut errors = self.errors.get();
260        if let Some(err) = error {
261            errors.insert(name.to_string(), err);
262        } else {
263            errors.remove(name);
264        }
265        self.set_errors.set(errors);
266    }
267
268    /// Check if a field has been touched
269    pub fn is_touched(&self, name: &str) -> bool {
270        self.touched.get().get(name).copied().unwrap_or(false)
271    }
272
273    /// Mark a field as touched
274    pub fn set_touched(&self, name: &str, is_touched: bool) {
275        let mut touched = self.touched.get();
276        touched.insert(name.to_string(), is_touched);
277        self.set_touched.set(touched);
278    }
279
280    /// Validate a specific field
281    pub fn validate_field(&self, name: &str, value: &str) -> bool {
282        if let Some(validators) = self.validators.get(name) {
283            for validator in validators {
284                if let Some(error) = validator.validate(value) {
285                    self.set_error(name, Some(error));
286                    return false;
287                }
288            }
289        }
290        self.set_error(name, None);
291        true
292    }
293
294    /// Validate all fields in the form
295    pub fn validate_all(&self) -> bool {
296        let values = self.values.get();
297        let mut all_valid = true;
298
299        for (name, value) in values.iter() {
300            if !self.validate_field(name, value) {
301                all_valid = false;
302            }
303        }
304
305        self.set_is_valid.set(all_valid);
306        all_valid
307    }
308
309    /// Reset the form to initial values
310    pub fn reset(&self, initial_values: HashMap<String, String>) {
311        self.set_values.set(initial_values);
312        self.set_errors.set(HashMap::new());
313        self.set_touched.set(HashMap::new());
314        self.set_is_submitting.set(false);
315        self.set_is_valid.set(true);
316    }
317
318    /// Submit the form
319    pub fn submit(&self) {
320        // Mark all fields as touched
321        let values = self.values.get();
322        let mut touched = HashMap::new();
323        for name in values.keys() {
324            touched.insert(name.clone(), true);
325        }
326        self.set_touched.set(touched);
327
328        // Validate all fields
329        if self.validate_all() {
330            self.set_is_submitting.set(true);
331            (self.on_submit)(self.values.get());
332            self.set_is_submitting.set(false);
333        }
334    }
335
336    /// Check if the form is currently submitting
337    pub fn is_submitting(&self) -> bool {
338        self.is_submitting.get()
339    }
340
341    /// Check if the form is valid
342    pub fn is_valid(&self) -> bool {
343        self.is_valid.get()
344    }
345
346    /// Get all form values
347    pub fn get_values(&self) -> HashMap<String, String> {
348        self.values.get()
349    }
350
351    /// Get all form values (alias for get_values)
352    pub fn get_all_values(&self) -> HashMap<String, String> {
353        self.get_values()
354    }
355
356    /// Get all errors
357    pub fn get_errors(&self) -> HashMap<String, String> {
358        self.errors.get()
359    }
360
361    /// Check if the form has any errors
362    pub fn has_errors(&self) -> bool {
363        !self.errors.get().is_empty()
364    }
365
366    /// Check if any field is dirty (modified from initial value)
367    pub fn is_dirty(&self) -> bool {
368        !self.touched.get().is_empty()
369    }
370}
371
372/// Field registration information
373#[derive(Debug, Clone)]
374pub struct FieldRegistration {
375    /// Field name
376    pub name: String,
377
378    /// Current field value
379    pub value: String,
380
381    /// Current error message, if any
382    pub error: Option<String>,
383
384    /// Whether the field has been touched
385    pub touched: bool,
386}
387
388impl FieldRegistration {
389    /// Check if the field has an error
390    pub fn has_error(&self) -> bool {
391        self.error.is_some()
392    }
393
394    /// Get the error message
395    pub fn error_message(&self) -> Option<&str> {
396        self.error.as_deref()
397    }
398}