How to Build a Listing Form Interface in React and Stripe
We'll now begin to work on the form on the client application where a user can create (i.e. host) a new listing. The form we'll build will essentially be the UI of the `/host` route of our application.
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 with a single-time purchase.
[00:00 - 00:10] The form we'll build that would allow the user to create a new listing would essentially be the UI of the host route. The form isn't going to be very difficult to establish.
[00:11 - 00:23] It will essentially provide different form inputs that represent the information we'll want the user to provide for their new listing. And the entire form will be part of a single component called the host component.
[00:24 - 00:47] If a user is either not signed into our application or hasn't connected with Stripe, they'll be unable to see the form and instead will be told that they'll need to sign in and connect with Stripe to create a listing. In this lesson we'll begin by simply getting the UI of the entire page before we plan to introduce the host listing mutation and the capability to run the mutation.
[00:48 - 01:02] And before we begin building the form, let's get the other parts of the page set up. In the host component file we have in the sections folder, let's import the first few and design components we'll need.
[01:03 - 01:21] We'll import the layout and typography components. We'll destruct the content sub component from layout and we'll destruct the text and title sub components from typography.
[01:22 - 02:07] In the components return statement we'll return the content component that is to contain a div element that further contains a title that says hi, let's get started listing your place. We'll add some secondary text right below it that says in this form we'll collect some basic and additional information about your listing.
[02:08 - 02:24] If we save our file and if we take a look at our app at this moment, we'll see the title section we'll want to show for this page. As we mentioned we only want the user to create a listing in this host page when they've logged into our application and have connected with Stripe.
[02:25 - 02:44] The viewer state object we create in the parent app instance has information for both of these cases. If the viewer is not logged in, there won't be a viewer ID or any other field in this object. And if the viewer is not connected with Stripe, the has wallet field in this object will return false.
[02:45 - 03:11] So with that said, let's pass this viewer object down as props to the host component rendered in the host route. We'll employ the render props pattern to render the host component and we'll pass the viewer state object as props down.
[03:12 - 03:51] And in the host component file we'll declare that the viewer prop is expected to be passed in. We'll import the viewer interface from the lib types file to describe the shape of the viewer object prop. In the host components we'll check if the viewer ID doesn't exist or if the viewer has wallet field is false.
[03:52 - 04:20] If either of these statements are true, we'll have our components return a title that says you'll have to be signed in and connected with Stripe to host a listing. We'll also provide some secondary text that says we only allow users to sign into our application and have connected with Stripe to host new listings.
[04:21 - 04:55] We'll also provide a link to the login route of our app to essentially tell the user that you can login from here. And we'll be sure to import the link components from React Router down.
[04:56 - 05:13] Now if a user is either not logged in or isn't connected with Stripe, they'll see the title and text first telling them to do so. We definitely want the user to be logged in since every listing that's going to be created will be attached to a certain user in our application.
[05:14 - 05:47] Furthermore, we do want them to be connected with Stripe since this is the only way users will be able to receive payments for their listings. However, we can't prevent them from disconnecting from Stripe after they've already created their listing, which is why later on when we build out the booking capability, we'll look to prevent a user from booking a listing where the host of that listing has pulled away their Stripe information.
[05:48 - 06:04] Let's see how we can set up the UI for the host form. Forms and form validations in client-side applications are often a very interesting topic because oftentimes they begin through very simple means, but they can become very complicated very quickly.
[06:05 - 06:24] And oftentimes this is exaggerated when one has to think about how to deal with form and/or field-level validations within forms. We're going to utilize an important advantage thanks to basically being able to use a pretty powerful component that AddDesign gives us, called the Form Components.
[06:25 - 06:44] The Form Component from AddDesign doesn't necessarily give us the UI for certain form elements, like a radio input or a checkbox since other components exist for this. But it provides the capability to validate fields with certain rules and the ability to collect information.
[06:45 - 06:58] The Form Component from AddDesign provides two really useful features. It provides a sub-component called item that can be used to help display a form label, some help text, etc.
[06:59 - 07:32] And as of the current version of AddDesign version 3, when this was being screencast, the Form Component also contains a function that acts as a higher order component function that receives a component and provides another component where a Form Prop Object is provided. This Form Prop Object provides the capability to actually introduce validations on form fields with the Form Components as well as the capability to capture the values of the Form fields within the Form Components.
[07:33 - 07:50] Let's look to see how this Form Component can help us, we'll import it from Add Design and we'll wrap our existing div element with it. We'll specify in the Layout Prop provided that the Form is going to be in the Vertical Layout.
[07:51 - 08:07] We'll also destruct an item sub-component from the Form Components. As we've mentioned, the Form Component doesn't provide the actual UI elements we'll need to show.
[08:08 - 08:19] So with that said, let's import some things we'll need. We've used the Input Component before, so we'll import it from AddDesign and we 'll use it to capture the title of the listing.
[08:20 - 08:42] We'll place the Inputs and provide a placeholder of an example of a title, something like the iconic and luxurious Bel Air Mansion. Remember, on the server we added a Server side validation where the title can only have a maximum number of 100 characters.
[08:43 - 08:51] This was set up as some sort of safeguard. On the client will even be more restrictive and add a maximum length of 45.
[08:52 - 09:16] We'll then use the Item Sub-Component from the Form to provide a label to this Form element saying Title, and then we can apply some extra help text that says Max Character Count of 45. If we take a look at our page now, we'll see the Form Input and we'll see the Title and Extra Help text shown with it.
[09:17 - 09:32] Great. Similarly, we'll display another input, but this time we'll display the Text Area Sub-Component within Input to have a text area be shown to capture more text for the description of the listing.
[09:33 - 09:42] We can declare the number of rows in this text area to perhaps be 3. We'll provide a maximum length of 400 characters.
[09:43 - 09:55] Once again, less than the Server side extra check of 5,000 characters. If you want, you can make them the same, but in this case we'll make our client side validations a little bit more restrictive.
[09:56 - 10:25] And we'll provide a placeholder of an example description of a listing, something like "Modern Clean and Iconic Home of the Fresh Prince" situated in the heart of Bel Air Los Angeles. We'll wrap this Input Text Area with the Item Form component, which is to have a title that says "Description of Listing" and will provide extra help text of Max Character Count of 400.
[10:26 - 10:50] We'll look to display a series of other text inputs. One to capture the direct address of the listing. We'll provide a placeholder here that says "251 North Bristol Avenue" and in the Item Label we'll just say " Address".
[10:51 - 11:10] Another to capture the City or Town will provide a label of City Town and an Input placeholder of Los Angeles. We'll provide another input to capture the State or Province, so we'll provide an Item Label of State Province and an Input placeholder of California.
[11:11 - 11:37] We'll finally specify an input to capture the Zip Postal Code, so we'll provide an Item Label of Zip Postal Code and an Input placeholder of "Please enter a Zip Code for your listing". If we take a look at the Host page at this moment, we'll see the form inputs we 've created presented and available to us. Great!
[11:38 - 11:53] There's a few other inputs in our form that aren't going to be direct text inputs. We're going to need to capture the price value of the listing through an input as well, but in this case we'll want the user to only provide a value of a number.
[11:54 - 12:13] With AntDesign we can create a number input by using the Input Number component , which can be used to help capture numbers between certain ranges. So with that said, let's import the Input Number component. We'll use it and we 'll specify a minimum value of 0.
[12:14 - 12:41] Since we wouldn't want the user to type a negative number, and then we'll specify a placeholder of 120. And the Form Item Label will say "Price" and for the extra help text, we'll tell the user that this is supposed to be the price in $USD per day.
[12:42 - 13:08] If we took a look at our page now, we'll see a number input shown to us. If we try to type a negative number and click elsewhere, it'll be incremented automatically to the minimum value of 0. When it comes to having the user decide between a listing type of a house or an apartment, we can leverage AntDesign's radio component to provide the user with radio inputs to pick one or the other value.
[13:09 - 13:39] So with that said, we'll import the radio components from AntDesign and we'll look to place the radio group at the top of our Form inputs. We'll use the radio group component to group our radio inputs and within we'll use the radio dot button components to create the radio buttons.
[13:40 - 13:55] And finally, we'll have our radio group kept within a Form Item that has a Label of Home type. If we take a look at our app right now, we'll see two radio buttons where both of them seem to be selected.
[13:56 - 14:13] This is due to the fact that we haven't provided values for the different Ant Design radio buttons. Radio inputs don't have an inherited value, so we'll have to provide concrete values for what each of these radio inputs will refer to when the user selects one or the other.
[14:14 - 14:30] The two different listing types we have in our app are Apartment and House. Instead of specifying these values directly, we can perhaps use the Listings Type Enum available as a global type from our auto-generated TypeScript definitions.
[14:31 - 14:46] By using this enum, it will ensure that we're providing a value that matches one of these two. So with that said, we'll import the Listings Type Enum from the global types file kept in our lib GraphQL folder.
[14:47 - 15:06] And we'll use the enum to declare the values of each radio button, a listing type dot apartment and a listing type dot house. In addition, we can look to provide icons to each radio button for present ational purposes.
[15:07 - 15:33] So with that said, we'll import the icon component from AntDesign and we'll also import the icon color constants we have in our libutels file that we use for the color of icons within our app. Within each radio button, we'll place icons and use the Style tag to state the color of the icon should be dead of the icon color constant.
[15:34 - 15:52] Now, I wasn't able to find a suitable icon from AntDesign that resembled an apartment, so I went with using the Bank icon, which I hope can be sort of mis construed to be an apartment. And for the house, we'll use the Home icon.
[15:53 - 16:07] If we take a look at our app right now, we'll see the two radio buttons presented to us each having an icon. If we collect one, AntDesign will give us the selected styling around it that indicates we've selected which one.
[16:08 - 16:18] There's one other form item we're interested in capturing and that's the image of a listing. This is a little bit more complicated, so we'll spend a little bit more time here.
[16:19 - 16:37] AntDesign provides a fairly powerful upload component that allows us to upload files. There's a few different variations of this component, but we'll be interested in the example shown here that's labeled Avatar, where when an image is uploaded, a preview of the image is actually shown.
[16:38 - 16:56] So we can actually give this a try. If I selected my machine file navigator launches, I can select an image, we can see a brief loading indicator, and when it's finished uploading, I will see the image. Great. But what's really happening here, and how is this image actually being uploaded?
[16:57 - 17:16] In this example, the AntDesign upload component takes the image uploaded from the file navigator and actually prepares the image in base64 format. Base64 encoding, in simple terms, is essentially a way to convert data into simple printable characters.
[17:17 - 17:37] Base64 is often used when data needs to be stored and transferred over a medium that expects textual-based data. The image of a listing is a good example. We can't transfer this listing image from the client to the server as is through our GraphQL API.
[17:38 - 18:04] Instead, we can convert it to base64 format, which is a string representation of the data, where we're then able to send it to the server through the GraphQL API. So let's prepare this image upload component. We'll look to prepare the entire image uploader elements with all the additional functions, and we'll explain things as we go.
[18:05 - 18:19] So the first thing we'll do is we'll import the upload component from AntDesign . We'll prepare another form item just before the price form item with a label of image.
[18:20 - 18:47] We'll actually have a few restrictions here, so in the form item, extra help text will say images have to be under 1MB in size and of type jpeg or png. And we'll place the upload component within a div element in the form item.
[18:48 - 19:03] Though probably not very important, we'll provide a value for the name property called image. The upload component has a list type prop, which helps dictate the style of the upload element.
[19:04 - 19:23] We'll provide a value of picture-card, which is the style of the example we just saw in the documentation. The show upload list prop helps dictate whether we want to show some list- related UI where actions like remove, delete, or preview the image is shown.
[19:24 - 19:42] We're not interested in this, so we'll provide a value of false here. AntDesign's upload component contains an action prop where an Ajax upload request is supposed to be declared, which is intended to be fired the moment the actual upload is made.
[19:43 - 19:58] Now the thing is we don't want to fire an Ajax request the moment the image is uploaded. We just want the preview of the image to be shown, and we want to capture the base64 value, which will only send to the server after the entire form is sent to the server.
[19:59 - 20:23] The other thing is this action is required to preview the image, which is a little unfortunate. What we can do is something a little bit hacky, and we can bypass this action by placing a mock HTTP response that's also used in the AntDesign documentation to mock the fact that an image was uploaded as part of an actual request.
[20:24 - 20:50] There are two callback function props we'll use before upload and on change. Before upload is a function that will execute just before the upload has been made, and it's where we can actually check if the image is of a valid type, JPEG or PNG, and if it's less than 1MB in size.
[20:51 - 21:11] So we'll use the before upload prop, and we'll have it call a function called before image upload. We can have the before image upload function be created outside of our component function, since it will have no need to access or affect anything within the component.
[21:12 - 21:26] It will simply receive the image file from the callback function, and we'll have it return a boolean. The type of the file would actually be the file interface that's available within the scope of TypeScript in general.
[21:27 - 21:40] The file interface provides information about files and allows JavaScript in a webpage to access their content. We can first check if the file type is either JPEG or PNG.
[21:41 - 22:07] We'll create a constant to represent this called file is valid image, and we can check for the type of the file from the type field from the file object. We can say that the file is valid image constant will be true when the file type is either of image/jpeg or image/png.
[22:08 - 22:22] Next, we can check if the file size is going to be less than 1MB. We can do this with the file.size property, and we'll check this in a constant call file is valid size.
[22:23 - 22:34] The file.size property is in bytes. To have it in megabytes in binary form, we can divide the bytes by 1024 twice.
[22:35 - 22:48] Finally, we can check if the result of this is less than 1. Bites divided by 1024 twice converts it to megabytes in binary form, and we just check if this is less than the value of 1.
[22:49 - 23:18] If the file is not a valid image or is not a valid size, we can have our function return false, otherwise we'll basically return both properties which will be true. Instead of only returning false, if one of these aren't satisfied, we can look to display an error message to the user to notify them of the issue.
[23:19 - 23:44] To do this, we'll first import the display error message function from our lib utels file, and for each of the invalid file cases we'll display an error. So if the file is not a valid image, we'll fire an error message of you're only able to upload valid jpeg or png files.
[23:45 - 24:13] If the file is not a valid size, we'll fire an error message of you're only able to upload valid image files of under 1 megabyte in size. So with our validation check performed in the before upload callback function, let's prepare what would happen when the upload is made successfully with a callback function to be applied to the on change prop.
[24:14 - 24:37] Keep in mind that this on change function will trigger when a change is made, so it will trigger when the image is first uploaded and when the image upload is complete. We'll have it call another function we'll create called handle image upload, which will keep in the components function since we'll need to modify some component state.
[24:38 - 24:50] There's going to be two state values we'll want to track in the components, so let's first import the use state hook from react. We're going to want to track two different states.
[24:51 - 25:03] The first is when the image upload is loading. This will help us show a different UI in the upload element when the image is being uploaded as opposed to when the upload is actually complete.
[25:04 - 25:34] So let's call this state property image loading and the function responsible in updating it will be called set image loading and will initialize it with a value of false. We'll also want to be able to track the actual base 64 value of the image, so we'll create a state property called image base 64 value and the function responsible in updating it set image base 64 value.
[25:35 - 26:04] We'll initialize this state value as null, but we'll state the type to either be a string or null since we'll expect it to be a string when the value is finally available. When this handle image upload function gets called from the change of the upload component, the upload component will pass an info object that contains information about the upload as well as the information about the file that's been uploaded.
[26:05 - 26:29] We can declare the type of this info object by importing an interface called upload change per ram directly from the upload component in ant design. From this info argument passed in we can destruct and retrieve the file object.
[26:30 - 26:41] This file object doesn't just represent the file that's been uploaded. It's of a particular type from the upload component that gives us information about the status being uploaded.
[26:42 - 26:53] For example, we can check in the status property available if this file is uploading. If so, this would mean that our upload element should be in the loading state.
[26:54 - 27:07] To help facilitate this later, we'll call the set image loading function and pass a value of true to set the image loading state property to true. We'll also return early.
[27:08 - 27:28] If the file status is done, we can determine and see if this file object contains another property called origin file object. And this origin file object property is what the upload component gives us as the original file object.
[27:29 - 27:48] Here we can call another function that we'll create later on called getBase64 Value that will do the job to actually retrieve the base64 value of the image file. The first argument we can pass in would be the image file itself.
[27:49 - 28:08] So this will be the file.originFileObject property. And for the second argument, what we can do here is we can pass in a callback function that would receive a parameter and when available, we'll simply use the setImageBase64Value function and pass that parameter along.
[28:09 - 28:27] In addition, we'll also ensure we set the image loading property back to false. This might make more sense when we actually create this getBase64Value function , so let's go ahead and create it.
[28:28 - 28:43] We'll create it outside of our component function since it will simply use the callback to actually pass in the base64 value. It will have no need to do anything else with regards to properties in our component, so we can declare it outside of our component.
[28:44 - 28:56] The first argument would be the image file itself. We've mentioned before in the function above that this would be of the type file, which is a reference to the file interface available in TypeScript.
[28:57 - 29:13] However, the upload component tells us that this image item or file could either be a file or a blob as well. A blob is essentially a file-like object which has some minor differences to what a traditional file object is.
[29:14 - 29:36] This isn't really a big deal since whether it's a file or a blob will be able to get the base64 value with the same means. The second argument is the callback function where the actual base64 value of the image is expected to be passed in and void, or that is to say nothing, can be returned.
[29:37 - 29:54] Now let's see how we can try and get the base64 value of the image and then trigger the callback function to update the state property we have in our host component. To get the base64 value we can use the file reader constructor class in JavaScript.
[29:55 - 30:17] The file reader object allows us to read the contents of a file or a blob, so we'll run the file reader object function and pass the result to a constant we 'll call reader. We can then run the read as data URL function available from the file reader to read the contents of the image file.
[30:18 - 30:49] The file reader has an onload property that has an event handler that is executed when the load event is fired, and this happens when the file has been read, which in our case will be done from the function here, read as data URL. So when this onload is triggered, this basically means the file has been read and we can call our callback function and pass the result of the reader, which in this moment should be the base64 value.
[30:50 - 31:18] This is where we'll do a bit of a hack to get by. The file reader API tells us that the result of the reader can either be a string, which is what we want and expect, or an array buffer type or no. From what I've gathered, this result should most likely be a string and it's unlikely for it to be either null or of an array buffer type if the image of a valid type is uploaded to the upload component.
[31:19 - 31:43] So to bypass this, we can use type assertion and simply assert the type as a string. Now, keep in mind, this is a bit of a hack. If the result was ever null or an array buffer type, we bypass type scripts checking, and in this moment, we would try and have to pass an invalid type value to our state property we have in our component.
[31:44 - 32:00] However, I think this is unlikely to happen, so we'll just continue with this. Notice the use of our own callback function. Callback functions are just functions that are passed to other functions as arguments.
[32:01 - 32:28] We've passed a function that would call setImageBase64 value in the component and pass the base64 result from the file reader, and we also call setImage loading to reload or reset the loading status to false. And we have this callback function only executed when the base64 result is available from the file reader.
[32:29 - 32:55] With our functions prepared, we can now use the loading and base64 state values in the component. In the upload element, within we'll use a conditional statement and say, if the imageBase64 state value exists, we'll display an image element where the source is the imageBase64 value.
[32:56 - 33:11] If it doesn't exist, we'll look to display a div element that is to have an icon and some text that says upload. For the icon, we can control the type of the icon depending on whether our upload is in the loading state.
[33:12 - 33:23] If it's loading, we'll use the loading icon type. If it's not, we'll show the plus icon type, and we're using the image loading state property to determine this.
[33:24 - 33:36] Now, let's see how our page looks. We'll notice a new form item that has the upload element shown.
[33:37 - 33:48] When we click it, our file navigator opens and we're able to select an image. Let's try and select an image we have here that is 2MB in size, well over our image limits.
[33:49 - 34:02] We'll get the error message saying you're only able to upload valid image files of under 1MB in size and our image won't be uploaded. What if we try and upload a file of some other format, like a text file?
[34:03 - 34:17] We'll see the error message that says you're only able to upload valid JPEG or PNG files. Before we upload a valid image, let's place a console log in our components for the imageBase64 value state property.
[34:18 - 34:40] We'll go back and when we try to upload the valid image, we'll see the loading indicator. And when complete, we'll get the base64 value.
[34:41 - 34:57] Great, and it's with this base64 value we can send to this server to represent the data for this image. And the user or ourselves at this moment can see a preview of this image right within this upload element.
[34:58 - 35:10] Now, as a quick summary, let's go over how this upload capability works. We essentially used the upload components from ant design. We specify a list type to get the UI the way you want it to be shown.
[35:11 - 35:26] We said false for the upload show upload list prop, which helps avoid showing additional list elements such as preview or delete. We used a mock action to actually mock the response of a successful image upload through an actual request.
[35:27 - 35:38] We're doing this because we don't need to or we don't want to make a request when an image is uploaded. We want to capture its value and only send that value to the server after the form is submitted.
[35:39 - 35:54] Just before upload, we run a function to verify and validate that the image is of a valid type and is under 1MB in size. And finally, when an on change happens, in the very beginning, when it's loading, we set the image loading property to true.
[35:55 - 36:08] This helps us show the loading indicator in the element. And when it's done, we obtain the base64 value and then we pass it down to our function to update the image base64 value state property.
[36:09 - 36:21] Now, as an important takeaway, the way this upload component really works isn't the important takeaway here. We've just conformed and actually followed the documentation and example shown to us from ant design.
[36:22 - 36:42] The main takeaway is we want to show a preview of the image and we want to capture the base64 value with which we can send to the server later. Now, an important or very valid question at this moment might be, why don't we create or why are we not creating state values and the capability to update the state values for the other form elements.
[36:43 - 36:54] Technically, it might make very well-sense to basically capture the title, the description, the address, etc. This is where the form component itself is going to be very useful.
[36:55 - 37:17] In the next lesson, we're going to see how this form component allows us to collect information without the need for us to explicitly define the state for each form element. We've only specified state properties for the image because we've done a little bit of custom work to basically capture the base64 value and then show some loading information.
[37:18 - 37:28] That's the only reason. Now, there is probably other ways to upload images or files without the need of doing all this and probably we can conform to just using the form to help us here.
[37:29 - 37:35] But in this case, we're going through a slightly more custom approach just for the image of the listing. Fantastic.
[37:36 - 38:03] The last thing we'll do in this lesson is look to display a button that the user would use to submit the form. So, let's first import the button component from End Design and we'll place a form item at the end of the form that contains the button element of type primary and we'll have text that says submit.
[38:04 - 38:18] When we take a look at our page right now, at the very bottom of our form, we 'll see this submit button. And when we build out the functionality to actually submit the form, this button will be the action that the user would have to select.
[38:19 - 38:20] Great.