use std::sync::Arc;
use rosace_state::use_atom;
use crate::validator::Validator;
use crate::error::FieldError;
#[derive(Clone)]
pub struct FormField {
pub name: String,
value: rosace_state::Atom<String>,
validators: Vec<Arc<dyn Validator>>,
errors: rosace_state::Atom<Vec<FieldError>>,
touched: rosace_state::Atom<bool>,
}
impl FormField {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
value: use_atom(String::new()),
validators: Vec::new(),
errors: use_atom(Vec::new()),
touched: use_atom(false),
}
}
pub fn for_ctx(ctx: &mut rosace_core::Context, name: impl Into<String>) -> Self {
let field = ctx.state(Self::new(name)).get();
let id = ctx.component_id();
field.value.subscribe(id);
field.touched.subscribe(id);
field.errors.subscribe(id);
field
}
pub fn with_value(self, v: impl Into<String>) -> Self {
self.value.set(v.into());
self
}
pub fn rule(mut self, v: impl Validator) -> Self {
self.validators.push(Arc::new(v));
self
}
pub fn get(&self) -> String { self.value.get() }
pub fn set(&self, v: impl Into<String>) {
self.touched.set(true);
self.value.set(v.into());
}
pub fn validate(&self) -> bool {
let val = self.value.get();
let errs: Vec<FieldError> = self.validators.iter()
.filter_map(|v| v.validate(&val).map(|msg| FieldError::new(&self.name, msg)))
.collect();
let ok = errs.is_empty();
self.errors.set(errs);
ok
}
pub fn errors(&self) -> Vec<FieldError> { self.errors.get() }
pub fn is_valid(&self) -> bool { self.errors.get().is_empty() }
pub fn is_touched(&self) -> bool { self.touched.get() }
pub fn reset(&self) {
self.value.set(String::new());
self.errors.set(Vec::new());
self.touched.set(false);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::validator::{Required, MinLength};
#[test]
fn form_field_new_empty() {
let f = FormField::new("username");
assert_eq!(f.name, "username");
assert_eq!(f.get(), "");
assert!(!f.is_touched());
assert!(f.errors().is_empty());
}
#[test]
fn form_field_set_marks_touched() {
let f = FormField::new("username");
f.set("alice");
assert!(f.is_touched());
assert_eq!(f.get(), "alice");
}
#[test]
fn form_field_validate_no_rules_passes() {
let f = FormField::new("bio");
assert!(f.validate());
assert!(f.is_valid());
}
#[test]
fn form_field_validate_required_fails_empty() {
let f = FormField::new("name").rule(Required);
assert!(!f.validate());
assert!(!f.is_valid());
}
#[test]
fn form_field_validate_passes_with_value() {
let f = FormField::new("name").rule(Required);
f.set("alice");
assert!(f.validate());
assert!(f.is_valid());
}
#[test]
fn form_field_multiple_rules_all_checked() {
let f = FormField::new("name").rule(Required).rule(MinLength(5));
f.set("ab");
assert!(!f.validate());
assert_eq!(f.errors().len(), 1);
assert!(f.errors()[0].message.contains("5 characters"));
}
#[test]
fn form_field_errors_after_validate() {
let f = FormField::new("email").rule(Required);
f.validate();
assert!(!f.errors().is_empty());
assert_eq!(f.errors()[0].field, "email");
}
#[test]
fn form_field_reset_clears() {
let f = FormField::new("name").rule(Required);
f.set("alice");
f.validate();
f.reset();
assert_eq!(f.get(), "");
assert!(!f.is_touched());
assert!(f.errors().is_empty());
}
#[test]
fn form_field_with_value() {
let f = FormField::new("city").with_value("London");
assert_eq!(f.get(), "London");
}
#[test]
fn cloning_a_field_shares_the_same_live_state() {
let original = FormField::new("name").rule(Required);
let widget_copy = original.clone();
widget_copy.set("alice");
assert_eq!(original.get(), "alice", "a clone's write must be visible through the original handle");
assert!(original.is_touched());
original.validate();
assert!(widget_copy.is_valid(), "a clone must see validation results run through a DIFFERENT clone");
}
}