1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3
4use fret_runtime::{Model, ModelHost, ModelId, ModelStore};
5use fret_ui::action::UiActionHost;
6
7use crate::headless::form_state::{FormFieldId, FormState, FormValidateMode};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum FormRevalidateMode {
11 #[default]
12 Never,
13 OnChange,
14}
15
16#[derive(Debug, Clone)]
17pub struct FormRegistryOptions {
18 pub touch_on_change: bool,
19 pub revalidate_mode: FormRevalidateMode,
20}
21
22impl Default for FormRegistryOptions {
23 fn default() -> Self {
24 Self {
25 touch_on_change: false,
26 revalidate_mode: FormRevalidateMode::OnChange,
27 }
28 }
29}
30
31#[derive(Debug, Clone)]
32pub struct FieldEval {
33 pub dirty: bool,
34 pub error: Option<Arc<str>>,
35}
36
37type FieldEvalFn = Arc<dyn Fn(&ModelStore, bool) -> FieldEval + 'static>;
38
39#[derive(Clone)]
40struct RegisteredField {
41 id: FormFieldId,
42 eval: FieldEvalFn,
43}
44
45pub trait IntoFormValueModel<T> {
50 fn into_form_value_model(self) -> Model<T>;
51}
52
53impl<T> IntoFormValueModel<T> for Model<T> {
54 fn into_form_value_model(self) -> Model<T> {
55 self
56 }
57}
58
59impl<T> IntoFormValueModel<T> for &Model<T> {
60 fn into_form_value_model(self) -> Model<T> {
61 self.clone()
62 }
63}
64
65#[derive(Clone, Default)]
72pub struct FormRegistry {
73 options: FormRegistryOptions,
74 fields: Vec<RegisteredField>,
75 by_model_id: HashMap<ModelId, usize>,
76}
77
78impl std::fmt::Debug for FormRegistry {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 f.debug_struct("FormRegistry")
81 .field("options", &self.options)
82 .field("fields_len", &self.fields.len())
83 .finish()
84 }
85}
86
87impl FormRegistry {
88 pub fn new() -> Self {
89 Self::default()
90 }
91
92 pub fn options(mut self, options: FormRegistryOptions) -> Self {
93 self.options = options;
94 self
95 }
96
97 pub fn field_ids(&self) -> impl Iterator<Item = &FormFieldId> {
98 self.fields.iter().map(|f| &f.id)
99 }
100
101 pub fn register_into_form_state<H: ModelHost>(
102 &self,
103 host: &mut H,
104 form_state: &Model<FormState>,
105 ) {
106 let fields: Vec<FormFieldId> = self.field_ids().cloned().collect();
107 let _ = host.models_mut().update(form_state, move |st| {
108 for id in fields.iter().cloned() {
109 st.register_field(id);
110 }
111 });
112 }
113
114 pub fn register_field<T>(
115 &mut self,
116 id: impl Into<FormFieldId>,
117 model: impl IntoFormValueModel<T>,
118 initial: T,
119 validate: impl Fn(&T) -> Option<Arc<str>> + 'static,
120 ) where
121 T: Clone + PartialEq + 'static,
122 {
123 let id: FormFieldId = id.into();
124 let model = model.into_form_value_model();
125 let model_id = model.id();
126 let eval: FieldEvalFn = Arc::new(move |store, force_validate| {
127 let current = store
128 .read(&model, |v| v.clone())
129 .unwrap_or_else(|_| initial.clone());
130 let dirty = current != initial;
131 let error = force_validate.then(|| validate(¤t)).flatten();
132 FieldEval { dirty, error }
133 });
134
135 let idx = self.fields.len();
136 self.fields.push(RegisteredField { id, eval });
137 self.by_model_id.insert(model_id, idx);
138 }
139
140 pub fn handle_model_changes<H: ModelHost>(
141 &self,
142 host: &mut H,
143 form_state: &Model<FormState>,
144 changed: &[ModelId],
145 ) {
146 if self.fields.is_empty() || changed.is_empty() {
147 return;
148 }
149
150 let (validate_mode, submit_count, error_fields) = host
151 .models()
152 .read(form_state, |st| {
153 (
154 st.validate_mode,
155 st.submit_count,
156 st.errors.keys().cloned().collect::<HashSet<_>>(),
157 )
158 })
159 .unwrap_or((FormValidateMode::default(), 0, HashSet::new()));
160
161 let store = host.models();
162 let mut updates: Vec<(FormFieldId, FieldEval)> = Vec::new();
163 for &id in changed {
164 let Some(&idx) = self.by_model_id.get(&id) else {
165 continue;
166 };
167 let field = &self.fields[idx];
168 let has_error = error_fields.contains(&field.id);
169 let should_validate = match validate_mode {
170 FormValidateMode::OnChange | FormValidateMode::All => true,
171 FormValidateMode::OnSubmit => {
172 has_error
173 || (submit_count > 0
174 && matches!(self.options.revalidate_mode, FormRevalidateMode::OnChange))
175 }
176 };
177 let eval = (field.eval)(store, should_validate);
178 updates.push((Arc::clone(&field.id), eval));
179 }
180
181 if updates.is_empty() {
182 return;
183 }
184
185 let touch_on_change = self.options.touch_on_change;
186 let _ = host.models_mut().update(form_state, move |st| {
187 for (id, eval) in updates.iter() {
188 st.set_dirty(Arc::clone(id), eval.dirty);
189 if touch_on_change && eval.dirty {
190 st.touch(Arc::clone(id));
191 }
192 st.set_error_opt(Arc::clone(id), eval.error.clone());
193 }
194 });
195 }
196
197 pub fn submit<H: ModelHost>(&self, host: &mut H, form_state: &Model<FormState>) -> bool {
198 let store = host.models();
199 let mut evals: Vec<(FormFieldId, FieldEval)> = self
200 .fields
201 .iter()
202 .map(|f| (Arc::clone(&f.id), (f.eval)(store, true)))
203 .collect();
204
205 let _ = host.models_mut().update(form_state, move |st| {
206 st.begin_submit();
207 st.touch_all_registered();
208 for (id, eval) in evals.drain(..) {
209 st.set_dirty(Arc::clone(&id), eval.dirty);
210 st.set_error_opt(id, eval.error);
211 }
212 st.end_submit();
213 });
214
215 host.models()
216 .read(form_state, |st| st.is_valid())
217 .unwrap_or(false)
218 }
219
220 pub fn submit_action_host(
226 &self,
227 host: &mut dyn UiActionHost,
228 form_state: &Model<FormState>,
229 ) -> bool {
230 let store = host.models_mut();
231 let mut evals: Vec<(FormFieldId, FieldEval)> = self
232 .fields
233 .iter()
234 .map(|f| (Arc::clone(&f.id), (f.eval)(&*store, true)))
235 .collect();
236
237 let _ = store.update(form_state, move |st| {
238 st.begin_submit();
239 st.touch_all_registered();
240 for (id, eval) in evals.drain(..) {
241 st.set_dirty(Arc::clone(&id), eval.dirty);
242 st.set_error_opt(id, eval.error);
243 }
244 st.end_submit();
245 });
246
247 store.read(form_state, |st| st.is_valid()).unwrap_or(false)
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 const SOURCE: &str = include_str!("form.rs");
254
255 fn normalize_ws(source: &str) -> String {
256 source.split_whitespace().collect()
257 }
258
259 #[test]
260 fn form_registry_register_field_keeps_a_narrow_model_bridge() {
261 let implementation = SOURCE.split("#[cfg(test)]").next().unwrap_or(SOURCE);
262 let normalized = normalize_ws(implementation);
263
264 assert!(
265 normalized.contains(
266 "pubtraitIntoFormValueModel<T>{fninto_form_value_model(self)->Model<T>;}"
267 ),
268 "form registry should keep a dedicated narrow bridge trait instead of a broad generic model conversion story"
269 );
270 assert!(
271 normalized.contains(
272 "pubfnregister_field<T>(&mutself,id:implInto<FormFieldId>,model:implIntoFormValueModel<T>,initial:T,validate:implFn(&T)->Option<Arc<str>>+'static,)whereT:Clone+PartialEq+'static,"
273 ),
274 "register_field should accept the dedicated form-value bridge"
275 );
276 assert!(
277 !normalized.contains(
278 "pubfnregister_field<T>(&mutself,id:implInto<FormFieldId>,model:Model<T>,"
279 ),
280 "register_field should not regress to a raw Model<T>-only signature"
281 );
282 }
283}