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.