fret_ui_headless/
form_state.rs1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3
4pub type FormFieldId = Arc<str>;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7pub enum FormValidateMode {
8 #[default]
10 OnSubmit,
11 OnChange,
13 All,
15}
16
17#[derive(Debug, Clone, Default)]
23pub struct FormState {
24 pub validate_mode: FormValidateMode,
25 pub submit_count: u64,
26 pub is_submitting: bool,
27 pub registered_fields: Vec<FormFieldId>,
28 pub dirty_fields: HashSet<FormFieldId>,
29 pub touched_fields: HashSet<FormFieldId>,
30 pub errors: HashMap<FormFieldId, Arc<str>>,
31}
32
33impl FormState {
34 pub fn is_dirty(&self) -> bool {
35 !self.dirty_fields.is_empty()
36 }
37
38 pub fn is_touched(&self) -> bool {
39 !self.touched_fields.is_empty()
40 }
41
42 pub fn is_valid(&self) -> bool {
43 self.errors.is_empty()
44 }
45
46 pub fn has_error(&self, field: &str) -> bool {
47 self.errors.keys().any(|k| k.as_ref() == field)
48 }
49
50 pub fn error_for(&self, field: &str) -> Option<&Arc<str>> {
51 self.errors
52 .iter()
53 .find_map(|(k, v)| if k.as_ref() == field { Some(v) } else { None })
54 }
55
56 pub fn is_registered(&self, field: &str) -> bool {
57 self.registered_fields.iter().any(|k| k.as_ref() == field)
58 }
59
60 pub fn register_field(&mut self, field: impl Into<FormFieldId>) {
61 let field = field.into();
62 if self
63 .registered_fields
64 .iter()
65 .any(|k| k.as_ref() == field.as_ref())
66 {
67 return;
68 }
69 self.registered_fields.push(field);
70 }
71
72 pub fn unregister_field(&mut self, field: &str) {
73 if let Some(idx) = self
74 .registered_fields
75 .iter()
76 .position(|k| k.as_ref() == field)
77 {
78 let removed = self.registered_fields.remove(idx);
79 self.dirty_fields.remove(&removed);
80 self.touched_fields.remove(&removed);
81 self.errors.remove(&removed);
82 }
83 }
84
85 pub fn touch(&mut self, field: impl Into<FormFieldId>) {
86 self.touched_fields.insert(field.into());
87 }
88
89 pub fn touch_all_registered(&mut self) {
90 for field in self.registered_fields.iter().cloned() {
91 self.touched_fields.insert(field);
92 }
93 }
94
95 pub fn set_dirty(&mut self, field: impl Into<FormFieldId>, dirty: bool) {
96 let field = field.into();
97 if dirty {
98 self.dirty_fields.insert(field);
99 } else {
100 self.dirty_fields.remove(&field);
101 }
102 }
103
104 pub fn set_error(&mut self, field: impl Into<FormFieldId>, message: impl Into<Arc<str>>) {
105 self.errors.insert(field.into(), message.into());
106 }
107
108 pub fn set_error_opt(&mut self, field: impl Into<FormFieldId>, message: Option<Arc<str>>) {
109 let field = field.into();
110 match message {
111 Some(message) => {
112 self.errors.insert(field, message);
113 }
114 None => {
115 self.errors.remove(&field);
116 }
117 }
118 }
119
120 pub fn clear_error(&mut self, field: &str) {
121 if let Some(key) = self.errors.keys().find(|k| k.as_ref() == field).cloned() {
122 self.errors.remove(&key);
123 }
124 }
125
126 pub fn clear_errors(&mut self) {
127 self.errors.clear();
128 }
129
130 pub fn begin_submit(&mut self) {
131 self.is_submitting = true;
132 }
133
134 pub fn end_submit(&mut self) {
135 self.is_submitting = false;
136 self.submit_count = self.submit_count.saturating_add(1);
137 }
138
139 pub fn reset(&mut self) {
140 *self = Self::default();
141 }
142
143 pub fn validate_field(
144 &mut self,
145 field: impl Into<FormFieldId>,
146 validate: impl FnOnce() -> Option<Arc<str>>,
147 ) -> bool {
148 let field = field.into();
149 let error = validate();
150 self.set_error_opt(field, error);
151 self.is_valid()
152 }
153
154 pub fn validate_registered_fields(
155 &mut self,
156 mut validate: impl FnMut(&FormFieldId) -> Option<Arc<str>>,
157 ) -> bool {
158 let fields: Vec<FormFieldId> = self.registered_fields.to_vec();
159 for field in fields.iter() {
160 let error = validate(field);
161 self.set_error_opt(Arc::clone(field), error);
162 }
163 self.is_valid()
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn form_state_tracks_dirty_touched_and_errors() {
173 let mut st = FormState::default();
174 assert!(st.is_valid());
175 assert!(!st.is_dirty());
176 assert!(!st.is_touched());
177
178 st.touch("name");
179 st.set_dirty("name", true);
180 st.set_error("name", Arc::from("Required"));
181
182 assert!(st.is_touched());
183 assert!(st.is_dirty());
184 assert!(!st.is_valid());
185 assert!(st.has_error("name"));
186 assert_eq!(st.error_for("name").map(|s| s.as_ref()), Some("Required"));
187
188 st.clear_error("name");
189 assert!(st.is_valid());
190 }
191
192 #[test]
193 fn registered_fields_drive_bulk_validation() {
194 let mut st = FormState::default();
195 st.register_field("name");
196 st.register_field("email");
197
198 st.validate_registered_fields(|id| match id.as_ref() {
199 "name" => Some(Arc::from("Required")),
200 "email" => None,
201 _ => None,
202 });
203
204 assert!(!st.is_valid());
205 assert_eq!(st.error_for("name").map(|v| v.as_ref()), Some("Required"));
206 assert!(st.error_for("email").is_none());
207
208 st.unregister_field("name");
209 assert!(st.is_valid());
210 }
211}