tpt_appfront_core/
form.rs1use std::cell::RefCell;
13use std::collections::HashMap;
14use std::rc::Rc;
15
16use crate::signal::Signal;
17
18type FieldValidator = dyn Fn(&str) -> Option<String>;
19
20struct StringField {
21 value: Signal<String>,
22 error: Signal<Option<String>>,
23 validator: Option<Rc<FieldValidator>>,
24}
25
26#[derive(Clone)]
33pub struct FormState {
34 fields: Rc<RefCell<HashMap<String, StringField>>>,
35 checks: Rc<RefCell<HashMap<String, Signal<bool>>>>,
36}
37
38impl FormState {
39 pub fn new() -> Self {
40 FormState {
41 fields: Rc::new(RefCell::new(HashMap::new())),
42 checks: Rc::new(RefCell::new(HashMap::new())),
43 }
44 }
45
46 pub fn field(&self, name: &str, default: impl Into<String>) -> Signal<String> {
49 self.fields
50 .borrow_mut()
51 .entry(name.to_string())
52 .or_insert_with(|| StringField {
53 value: Signal::new(default.into()),
54 error: Signal::new(None),
55 validator: None,
56 })
57 .value
58 .clone()
59 }
60
61 pub fn validate(&self, name: &str, f: impl Fn(&str) -> Option<String> + 'static) {
66 self.fields
67 .borrow_mut()
68 .entry(name.to_string())
69 .or_insert_with(|| StringField {
70 value: Signal::new(String::new()),
71 error: Signal::new(None),
72 validator: None,
73 })
74 .validator = Some(Rc::new(f));
75 }
76
77 pub fn set(&self, name: &str, value: impl Into<String>) {
80 let value = value.into();
81 let mut fields = self.fields.borrow_mut();
82 let field = fields
83 .entry(name.to_string())
84 .or_insert_with(|| StringField {
85 value: Signal::new(String::new()),
86 error: Signal::new(None),
87 validator: None,
88 });
89 let error = field.validator.as_ref().and_then(|v| v(&value));
90 field.value.set(value);
91 field.error.set(error);
92 }
93
94 pub fn error(&self, name: &str) -> Option<String> {
96 self.fields.borrow().get(name).and_then(|f| f.error.get())
97 }
98
99 pub fn checkbox(&self, name: &str, default: bool) -> Signal<bool> {
102 self.checks
103 .borrow_mut()
104 .entry(name.to_string())
105 .or_insert_with(|| Signal::new(default))
106 .clone()
107 }
108
109 pub fn set_checkbox(&self, name: &str, value: bool) {
111 self.checks
112 .borrow_mut()
113 .entry(name.to_string())
114 .or_insert_with(|| Signal::new(value))
115 .set(value);
116 }
117
118 pub fn is_valid(&self) -> bool {
122 self.fields.borrow().values().all(|f| f.error.get().is_none())
123 }
124
125 pub fn values(&self) -> HashMap<String, String> {
129 self.fields
130 .borrow()
131 .iter()
132 .map(|(k, f)| (k.clone(), f.value.get()))
133 .collect()
134 }
135}
136
137impl Default for FormState {
138 fn default() -> Self {
139 Self::new()
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn field_defaults_and_updates() {
149 let form = FormState::new();
150 let email = form.field("email", "a@example.com");
151 assert_eq!(email.get(), "a@example.com");
152
153 form.set("email", "b@example.com");
154 assert_eq!(email.get(), "b@example.com");
155 assert_eq!(form.field("email", "unused").get(), "b@example.com");
156 }
157
158 #[test]
159 fn validator_runs_on_set_and_reports_error() {
160 let form = FormState::new();
161 form.field("age", "");
162 form.validate("age", |v| {
163 if v.parse::<u32>().is_ok() {
164 None
165 } else {
166 Some("must be a number".to_string())
167 }
168 });
169
170 form.set("age", "abc");
171 assert_eq!(form.error("age").as_deref(), Some("must be a number"));
172 assert!(!form.is_valid());
173
174 form.set("age", "42");
175 assert_eq!(form.error("age"), None);
176 assert!(form.is_valid());
177 }
178
179 #[test]
180 fn checkbox_field_defaults_and_updates() {
181 let form = FormState::new();
182 let agree = form.checkbox("agree", false);
183 assert!(!agree.get());
184
185 form.set_checkbox("agree", true);
186 assert!(agree.get());
187 assert!(form.checkbox("agree", false).get());
188 }
189
190 #[test]
191 fn values_snapshots_string_fields() {
192 let form = FormState::new();
193 form.set("name", "Ada");
194 form.set("email", "ada@example.com");
195
196 let values = form.values();
197 assert_eq!(values.get("name").map(String::as_str), Some("Ada"));
198 assert_eq!(values.get("email").map(String::as_str), Some("ada@example.com"));
199 }
200}