1use std::sync::Arc;
2use rosace_state::use_atom;
3use crate::validator::Validator;
4use crate::error::FieldError;
5
6#[derive(Clone)]
14pub struct FormField {
15 pub name: String,
16 value: rosace_state::Atom<String>,
17 validators: Vec<Arc<dyn Validator>>,
18 errors: rosace_state::Atom<Vec<FieldError>>,
20 touched: rosace_state::Atom<bool>,
22}
23
24impl FormField {
25 pub fn new(name: impl Into<String>) -> Self {
31 Self {
32 name: name.into(),
33 value: use_atom(String::new()),
34 validators: Vec::new(),
35 errors: use_atom(Vec::new()),
36 touched: use_atom(false),
37 }
38 }
39
40 pub fn for_ctx(ctx: &mut rosace_core::Context, name: impl Into<String>) -> Self {
47 let field = ctx.state(Self::new(name)).get();
48 let id = ctx.component_id();
54 field.value.subscribe(id);
55 field.touched.subscribe(id);
56 field.errors.subscribe(id);
57 field
58 }
59
60 pub fn with_value(self, v: impl Into<String>) -> Self {
61 self.value.set(v.into());
62 self
63 }
64
65 pub fn rule(mut self, v: impl Validator) -> Self {
66 self.validators.push(Arc::new(v));
67 self
68 }
69
70 pub fn get(&self) -> String { self.value.get() }
72
73 pub fn set(&self, v: impl Into<String>) {
75 self.touched.set(true);
76 self.value.set(v.into());
77 }
78
79 pub fn validate(&self) -> bool {
85 let val = self.value.get();
86 let errs: Vec<FieldError> = self.validators.iter()
87 .filter_map(|v| v.validate(&val).map(|msg| FieldError::new(&self.name, msg)))
88 .collect();
89 let ok = errs.is_empty();
90 self.errors.set(errs);
91 ok
92 }
93
94 pub fn errors(&self) -> Vec<FieldError> { self.errors.get() }
96
97 pub fn is_valid(&self) -> bool { self.errors.get().is_empty() }
104
105 pub fn is_touched(&self) -> bool { self.touched.get() }
110
111 pub fn reset(&self) {
113 self.value.set(String::new());
114 self.errors.set(Vec::new());
115 self.touched.set(false);
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use crate::validator::{Required, MinLength};
123
124 #[test]
125 fn form_field_new_empty() {
126 let f = FormField::new("username");
127 assert_eq!(f.name, "username");
128 assert_eq!(f.get(), "");
129 assert!(!f.is_touched());
130 assert!(f.errors().is_empty());
131 }
132
133 #[test]
134 fn form_field_set_marks_touched() {
135 let f = FormField::new("username");
136 f.set("alice");
137 assert!(f.is_touched());
138 assert_eq!(f.get(), "alice");
139 }
140
141 #[test]
142 fn form_field_validate_no_rules_passes() {
143 let f = FormField::new("bio");
144 assert!(f.validate());
145 assert!(f.is_valid());
146 }
147
148 #[test]
149 fn form_field_validate_required_fails_empty() {
150 let f = FormField::new("name").rule(Required);
151 assert!(!f.validate());
152 assert!(!f.is_valid());
153 }
154
155 #[test]
156 fn form_field_validate_passes_with_value() {
157 let f = FormField::new("name").rule(Required);
158 f.set("alice");
159 assert!(f.validate());
160 assert!(f.is_valid());
161 }
162
163 #[test]
164 fn form_field_multiple_rules_all_checked() {
165 let f = FormField::new("name").rule(Required).rule(MinLength(5));
166 f.set("ab");
167 assert!(!f.validate());
168 assert_eq!(f.errors().len(), 1);
170 assert!(f.errors()[0].message.contains("5 characters"));
171 }
172
173 #[test]
174 fn form_field_errors_after_validate() {
175 let f = FormField::new("email").rule(Required);
176 f.validate();
177 assert!(!f.errors().is_empty());
178 assert_eq!(f.errors()[0].field, "email");
179 }
180
181 #[test]
182 fn form_field_reset_clears() {
183 let f = FormField::new("name").rule(Required);
184 f.set("alice");
185 f.validate();
186 f.reset();
187 assert_eq!(f.get(), "");
188 assert!(!f.is_touched());
189 assert!(f.errors().is_empty());
190 }
191
192 #[test]
193 fn form_field_with_value() {
194 let f = FormField::new("city").with_value("London");
195 assert_eq!(f.get(), "London");
196 }
197
198 #[test]
199 fn cloning_a_field_shares_the_same_live_state() {
200 let original = FormField::new("name").rule(Required);
204 let widget_copy = original.clone();
205 widget_copy.set("alice");
206 assert_eq!(original.get(), "alice", "a clone's write must be visible through the original handle");
207 assert!(original.is_touched());
208 original.validate();
209 assert!(widget_copy.is_valid(), "a clone must see validation results run through a DIFFERENT clone");
210 }
211}