React package to create booking forms

Want to create a booking form similar to airbnb.com or booking.com without spending weeks fixing bugs, without heavy dependencies and style it however (and with whatever) you want?

Previews

Live Playground

TypeScript + TailwindCSS Example

Here's a simple and quick way to get started:

1. Setup

For this tutorial you need to already be familiar with how to kickstart a basic React application with TypeScript. Let's assume we have Gatsby installed with a TS starter package:

npx gatsby new hotel-website https://github.com/jpedroschmitz/gatsby-starter-ts

Add the packages:

yarn add react-booking-form react-icons

Now boot your node server with yarn start and check if there's something on your localhost:8000 πŸš€

2. Import The Library

This part is simple and straight forward.

Create a new file ./src/pages/BookingForm.tsx

import {
  DateInput,
  FormSchema,
  GuestsSelect,
  LocationSelect,
  useReactBookingForm,
  BookingForm as BookingFormType,
} from "react-booking-form"
import "flatpickr/dist/themes/material_green.css"

Note: You can import other CSS themes for the calendar (flatpickr import above ^) or create your own. Read more on flatpickr themes here

3. Helper Functions

Here's some helpers that represent something similar to how we would fetch city data in the real-world application for the location selector:

// cities is an array of strings such as ["New York", "Alabama", ...]
import { cities } from "./dummy-data/cities"

// This is mocking a call to API that would return location search results
// whenever user types into the location input field.
const searchPlace = async (query) =>
  new Promise((resolve, _reject) => {
    setTimeout(() => resolve(filterAndMapCiies(query)), 600)
  })

// This is what might happen on the backend in real-life application: it would search for the city and return the results in correct format `{value: string, label: string}`.
const filterAndMapCiies = (query) =>
  cities
    .filter((city) => city.toLowerCase().includes(query.toLowerCase()))
    .map((city) => ({ value: city.toLowerCase(), label: city }))

// This is intended to be loaded into the location input field by default
const defaultLocationOptions = [
  { value: "new-york", label: "New York" },
  { value: "barcelona", label: "Barcelona" },
  { value: "los-angeles", label: "Los Angeles" },
]

4. Define Form Schema

This package uses a flexible form schema that allows you to build as many fields as you want. There's 3 types of fields it allows (you can create your own completely separate fields in the middle as well, don't be afraid πŸ€“): location, date (it allows datetime too) and peopleCount selector.

Here goes:

const formSchema: FormSchema = {
  location: {
    type: "location",
    focusOnNext: "checkIn",
    options: { defaultLocationOptions, searchPlace },
  },
  checkIn: {
    type: "date",
    focusOnNext: "checkOut",
    options: {
    // These are entirely flatpickr options
      altInput: true,
      altFormat: "M j, Y",
      dateFormat: "Y-m-d",
      minDate: "today",
      wrap: true,
    },
  },
  checkOut: {
    type: "date",
    focusOnNext: "guests",
    options: {
      minDateFrom: "checkIn",
      // These are entirely flatpickr options
      altInput: true,
      altFormat: "M j, Y",
      dateFormat: "Y-m-d",
      wrap: true,
    },
  },
  guests: {
    type: "peopleCount",
    defaultValue: [
      {
        name: "adults",
        label: "Adults",
        description: "Ages 13+",
        value: 1,
        min: 0,
        max: 10,
      },
      {
        name: "children",
        label: "Children",
        description: "Ages 4-12",
        value: 0,
        min: 0,
        max: 10,
      },
      {
        name: "infants",
        label: "Infants",
        description: "Under 4 years old",
        value: 0,
        min: 0,
        max: 10,
      },
    ],
  },
}

The format is self-descriptive. The key names can be whatever we want, but each value of the object has to follow a specific type. Refer to the documentation in the repo to read more on that.

