1206 lines
38 KiB
Markdown
Raw Permalink Normal View History

# Component Cookbook
Comprehensive examples for all vapora-leptos-ui components.
## Table of Contents
- [Primitives](#primitives)
- [Button](#button)
- [Input](#input)
- [Badge](#badge)
- [Spinner](#spinner)
- [Layout](#layout)
- [Card](#card)
- [Modal](#modal)
- [Data](#data)
- [Table](#table)
- [Pagination](#pagination)
- [StatCard](#statcard)
- [Forms](#forms)
- [FormField](#formfield)
- [Validation](#validation)
- [Feedback](#feedback)
- [Toast](#toast)
- [Navigation](#navigation)
- [SpaLink](#spalink)
- [Patterns](#patterns)
- [Modal with Form](#modal-with-form)
- [Table with Sorting and Pagination](#table-with-sorting-and-pagination)
- [Dashboard with Stats](#dashboard-with-stats)
---
## Primitives
### Button
```rust
use leptos::prelude::*;
use vapora_leptos_ui::{Button, Variant, Size};
#[component]
fn ButtonExamples() -> impl IntoView {
let (count, set_count) = signal(0);
view! {
<div class="space-y-4">
// Primary button
<Button
variant=Variant::Primary
size=Size::Large
on_click=Some(Callback::new(move |_| {
set_count.update(|c| *c += 1);
}))
>
{move || format!("Clicked {} times", count.get())}
</Button>
// Secondary button
<Button variant=Variant::Secondary size=Size::Medium>
"Cancel"
</Button>
// Danger button
<Button variant=Variant::Danger size=Size::Small>
"Delete"
</Button>
// Ghost button
<Button variant=Variant::Ghost>
"Subtle Action"
</Button>
// Loading button
<Button variant=Variant::Primary loading=true>
"Saving..."
</Button>
// Disabled button
<Button variant=Variant::Primary disabled=true>
"Disabled"
</Button>
</div>
}
}
```
### Input
```rust
use leptos::prelude::*;
use vapora_leptos_ui::Input;
/// Extract value from input event (Leptos 0.8 helper)
#[cfg(target_arch = "wasm32")]
fn event_target_value(ev: &leptos::ev::Event) -> String {
use wasm_bindgen::JsCast;
ev.target()
.and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok())
.map(|input| input.value())
.unwrap_or_default()
}
#[component]
fn InputExamples() -> impl IntoView {
let (username, set_username) = signal(String::new());
let (password, set_password) = signal(String::new());
let (email, set_email) = signal(String::new());
view! {
<div class="space-y-4">
// Text input
<Input
input_type="text"
placeholder="Enter username"
on_input=Callback::new(move |ev| {
set_username.set(event_target_value(&ev));
})
/>
// Password input
<Input
input_type="password"
placeholder="Enter password"
on_input=Callback::new(move |ev| {
set_password.set(event_target_value(&ev));
})
/>
// Email input
<Input
input_type="email"
placeholder="Enter email"
on_input=Callback::new(move |ev| {
set_email.set(event_target_value(&ev));
})
/>
// Disabled input
<Input
input_type="text"
placeholder="Disabled field"
disabled=true
on_input=Callback::new(|_| {})
/>
// Display values
<div class="text-white">
<div>"Username: " {move || username.get()}</div>
<div>"Password: " {move || "*".repeat(password.get().len())}</div>
<div>"Email: " {move || email.get()}</div>
</div>
</div>
}
}
```
### Badge
```rust
use leptos::prelude::*;
use vapora_leptos_ui::Badge;
#[component]
fn BadgeExamples() -> impl IntoView {
view! {
<div class="flex gap-2">
<Badge class="bg-green-500/20 text-green-400">"Active"</Badge>
<Badge class="bg-blue-500/20 text-blue-400">"Pending"</Badge>
<Badge class="bg-red-500/20 text-red-400">"Error"</Badge>
<Badge class="bg-purple-500/20 text-purple-400">"Premium"</Badge>
<Badge class="bg-gray-500/20 text-gray-400">"Archived"</Badge>
</div>
}
}
```
### Spinner
```rust
use leptos::prelude::*;
use vapora_leptos_ui::{Spinner, Size};
#[component]
fn SpinnerExamples() -> impl IntoView {
view! {
<div class="flex gap-8 items-center">
<div class="flex flex-col items-center gap-2">
<Spinner size=Size::Small />
<span class="text-white text-sm">"Small"</span>
</div>
<div class="flex flex-col items-center gap-2">
<Spinner size=Size::Medium />
<span class="text-white text-sm">"Medium"</span>
</div>
<div class="flex flex-col items-center gap-2">
<Spinner size=Size::Large />
<span class="text-white text-sm">"Large"</span>
</div>
</div>
}
}
```
---
## Layout
### Card
```rust
use leptos::prelude::*;
use vapora_leptos_ui::{Card, GlowColor, Badge};
#[component]
fn CardExamples() -> impl IntoView {
view! {
<div class="grid grid-cols-3 gap-6">
// Basic card
<Card>
<h3 class="text-lg font-semibold text-white mb-2">"Basic Card"</h3>
<p class="text-gray-400">"Simple glassmorphism card"</p>
</Card>
// Hoverable card with glow
<Card hoverable=true glow=GlowColor::Cyan>
<h3 class="text-lg font-semibold text-cyan-400 mb-2">"Hoverable"</h3>
<p class="text-gray-400">"Hover for effect"</p>
</Card>
// Card with content
<Card glow=GlowColor::Purple>
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-purple-400">"Project"</h3>
<Badge class="bg-green-500/20 text-green-400">"Active"</Badge>
</div>
<p class="text-gray-400 text-sm mb-3">
"Multi-agent development platform"
</p>
<div class="flex gap-2">
<Badge class="bg-blue-500/20 text-blue-400">"Rust"</Badge>
<Badge class="bg-purple-500/20 text-purple-400">"Leptos"</Badge>
</div>
</Card>
</div>
}
}
```
### Modal
```rust
use leptos::prelude::*;
use vapora_leptos_ui::{Modal, Button, Variant};
#[component]
fn ModalExample() -> impl IntoView {
let (show_modal, set_show_modal) = signal(false);
let (show_large_modal, set_show_large_modal) = signal(false);
view! {
<div class="space-y-4">
// Trigger buttons
<Button
variant=Variant::Primary
on_click=Some(Callback::new(move |_| set_show_modal.set(true)))
>
"Open Modal"
</Button>
<Button
variant=Variant::Secondary
on_click=Some(Callback::new(move |_| set_show_large_modal.set(true)))
>
"Open Large Modal"
</Button>
// Simple modal
<Show when=move || show_modal.get()>
<Modal on_close=Callback::new(move |_| set_show_modal.set(false))>
<h2 class="text-2xl font-bold text-white mb-4">"Confirmation"</h2>
<p class="text-gray-300 mb-6">
"Are you sure you want to proceed?"
</p>
<div class="flex gap-3 justify-end">
<Button
variant=Variant::Secondary
on_click=Some(Callback::new(move |_| set_show_modal.set(false)))
>
"Cancel"
</Button>
<Button
variant=Variant::Primary
on_click=Some(Callback::new(move |_| {
set_show_modal.set(false);
// Perform action
}))
>
"Confirm"
</Button>
</div>
</Modal>
</Show>
// Large modal with content
<Show when=move || show_large_modal.get()>
<Modal on_close=Callback::new(move |_| set_show_large_modal.set(false))>
<div class="max-w-2xl">
<h2 class="text-2xl font-bold text-white mb-4">"Project Details"</h2>
<div class="space-y-4 text-gray-300">
<p>"This is a large modal with more content."</p>
<p>"Modal features:"</p>
<ul class="list-disc list-inside">
<li>"Press Escape to close"</li>
<li>"Click backdrop to close"</li>
<li>"Tab navigation trapped inside"</li>
<li>"Auto-focus on first focusable element"</li>
</ul>
</div>
<div class="mt-6 flex justify-end">
<Button
variant=Variant::Primary
on_click=Some(Callback::new(move |_| {
set_show_large_modal.set(false);
}))
>
"Close"
</Button>
</div>
</div>
</Modal>
</Show>
</div>
}
}
```
---
## Data
### Table
```rust
use leptos::prelude::*;
use vapora_leptos_ui::{Table, TableColumn};
#[component]
fn TableExample() -> impl IntoView {
let columns = vec![
TableColumn::new("Name", "name").sortable(),
TableColumn::new("Email", "email").sortable(),
TableColumn::new("Status", "status").sortable(),
TableColumn::new("Role", "role"),
];
let rows = vec![
vec![
"Alice Smith".to_string(),
"alice@example.com".to_string(),
"Active".to_string(),
"Admin".to_string(),
],
vec![
"Bob Johnson".to_string(),
"bob@example.com".to_string(),
"Inactive".to_string(),
"User".to_string(),
],
vec![
"Carol Williams".to_string(),
"carol@example.com".to_string(),
"Active".to_string(),
"Moderator".to_string(),
],
];
view! {
<Table columns=columns rows=rows />
}
}
```
### Pagination
```rust
use leptos::prelude::*;
use vapora_leptos_ui::Pagination;
#[component]
fn PaginationExample() -> impl IntoView {
let (current_page, set_current_page) = signal(1usize);
let total_pages = 10;
view! {
<div class="space-y-4">
<div class="text-white text-center">
"Current page: " {move || current_page.get()}
</div>
<Pagination
current_page=current_page.get()
total_pages=total_pages
on_page_change=Callback::new(move |page| {
set_current_page.set(page);
})
/>
</div>
}
}
```
### StatCard
```rust
use leptos::prelude::*;
use vapora_leptos_ui::StatCard;
#[component]
fn StatCardExamples() -> impl IntoView {
view! {
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
<StatCard
label="Total Users".to_string()
value="1,234".to_string()
/>
<StatCard
label="Active Projects".to_string()
value="42".to_string()
trend_positive=true
/>
<StatCard
label="Revenue".to_string()
value="$12,345".to_string()
trend_positive=true
/>
<StatCard
label="Pending Tasks".to_string()
value="8".to_string()
trend_positive=false
/>
</div>
}
}
```
---
## Forms
### FormField
```rust
use leptos::prelude::*;
use vapora_leptos_ui::{FormField, Input, Button, Variant};
#[component]
fn FormFieldExample() -> impl IntoView {
let (username, set_username) = signal(String::new());
let (email, set_email) = signal(String::new());
let (username_error, set_username_error) = signal::<Option<String>>(None);
let handle_submit = move |ev: leptos::ev::SubmitEvent| {
ev.prevent_default();
// Validate
if username.get().trim().is_empty() {
set_username_error.set(Some("Username is required".to_string()));
return;
}
// Submit form
// ...
};
view! {
<form on:submit=handle_submit class="space-y-4">
// Field with error
{move || {
if let Some(err) = username_error.get() {
view! {
<FormField
label="Username".to_string()
error=err
required=true
has_error=true
>
<Input
input_type="text"
placeholder="Enter username"
on_input=Callback::new(move |ev| {
set_username.set(event_target_value(&ev));
set_username_error.set(None);
})
/>
</FormField>
}.into_any()
} else {
view! {
<FormField
label="Username".to_string()
required=true
has_error=false
>
<Input
input_type="text"
placeholder="Enter username"
on_input=Callback::new(move |ev| {
set_username.set(event_target_value(&ev));
})
/>
</FormField>
}.into_any()
}
}}
// Field with help text
<FormField
label="Email".to_string()
help_text="We'll never share your email".to_string()
required=false
has_error=false
>
<Input
input_type="email"
placeholder="you@example.com"
on_input=Callback::new(move |ev| {
set_email.set(event_target_value(&ev));
})
/>
</FormField>
<Button variant=Variant::Primary button_type="submit">
"Submit"
</Button>
</form>
}
}
```
### Validation
```rust
use leptos::prelude::*;
use vapora_leptos_ui::{
validate_required, validate_email, validate_min_length, validate_max_length,
FormField, Input, Button, Variant
};
#[component]
fn ValidationExample() -> impl IntoView {
let (email, set_email) = signal(String::new());
let (password, set_password) = signal(String::new());
let (email_error, set_email_error) = signal::<Option<String>>(None);
let (password_error, set_password_error) = signal::<Option<String>>(None);
let validate_form = move || -> bool {
let mut valid = true;
// Validate email
if let Err(err) = validate_required(&email.get(), "Email") {
set_email_error.set(Some(err));
valid = false;
} else if let Err(err) = validate_email(&email.get()) {
set_email_error.set(Some(err));
valid = false;
} else {
set_email_error.set(None);
}
// Validate password
if let Err(err) = validate_required(&password.get(), "Password") {
set_password_error.set(Some(err));
valid = false;
} else if let Err(err) = validate_min_length(&password.get(), 8, "Password") {
set_password_error.set(Some(err));
valid = false;
} else if let Err(err) = validate_max_length(&password.get(), 100, "Password") {
set_password_error.set(Some(err));
valid = false;
} else {
set_password_error.set(None);
}
valid
};
view! {
<form on:submit=move |ev| {
ev.prevent_default();
if validate_form() {
// Submit
}
} class="space-y-4">
// Email field with validation
{move || {
if let Some(err) = email_error.get() {
view! {
<FormField label="Email".to_string() error=err required=true has_error=true>
<Input
input_type="email"
placeholder="you@example.com"
on_input=Callback::new(move |ev| {
set_email.set(event_target_value(&ev));
set_email_error.set(None);
})
/>
</FormField>
}.into_any()
} else {
view! {
<FormField label="Email".to_string() required=true has_error=false>
<Input
input_type="email"
placeholder="you@example.com"
on_input=Callback::new(move |ev| {
set_email.set(event_target_value(&ev));
})
/>
</FormField>
}.into_any()
}
}}
// Password field with validation
{move || {
if let Some(err) = password_error.get() {
view! {
<FormField
label="Password".to_string()
error=err
required=true
has_error=true
help_text="8-100 characters".to_string()
>
<Input
input_type="password"
placeholder="Enter password"
on_input=Callback::new(move |ev| {
set_password.set(event_target_value(&ev));
set_password_error.set(None);
})
/>
</FormField>
}.into_any()
} else {
view! {
<FormField
label="Password".to_string()
required=true
has_error=false
help_text="8-100 characters".to_string()
>
<Input
input_type="password"
placeholder="Enter password"
on_input=Callback::new(move |ev| {
set_password.set(event_target_value(&ev));
})
/>
</FormField>
}.into_any()
}
}}
<Button variant=Variant::Primary button_type="submit">
"Sign Up"
</Button>
</form>
}
}
```
---
## Feedback
### Toast
```rust
use leptos::prelude::*;
use vapora_leptos_ui::{ToastProvider, use_toast, ToastType, Button, Variant};
#[component]
fn ToastExample() -> impl IntoView {
view! {
<ToastProvider>
<ToastButtons />
</ToastProvider>
}
}
#[component]
fn ToastButtons() -> impl IntoView {
let toast = use_toast();
view! {
<div class="space-x-4">
<Button
variant=Variant::Primary
on_click=Some(Callback::new(move |_| {
toast.show_toast(
"Operation successful!".to_string(),
ToastType::Success
);
}))
>
"Show Success"
</Button>
<Button
variant=Variant::Danger
on_click=Some(Callback::new(move |_| {
toast.show_toast(
"Something went wrong!".to_string(),
ToastType::Error
);
}))
>
"Show Error"
</Button>
<Button
variant=Variant::Secondary
on_click=Some(Callback::new(move |_| {
toast.show_toast(
"Here's some information".to_string(),
ToastType::Info
);
}))
>
"Show Info"
</Button>
</div>
}
}
```
---
## Navigation
### SpaLink
```rust
use leptos::prelude::*;
use vapora_leptos_ui::SpaLink;
#[component]
fn SpaLinkExamples() -> impl IntoView {
view! {
<div class="space-y-4">
// Internal link (SPA navigation)
<SpaLink
href="/projects".to_string()
class=Some("text-cyan-400 hover:text-cyan-300".to_string())
>
"View Projects"
</SpaLink>
// External link (opens in new tab)
<SpaLink
href="https://github.com".to_string()
external=true
class=Some("text-purple-400 hover:text-purple-300".to_string())
>
"GitHub"
</SpaLink>
// Mailto link (external)
<SpaLink
href="mailto:support@example.com".to_string()
class=Some("text-blue-400 hover:text-blue-300".to_string())
>
"Email Support"
</SpaLink>
</div>
}
}
```
---
## Patterns
### Modal with Form
Complete example of a modal containing a validated form:
```rust
use leptos::prelude::*;
use vapora_leptos_ui::{
Modal, FormField, Input, Button, Variant, use_toast, ToastType,
validate_required
};
#[component]
fn CreateProjectModal() -> impl IntoView {
let (show_modal, set_show_modal) = signal(false);
let (title, set_title) = signal(String::new());
let (description, set_description) = signal(String::new());
let (error, set_error) = signal::<Option<String>>(None);
let toast = use_toast();
let handle_submit = move |ev: leptos::ev::SubmitEvent| {
ev.prevent_default();
// Validate
if let Err(err) = validate_required(&title.get(), "Project title") {
set_error.set(Some(err));
return;
}
// Submit (in real app, call API)
toast.show_toast(
format!("Created project: {}", title.get()),
ToastType::Success
);
// Reset and close
set_title.set(String::new());
set_description.set(String::new());
set_error.set(None);
set_show_modal.set(false);
};
view! {
<div>
<Button
variant=Variant::Primary
on_click=Some(Callback::new(move |_| set_show_modal.set(true)))
>
"+ New Project"
</Button>
<Show when=move || show_modal.get()>
<Modal on_close=Callback::new(move |_| {
set_show_modal.set(false);
set_title.set(String::new());
set_description.set(String::new());
set_error.set(None);
})>
<h2 class="text-2xl font-bold text-white mb-6">"Create New Project"</h2>
<form on:submit=handle_submit>
<div class="flex flex-col gap-4">
// Title field with validation
{move || {
if let Some(err) = error.get() {
view! {
<FormField
label="Project Title".to_string()
error=err
required=true
has_error=true
>
<Input
input_type="text"
placeholder="Enter project name"
on_input=Callback::new(move |ev| {
set_title.set(event_target_value(&ev));
set_error.set(None);
})
/>
</FormField>
}.into_any()
} else {
view! {
<FormField
label="Project Title".to_string()
required=true
has_error=false
>
<Input
input_type="text"
placeholder="Enter project name"
on_input=Callback::new(move |ev| {
set_title.set(event_target_value(&ev));
})
/>
</FormField>
}.into_any()
}
}}
// Description field (optional)
<FormField
label="Description".to_string()
help_text="Optional project description".to_string()
required=false
has_error=false
>
<Input
input_type="text"
placeholder="Describe your project"
on_input=Callback::new(move |ev| {
set_description.set(event_target_value(&ev));
})
/>
</FormField>
// Action buttons
<div class="flex gap-3 justify-end mt-4">
<Button
variant=Variant::Secondary
on_click=Some(Callback::new(move |_| {
set_show_modal.set(false);
}))
>
"Cancel"
</Button>
<Button variant=Variant::Primary button_type="submit">
"Create Project"
</Button>
</div>
</div>
</form>
</Modal>
</Show>
</div>
}
}
```
### Table with Sorting and Pagination
Complete example of a data table with sorting and pagination:
```rust
use leptos::prelude::*;
use vapora_leptos_ui::{Table, TableColumn, Pagination, Spinner};
const ITEMS_PER_PAGE: usize = 10;
#[derive(Clone, Debug)]
struct User {
id: String,
name: String,
email: String,
status: String,
role: String,
}
#[component]
fn UsersTable() -> impl IntoView {
let (users, set_users) = signal(Vec::<User>::new());
let (loading, set_loading) = signal(true);
let (current_page, set_current_page) = signal(1usize);
// Fetch users (simulated)
Effect::new(move |_| {
spawn_local(async move {
// Simulate API call
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
let mock_users = vec![
User {
id: "1".to_string(),
name: "Alice Smith".to_string(),
email: "alice@example.com".to_string(),
status: "Active".to_string(),
role: "Admin".to_string(),
},
// ... more users
];
set_users.set(mock_users);
set_loading.set(false);
});
});
view! {
<div class="space-y-6">
<Show
when=move || !loading.get()
fallback=|| view! {
<div class="flex flex-col items-center justify-center py-12 gap-4">
<Spinner />
<div class="text-lg text-white/80">"Loading users..."</div>
</div>
}
>
{move || {
let all_users = users.get();
let total_items = all_users.len();
let total_pages = total_items.div_ceil(ITEMS_PER_PAGE);
// Paginate
let page = current_page.get();
let start_idx = (page - 1) * ITEMS_PER_PAGE;
let end_idx = (start_idx + ITEMS_PER_PAGE).min(total_items);
// Convert to table rows
let columns = vec![
TableColumn::new("Name", "name").sortable(),
TableColumn::new("Email", "email").sortable(),
TableColumn::new("Status", "status").sortable(),
TableColumn::new("Role", "role"),
];
let rows: Vec<Vec<String>> = all_users
.into_iter()
.skip(start_idx)
.take(end_idx - start_idx)
.map(|user| {
vec![user.name, user.email, user.status, user.role]
})
.collect();
view! {
<div class="flex flex-col gap-6">
<Table columns=columns rows=rows />
{move || {
if total_pages > 1 {
view! {
<div class="flex justify-center">
<Pagination
current_page=current_page.get()
total_pages=total_pages
on_page_change=Callback::new(move |page| {
set_current_page.set(page);
})
/>
</div>
}.into_any()
} else {
view! { <div /> }.into_any()
}
}}
</div>
}
}}
</Show>
</div>
}
}
```
### Dashboard with Stats
Complete dashboard example with stat cards and data display:
```rust
use leptos::prelude::*;
use vapora_leptos_ui::{StatCard, Card, Badge, GlowColor, Spinner};
#[component]
fn Dashboard() -> impl IntoView {
let (loading, set_loading) = signal(true);
let (total_users, set_total_users) = signal(0);
let (active_projects, set_active_projects) = signal(0);
let (revenue, set_revenue) = signal(0);
// Fetch stats
Effect::new(move |_| {
spawn_local(async move {
// Simulate API call
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
set_total_users.set(1234);
set_active_projects.set(42);
set_revenue.set(12345);
set_loading.set(false);
});
});
view! {
<div class="space-y-8">
<h1 class="text-3xl font-bold text-white">"Dashboard"</h1>
<Show
when=move || !loading.get()
fallback=|| view! {
<div class="flex justify-center py-12">
<Spinner />
</div>
}
>
// Stats cards
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<StatCard
label="Total Users".to_string()
value=move || total_users.get().to_string()
/>
<StatCard
label="Active Projects".to_string()
value=move || active_projects.get().to_string()
trend_positive=true
/>
<StatCard
label="Revenue".to_string()
value=move || format!("${}", revenue.get())
trend_positive=true
/>
</div>
// Recent projects
<div>
<h2 class="text-2xl font-semibold text-white mb-4">"Recent Projects"</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card glow=GlowColor::Cyan hoverable=true>
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-cyan-400">"Project Alpha"</h3>
<Badge class="bg-green-500/20 text-green-400">"Active"</Badge>
</div>
<p class="text-gray-400 text-sm">
"Multi-agent development platform"
</p>
</Card>
<Card glow=GlowColor::Purple hoverable=true>
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-purple-400">"Project Beta"</h3>
<Badge class="bg-blue-500/20 text-blue-400">"In Progress"</Badge>
</div>
<p class="text-gray-400 text-sm">
"AI-powered code reviews"
</p>
</Card>
<Card glow=GlowColor::Pink hoverable=true>
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-pink-400">"Project Gamma"</h3>
<Badge class="bg-gray-500/20 text-gray-400">"Completed"</Badge>
</div>
<p class="text-gray-400 text-sm">
"Automated testing suite"
</p>
</Card>
</div>
</div>
</Show>
</div>
}
}
```
---
## Best Practices
### Event Handling
Always use `Callback::new()` for event handlers:
```rust
// ✅ CORRECT
<Button on_click=Some(Callback::new(move |_| { /* handler */ }))>
// ❌ WRONG (type mismatch)
<Button on_click=Some(move |_| { /* handler */ })>
```
### Optional Props
For props marked `#[prop(optional)]` with `Option<T>`:
```rust
// ✅ CORRECT - Pass T directly (gets wrapped in Some)
<FormField error="Username is required".to_string() />
// ✅ CORRECT - Omit prop entirely (becomes None)
<FormField />
// ❌ WRONG - Don't pass Option<T>
<FormField error=Some("Error".to_string()) /> // Type mismatch
```
### Conditional Rendering
Use conditional rendering for optional props with dynamic values:
```rust
{move || {
if let Some(err) = error.get() {
view! { <FormField error=err /> }
} else {
view! { <FormField /> }
}
}}
```
### Extract Input Values
Helper for Leptos 0.8 (no built-in `event_target_value`):
```rust
#[cfg(target_arch = "wasm32")]
fn event_target_value(ev: &leptos::ev::Event) -> String {
use wasm_bindgen::JsCast;
ev.target()
.and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok())
.map(|input| input.value())
.unwrap_or_default()
}
```
---
**Last Updated**: 2026-02-08 (v1.2.0)