use revue::patterns::form::{FormState, ValidationError, Validators};
use revue::prelude::*;
struct SignupForm {
form: FormState,
submitted_data: Signal<Option<UserData>>,
}
#[derive(Clone, Debug)]
struct UserData {
username: String,
email: String,
age: String,
}
impl SignupForm {
fn new() -> Self {
let form = FormState::new()
.field("username", |f| {
f.label("Username")
.placeholder("3-20 alphanumeric characters")
.required()
.min_length(3)
.max_length(20)
.validator(Validators::alphanumeric())
})
.field("email", |f| {
f.email()
.label("Email Address")
.placeholder("you@example.com")
.required()
})
.field("age", |f| {
f.number()
.label("Age")
.placeholder("Must be 13 or older")
.min(13.0)
.max(120.0)
})
.field("password", |f| {
f.password()
.label("Password")
.placeholder("Minimum 8 characters")
.required()
.min_length(8)
})
.field("confirm_password", |f| {
f.password()
.label("Confirm Password")
.placeholder("Re-enter your password")
.required()
.matches("password")
})
.field("terms", |f| {
f.label("Accept Terms")
.placeholder("Type 'yes' to accept")
.validator(Box::new(|v: &str| {
if v.to_lowercase() == "yes" {
Ok(())
} else {
Err(ValidationError::new("You must type 'yes' to accept terms"))
}
}))
})
.build();
form.focus("username");
Self {
form,
submitted_data: signal(None),
}
}
fn handle_input(&mut self, c: char) {
if let Some(name) = self.form.focused() {
let mut value = self.form.value(&name).unwrap_or_default();
value.push(c);
self.form.set_value(&name, &value);
}
}
fn handle_backspace(&mut self) {
if let Some(name) = self.form.focused() {
let mut value = self.form.value(&name).unwrap_or_default();
value.pop();
self.form.set_value(&name, &value);
}
}
fn submit(&mut self) {
if self.form.submit() {
let values = self.form.values();
let data = UserData {
username: values.get("username").cloned().unwrap_or_default(),
email: values.get("email").cloned().unwrap_or_default(),
age: values.get("age").cloned().unwrap_or_default(),
};
self.submitted_data.set(Some(data));
self.form.reset();
self.form.focus("username");
}
}
fn handle_key(&mut self, key: &Key) -> bool {
match key {
Key::Char(c) if !c.is_control() => {
self.handle_input(*c);
true
}
Key::Backspace => {
self.handle_backspace();
true
}
Key::Tab => {
self.form.focus_next();
true
}
Key::BackTab => {
self.form.focus_prev();
true
}
Key::Enter => {
self.submit();
true
}
Key::Escape => {
self.form.reset();
self.submitted_data.set(None);
self.form.focus("username");
true
}
_ => false,
}
}
}
impl View for SignupForm {
fn render(&self, ctx: &mut RenderContext) {
let form_valid = self.form.is_valid();
let focused_name = self.form.focused();
let submitted = self.submitted_data.get();
let mut main_view = vstack().gap(1);
main_view = main_view.child(
Border::double().fg(Color::CYAN).child(
Text::new(" Signup Form - Validation Demo ")
.bold()
.fg(Color::CYAN),
),
);
if let Some(data) = submitted {
main_view = main_view.child(
Border::success_box().title("Success!").child(
vstack()
.child(Text::success("Account created successfully!"))
.child(Text::new(format!("Username: {}", data.username)))
.child(Text::new(format!("Email: {}", data.email)))
.child(if !data.age.is_empty() {
Text::new(format!("Age: {}", data.age))
} else {
Text::new("Age: Not provided")
}),
),
);
}
for name in self.form.field_names() {
if let Some(field) = self.form.get(name) {
let is_focused = focused_name.as_deref() == Some(name);
let value = field.value();
let errors = field.errors();
let is_valid = field.is_valid();
let is_touched = field.is_touched();
let border = if is_focused {
Border::double().fg(Color::CYAN)
} else if is_touched && !is_valid {
Border::single().fg(Color::RED)
} else if is_touched && is_valid && !value.is_empty() {
Border::single().fg(Color::GREEN)
} else {
Border::single()
};
let (status_icon, status_color) = if value.is_empty() && !is_touched {
("○", Color::rgb(100, 100, 100))
} else if is_valid {
("✓", Color::GREEN)
} else {
("✗", Color::RED)
};
let display_value = if field.field_type
== revue::patterns::form::FieldType::Password
&& !value.is_empty()
{
"•".repeat(value.len())
} else if value.is_empty() {
field.placeholder.clone()
} else {
value.clone()
};
let value_color = if is_focused {
Color::YELLOW
} else if value.is_empty() {
Color::rgb(100, 100, 100)
} else {
Color::WHITE
};
let mut field_view = vstack()
.child(
hstack()
.gap(1)
.child(Text::new(status_icon).fg(status_color))
.child(Text::new(&field.label).bold())
.child(if is_focused {
Text::new("(editing)").fg(Color::CYAN)
} else {
Text::new("")
}),
)
.child(Text::new(&display_value).fg(value_color));
if is_touched && !errors.is_empty() {
for error in &errors {
field_view = field_view
.child(Text::new(format!(" ↳ {}", &error.message)).fg(Color::RED));
}
}
main_view = main_view.child(border.child(field_view));
}
}
let status_text = if form_valid {
Text::success("✓ All fields valid - Press Enter to submit")
} else {
let error_count = self.form.errors().len();
Text::error(format!(
"✗ {} validation error{} - Fix before submitting",
error_count,
if error_count == 1 { "" } else { "s" }
))
};
main_view = main_view.child(status_text);
main_view = main_view.child(
Border::rounded().title("Controls").child(
hstack()
.gap(3)
.child(Text::muted("[Tab] Next"))
.child(Text::muted("[Shift+Tab] Prev"))
.child(Text::muted("[Enter] Submit"))
.child(Text::muted("[Esc] Reset"))
.child(Text::muted("[q] Quit")),
),
);
main_view.render(ctx);
}
fn meta(&self) -> WidgetMeta {
WidgetMeta::new("SignupForm")
}
}
fn main() -> Result<()> {
println!("Form Validation Example");
println!("Demonstrates various validation rules with the FormState API.\n");
let mut app = App::builder().build();
let form = SignupForm::new();
app.run(form, |event, form, _app| match event {
Event::Key(key_event) => form.handle_key(&key_event.key),
_ => false,
})
}