Here we just say we want 4 fields:

  • First being location search field that accepts searchPlace (from helpers above) that would get executed in a "debounced" manner every time user types something in the field. After selection it would focus on the checkIn field which is the...
  • Date field for check-in date. It uses a lightweight and powerful library called flatpickr. You can look up it's options to understand more on the configuration chosen here for this field in the options key. And it would focus on the...
  • Date field for the checkout. This one has an extra-option called "minDateFrom" set to checkIn that would restrict users from selecting a date here that's prior to the checkIn value. And when it changes it would focus on...
  • The guest / passenger selector. It's a smart selector that allows to indicate how many people of what class are booking the service / place (again: completely customizable in terms of styling).

5. Booking Form JSX

We're almost at the end. Here's the JSX pattern for BookingForm component:

export const BookingForm = () => {
  const form = useReactBookingForm({ formSchema })

  return (
    <Container>
      <InputContainer>
        <Label>{"Location"}</Label>
        <LocationSelect
          form={form}
          menuContainer={MenuContainer}
          optionContainer={OptionContainer}
          inputComponent={InputComponent}
          name="location"
          inputProps={{ placeholder: "Where are you going?" }}
        />
      </InputContainer>
      <InputContainer>
        <Label>{"Check in"}</Label>
        <DatePicker placeholder="Add date" form={form} name={"checkIn"} />
      </InputContainer>
      <InputContainer>
        <Label>{"Check out"}</Label>
        <DatePicker placeholder="Add date" form={form} name={"checkOut"} />
      </InputContainer>
      <InputContainer>
        <Label>{"Guests"}</Label>
        <GuestsSelect
          form={form}
          menuContainer={MenuContainer}
          optionComponent={OptionComponent}
          controlComponent={ControlComponent}
          controlProps={{ placeholder: "Add guests" }}
          name={"guests"}
        />
      </InputContainer>
      <InputContainer>
        <MainButton>
          <FaSearch/>
          <ButtonText>{"Search"}</ButtonText>
        </MainButton>
      </InputContainer>
    </Container>
  )
}

Simple, right? Now we want to make it work with TailwindCSS and for the sake of speed (and to save some lines of code for readability) we will transform it a little:

export const BookingForm = () => {
  const form = useReactBookingForm({ formSchema })

  return (
    <div
      className="w-full mx-auto rounded-full bg-black bg-opacity-30 backdrop-filter backdrop-blur p-6 flex justify-between flex-col md:flex-row md:space-x-2 md:space-y-0 space-y-2 border border-purple-500"
      style={{ boxShadow: "0px 0px 50px #a025da44 inset" }}
    >
      <div className="relative w-full md:w-1/3 border-l-0 flex flex-col justify-center items-center pl-2">
        <Label>{"Location"}</Label>
        <LocationSelect
          form={form}
          menuContainer={MenuContainer}
          optionContainer={OptionContainer}
          inputComponent={InputComponent}
          name="location"
          inputProps={{ placeholder: "Where are you going?" }}
        />
      </div>
      <div className="relative w-full md:w-1/3 border-l-0 flex flex-col justify-center items-center pl-2">
        <Label>{"Check in"}</Label>
        <DatePicker placeholder="Add date" form={form} name={"checkIn"} />
      </div>
      <div className="relative w-full md:w-1/3 border-l-0 flex flex-col justify-center items-center pl-2">
        <Label>{"Guests"}</Label>
        <GuestsSelect
          form={form}
          menuContainer={MenuContainer}
          optionComponent={OptionComponent}
          controlComponent={ControlComponent}
          controlProps={{ placeholder: "Add guests" }}
          name={"guests"}
        />
      </div>
      <div className="relative w-full md:w-1/3 border-l-0 flex flex-col justify-center items-center pl-2">
        <button className="appearance-none mt-5 border w-full h-10 bg-purple-900 hover:bg-purple-500 transition border-purple-500 rounded-full flex justify-center items-center bg-transparent text-white font-bold px-3 font-title-2 uppercase">
          {"Book"}
        </button>
      </div>
    </div>
  )
}

6. Style it! 🎩

And now we just add / style our complementary components in any manner we want.
This example uses TailwindCSS, but you can go with Styled-Components, twin.macro, modular SCSS or any other method if you understand the pattern:

