Build a Booking Form With React and GraphQL
In this lesson, we'll finish up our client update for the listing page by creating the component where a user will eventually use to book for a listing.
Get the project source code below, and follow along with the lesson material.
Download Project Source CodeTo set up the project on your local machine, please follow the directions provided in the README.md
file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.
This lesson preview is part of the TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two course and can be unlocked immediately with a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two, plus 70+ \newline books, guides and courses with the \newline Pro subscription.
[00:00 - 00:18] In the last few lessons, we managed to create our GraphQL query to retrieve information for a single listing and we built the UI to display that listing information within a component that we called the listing details component. And we basically displayed the listing image, the title, the description, etc.
[00:19 - 00:37] In this lesson, we're going to build the initial UI of the calendar portion or in other words, the date pickers in the listing page. And we'll build this within a component that we'll call called listing create booking. This component would eventually allow the user to begin the booking creation process.
[00:38 - 00:55] And we'll complete that portion later on, but in this lesson, we'll begin building the UI for our date pickers. The listing create booking component will be a fairly straightforward component , where the more complicated aspect of it will be how we build and set up the date pick ers in this component.
[00:56 - 01:11] By the end, we would want to have two date pickers. In the first date picker, the user will be able to pick the date of check in for their booking. And in the second date picker, the user will be able to pick the date of checking out from their booking.
[01:12 - 01:29] Building the date pickers from scratch will be a little bit of a difficult task from a UI perspective, since there will be a lot of things we'll have to keep in mind and do. The good news is the UI framework we're using and design gives us a pretty robust date picker component that would help us quite a bit here.
[01:30 - 01:44] And design's date picker component gives us many different options with how we 'd like to set up a date picker. There's the basic use case. We can set a certain date format, different sizes, specify certain time formats, etc.
[01:45 - 02:09] It even gives us the option to set up a date picker to capture one value or a date picker to capture two values together. We'll look to build two separate date pickers without trying to do anything fancy and in the most basic way that we can. The one thing we'll have to do to comply with ant design's date picker components is to provide date values from the Moment.js library.
[02:10 - 02:38] If we recall, we've mentioned that in the server, we for the most part handle dates simply as strings. We've said we're doing this because handling dates is a complicated topic and we don't want to spend too much time diving into the best date handling solution. With that being said, however, the inputs from these date pickers in this framework expected the values to be date objects derived from the Moment.js library.
[02:39 - 04:00] For those who don't know, Moment.js is a fantastic JavaScript library that provides an incredible amount of utility functions that allow us to parse, validate, manipulate, and display dates and times. For the most part, the way this library works is it provides us with a moment's constructor that has a format function with which we can format and manipulate the dates and times we want. And it also has a few other utility functions. We're not going to spend a lot of time highlighting how this library works, but instead we're simply going to use it to comply with the date picker components and capture the dates the user might select. When we eventually run the mutation to book a listing, we'll convert the date objects to string values before we send it to the server. And we'll get to that point later in the course. Now, when we use the date picker components, we're going to use the value prop available to us to determine what the user has selected, the format prop to set the date format being captured, the on change function prop that runs every time the user selects a different date, and a few other props as well. And we'll spend more time on each of these props that we begin to use when we construct and build the date pickers. Let's now go and build these date pickers.
[04:01 - 04:52] The very first thing we'll do is we'll install the moment JS library and PM install moment. The moment library already has static typing associated with it, so we won't have to install an additional type declaration file. The next thing we'll do is create a file for the new component that will set up. We'll create a folder called listing create booking within the components folder in the listing module. And the listing create booking folder will have an index dot TSX file. And in the index file of the components folder of the listing module, we'll re export the listing create booking components will soon create.
[04:53 - 06:27] Before we start diving in and discussing how the date pickers are going to be set up, we'll first move past the simpler portions and look to build the basic UI of the component which consists of the card, the card titles, etc. This is purely UI work where we take advantage of the components and design gives us like we've seen before, so we won't spend too much time doing this. First thing we'll do is we'll import the react library and we 'll import some of the components we'll need from ant design, the button component, the card component, the divider and the typography components. We'll destruct the paragraph text and title components from typography and we'll then create and export the component function. And the template of the component will render the ant design card component which will have a paragraph at the top responsible in showing the price of the listing.
[06:28 - 06:38] Before we show the actual number value, we'll simply say here will be the price for now. We'll place a divider between the first paragraph and the upcoming paragraph.
[06:39 - 07:54] We'll have two other paragraph elements be shown responsible in displaying the check in and check out date pickers. This will come in a little bit but for now we'll simply show the titles, check in and check out. We'll place another divider and finally show the button that the user would eventually use to confirm the booking. The button title will be request to book. We'll now want to render this component to see if it visually appears the way we expected to. So in the parent listing component file, we'll first import the listing create booking component from the components folder. We'll create a constant named listing create booking element that will for now simply directly be the listing create booking component.
[07:55 - 08:55] And in the return statement for the listing component, we'll create a new column in the row that will take the entire width in extra small viewports and it will take slightly less width in large viewports than the details in the bookings. And this is where we'll place the listing create booking element. Now when we take a look at our listing page and recall that we're seeing this page by appending a valid listing ID in the route of slash listing. And we can see the listing create booking component on the right corner. Great. Now what we're going to try and do is get the price of the listing shown within this component in the title. In the parent listing component, we have the listing data object available to us, which contains a price field that determines the price of a listing for a certain day. So what we can do is check to see if this listing object is present.
[08:56 - 17:24] In other words, the query has resolved. And if so, we'll have the price value of this listing object passed down as a price prop to the listing create booking component. If the listing object doesn't exist, we will still be showing the skeleton UI. So our listing create booking element would just be null. And in the listing create booking component, we'll say it expects a price prop of type number. And we'll simply display the price in the title of our component. When we take a look at our page, we can see that our price is being shown, but it's shown in sense without any formatting. We've already created a utility function before that's responsible in formatting the price values we get from our server. So with that said, let's import this format listing price function from the libutils file. And in the card title, we'll use the format listing price function to format the price. And since this is price per day, we'll add some text to reference this. And if we take a look at the card in our page again, we'll see that the price is now being formatted to show the dollar sign, and not to show any additional decimal places. Great. We'll now look to use the date picker components from end design , and we'll create the check in and check out date picker options. And we'll attempt to build this step by step. So the first thing we'll do is we'll import the date picker component from the end design library. And we'll then place it below the check in and check out paragraph elements. Just by doing that alone, let me take a look at our page, we'll see two pretty good looking date picker components. Amazing. However, though the UI work is available, it doesn't behave exactly the way we want it to right now. There's certain things we're going to have to handle with our date pickers. Some will handle now, and some will handle later when we build a functionality to allow users to book listings. The first thing we'll do, which might be pretty apparent is track the values of the check in and check out dates that the user is to select. When it comes to tracking data within components, this is where react component state comes in. We could track the states for the check in and check out dates within the listing create booking component. However, we'll create these state values in the parent listing component instead. The reason being is that once the user is to click the request to book button in the card, we're going to surface a modal that we'll build later on that tells the user the dates they've picked, as well as the price of the booking, as well as give the option to the user to enter their payment details and actually book the listing. When we build this modal, we're going to have to create it as a child of the listing component. And as a result, we'll have the state values be created in the listing component and the functions responsible in updating the state values in the listing component. And we'll pass the state values down to both the listing create booking component and the modal component will create later. With that said, in the listing component, we'll use the use state hook to create two new state properties, check in date and check out dates. And the setter functions will be named set check in date and set check out dates respectfully. We'll define the initial values of the check in date and check out date state properties as null. Since these state values will eventually be the date values to be used in the date pickers, we'll need to say that the types of these state values will either be no or a moment date object. Now conveniently, the moment library allows us to export the moment date object interface that defines the shape of a moment date object. So we'll import that interface. And in the use state hook definitions, we'll declare the types of the date state properties as either a moment date object or null. We 'll pass the newly created state properties and the functions responsible in updating these state properties into the listing create booking component. So we'll pass check in date, check out date, the set check in date function and the set check out date function. In the listing create booking component will specify that the component expects the new props will first import the moment interface from the moment library that will use to define the shape of the props that might be passed in. We'll state that the check in and check out props can either be of type moment or null. And the way we'll use the set check in and set check out functions, there'll be functions that accept the check in and check out date values respectfully of type moment or no. And these functions won't return anything. So we'll say they 'll return avoid. We'll also remove the imports of the text component from typography, since I don't believe we'll need it right now. And we'll destruct the new props in our listing create booking component function. In our date picker components, we can now use these props to help capture the values the user may select. The date picker components have a value prop that conveys the date value being captured. So with that said, we can say the value of the first date picker is the check in date state prop. And the value of the second date picker is the check out date state prop. Now, and design or these date pickers in and design seems to only accept a moment date object or an undefined value for the value prop. Something tells me this perhaps should accept a null instead of undefined. But regardless , what we'll do here is we'll use a ternary statement to convey that if the state date properties don't exist, we'll just pass in undefined. The date picker components also have an on change function prop that gets triggered whenever a change is made in the date picker. This on change function prop receives the value of the date the user selected with which we can receive and pass along to the appropriate setter functions to update the date state values. So for the first date picker, we'll say when a change is made, trigger the set check in date function to update the check in date state property in the parent . And for the second date picker, this will be similar. And we'll say when a change is made, it will update the check out date state property. Great. At this moment, the values conveyed in our date pickers are the state values we've created in the parent listing component and when a change is made, it will update those state values to avoid any confusion that these check in date parameter values from the on change function is different than the prop being passed in, we'll simply call the on change date values being passed in as value or maybe just date value to be a little bit more specific. So in either case, whenever a date is being selected in one of these pickers, the on change function would run the date value parameter will be passed into the functions we specify here.
[17:25 - 19:34] And it would update the setters to update the appropriate state values. If we take a look at our page, everything should work as expected. However, as a preference thing, and just like how we want our dates to be formatted and sent to the server, it'll be similar to what we have here. But the year and month and day portions will be separated with sl ashes instead of dashes. This is more of a preference thing. However, the date picker components also accept a format prop that allows us to format our date values or date strings here. So we can just specify what we want as a preference will say, year, year, year, for digits of the year, divided with a slash two digits of the month slash and two digits of the day. If we take a look at the date pickers now and select the date, we'll see the format as we 've specified with the slashes. Great. Though we're now capturing the date as state values, there's still a few other things we'll need to handle here from a client perspective that'll make the UI more intuitive to the user. The first thing we need to think about is the fact that a user should not be able to select a date before today for either checking in or checking out because no one can travel back in time. So the first thing we'll need to do is we'll need to look into disabling dates before today. Now, another huge benefit of and designs date picker components is the disabled date prop. The disabled date prop is a function prop that receives the current date and allows us to return a Boolean. The disabled date prop function essentially runs for every date element within the grid. And when we specify that we wanted to return true for a certain case, it would disable that particular date value. Now, this might not make much sense. So let's give this a quick try and see how this works. We'll add the disabled date prop to the first date picker. And in the function we'll place a console log to verify when this function runs and will always return true.
[19:35 - 24:24] If we take a look at the page and we try to open the first date picker, we'll see a console message for every date element being shown in a certain grid. And when we navigate to different months, we'll see the console messages get shown again. And all the date elements we see here are disabled, because we've returned true in our disabled date function. Okay. So with this function now available to us, let's prepare the case only where the dates before today are going to be disabled. We'll have the disabled date function prop call a function in the component of the same name. And we'll specify the current date as the parameter that's going to be passed along. Now remember, current date here is a reference to the current or the accessed date object within the grid, since it iterates through every single date in the visible grid. It isn't a reference to the date of today. So it appears that the disabled date function might pass this current date value as either a moment date object or undefined. So we'll comply and we'll specify that. At any case, if the current date value here for some reason or another doesn't exist, we'll just have our disabled date function just return false. If the current date value exists, we'll now look to determine at what conditions will this function return true. If the current date values ever a day before today, we'll want this function to return true. Or in other words, we want this current date to be disabled. Now this is where the moment JS library really helps us, because it really makes date comparisons very easy. So the first thing we'll do is we 'll import the actual moment function constructor from the moment JS library. We'll create a constant value called date is before end of day. That'll be a Boolean to dictate if the current date being assessed is before the end of today. And we can do this by simply checking if the current date value is before today's date, with which we can get by simply running the moment function. And in the end of the if block will then just return the date is before end of day constant. If you've never used the moment library before, this statement might seem a little strange, but the key takeaway here is we're comparing the date property passed in from the function, which is the iterated date values in the grid. And we're saying if it's before today, the constant will be true. If it's not before today, that is to say if it's after, it'll be false. And we're getting today's date by simply running the moment function without passing any parameters with which moment allows. If we take a look at the UI right now, we'll see every date before today. And today included is disabled. Great. Now today might be disabled because this is where it starts to compare the time values between the current date object and the today object. We'll want to confirm that today can never be booked. And only the day starting tomorrow will be the first day able to be booked. So to make this confirmation, what we'll do is we'll compare the current date with today's date and we'll append end of day, which tells moments we want this date object to represent the end of today. Now, regardless what time the current date object is, if it falls on today, it will probably be before the end of day, where this will then be true and will prevent the user from booking today's dates. Another limitation we could add is how far the user can book into the future. And this could be completely up to us. We could say users can only book within a certain week, within a certain month, within a certain year, etc. We won't handle this use case and we would just allow users to basically book at any time into the future. However, if you wanted to add the client side guard, you would probably add it right here and you would check to see if the iterated date object is after today's date by a certain margin. Now, one thing we do want to stress is what we've done so far is simply a client side guard to prevent users from booking days before today. Ideally, on the server, we should also be preventing any cases in which the user somehow books something in the past. We 'll talk more about this and handle this when we build out the booking mutation feature later in the course.
[24:25 - 24:48] At this moment, now let's add the disabled date function prop to the checkout date picker as well. And if we look at our page, we'll see that for both date pickers, we'll be unable to select a date before today or today as well. We'll add a few more UI tweaks before we close.
[24:49 - 25:30] One important thing we'll need to do is we'll need to prevent the user from picking a checkout date that is before the check-in date, because that wouldn't make any sense at all. There's a few ways we can handle this. One way we can handle it is by giving the user the capability to select the dates in the UI, but we'll show an error message if they ever selected a checkout date before the check-in date and will prevent them from setting the state of the checkout value. Let's see how we can do this. For the on-change handler in the checkout date picker, we'll call another function in the component that will label as "Verify and Set Checkout Date".
[25:31 - 26:26] This function will receive the checkout dates or no, since that's what the on- change handler is typed to do. We have a display error message utility function we've created before that helps display an error message in our UI. So with that said, let's import this display error message function from the libutils file. In the verify and set checkout date function, we'll check if the checkout date selected is before the check-in date state property, and if so, we'll return early and prevent the update of the checkout date state property. If the checkout date is after the check-in date, we'll simply call the set checkout date function and we'll pass the selected date. With all that said, this will look something like the following.
[26:27 - 27:46] [silence] To make things a little bit more apparent, let's change the parameter name here to instead be selected, checkout date, and we'll reference that appropriately here, here, and here as well. So now we'll fully understand that checkout date is essentially the prop value and selected checkout date is the value being passed into this function that it might be used to update the state property. Let's walk through what we've done here. Since these check-in and selected checkout dates could be null according to their type definitions, we first check if they're actually defined. If either of these are not defined for some reason, we simply call the parent set checkout date function and pass along the null selected checkout date value if ever to happen, which is unlikely.
[27:47 - 29:39] Additionally, if the dates are defined and selected, we check to see if the selected date is before the check-in date by a period of days. This essentially means is the checkout date a day or some days before the check-in date? If yes, we run the display error message function, which displays the error message in the UI and we return early. If not, this means the checkout date is after the check-in date and we simply set the state accordingly. Now, if we try to book a checkout date that is before the check-in date, we'll get the error message shown to us and the state for checkout will not be updated. Note that we can still pick a checkout date that is the same day of the check-in date, which in our case is a valid use case. Maybe somebody does want to check in and check out within the same day. This is mostly where we'll end the lesson. There's a few more UI tweaks we'll make that isn't very important, but we'll do regardless. The first thing we'll do is we don't want the "today" tag be shown at the bottom of the date picker, which is being shown by default, and it's disabled right now because the existing day or today's day is disabled . So we can prevent showing this section by simply using the "show today" prop in our date picker and providing a value of false. If we now take a look at the UI, we'll see that "today" footer call to action is now not shown. Another thing that we'll want is the checkout date picker should be disabled only until a check-in date is selected and we'll want the button action to be disabled only until the check-in and checkout dates have both been selected.
[29:40 - 31:05] The date pickers have a disabled prop with which we can use for the checkout date picker, and we can say it's disabled if the check-in state property is undefined or null. We're going to introduce more conditionals at a later point in this course to dictate the disabled state of these date pickers, so we'll do this above our return statement for now, and we'll create a checkout input disabled constant that will dictate when the checkout input should be disabled, and as of now, it will only be disabled when check-in or the check-in date value doesn't exist. Similarly, we'll have a button disabled constant that will be true when either the check-in or checkout dates are defined, and we'll place this constant value as the value of the button's disabled status. And if we now check our UI, we'll notice the checkout date picker is disabled until a check-in date is provided, and the button is disabled until both the check-in and checkout dates are both provided. Great. Now, one peculiar UI bug exists at this moment, and this can be seen if I was to move the date of check-in to be after what I selected for checkout.
[31:06 - 32:29] So at this moment now, we can see that checkout is before check-in, and this was the bug we tried to prevent before. However, we were only able to prevent that bug based on what the user selects for checkout. So to simply avoid this weird situation, is one thing we can do is the minute the user ever opens the check-in date picker, we automatically reset the checkout date picker back to no, or its original undefined value. And we can achieve this by using the on open change callback function prop available to us. This particular callback function gets called the moment the date picker gets open, which is exactly what we want in this case. So what we can say is the moment the check-in date picker ever gets open, we'll simply call the set check-out date setter function and set the checkout date value to no automatically, even if a date was selected before. And now if we head to the UI, and if I was to pick a check-in date, if I picked a checkout date that was after my check-in, and if I try to change my check-in date to something else, my checkout date will be resorted back to no, which helps avoid that weird UI situation.
[32:30 - 33:08] And we're going to stop here for now. What we've done could have appeared complicated, but it's actually pretty straightforward. We declare the check-in and checkout state properties in the parent listing component, and we pass it down to the listing create booking component. We render two date pickers for where the user can pick a check-in date and a checkout date for their booking. We prevent users from picking dates before today, and we've added an extra guard to prevent folks from picking a checkout date before checking. We enable the checkout date picker and the button only after the previous information has been provided.
[33:09 - 33:37] There is more UI improvements we can make. For example, we can perhaps display some footer information in each of the date pickers to tell the user something. We could visually attempt to show what the check-in date value is in the checkout date picker. We could add a restriction, like we mentioned before, where users are unable to book at a certain time in the future. Maybe we can say "Six months is the furthest a user can only book."
[33:38 - 34:22] However, we won't be doing these minor UI tweaks, but we do think those are great suggestions for you to try out if time permits and you want to actually attempt these. Now, there are other things we'll need to take care of when we start to actually work on the booking functionality. We'll need to consider things like "We shouldn't let a user book a listing if they're not logged into our app." When bookings are made to a listing, we should show them as disabled dates in our date pickers so someone else can't override those dates. When a request to book has been made, we want to show a modal to help confirm the booking and where the user can then provide their credit card information. And there's other things we'll also need to keep in mind as well.
[34:23 - 34:29] However, we'll be talking about all these other pieces when we reach that part of the course. (chuckles)