Build a GraphQL Mutation to Store Listing Data and Uploads
The majority of the work we've needed to do for the form shown in the `/host` page was completed in the last lesson. In this lesson, we collect the values a user is to enter in the form and when the user decides to submit the form, we'll trigger the `hostListing` mutation we have in our API and pass the relevant input along.
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:17] the majority of the work we've needed to do for the host form, we completed in the last lesson. In this lesson, we'll collect the values the user enters, and when the user decides to submit the form, we'll trigger the host listing mutation we have in our API and pass the relevant input along.
[00:18 - 01:01] So with that said, the first thing we'll do is ensure we have the GraphQL mutation document for the host listing mutation. So in the libgraphql mutations folder for our client, we'll create another folder to contain an index file that would have the host listing mutation document. And in the index file within the mutations directory of our libgraph ql folder, we'll re-export the soon-to-be-created host listing mutation constant. In the host listing mutation index file, we'll import the GQL tag from a polar boost and we'll construct the mutation document host listing.
[01:02 - 01:58] We'll specify the mutation and the input that it is to accept of type host listing input, and we'll pass that input to the host listing mutation field that we'll call, if we recall, the host listing mutation will return the listing document itself that is created and added to the listings collection in our database. The only thing we'll need from the returned listing object is the listing ID, so we'll specify that. We'll talk about why we'll need the ID shortly. We'll head over to the terminal and in our client project, we'll run the code gen schema command to ensure the schema JSON file in our client application is up to date.
[01:59 - 02:40] When complete, we'll run the code gen generate command to generate the typings for our GraphQL documents including the host listing mutation we've just created. We'll now head over to the host component file. Before we import and use the host listing mutation, let's see how we can access the values the form component from and design is able to capture.
[02:41 - 03:44] We mentioned that the form component from and design contains a function that acts as a higher order function that allows us to produce another component where the form data object is going to be available. We'll want this higher order component prepared and exported. So what we'll do at the bottom of our file here, we'll create another component constant called wrapped host. And this is to represent the host component that is to be wrapped with form specific functionality. Or in other words, this will be the higher order host component. We'll use the create function available in the imported form component that takes an options object with which we'll only provide a value for the name with of host form. The form create function receives an options object and it returns a function that receives the component we want to wrap.
[03:45 - 04:25] So with that said, we can simply pass the component we want to wrap right beside the form create function. So this might look a little strange, but to reiterate the form create function returns another function. This other function is where we're passing in this particular argument, which is the component we want to wrap. Instead of assigning the result of this to a function and running that function again, we can do this by simply just concatenating the second function argument right beside the first one. So we create the form with form dot create this returns a function.
[04:26 - 07:26] And for that function, we pass in this host component. And with that, we create the wrapped host higher order component form dot create is a generic that is intended to receive the props of the form as well as that of the component being wrapped. We already have a props interface created to declare the type of the viewer prop. So we'll import a form component props interface from the form components file in ant design that describes the shape of the form object available as props within form create. And in the form create function, we'll pass in the generic of an intersection type of our component props and the form component props. And in our host component function, we can now access the form object that will be available from the higher order function. We'll specify that the type of arguments of the host component function is to be an intersection of the props and form component props interfaces. This form object contains a series of functions and properties where we can validate our form, collect information, etc. For example, we'll use the get field decorator function available in this form object to decorate our form items and help provide some field level validations. Let's see how this can work. We'll destruct the get field decor ator function from the form object. And within our first form item, for trying to capture the listing type, we'll call the get field decorator function within the form item. Get field decorator receives a few arguments and returns a function that accepts a react node with which it decorates. Now the syntax here might appear strange as well, but it's similar to how we've seen it in the form create function. In the first set of brackets, we're going to pass in the parameters and arguments, the get field decorator function accepts this function with the set of parameters will return another function that expects a react node. So we pass the value of the second function expected argument as the node we want to decorate, which is the entire radio group here. There's a few different things you can do in our decorator, but the only thing we'll do for all our form items is to simply validate that the actual form item input is required. It needs to be filled in. So in the first argument to the decorator is a unique ID will pass an ID of what this form item is. In this case, we'll just say type to constitute that it refers to the listing type. In the second argument, we can specify some options.
[07:27 - 08:16] Within these options will only declare a rules array to dictate that this field is required and a warning message will be shown if the user doesn't fill it in of please select a home type. We'll specify something similar to this for every single input in our form to basically say that they're all required. In the last lesson, I believe I might have forgotten to have an input to capture the maximum number of guests. So let's first create this input right after our radio group with an item label of a max number of guests and a minimum value for the input number of one and we'll specify a placeholder of four.
[08:17 - 09:07] We'll then declare a form decorator to dictate that this field is required with a warning message of please enter the max number of guests and for the idea of this decorator will just say it's number of guests or numb of guests. For the title input, we'll have a decorator to make sure it's required in a warning message of please enter a title for your listing and the idea of this decorator will just be title.
[09:08 - 09:55] For the description field, we'll add a decorator to say it's required as well and a message of please enter a description for your listing. The type of this particular decorator would be description. For the address, city, state, and zip inputs, they'll all have decorators that reference their fields that look very similar. They'll all be required and they'll all have warning messages saying this particular field needs to be filled in.
[09:56 - 10:36] Thank you. [ Silence ] Similarly, we'll add a decorator to the image upload input.
[10:37 - 10:51] That is to be required and we'll have a warning message of something along the lines of, please provide an image for your listing. And finally, we'll have a fuel decorator for the price input to say that a price is also required.
[10:52 - 11:27] [ Silence ] So let's see how this would currently appear. The one thing we'll need to do is ensure we're importing the higher order host component, so in the source index file, we'll import wrapped host instead of just host, and we'll name it host for our return statement.
[11:28 - 11:46] If we take a look at our page now, we'll see the form inputs as normal, however , we'll also see a small star symbol along each form item label, which is a tell that the inputs are to be required. At this moment, the submit button doesn't really do anything, since we haven't really set it up.
[11:47 - 11:59] However, if we attempted to type something in an input, erase it and click elsewhere, we'll see the warning message shown. Pretty cool, right? The form field decorator took care of this for us.
[12:00 - 12:12] Now, though this is pretty helpful, our intention is that when the form is submitted and an input is missing, we'll see the field level validation. So let's go back to the code and address this.
[12:13 - 12:31] We'll want to trigger a form submit event when the submit button at the bottom of our form is clicked. This can often be done in normal form elements in HTML by attaching a type attribute to the button of submit and attaching an on submit attribute to the form element.
[12:32 - 12:54] With the "ant design" button, we can do this with the HTML type prop attribute available, with which we'll provide a value of submit, and in the form element, we'll attach an on submit prop that will trigger a function we'll set up called handle host listing. This function will be run when the submit button at the bottom of the form is clicked.
[12:55 - 13:15] We'll create this handle host listing function in the component, and it would expect an event object that is to be passed in from the form submit. We can declare the type of the form event in React by using the form event interface we can import from the React library.
[13:16 - 13:24] When a form gets submitted, natively, it makes a request and actually reloads the page. We don't want that.
[13:25 - 13:41] We want the event to be triggered where we can capture the form values and then call our mutation. So with that said, we'll run the prevent default function of the event object to prevent the default situation of making a request when a form is submitted.
[13:42 - 14:02] At this moment, we'll want to capture the values from the form and verify if the form information has all been provided. We can achieve this by running the validates fields method that exists within the form objects, with which the first argument is an error property that determines if any errors exist in the form.
[14:03 - 14:19] The second argument is the actual values that have been captured from the form. Within this validate fields function, we can check and say errors will be present if any of the form inputs that are required haven't been given a value.
[14:20 - 14:39] So we'll say if these errors exist, we'll display an error message that tells the user please complete all required form fields and will return early. If errors don't exist, let's place a console log for the values.
[14:40 - 15:06] Now, if we head back to our page and we try to click the submit button right away, the form submit event will be fired, the handle host listing function will be called, and the validate fields method of the form object is triggered. If any errors exist, all field level validation errors are shown, and we have the error message we've set up that says please complete all required form fields.
[15:07 - 15:36] Okay, so with that said, let's try and fill out all the fields. We'll fill the type number of guests, title, description, address, city, state, postal code, image, and price. And when we click submit, and we take a look at our console, we'll see all the values for the fields in the form. Fantastic.
[15:37 - 16:11] Notice in the image field, within the values of the form, we're getting information about the image file type, but there's no base 64 value here, which is why we've created a state value to track the base 64 value of the image. And the names of the fields here are the IDs we've passed for every field decor ator for every form item, image, title, city, state, etc.
[16:12 - 16:26] At this moment, we've been able to provide some decent field and form level validations on the client, and we're able to capture the values in the form. Now we just need to prepare our data, trigger the host listing mutation, and pass the input data along.
[16:27 - 17:18] So with that said, we'll import the use mutation hook from reactor polo, we'll import the host listing mutation document from the libgraph qo mutations folder, and we'll import the corresponding type definitions for the host listing mutation. We'll use the use mutation hook to construct our host listing mutation.
[17:19 - 17:48] We'll pass in the TypeScript data and type variables and the mutation document, and we'll destruct the mutation function as well as the loading and data properties of the mutation results. In our handle host listing function, we'll now prepare our data when it's available and call the mutation function.
[17:49 - 18:03] A few lessons back, remember how you mentioned that we're going to pass a single address field in the input object, and this will be a concatenation of all the address information provided in the form. So let's prepare this address field.
[18:04 - 18:37] We'll create a constant called full address, and this will simply be a string that contains the address, city, state, and postal code fields from our values object, with each piece separated with a comma. We'll then prepare an input object for the input variable expected for the mutation.
[18:38 - 18:49] We'll use the spread operator to pass all the fields in form values along. We'll specify the address to be the full address value we've constructed.
[18:50 - 19:14] We'll update the image field to be that of the base64 value, so we'll use the state image base64 value, and further price will make a small change. Since we ask the user to provide the value in dollars, on the server we store the monetary values as cents, so we'll multiply the price value by 100 for the input object.
[19:15 - 19:32] At this moment, this input object has all the fields the mutation expects, however, it also contains a few additional fields, like the city, state, and postal code from the form. We don't need these since we only are expected to pass a single address field.
[19:33 - 19:58] So with that said, we'll use the JavaScript delete operator to simply delete the city, state, and postal code fields from input. And we'll then call the host listing mutation function and pass this input object as a variable.
[19:59 - 20:24] Now as a quick note, if we took a look at the types of the error and values arguments from the validate fields function, we'll notice that they're typed as any. The reason being is if we wanted to provide an appropriate type for these, particularly the values objects, we can use the generic that the form component props interface from antisein expects, and we can pass the shape of values that we think will be in the form.
[20:25 - 20:44] We won't do this here, and we're okay with that because as long as our host listing mutation expects variables of a certain type, it does a suitable check for us that we're preparing the input as expected. This is pretty much how we'll want to conduct our host listing mutation.
[20:45 - 21:05] Now let's just simply handle the loading, success, and error states of this mutation. When this particular mutation is loading, we'll simply have a title and text be shown that says, please wait, as well as we're creating your listing now.
[21:06 - 21:41] If our mutation was to ever error, we'll use the on error callback result of our use mutation hook To display an error message that says, sorry, we weren't able to create your listing, please try again later. When the mutation is successful, we'll do one of two things.
[21:42 - 22:16] First, we'll import and use the display success notification to show a success message that says, you've successfully created your listing, and we'll have this success notification function run on the on completed callback function within our use mutation results. The other thing we're going to do is take the user away from the host page the moment the listing has been created.
[22:17 - 22:44] We'll like to take the user directly to the listing page of the recently created listing. To help make this redirect, we'll import the redirect component from React, R oush, and in our component function, we can check that when data and the host listing object within data from our mutation is available, we'll redirect the user to the slash listing route.
[22:45 - 23:11] The listing route accepts an ID URL parameter of the listing ID itself. This is why we have our mutation document simply returned the ID of the created listing, with which we'll use here as the ID parameter of the target route path of the redirect.
[23:12 - 23:22] And now, let's see how our page is going to behave. We'll first open the network logs just to verify in case everything goes well or any mistakes happen.
[23:23 - 23:34] We'll fill out the form, so I'll say my listing will be an apartment. Now the maximum number of four guests for the title, I'll just say, Belair.
[23:35 - 23:47] Mention, I won't really fill the description, I'll just say, modern and clean. The address 251 North Bristol Avenue, City is less LA, Status California.
[23:48 - 23:56] The postal or zip code will be 90210. The image, I'll use the same image I've been using lately.
[23:57 - 24:08] And for the price, I'll say $200 per day. And now if I try to actually submit my listing, I see that a request is made, but I see an error occurring.
[24:09 - 24:17] If I look at my network logs, in the preview it tells me that the request entity was too large. Why is this happening?
[24:18 - 24:34] The default HTTP request body object size in our server is 1 megabytes. Base 64 representation of the image actually increases the size of the image due to how the base 64 encoding is set up.
[24:35 - 24:54] So what's most likely happening is that our entire request exceeds 1 megabytes, thanks mostly in part to the base 64 image, which is being too large of an entity. So what we can do in our server project is simply increase the body size limit of our requests.
[24:55 - 25:07] So at this moment, I'm in my server project and will import the body parser package in the root index file. We have the body parser package installed in the application.
[25:08 - 25:25] You can install it if it's not currently available. So now right before we instantiate our pull to server instance, we'll apply middleware to our express app and we'll specify in our body parser JSON function, the limit would be 2 megabytes.
[25:26 - 25:39] We're picking 2 megabytes here, which should accommodate mostly all requests, since we do have a client side limit for the image uploaded in our form. So now let's see how our application would behave.
[25:40 - 25:46] The server is running for this particular project. I'll do encourage you to exit and restart your server.
[25:47 - 25:54] However, but these changes should be taken into effect. We can head back to our host page and we'll provide information for a listing.
[25:55 - 26:08] So home type, you can say apartment for the maximum number of guests title. We'll just say Belair Mansion description will be modern and clean address 251 North Bristol Avenue.
[26:09 - 26:13] City Artan will be Los Angeles. State is California.
[26:14 - 26:20] The zip proposal code is 90210. For the image, we'll go ahead and upload the image we've used before.
[26:21 - 26:29] And we'll specify a price of $200 per day. Now click submit to submit the listing.
[26:30 - 26:44] We see the loading states were taken to the listing page where we have a message saying you've successfully created your listing where in the appropriate route. Now we actually see information about our listing.
[26:45 - 26:47] Fantastic. Notice how efficient that worked.
[26:48 - 26:56] We've already built the listing page before. So all we needed to do was just direct us to the listing page and provide the appropriate ID parameter.
[26:57 - 27:05] Everything else was taken care of. And notice that in this particular case we're using the location that's being returned from the geocoder.
[27:06 - 27:19] So the address here is just simply of a concatenation of all the information the user provided. However, at this very moment, if I search for Los Angeles.
[27:20 - 27:29] I'll see the listing we've just created as a listing in the list. Remember, when we actually provide the address, we run it through the geocoder in our server.
[27:30 - 27:38] The geocoder determines the city, admin and country for that location. And only then will it save it into our database.
[27:39 - 27:48] So despite what the user types, they can type any particular address number. We just need the geocoder, Google's geocoder to determine the appropriate location.
[27:49 - 27:59] And when it does, it actually tells us the location here at Los Angeles and we 're able to find it when we actually search for it. Amazing work so far.
[28:00 - 28:07] We're actually at a pretty decent point of our application. And the vast majority of the features we want in our app, we've completed.
[28:08 - 28:43] In the next module, we'll talk a little bit more about how we have our images being prepared and see if there's a better way of storing these images in our database. [ Silence ]