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?
react-booking-form package on GitHub allows you to do just that!
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 thecheckIn
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 thecheckIn
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 π
Top comments (0)