# Tutorial 3: Basic Form Patterns **Video Length**: ~20 minutes | **Difficulty**: Beginner | **Series**: Getting Started ## Overview Learn how to build user-friendly forms with validation using leptos-shadcn-ui form components. We'll create a registration form that demonstrates common patterns like controlled inputs, validation feedback, and form submission. ## What You'll Learn - Creating controlled form inputs with signals - Building form layouts with proper structure - Implementing client-side validation - Displaying validation errors and success messages - Handling form submission - Using form components: Input, Label, Button, Checkbox, Select ## Prerequisites - Completed [Tutorial 2: Your First Component](02-first-component.md) - Understanding of signals and event handlers ## Video Outline **[0:00]** Introduction to form patterns **[1:30]** Form component overview **[3:00]** Creating controlled inputs **[5:30]** Building a form layout **[8:00]** Adding validation rules **[11:00]** Displaying error messages **[13:30]** Handling form submission **[16:00]** Form accessibility best practices **[18:00]** Complete example walkthrough **[19:30]** Summary and next steps ## Step-by-Step Guide ### Understanding Controlled Inputs In Leptos, form inputs are "controlled" by signals: ```rust use leptos::*; use leptos_shadcn_input::Input; use leptos_shadcn_label::Label; #[component] pub fn ControlledInput() -> impl IntoView { let (value, set_value) = create_signal(String::new()); view! {

"You entered: " {value}

} } ``` ### Building a Registration Form Let's create a complete registration form: ```rust use leptos::*; use leptos_shadcn_button::Button; use leptos_shadcn_input::Input; use leptos_shadcn_label::Label; use leptos_shadcn_card::Card; use leptos_shadcn_checkbox::Checkbox; #[component] pub fn RegistrationForm() -> impl IntoView { // Form state let (name, set_name) = create_signal(String::new()); let (email, set_email) = create_signal(String::new()); let (password, set_password) = create_signal(String::new()); let (terms, set_terms) = create_signal(false); let (is_submitting, set_is_submitting) = create_signal(false); // Validation state let (errors, set_errors) = create_signal(Vec::new()); let (success_message, set_success_message) = create_signal(Option::::None); // Validate form let validate = move || { let mut new_errors = Vec::new(); if name.get().trim().is_empty() { new_errors.push("Name is required".to_string()); } else if name.get().len() < 2 { new_errors.push("Name must be at least 2 characters".to_string()); } if email.get().trim().is_empty() { new_errors.push("Email is required".to_string()); } else if !email.get().contains('@') { new_errors.push("Email must be valid".to_string()); } if password.get().len() < 8 { new_errors.push("Password must be at least 8 characters".to_string()); } if !terms.get() { new_errors.push("You must accept the terms".to_string()); } set_errors.set(new_errors); new_errors.is_empty() }; // Handle submission let on_submit = move |ev: leptos::ev::SubmitEvent| { ev.prevent_default(); if !validate() { return; } set_is_submitting.set(true); // Simulate API call set_timeout( move || { set_is_submitting.set(false); set_success_message.set(Some( "Registration successful! Check your email.".to_string() )); }, std::time::Duration::from_secs(1), ); }; view! {

"Create Account"

"Enter your information to get started"

// Name field
// Email field
// Password field

"Must be at least 8 characters"

// Terms checkbox
// Error messages {move || { errors.get().is_empty().then_some(()).map(|_| view! {}) }}
    {move || { errors.get().into_iter().map(|error| { view! {
  • {error}
  • } }).collect_view() }}
// Success message {move || { success_message.get().map(|msg| { view! {
{msg}
} }) }} // Submit button
} } ``` ### Creating Reusable Form Components Extract common form patterns into reusable components: ```rust #[component] pub fn FormField( label: String, id: String, #[prop(default = String::new())] error: String, help_text: Option, children: Children, ) -> impl IntoView { view! {
{children()} {move || { help_text.as_ref().map(|text| { view! {

{text.clone()}

} }) }} {move || { (!error.is_empty()).then(|| { view! {

{error.clone()}

} }) }}
} } ``` Usage: ```rust view! { } ``` ### Form with Select Component Add a select dropdown to your form: ```rust use leptos_shadcn_select::{Select, SelectContent, SelectItem, SelectTrigger, SelectValue}; #[component] pub fn ProfileForm() -> impl IntoView { let (role, set_role) = create_signal("user".to_string()); let (country, set_country) = create_signal(String::new()); view! {
// Role selection
// Country selection
} } ``` ### Real-time Validation with Debouncing Add validation that runs as the user types: ```rust use std::time::Duration; use web_sys::HtmlInputElement; #[component] pub fn ValidatedInput() -> impl IntoView { let (value, set_value) = create_signal(String::new()); let (error, set_error) = create_signal(Option::::None); let (is_validating, set_is_validating) = create_signal(false); // Debounced validation let validate_value = move |val: String| { set_is_validating.set(true); set_timeout( move || { let error = if val.len() < 3 { Some("Must be at least 3 characters".to_string()) } else if !val.chars().all(|c| c.is_alphanumeric()) { Some("Must be alphanumeric only".to_string()) } else { None }; set_error.set(error); set_is_validating.set(false); }, Duration::from_millis(300), ); }; view! {
{move || { is_validating.get().then(|| { view! { "Validating..." } }) }} {move || { error.get().map(|err| { view! { {err} } }) }} {move || { (value.get().len() >= 3 && error.get().is_none()).then(|| { view! { "✓ Available" } }) }}
} } ``` ## Form Validation Patterns ### Pattern 1: Required Field Validation ```rust let validate_required = move |value: String| { if value.trim().is_empty() { Err("This field is required".to_string()) } else { Ok(()) } }; ``` ### Pattern 2: Email Validation ```rust let validate_email = move |value: String| { if value.contains('@') && value.contains('.') { Ok(()) } else { Err("Please enter a valid email".to_string()) } }; ``` ### Pattern 3: Length Validation ```rust let validate_length = move |value: String, min: usize, max: usize| { if value.len() < min { Err(format!("Must be at least {} characters", min)) } else if value.len() > max { Err(format!("Must be at most {} characters", max)) } else { Ok(()) } }; ``` ### Pattern 4: Pattern Matching ```rust let validate_pattern = move |value: String, pattern: &str| { let regex = regex::Regex::new(pattern).unwrap(); if regex.is_match(&value) { Ok(()) } else { Err("Invalid format".to_string()) } }; ``` ## Accessibility Best Practices 1. **Always associate labels with inputs** using `for` and `id` attributes 2. **Use appropriate input types** (`email`, `password`, `tel`, etc.) 3. **Provide error descriptions** with `aria-describedby` 4. **Mark required fields** with `required` attribute 5. **Use semantic HTML** (`
`, `