React package to create booking forms
Tags: react, javascript, webdev
Published: 2021-11-24 11:57:49

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

[**react-booking-form**](https://github.com/max-sym/react-booking-form) package on GitHub allows you to do just that!


## Previews

![Preview 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/20sz71atjshhjjghbpl0.gif)
![Preview 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8w0h7xe13nxvcdrf5jr1.gif)

## Live Playground

{% codesandbox react-booking-form-example-9z489 %}

## 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](https://github.com/jpedroschmitz/gatsby-starter-ts):

```
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`

```jsx
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](https://flatpickr.js.org/themes/)

## 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:

```jsx
// 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:

```jsx
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](https://github.com/flatpickr/flatpickr). You can look up it's [options](https://flatpickr.js.org/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:

```jsx
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:

```jsx
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:

```jsx
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:

```jsx
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:

![Preview 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/20sz71atjshhjjghbpl0.gif)

Here's a [link to the repository](https://github.com/max-sym/react-booking-form-example) on GitHub you can play around with. Piece 🚀