const DatePickerInput = ({ placeholder, inputRef }) => (
  <div className="relative flex group h-10 w-full" ref={inputRef}>
    <InputCore type="input" data-input placeholder={placeholder} />
  </div>
)

const DatePicker = (props) => (
  <DateInput className="w-full" inputComponent={DatePickerInput} {...props} />
)

const MenuContainer = React.forwardRef(
  ({ isOpen, children, style, ...props }: any, ref) => (
    <div
      className={`w-full w-64 border border-purple-500 z-10 mt-12 transform transition ease-in-out bg-black bg-opacity-60 backdrop-filter backdrop-blur rounded-3xl overflow-y-auto overflow-x-hidden
        ${
          isOpen
            ? "opacity-100"
            : "opacity-0 -translate-y-4 pointer-events-none"
        }
      `}
      style={{ ...style, maxWidth: "240px" }}
      ref={ref}
      {...props}
    >
      {children}
    </div>
  ),
)

const inputClassName =
  "appearance-none border rounded-full w-full outline-none transition pl-4 pr-6 bg-transparent border-purple-500 cursor-pointer flex items-center text-white"

const InputCore = React.forwardRef((props, ref) => (
  <input className={inputClassName} ref={ref} {...props} />
))

const RoundButton = ({ children, ...props }) => (
  <button
    {...props}
    className="appearance-none rounded-full p-2 flex items-center justify-center h-full overflow-hidden border border-gray-500 text-gray-500 hover:text-white hover:bg-purple-500 hover:border-transparent transition ease-in-out disabled:opacity-50"
  >
    {children}
  </button>
)

const OptionComponent = ({
  form,
  name,
  option,
}: {
  form: BookingFormType
  name: string
  option: any
}) => {
  const onPlusClick = () => {
    form.setGuestOptionValue(name, option, option.value + 1)
  }

  const onMinusClick = () => {
    form.setGuestOptionValue(name, option, option.value - 1)
  }

  return (
    <div className="transition ease-in-out relative py-2 px-4 flex justify-between items-center">
      <div>
        <p className="font-title font-bold text-sm text-white">
          {option.label}
        </p>
        <p className="text-white text-sm">{option.description}</p>
      </div>
      <div className="flex justify-center items-center gap-x-2">
        <RoundButton
          onClick={onPlusClick}
          disabled={option.value >= (option.max || 100)}
        >
          <FaPlus />
        </RoundButton>
        <p className="font-title font-bold text-sm text-white">
          {option.value}
        </p>
        <RoundButton onClick={onMinusClick} disabled={option.value === 0}>
          <FaMinus />
        </RoundButton>
      </div>
    </div>
  )
}

const InputComponent = ({ form, name, isLoading, ...props }) => (
  <div className="relative flex group h-10 w-full">
    <InputCore ref={form.refs[name]} {...props} />
  </div>
)

const OptionContainer = ({ children, ...props }) => (
  <div
    className="transition ease-in-out relative py-2 px-4 hover:bg-gray-800 cursor-pointer text-white"
    {...props}
  >
    {children}
  </div>
)

const ControlComponent = ({
  form,
  name,
  placeholder,
  ...props
}: {
  form: BookingFormType
  name: string
  placeholder?: string
}) => {
  const count = form.state[name].totalCount

  return (
    <div className="relative flex group h-10 w-full">
      <div
        className={inputClassName}
        ref={form.refs[name]}
        tabIndex={-1}
        {...props}
      >
        <p>{count ? `${count} guest${count > 1 ? "s" : ""}` : ""} </p>
        <div>{count ? "" : placeholder}</div>
      </div>
    </div>
  )
}

const Label = ({ children }) => (
  <div className="text-sm w-full font-bold mb-1 text-white">{children}</div>
)

Results

Now just import the BookingForm in the ./pages/index.tsx and render it:

import { BookingForm } from "./BookingForm.tsx"

...
const Home = () => (
  ...
    <BookingForm />
  ...
)

And now you should be able to see something in your browser 🎩

If you play around you can create something like this:

Here's a link to the repository on GitHub you can play around with. Piece πŸš€

18