# Tutorial: Form Components Deep Dive
**Video Length**: ~25 minutes | **Difficulty**: Intermediate | **Series**: Component Series
## Overview
A comprehensive guide to form components in leptos-shadcn-ui. Learn advanced patterns, composition techniques, and best practices for building robust forms.
## What You'll Learn
- Advanced input patterns (masked, formatted, validated)
- Building custom form controls
- Form validation architecture
- Composing complex forms
- Accessible form layouts
- Integrating with backend APIs
## Prerequisites
- Completed Getting Started series
- Understanding of signals and reactivity
- Familiarity with basic forms
## Video Outline
**[0:00]** Introduction to form components ecosystem
**[2:00]** Input component deep dive
**[5:00]** Textarea and rich text inputs
**[7:30]** Select and combobox patterns
**[10:00]** Checkbox and radio groups
**[12:30]** Switch and toggle controls
**[14:30]** Date and time pickers
**[17:00]** Form validation architecture
**[19:30]** Building a multi-step form
**[22:00]** Form accessibility patterns
**[24:00]** Summary and resources
## Component Library
### Input Component
The Input component supports various types and configurations:
```rust
use leptos::*;
use leptos_shadcn_input::Input;
use leptos_shadcn_label::Label;
#[component]
pub fn InputExamples() -> impl IntoView {
let (text_value, set_text_value) = create_signal(String::new());
let (email_value, set_email_value) = create_signal(String::new());
let (password_value, set_password_value) = create_signal(String::new());
let (number_value, set_number_value) = create_signal(0);
view! {
// Text input with icon
// Email input with validation
"Email"
// Password input with visibility toggle
// Number input
"Quantity"
}
}
```
### Custom Password Input Component
```rust
#[component]
pub fn PasswordInput(
id: String,
value: ReadSignal,
on_change: WriteSignal,
#[prop(default = false)]
required: bool,
) -> impl IntoView {
let (show_password, set_show_password) = create_signal(false);
view! {
{move || if show_password.get() {
"🙈"
} else {
"👁️"
}}
}
}
```
### Textarea Component
```rust
use leptos_shadcn_textarea::Textarea;
#[component]
pub fn TextareaExamples() -> impl IntoView {
let (message, set_message) = create_signal(String::new());
let char_count = move || message.get().chars().count();
view! {
// Basic textarea
"Message"
"Optional"
{char_count()}"/500"
// Auto-resizing textarea
}
}
```
### Select Component
```rust
use leptos_shadcn_select::{Select, SelectContent, SelectItem, SelectTrigger, SelectValue, SelectLabel, SelectSeparator};
#[component]
pub fn SelectExamples() -> impl IntoView {
let (role, set_role) = create_signal(String::new());
let (status, set_status) = create_signal("active".to_string());
let (categories, set_categories) = create_signal(Vec::::new());
view! {
// Basic select
"Role"
"User Roles"
"User"
"Administrator"
"Moderator"
"System Roles"
"System"
"Bot"
// Select with groups
"Status"
"🟢 Active"
"🔴 Busy"
"🟡 Away"
"⚫ Offline"
// Multi-select (checkbox group)
"Categories (Multi-select)"
}
}
```
### Checkbox and Radio Components
```rust
use leptos_shadcn_checkbox::Checkbox;
use leptos_shadcn_radio_group::{RadioGroup, RadioGroupItem};
#[component]
pub fn CheckboxRadioExamples() -> impl IntoView {
let (terms, set_terms) = create_signal(false);
let (newsletter, set_newsletter) = create_signal(true);
let (plan, set_plan) = create_signal("pro".to_string());
let (preferences, set_preferences) = create_signal(Vec::::new());
view! {
// Single checkbox
"I accept the terms and conditions"
// Checkbox list
"Notifications"
{["Email", "SMS", "Push", "In-app"].iter().map(|¬if| {
let notif_string = notif.to_string();
let is_checked = create_memo(move |_| {
preferences.get().contains(¬if_string)
});
view! {
{notif}
}
}).collect_view()}
// Radio group
"Select a plan"
{[
("free".to_string(), "Free - $0/month".to_string()),
("pro".to_string(), "Pro - $29/month".to_string()),
("enterprise".to_string(), "Enterprise - Custom".to_string()),
].into_iter().map(|(value, label)| {
view! {
{label}
}
}).collect_view()}
}
}
```
### Switch Component
```rust
use leptos_shadcn_switch::Switch;
#[component]
pub fn SwitchExamples() -> impl IntoView {
let (notifications, set_notifications) = create_signal(true);
let (auto_save, set_auto_save) = create_signal(false);
let (public_profile, set_public_profile) = create_signal(false);
view! {
// Switch with label
"Notifications"
"Receive push notifications"
// Switch list
"Auto-save"
"Save changes automatically"
"Public profile"
"Make your profile visible to everyone"
}
}
```
## Form Validation Architecture
### Validation Schema
```rust
use std::collections::HashMap;
pub type ValidationResult = Result<(), String>;
pub type Validator = fn(&str) -> ValidationResult;
pub struct ValidationSchema {
rules: HashMap>,
}
impl ValidationSchema {
pub fn new() -> Self {
Self {
rules: HashMap::new(),
}
}
pub fn add_rule(mut self, field: &str, validator: Validator) -> Self {
self.rules
.entry(field.to_string())
.or_insert_with(Vec::new)
.push(validator);
self
}
pub fn validate_field(&self, field: &str, value: &str) -> ValidationResult {
if let Some(validators) = self.rules.get(field) {
for validator in validators {
validator(value)?;
}
}
Ok(())
}
pub fn validate_form(&self, data: &HashMap) -> HashMap {
let mut errors = HashMap::new();
for (field, validators) in &self.rules {
if let Some(value) = data.get(field) {
for validator in validators {
if let Err(error) = validator(value) {
errors.insert(field.clone(), error);
break;
}
}
} else {
errors.insert(field.clone(), "This field is required".to_string());
}
}
errors
}
}
// Common validators
pub fn required(value: &str) -> ValidationResult {
if value.trim().is_empty() {
Err("This field is required".to_string())
} else {
Ok(())
}
}
pub fn min_length(min: usize) -> impl Fn(&str) -> ValidationResult {
move |value: &str| {
if value.len() < min {
Err(format!("Must be at least {} characters", min))
} else {
Ok(())
}
}
}
pub fn email_format(value: &str) -> ValidationResult {
if value.contains('@') && value.contains('.') {
Ok(())
} else {
Err("Must be a valid email address".to_string())
}
}
pub fn matches_pattern(pattern: &str) -> impl Fn(&str) -> ValidationResult + '_ {
move |value: &str| {
// Simple pattern matching - use regex crate for complex patterns
if value.is_empty() {
return Err("Value cannot be empty".to_string());
}
Ok(())
}
}
```
### Form Component with Validation
```rust
#[component]
pub fn ValidatedForm() -> 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());
// Validation state
let (errors, set_errors) = create_signal(HashMap::new());
let (touched, set_touched) = create_signal(HashSet::new());
// Create validation schema
let schema = ValidationSchema::new()
.add_rule("name", required)
.add_rule("name", min_length(2))
.add_rule("email", required)
.add_rule("email", email_format)
.add_rule("password", required)
.add_rule("password", min_length(8));
// Validate a single field
let validate_field = move |field: String, value: String| {
if touched.get().contains(&field) {
let mut new_errors = errors.get();
match schema.validate_field(&field, &value) {
Ok(_) => {
new_errors.remove(&field);
}
Err(error) => {
new_errors.insert(field, error);
}
}
set_errors.set(new_errors);
}
};
// Handle field blur
let handle_blur = move |field: String| {
set_touched.update(|t| t.insert(field));
};
// Handle submit
let handle_submit = move |ev: SubmitEvent| {
ev.prevent_default();
let mut form_data = HashMap::new();
form_data.insert("name".to_string(), name.get());
form_data.insert("email".to_string(), email.get());
form_data.insert("password".to_string(), password.get());
let validation_errors = schema.validate_form(&form_data);
if validation_errors.is_empty() {
// Submit form
log!("Form submitted: {:?}", form_data);
} else {
set_errors.set(validation_errors);
}
};
view! {
}
}
```
## Complete Example: Multi-Step Registration Form
```rust
#[derive(Clone, Copy, PartialEq)]
pub enum FormStep {
Account,
Profile,
Preferences,
Review,
}
#[component]
pub fn MultiStepForm() -> impl IntoView {
let (current_step, set_current_step) = create_signal(FormStep::Account);
let (is_submitting, set_is_submitting) = create_signal(false);
// Form data
let form_data = create_rw_signal(RegistrationData {
email: String::new(),
password: String::new(),
name: String::new(),
bio: String::new(),
notifications: true,
newsletter: false,
});
// Step validation
let can_proceed = create_memo(move |_| {
match current_step.get() {
FormStep::Account => {
let data = form_data.get();
!data.email.is_empty() && data.password.len() >= 8
}
FormStep::Profile => {
let data = form_data.get();
!data.name.is_empty()
}
_ => true,
}
});
let handle_next = move |_| {
if can_proceed.get() {
match current_step.get() {
FormStep::Account => set_current_step(FormStep::Profile),
FormStep::Profile => set_current_step(FormStep::Preferences),
FormStep::Preferences => set_current_step(FormStep::Review),
FormStep::Review => {
set_is_submitting.set(true);
// Submit to API
}
}
}
};
let handle_back = move |_| {
match current_step.get() {
FormStep::Profile => set_current_step(FormStep::Account),
FormStep::Preferences => set_current_step(FormStep::Profile),
FormStep::Review => set_current_step(FormStep::Preferences),
_ => {}
}
};
view! {
// Progress indicator
{["Account", "Profile", "Preferences", "Review"]
.iter()
.enumerate()
.map(|(i, label)| {
let step_num = i + 1;
let is_active = match current_step.get() {
FormStep::Account => i == 0,
FormStep::Profile => i == 1,
FormStep::Preferences => i == 2,
FormStep::Review => i == 3,
};
view! {
}
})
.collect_view()}
"25",
FormStep::Profile => "50",
FormStep::Preferences => "75",
FormStep::Review => "100",
})
>
// Step content
{move || match current_step.get() {
FormStep::Account => view! {
}.into_any(),
FormStep::Profile => view! { }.into_any(),
FormStep::Preferences => view! { }.into_any(),
FormStep::Review => view! { }.into_any(),
}}
// Navigation buttons
"Back"
{move || match current_step.get() {
FormStep::Review => "Submit".to_string(),
_ => "Next".to_string(),
}}
}
}
#[derive(Clone, Debug)]
pub struct RegistrationData {
email: String,
password: String,
name: String,
bio: String,
notifications: bool,
newsletter: bool,
}
#[component]
fn AccountStep(data: RwSignal) -> impl IntoView {
view! {
}
}
```
## Exercise
1. Create a form with all component types (input, textarea, select, checkbox, radio, switch)
2. Implement a reusable validation system
3. Add form state persistence to localStorage
4. Create a custom form component that composes multiple inputs
5. Add accessibility features (ARIA labels, keyboard navigation)
## What's Next?
- [Layout Components](02-layout-components.md) - Cards, tabs, and accordions
- [Advanced Form Patterns](../../advanced/02-form-validation.md) - Complex validation scenarios
- [Form Component API](../../components/forms.md) - Complete component reference
---
**Previous**: [Getting Started](../getting-started/04-styling-theming.md) | **Next**: [Layout Components](02-layout-components.md)