- Complete documentation reorganization into professional structure - Achieved 90%+ test coverage across all components - Created sophisticated WASM demo matching shadcn/ui quality - Fixed all compilation warnings and missing binary files - Optimized dependencies across all packages - Professional code standards and performance optimizations - Cross-browser compatibility with Playwright testing - New York variants implementation - Advanced signal management for Leptos 0.8.8+ - Enhanced testing infrastructure with TDD approach
40 KiB
Interactive Tutorial Guide - New York Theme Components
🎯 Overview
This comprehensive tutorial guide will walk you through building interactive applications using the New York theme variants of our Leptos shadcn/ui components. You'll learn how to create engaging user interfaces with proper state management, form validation, and component interactions.
📚 Table of Contents
- Getting Started
- Component Basics
- State Management
- Form Handling
- Interactive Features
- Advanced Patterns
- Best Practices
- Troubleshooting
🚀 Getting Started
Prerequisites
Before starting this tutorial, make sure you have:
- Rust 1.70+ installed
- Leptos 0.8+ framework
- Basic understanding of Rust and Leptos
- Familiarity with HTML/CSS concepts
Project Setup
- Create a new Leptos project:
cargo new my-leptos-app --bin
cd my-leptos-app
- Add dependencies to Cargo.toml:
[dependencies]
leptos = "0.8"
leptos-shadcn-button = "0.8"
leptos-shadcn-card = "0.8"
leptos-shadcn-input = "0.8"
leptos-shadcn-form = "0.8"
- Import New York theme components:
use leptos_shadcn_button::new_york::{Button as ButtonNewYork, ButtonVariant as ButtonVariantNewYork};
use leptos_shadcn_card::new_york::{Card as CardNewYork, CardHeader as CardHeaderNewYork};
use leptos_shadcn_input::new_york::Input as InputNewYork;
🧩 Component Basics
Button Components
The New York theme provides several button variants with consistent styling:
#[component]
pub fn ButtonShowcase() -> impl IntoView {
view! {
<div class="space-y-4">
// Default button
<ButtonNewYork variant=ButtonVariantNewYork::Default>
"Default Button"
</ButtonNewYork>
// Destructive button
<ButtonNewYork variant=ButtonVariantNewYork::Destructive>
"Delete Item"
</ButtonNewYork>
// Outline button
<ButtonNewYork variant=ButtonVariantNewYork::Outline>
"Cancel"
</ButtonNewYork>
// Secondary button
<ButtonNewYork variant=ButtonVariantNewYork::Secondary>
"Secondary Action"
</ButtonNewYork>
// Ghost button
<ButtonNewYork variant=ButtonVariantNewYork::Ghost>
"Ghost Button"
</ButtonNewYork>
// Link button
<ButtonNewYork variant=ButtonVariantNewYork::Link>
"Learn More"
</ButtonNewYork>
</div>
}
}
Card Components
Cards provide structured content containers:
#[component]
pub fn CardShowcase() -> impl IntoView {
view! {
<CardNewYork class="max-w-md">
<CardHeaderNewYork>
<CardTitleNewYork>"Card Title"</CardTitleNewYork>
<CardDescriptionNewYork>
"This is a description of the card content."
</CardDescriptionNewYork>
</CardHeaderNewYork>
<CardContentNewYork>
<p>"This is the main content of the card."</p>
</CardContentNewYork>
<CardFooterNewYork>
<ButtonNewYork variant=ButtonVariantNewYork::Default>
"Action"
</ButtonNewYork>
</CardFooterNewYork>
</CardNewYork>
}
}
Input Components
Input components handle user data entry:
#[component]
pub fn InputShowcase() -> impl IntoView {
let (value, set_value) = signal("".to_string());
view! {
<div class="space-y-4">
<InputNewYork
value=move || value.get()
on_change=move |new_value| set_value.set(new_value)
placeholder="Enter text here"
/>
<InputNewYork
value=move || value.get()
on_change=move |new_value| set_value.set(new_value)
placeholder="Email address"
input_type="email"
/>
<InputNewYork
value=move || value.get()
on_change=move |new_value| set_value.set(new_value)
placeholder="Password"
input_type="password"
/>
</div>
}
}
🔄 State Management
Basic State with Signals
Leptos uses signals for reactive state management:
#[component]
pub fn StateManagementExample() -> impl IntoView {
// Create reactive signals
let (count, set_count) = signal(0);
let (name, set_name) = signal("".to_string());
let (is_visible, set_is_visible) = signal(true);
// Derived state
let double_count = Signal::derive(move || count.get() * 2);
view! {
<div class="space-y-4">
// Counter example
<div class="p-4 border rounded-lg">
<h3 class="text-lg font-semibold mb-2">"Counter: " {count}</h3>
<p class="text-sm text-gray-600">"Double: " {double_count}</p>
<div class="mt-2 space-x-2">
<ButtonNewYork
variant=ButtonVariantNewYork::Default
on_click=move |_| set_count.update(|c| *c += 1)
>
"Increment"
</ButtonNewYork>
<ButtonNewYork
variant=ButtonVariantNewYork::Outline
on_click=move |_| set_count.update(|c| *c -= 1)
>
"Decrement"
</ButtonNewYork>
</div>
</div>
// Name input
<div class="p-4 border rounded-lg">
<label class="block text-sm font-medium mb-2">"Name:"</label>
<InputNewYork
value=move || name.get()
on_change=move |new_name| set_name.set(new_name)
placeholder="Enter your name"
/>
<p class="mt-2 text-sm text-gray-600">
"Hello, " {move || if name.get().is_empty() { "Anonymous".to_string() } else { name.get() }} "!"
</p>
</div>
// Visibility toggle
<div class="p-4 border rounded-lg">
<ButtonNewYork
variant=ButtonVariantNewYork::Default
on_click=move |_| set_is_visible.update(|v| *v = !*v)
>
{move || if is_visible.get() { "Hide" } else { "Show" }} " Content"
</ButtonNewYork>
{move || if is_visible.get() {
view! {
<div class="mt-2 p-2 bg-blue-50 rounded">
"This content is visible!"
</div>
}
} else {
view! { <div></div> }
}}
</div>
</div>
}
}
Complex State Management
For more complex applications, use structured state:
#[derive(Clone, Default)]
struct AppState {
user: Option<User>,
notifications: Vec<Notification>,
theme: String,
loading: bool,
}
#[derive(Clone)]
struct User {
name: String,
email: String,
preferences: UserPreferences,
}
#[derive(Clone)]
struct UserPreferences {
theme: String,
notifications_enabled: bool,
}
#[component]
pub fn ComplexStateExample() -> impl IntoView {
let (app_state, set_app_state) = signal(AppState::default());
// State update functions
let update_user = move |user: User| {
set_app_state.update(|state| state.user = Some(user));
};
let add_notification = move |notification: Notification| {
set_app_state.update(|state| {
state.notifications.push(notification);
if state.notifications.len() > 10 {
state.notifications.remove(0);
}
});
};
let set_loading = move |loading: bool| {
set_app_state.update(|state| state.loading = loading);
};
view! {
<div class="space-y-6">
// User info display
{move || {
if let Some(user) = app_state.get().user.clone() {
view! {
<CardNewYork>
<CardHeaderNewYork>
<CardTitleNewYork>"User Profile"</CardTitleNewYork>
</CardHeaderNewYork>
<CardContentNewYork>
<p>"Name: " {user.name}</p>
<p>"Email: " {user.email}</p>
<p>"Theme: " {user.preferences.theme}</p>
</CardContentNewYork>
</CardNewYork>
}
} else {
view! {
<CardNewYork>
<CardContentNewYork>
<p class="text-gray-500">"No user logged in"</p>
</CardContentNewYork>
</CardNewYork>
}
}
}}
// Loading state
{move || if app_state.get().loading {
view! {
<div class="flex items-center justify-center p-8">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span class="ml-2">"Loading..."</span>
</div>
}
} else {
view! { <div></div> }
}}
</div>
}
}
📝 Form Handling
Basic Form with Validation
#[derive(Clone, Default)]
struct ContactForm {
name: String,
email: String,
message: String,
}
#[derive(Clone)]
struct FormErrors {
name: Option<String>,
email: Option<String>,
message: Option<String>,
}
impl Default for FormErrors {
fn default() -> Self {
Self {
name: None,
email: None,
message: None,
}
}
}
#[component]
pub fn ContactFormExample() -> impl IntoView {
let (form_data, set_form_data) = signal(ContactForm::default());
let (errors, set_errors) = signal(FormErrors::default());
let (is_submitting, set_is_submitting) = signal(false);
let (is_submitted, set_is_submitted) = signal(false);
// Validation function
let validate_form = move || {
let mut new_errors = FormErrors::default();
let data = form_data.get();
if data.name.trim().is_empty() {
new_errors.name = Some("Name is required".to_string());
}
if data.email.trim().is_empty() {
new_errors.email = Some("Email is required".to_string());
} else if !data.email.contains('@') {
new_errors.email = Some("Please enter a valid email".to_string());
}
if data.message.trim().is_empty() {
new_errors.message = Some("Message is required".to_string());
}
set_errors.set(new_errors.clone());
new_errors.name.is_none() && new_errors.email.is_none() && new_errors.message.is_none()
};
// Form submission
let handle_submit = move |_| {
if validate_form() {
set_is_submitting.set(true);
// Simulate API call
set_timeout(move || {
set_is_submitting.set(false);
set_is_submitted.set(true);
set_form_data.set(ContactForm::default());
}, 2000);
}
};
view! {
<CardNewYork class="max-w-2xl mx-auto">
<CardHeaderNewYork>
<CardTitleNewYork>"Contact Us"</CardTitleNewYork>
<CardDescriptionNewYork>
"Send us a message and we'll get back to you soon."
</CardDescriptionNewYork>
</CardHeaderNewYork>
<CardContentNewYork>
{move || if is_submitted.get() {
view! {
<div class="text-center py-8">
<div class="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<span class="text-green-600 text-2xl">"✓"</span>
</div>
<h3 class="text-lg font-semibold text-gray-900 mb-2">"Message Sent!"</h3>
<p class="text-gray-600 mb-4">"Thank you for your message. We'll get back to you soon."</p>
<ButtonNewYork
variant=ButtonVariantNewYork::Default
on_click=move |_| set_is_submitted.set(false)
>
"Send Another Message"
</ButtonNewYork>
</div>
}
} else {
view! {
<form class="space-y-4" on:submit=move |ev| {
ev.prevent_default();
handle_submit(());
}>
// Name field
<div class="space-y-2">
<label class="text-sm font-medium text-gray-700">"Full Name"</label>
<InputNewYork
value=move || form_data.get().name.clone()
on_change=move |value| {
set_form_data.update(|data| data.name = value);
}
placeholder="Enter your full name"
class=move || {
if errors.get().name.is_some() {
"border-red-500 focus:border-red-500"
} else {
""
}
}
/>
{move || if let Some(error) = errors.get().name.clone() {
view! { <p class="text-sm text-red-600">{error}</p> }
} else {
view! { <div></div> }
}}
</div>
// Email field
<div class="space-y-2">
<label class="text-sm font-medium text-gray-700">"Email Address"</label>
<InputNewYork
value=move || form_data.get().email.clone()
on_change=move |value| {
set_form_data.update(|data| data.email = value);
}
placeholder="Enter your email"
input_type="email"
class=move || {
if errors.get().email.is_some() {
"border-red-500 focus:border-red-500"
} else {
""
}
}
/>
{move || if let Some(error) = errors.get().email.clone() {
view! { <p class="text-sm text-red-600">{error}</p> }
} else {
view! { <div></div> }
}}
</div>
// Message field
<div class="space-y-2">
<label class="text-sm font-medium text-gray-700">"Message"</label>
<textarea
class=format!("flex min-h-[120px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 {}",
if errors.get().message.is_some() { "border-red-500 focus:border-red-500" } else { "" }
)
placeholder="Enter your message"
prop:value=move || form_data.get().message.clone()
on:input=move |ev| {
let value = event_target_value(&ev);
set_form_data.update(|data| data.message = value);
}
/>
{move || if let Some(error) = errors.get().message.clone() {
view! { <p class="text-sm text-red-600">{error}</p> }
} else {
view! { <div></div> }
}}
</div>
// Submit button
<ButtonNewYork
variant=ButtonVariantNewYork::Default
disabled=move || is_submitting.get()
class="w-full"
>
{move || if is_submitting.get() { "Sending..." } else { "Send Message" }}
</ButtonNewYork>
</form>
}
}}
</CardContentNewYork>
</CardNewYork>
}
}
Dynamic Form Fields
#[derive(Clone)]
struct DynamicField {
id: String,
label: String,
value: String,
field_type: String,
}
#[component]
pub fn DynamicFormExample() -> impl IntoView {
let (fields, set_fields) = signal(Vec::<DynamicField>::new());
let (next_id, set_next_id) = signal(1);
let add_field = move |field_type: String| {
let id = format!("field_{}", next_id.get());
let new_field = DynamicField {
id: id.clone(),
label: format!("Field {}", next_id.get()),
value: "".to_string(),
field_type: field_type.clone(),
};
set_fields.update(|fields| fields.push(new_field));
set_next_id.update(|id| *id += 1);
};
let update_field_value = move |field_id: String, value: String| {
set_fields.update(|fields| {
if let Some(field) = fields.iter_mut().find(|f| f.id == field_id) {
field.value = value;
}
});
};
let remove_field = move |field_id: String| {
set_fields.update(|fields| {
fields.retain(|f| f.id != field_id);
});
};
view! {
<CardNewYork class="max-w-4xl mx-auto">
<CardHeaderNewYork>
<CardTitleNewYork>"Dynamic Form Builder"</CardTitleNewYork>
<CardDescriptionNewYork>
"Add and remove form fields dynamically"
</CardDescriptionNewYork>
</CardHeaderNewYork>
<CardContentNewYork>
// Add field buttons
<div class="flex flex-wrap gap-2 mb-6">
<ButtonNewYork
variant=ButtonVariantNewYork::Default
on_click=move |_| add_field("text".to_string())
>
"Add Text Field"
</ButtonNewYork>
<ButtonNewYork
variant=ButtonVariantNewYork::Default
on_click=move |_| add_field("email".to_string())
>
"Add Email Field"
</ButtonNewYork>
<ButtonNewYork
variant=ButtonVariantNewYork::Default
on_click=move |_| add_field("number".to_string())
>
"Add Number Field"
</ButtonNewYork>
</div>
// Dynamic fields
<div class="space-y-4">
{move || {
fields.get().into_iter().map(|field| {
let field_id = field.id.clone();
let field_type = field.field_type.clone();
view! {
<div class="flex items-center space-x-4 p-4 border rounded-lg">
<div class="flex-1 space-y-2">
<label class="text-sm font-medium text-gray-700">
{field.label}
</label>
<InputNewYork
value=move || field.value.clone()
on_change=move |value| update_field_value(field_id.clone(), value)
placeholder=format!("Enter {}", field_type)
input_type=field_type.clone()
/>
</div>
<ButtonNewYork
variant=ButtonVariantNewYork::Destructive
size=ButtonSizeNewYork::Sm
on_click=move |_| remove_field(field_id.clone())
>
"Remove"
</ButtonNewYork>
</div>
}
}).collect::<Vec<_>>()
}}
</div>
// Form data preview
{move || if !fields.get().is_empty() {
view! {
<div class="mt-6 p-4 bg-gray-50 rounded-lg">
<h4 class="text-sm font-medium text-gray-700 mb-2">"Form Data Preview:"</h4>
<pre class="text-xs text-gray-600 overflow-auto">
{move || {
let data: Vec<String> = fields.get().into_iter()
.map(|f| format!(" {}: \"{}\"", f.label, f.value))
.collect();
format!("{{\n{}\n}}", data.join(",\n"))
}}
</pre>
</div>
}
} else {
view! { <div></div> }
}}
</CardContentNewYork>
</CardNewYork>
}
}
🎮 Interactive Features
Modal and Dialog Management
#[component]
pub fn ModalExample() -> impl IntoView {
let (is_modal_open, set_is_modal_open) = signal(false);
let (modal_content, set_modal_content) = signal("".to_string());
let open_modal = move |content: String| {
set_modal_content.set(content);
set_is_modal_open.set(true);
};
let close_modal = move |_| {
set_is_modal_open.set(false);
};
view! {
<div class="space-y-4">
// Modal triggers
<div class="flex space-x-4">
<ButtonNewYork
variant=ButtonVariantNewYork::Default
on_click=move |_| open_modal("This is a simple modal with some content.".to_string())
>
"Open Simple Modal"
</ButtonNewYork>
<ButtonNewYork
variant=ButtonVariantNewYork::Outline
on_click=move |_| open_modal("This modal contains a form for user input.".to_string())
>
"Open Form Modal"
</ButtonNewYork>
</div>
// Modal overlay
{move || if is_modal_open.get() {
view! {
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<CardNewYork class="max-w-md w-full mx-4">
<CardHeaderNewYork>
<CardTitleNewYork>"Modal Dialog"</CardTitleNewYork>
<button
class="absolute top-4 right-4 text-gray-400 hover:text-gray-600"
on:click=close_modal
>
"×"
</button>
</CardHeaderNewYork>
<CardContentNewYork>
<p class="mb-4">{modal_content.get()}</p>
// Modal form (if needed)
{move || if modal_content.get().contains("form") {
view! {
<div class="space-y-4">
<InputNewYork
placeholder="Enter your name"
/>
<InputNewYork
placeholder="Enter your email"
input_type="email"
/>
</div>
}
} else {
view! { <div></div> }
}}
</CardContentNewYork>
<CardFooterNewYork>
<div class="flex justify-end space-x-2">
<ButtonNewYork
variant=ButtonVariantNewYork::Outline
on_click=close_modal
>
"Cancel"
</ButtonNewYork>
<ButtonNewYork
variant=ButtonVariantNewYork::Default
on_click=close_modal
>
"Confirm"
</ButtonNewYork>
</div>
</CardFooterNewYork>
</CardNewYork>
</div>
}
} else {
view! { <div></div> }
}}
</div>
}
}
Notification System
#[derive(Clone)]
struct Notification {
id: String,
message: String,
notification_type: NotificationType,
timestamp: chrono::DateTime<chrono::Utc>,
}
#[derive(Clone)]
enum NotificationType {
Success,
Error,
Info,
Warning,
}
#[component]
pub fn NotificationExample() -> impl IntoView {
let (notifications, set_notifications) = signal(Vec::<Notification>::new());
let add_notification = move |message: String, notification_type: NotificationType| {
let notification = Notification {
id: format!("notif_{}", chrono::Utc::now().timestamp_millis()),
message,
notification_type,
timestamp: chrono::Utc::now(),
};
set_notifications.update(|notifications| {
notifications.push(notification);
if notifications.len() > 5 {
notifications.remove(0);
}
});
// Auto-remove after 5 seconds
set_timeout(move || {
set_notifications.update(|notifications| {
notifications.retain(|n| n.id != notification.id);
});
}, 5000);
};
let remove_notification = move |id: String| {
set_notifications.update(|notifications| {
notifications.retain(|n| n.id != id);
});
};
view! {
<div class="space-y-4">
// Notification triggers
<div class="flex flex-wrap gap-2">
<ButtonNewYork
variant=ButtonVariantNewYork::Default
on_click=move |_| add_notification("Operation completed successfully!".to_string(), NotificationType::Success)
>
"Success Notification"
</ButtonNewYork>
<ButtonNewYork
variant=ButtonVariantNewYork::Destructive
on_click=move |_| add_notification("Something went wrong!".to_string(), NotificationType::Error)
>
"Error Notification"
</ButtonNewYork>
<ButtonNewYork
variant=ButtonVariantNewYork::Outline
on_click=move |_| add_notification("Here's some useful information.".to_string(), NotificationType::Info)
>
"Info Notification"
</ButtonNewYork>
<ButtonNewYork
variant=ButtonVariantNewYork::Secondary
on_click=move |_| add_notification("Please be careful with this action.".to_string(), NotificationType::Warning)
>
"Warning Notification"
</ButtonNewYork>
</div>
// Notification display
<div class="fixed top-4 right-4 z-50 space-y-2">
{move || {
notifications.get().into_iter().map(|notification| {
let notification_class = match notification.notification_type {
NotificationType::Success => "bg-green-100 border-green-500 text-green-700",
NotificationType::Error => "bg-red-100 border-red-500 text-red-700",
NotificationType::Info => "bg-blue-100 border-blue-500 text-blue-700",
NotificationType::Warning => "bg-yellow-100 border-yellow-500 text-yellow-700",
};
let id = notification.id.clone();
view! {
<div class=format!("p-4 border-l-4 rounded-md shadow-lg max-w-sm transform transition-all duration-300 {}", notification_class)>
<div class="flex justify-between items-start">
<div class="flex-1">
<p class="text-sm font-medium">{notification.message}</p>
<p class="text-xs opacity-75 mt-1">
{notification.timestamp.format("%H:%M:%S").to_string()}
</p>
</div>
<button
class="ml-2 text-lg leading-none hover:opacity-75"
on:click=move |_| remove_notification(id)
>
"×"
</button>
</div>
</div>
}
}).collect::<Vec<_>>()
}}
</div>
</div>
}
}
🚀 Advanced Patterns
Component Composition
#[component]
pub fn ComposedForm() -> impl IntoView {
let (form_data, set_form_data) = signal(FormData::default());
let (current_step, set_current_step) = signal(1);
view! {
<CardNewYork class="max-w-2xl mx-auto">
<CardHeaderNewYork>
<CardTitleNewYork>"Multi-Step Form"</CardTitleNewYork>
<CardDescriptionNewYork>
"Complete the form step by step"
</CardDescriptionNewYork>
</CardHeaderNewYork>
<CardContentNewYork>
// Step indicator
<StepIndicator current=current_step />
// Form content
<div class="mt-6">
{move || match current_step.get() {
1 => view! { <PersonalInfoStep data=form_data set_data=set_form_data /> },
2 => view! { <ContactInfoStep data=form_data set_data=set_form_data /> },
3 => view! { <ReviewStep data=form_data /> },
_ => view! { <div></div> }
}}
</div>
// Navigation
<div class="flex justify-between mt-6">
<ButtonNewYork
variant=ButtonVariantNewYork::Outline
disabled=move || current_step.get() <= 1
on_click=move |_| set_current_step.update(|s| *s -= 1)
>
"Previous"
</ButtonNewYork>
<ButtonNewYork
variant=ButtonVariantNewYork::Default
disabled=move || current_step.get() >= 3
on_click=move |_| set_current_step.update(|s| *s += 1)
>
"Next"
</ButtonNewYork>
</div>
</CardContentNewYork>
</CardNewYork>
}
}
#[component]
pub fn StepIndicator(current: Signal<usize>) -> impl IntoView {
view! {
<div class="flex items-center justify-between">
{[1, 2, 3].into_iter().map(|step| {
let is_active = move || current.get() == step;
let is_completed = move || current.get() > step;
view! {
<div class="flex items-center">
<div class=move || {
if is_active() {
"w-8 h-8 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-medium"
} else if is_completed() {
"w-8 h-8 bg-green-600 text-white rounded-full flex items-center justify-center text-sm font-medium"
} else {
"w-8 h-8 bg-gray-200 text-gray-600 rounded-full flex items-center justify-center text-sm font-medium"
}
}>
{move || if is_completed() { "✓" } else { step.to_string() }}
</div>
{move || if step < 3 {
view! {
<div class=move || {
if is_completed() {
"w-12 h-1 bg-green-600 mx-2"
} else {
"w-12 h-1 bg-gray-200 mx-2"
}
}></div>
}
} else {
view! { <div></div> }
}}
</div>
}
}).collect::<Vec<_>>()}
</div>
}
}
#[component]
pub fn PersonalInfoStep(
data: Signal<FormData>,
set_data: WriteSignal<FormData>
) -> impl IntoView {
view! {
<div class="space-y-4">
<h3 class="text-lg font-semibold">"Personal Information"</h3>
<InputNewYork
value=move || data.get().name.clone()
on_change=move |value| set_data.update(|d| d.name = value)
placeholder="Full Name"
/>
</div>
}
}
#[component]
pub fn ContactInfoStep(
data: Signal<FormData>,
set_data: WriteSignal<FormData>
) -> impl IntoView {
view! {
<div class="space-y-4">
<h3 class="text-lg font-semibold">"Contact Information"</h3>
<InputNewYork
value=move || data.get().email.clone()
on_change=move |value| set_data.update(|d| d.email = value)
placeholder="Email Address"
input_type="email"
/>
</div>
}
}
#[component]
pub fn ReviewStep(data: Signal<FormData>) -> impl IntoView {
view! {
<div class="space-y-4">
<h3 class="text-lg font-semibold">"Review Information"</h3>
<div class="p-4 bg-gray-50 rounded-lg">
<p>"Name: " {move || data.get().name}</p>
<p>"Email: " {move || data.get().email}</p>
</div>
</div>
}
}
✅ Best Practices
1. State Management
- Use signals for reactive state
- Keep state as close to where it's used as possible
- Use derived signals for computed values
- Structure complex state with custom types
2. Component Design
- Keep components focused and single-purpose
- Use props for configuration
- Compose components for complex UIs
- Follow the New York theme consistently
3. Performance
- Use
moveclosures to avoid unnecessary re-renders - Implement proper loading states
- Use timeouts for async operations
- Clean up resources when components unmount
4. Accessibility
- Use semantic HTML elements
- Provide proper labels and descriptions
- Ensure keyboard navigation works
- Test with screen readers
5. Error Handling
- Validate user input
- Provide clear error messages
- Handle network errors gracefully
- Use proper loading states
🔧 Troubleshooting
Common Issues
-
Components not updating:
- Check that you're using signals correctly
- Ensure you're calling
set_*functions to update state - Verify that your closures are properly capturing signals
-
Form validation not working:
- Make sure validation functions are called
- Check that error state is properly managed
- Verify that form submission is prevented when invalid
-
Styling issues:
- Ensure Tailwind CSS is properly configured
- Check that New York theme classes are applied
- Verify that custom classes don't conflict
-
Performance problems:
- Use
moveclosures to avoid unnecessary re-renders - Implement proper loading states
- Consider using
memofor expensive computations
- Use
Debug Tips
-
Use browser dev tools:
- Check the console for errors
- Inspect DOM elements
- Monitor network requests
-
Add logging:
let debug_signal = signal("initial".to_string()); Effect::new(move |_| { console_log!("Signal value: {}", debug_signal.get()); }); -
Test incrementally:
- Build components one at a time
- Test each feature separately
- Use simple examples first
📚 Additional Resources
🎉 Conclusion
This tutorial has covered the essential patterns for building interactive applications with the New York theme components. You've learned about:
- Component basics and variants
- State management with signals
- Form handling and validation
- Interactive features and notifications
- Advanced composition patterns
- Best practices and troubleshooting
Continue experimenting with these patterns and building more complex applications. The New York theme provides a solid foundation for creating beautiful, interactive user interfaces with Leptos and Rust.
Happy coding! 🚀