A Reusable Booking Form
The booking form has grown. It has its own state, validation logic, and UI. It belongs in its own component — one that can be configured with available services and notifies the parent when a booking is submitted.
What You’ll Learn
- Structure complex components
- Combine props with internal state
- Handle form submission in components
- Communicate events to parents
Component Responsibilities
The BookingForm component will:
- Accept available services as a prop
- Manage its own form state internally
- Validate input before submission
- Notify the parent when submitted
The parent page will:
- Provide the list of services
- Handle the actual booking logic (API calls, etc.)
Create the Component Shell
Start with the basic structure:
<!-- filename: src/lib/components/BookingForm.svelte -->
<script>
// Props: data from parent
let { services = [], onsubmit } = $props();
// Internal state: form fields
let customerName = $state('');
let customerEmail = $state('');
let bookingDate = $state('');
let selectedTime = $state('');
let selectedService = $state('');
let notes = $state('');
// Form status
let isSubmitting = $state(false);
</script>
<form>
<!-- Form fields will go here -->
</form>
<style>
/* Styles */
</style> Notice: services come from props, but form fields are internal state.
Add the Form Structure
Build out the form sections:
<!-- filename: src/lib/components/BookingForm.svelte -->
<script>
let { services = [], onsubmit } = $props();
let customerName = $state('');
let customerEmail = $state('');
let bookingDate = $state('');
let selectedTime = $state('');
let selectedService = $state('');
let notes = $state('');
let isSubmitting = $state(false);
// Derived values
let selectedServiceDetails = $derived(
services.find(s => s.slug === selectedService)
);
// Time slots
function generateTimeSlots() {
const slots = [];
for (let hour = 9; hour < 17; hour++) {
for (let minute of [0, 30]) {
const value = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
const period = hour >= 12 ? 'PM' : 'AM';
const displayHour = hour > 12 ? hour - 12 : hour;
const label = `${displayHour}:${minute.toString().padStart(2, '0')} ${period}`;
slots.push({ value, label });
}
}
return slots;
}
const timeSlots = generateTimeSlots();
</script>
<form>
<fieldset>
<legend>Contact Information</legend>
<div class="field">
<label for="name">Your Name</label>
<input
type="text"
id="name"
bind:value={customerName}
required
/>
</div>
<div class="field">
<label for="email">Email Address</label>
<input
type="email"
id="email"
bind:value={customerEmail}
required
/>
</div>
</fieldset>
<fieldset>
<legend>Select a Service</legend>
<div class="service-options">
{#each services as service (service.id)}
<label class="service-option" class:selected={selectedService === service.slug}>
<input
type="radio"
name="service"
value={service.slug}
bind:group={selectedService}
required
/>
<span class="service-name">{service.name}</span>
<span class="service-price">${service.price}</span>
</label>
{/each}
</div>
</fieldset>
<fieldset>
<legend>Appointment Time</legend>
<div class="field">
<label for="date">Date</label>
<input
type="date"
id="date"
bind:value={bookingDate}
required
/>
</div>
<div class="field">
<label for="time">Time</label>
<select id="time" bind:value={selectedTime} required>
<option value="">Choose a time...</option>
{#each timeSlots as slot}
<option value={slot.value}>{slot.label}</option>
{/each}
</select>
</div>
</fieldset>
<fieldset>
<legend>Additional Notes</legend>
<div class="field">
<label for="notes">Special Instructions (Optional)</label>
<textarea
id="notes"
bind:value={notes}
rows="3"
></textarea>
</div>
</fieldset>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Request Booking'}
</button>
</form> Handle Form Submission
When the form submits, gather the data and notify the parent:
<script>
let { services = [], onsubmit } = $props();
// ... state declarations ...
async function handleSubmit(event) {
event.preventDefault();
if (!onsubmit) return;
isSubmitting = true;
const bookingData = {
customerName,
customerEmail,
bookingDate,
selectedTime,
selectedService,
notes,
// Include computed values
serviceDetails: selectedServiceDetails
};
try {
await onsubmit(bookingData);
// Reset form on success
resetForm();
} catch (error) {
console.error('Booking failed:', error);
} finally {
isSubmitting = false;
}
}
function resetForm() {
customerName = '';
customerEmail = '';
bookingDate = '';
selectedTime = '';
selectedService = '';
notes = '';
}
</script>
<form onsubmit={handleSubmit}>
<!-- ... form fields ... -->
</form> The onsubmit prop is a function the parent provides.
Use the Component
Now use BookingForm in a page:
<!-- filename: src/routes/book/+page.svelte -->
<script>
import BookingForm from '$lib/components/BookingForm.svelte';
const services = [
{ id: '1', slug: 'lawn-mowing', name: 'Lawn Mowing', price: 50, duration: 60 },
{ id: '2', slug: 'hedge-trimming', name: 'Hedge Trimming', price: 75, duration: 90 },
{ id: '3', slug: 'consultation', name: 'Garden Consultation', price: 100, duration: 60 }
];
let bookingConfirmed = $state(false);
let confirmedBooking = $state(null);
async function handleBooking(data) {
// In a real app, send to server
console.log('Booking submitted:', data);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
confirmedBooking = data;
bookingConfirmed = true;
}
</script>
<h1>Book a Service</h1>
{#if bookingConfirmed}
<div class="confirmation">
<h2>✓ Booking Confirmed!</h2>
<p>Thank you, {confirmedBooking.customerName}!</p>
<p>We'll contact you at {confirmedBooking.customerEmail} to confirm your
{confirmedBooking.serviceDetails.name} appointment.</p>
<button onclick={() => bookingConfirmed = false}>
Book Another Service
</button>
</div>
{:else}
<BookingForm {services} onsubmit={handleBooking} />
{/if} The page controls the services list and handles what happens after submission.
Pre-select a Service
What if someone navigates from a service page to book that specific service?
<!-- filename: src/lib/components/BookingForm.svelte -->
<script>
let {
services = [],
onsubmit,
initialService = '' // New prop for pre-selection
} = $props();
// Initialize with the provided service
let selectedService = $state(initialService);
</script> Usage:
<!-- Pre-select lawn mowing -->
<BookingForm {services} onsubmit={handleBooking} initialService="lawn-mowing" /> The Complete Component
Here’s the full BookingForm with styling:
<!-- filename: src/lib/components/BookingForm.svelte -->
<script>
let {
services = [],
onsubmit,
initialService = ''
} = $props();
// Form state
let customerName = $state('');
let customerEmail = $state('');
let bookingDate = $state('');
let selectedTime = $state('');
let selectedService = $state(initialService);
let notes = $state('');
let isSubmitting = $state(false);
// Derived
let selectedServiceDetails = $derived(
services.find(s => s.slug === selectedService)
);
// Generate time slots
const timeSlots = (() => {
const slots = [];
for (let hour = 9; hour < 17; hour++) {
for (let minute of [0, 30]) {
const value = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
const period = hour >= 12 ? 'PM' : 'AM';
const displayHour = hour > 12 ? hour - 12 : hour;
slots.push({ value, label: `${displayHour}:${minute.toString().padStart(2, '0')} ${period}` });
}
}
return slots;
})();
async function handleSubmit(event) {
event.preventDefault();
if (!onsubmit) return;
isSubmitting = true;
try {
await onsubmit({
customerName,
customerEmail,
bookingDate,
selectedTime,
selectedService,
notes,
serviceDetails: selectedServiceDetails
});
resetForm();
} finally {
isSubmitting = false;
}
}
function resetForm() {
customerName = '';
customerEmail = '';
bookingDate = '';
selectedTime = '';
selectedService = initialService;
notes = '';
}
</script>
<form class="booking-form" onsubmit={handleSubmit}>
<!-- Form content as shown above -->
</form>
<style>
.booking-form {
max-width: 600px;
}
fieldset {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
legend {
font-weight: 600;
padding: 0 0.5rem;
}
.field {
margin-bottom: 1rem;
}
.field:last-child {
margin-bottom: 0;
}
label {
display: block;
margin-bottom: 0.25rem;
font-weight: 500;
}
input, select, textarea {
width: 100%;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
}
button[type="submit"] {
width: 100%;
padding: 0.75rem;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
}
button[type="submit"]:disabled {
background: #ccc;
cursor: not-allowed;
}
</style> Summary
Complex forms work well as components. Accept configuration through props, manage form state internally, and communicate submission through callback props. This keeps pages simple and forms reusable.
Key takeaways:
- Props for configuration data (services list)
- Internal state for form fields
- Callback props for events (onsubmit)
- Reset form on successful submission
Next Steps
Let’s create a smaller, simpler component. Continue with Create PriceBadge Component to learn about props with defaults.