Image Upload with Form Data and Memory Upload Handling
00:00 Let's open up the edit route and here in action, we're going to handle the file upload. But let's do the UI side of this first. I think that'll make it all make a little bit more sense. So we have this new component called the image chooser. This was built for us by Kelly.
00:17 You can feel free to dive into how all of this works, but it's pretty cool. It just handles all of the styling stuff. So you don't have to worry about doing this and also previewing the image and stuff like this. All of that has nothing to do with file upload itself. It's all just UI stuff. So feel free to dive into that if you so choose.
00:36 So let's use this image chooser right here, image chooser. We're going to pass the notes first image as image props. So this is for when you're editing a note that already has an image. So we've got the image prop and that'll be data.note.images. We'll just do the first one.
00:55 In the future, you'll be able to add multiple images. And so that's why the data model is set up to handle multiple images. But for now, we're just going to do the one. All right, so now inside the image chooser, we need to make a couple of adjustments. So first of all, if there's an existing image,
01:11 we need to communicate to the backend that, hey, there's already an image here. We're editing the alt text for it or something like that. And so you need to provide that image, the image ID, so that the backend knows what image ID to associate with the existing image.
01:29 So the way that we're going to do that is we're going to say, if there is an existing image, then we're going to render something. Otherwise, render null. We're going to render our input. That is a type hidden. And the name will be our image ID.
01:48 And the value will be the image.id. So that existing image is coming based on whether or not an image has been provided. So the way that flows is if the data.node.images exists, then we're going to have an existing image. And in that case, our preview image will be set to the resource route
02:07 for displaying the image. And that way, it displays all nice and everything. And then we have this hidden input that is there to communicate to the backend that, yes, indeed, there is an existing image. OK, great. So then we come down here. This is our input for choosing a new image.
02:23 So we can either change the existing image or upload a brand new one. And it has some logic for the preview and stuff, which is interesting. But we're going to add a name of the file here. So a name will be file.
02:38 And so this is the form data name value for our particular file. The type is file, so it's a file chooser. And then we're also going to add accept image. So we're going to accept any image. This is mostly for progressive enhancement stuff.
02:55 This is not necessarily going to validate or anything like that. You have to do validation on the backend. But this will make the experience a little bit better by saying, hey, whatever file chooser you pop up, I'm looking for images for this. That's the intent here.
03:14 OK, and then finally, for our text area, we're going to add a name of alt text so that on the backend, we have access to that. So if I save that, here we get our image chooser. I can choose an image. I can type in my alt text. And then if I submit that, let's take a look
03:30 at what that does when we submit right here. There's our network request. Now, nothing actually happened. We haven't implemented the backend piece of it yet. But let's take a look at this payload right here. So it is a form data object. We have our title content and our file. The file is just a string.
03:50 It's represented by the string. That doesn't seem right, right? We need to actually upload some binary data or some multi-part form data. Yeah, we can't upload the form data as query string parameters. We need to upload the form data with the binary file.
04:07 So there is a change we need to make in our form. Right here, it needs to specify the encoding type of multi-part form data. And with that change, it is going to be a little different. So let's select another image here, Cuddling Koalas. And then we hit Submit.
04:26 And now this will be a little different. So let's go to that post request that was made. And here we have form data, title content, and file represented as binary now. That's exactly what we want. We're going to stream this file to our action, to our server backend. And then we need to somehow take the chunk.
04:46 So it's not going to send it all at once. Sometimes these files can be multiple gigabytes. So it's going to be streaming it in as chunks, bits at a time. And we're going to load that up. And we've got a couple of options of things we can do. So as that stream is coming in, we could stream it off to another service that's going to be storing our files for us. That's pretty common.
05:05 Or we can stream it down into the file system, also pretty common. Or we just chunk it up into memory. So if we're going to make sure that they don't fill up our memory with something too big, we can do a little validation and say, hey, we're going to limit you to a certain amount of file size. So if we can do that validation, then we can just stick it into memory.
05:24 And then we can do whatever we want to with it. We can put it into a database, or we can save it to a file, or whatever. Most of the time, if you're going to accept bigger files, more than a couple megabytes, then you probably want to stream it to someplace and not let it just hang out in memory. But because we're going to add a little validation, it's fine.
05:42 We can have it stream or chunk up into memory. And then we can deal with it from there. So that's what we're going to do to make all this work. So starting here, we're going to switch this for parse multiple part form data. So let's actually, we need to import this first
06:02 because this is an unstable API. So let's get that unstable parse multiple part form data. And that's coming from remix run node. But I personally don't like having unstable in my code. And so I just alias it. Is that bad of me? I don't think so. I think it's fine.
06:18 But yeah, so we've got our multi-part form data parser. And to that, we pass the request itself. And then we can also pass some arguments for the file upload handler. So as those different pieces of the form data
06:37 are being passed or supplied to our parser, it's going to be passed into an upload handler. And that is, for us, our memory upload handler. So we're going to use create memory upload handler.
06:54 Again, that one also is stable or unstable for now. It'll be stable eventually. It hasn't experienced any changes in a while. The remix team will get to it eventually. I don't expect this to change. And if it does change, then I will add a note to the instructions and you can follow those changes. But like I said, I don't expect that to change. Okay, great.
07:13 So with that, we can pass an option. This will be max part size. And we want to limit it to three megabytes. And so that's what we're going to do right there. We just set that to three megabytes in size. And that is an async experience. So we're going to await that.
07:30 And then we get our form data from this. So there we go. Great, so we've got our form data. Everything else works as it did before. And we'll take care of adding this to our Zod schema a little bit later, because already this step of the exercise is getting kind of long. So just to wrap it up
07:50 so we can have something deliverable that we can ship, we're going to add an images array. And we're going to pass an object that has an ID. So this will be form data dot get image ID. So you'll recall down here, we have that hidden input. So this way you can change an existing image.
08:09 So if the image ID is there, then it will be there. And then we'll have our file that will be get the file. So the create memory upload handler will take that streamed multipart form data piece, and it will turn it into a buffer
08:26 or into a file object actually that has a buffer. And then we can deal with it that way. So ourselves with this update note that handles all that stuff. When we get to the database modeling stuff, we'll look at more carefully at how to handle these uploads into an actual database. But for now, we're just,
08:46 this is a little bit of an abstraction. Feel free to dive into it if you prefer. But yeah, so we're going to get that file. And then we're going to, yeah, specify the alt text. Awesome. And TypeScript's not going to be happy with this at all. We will fix that later. No worries. Let's just make sure that this thing is going to work.
09:03 So we'll select the koala cuddling and then submit. And boom, there it is. We can take a look at this and double check that the alt text is set properly. So we're in a good spot. Well done. And we can change the image as well. So here, actually, before I do that, let's look at this and we'll see,
09:21 we have that hidden input with that value. So that is going to be submitted properly and we can do cute and submit that. And that image was updated and has the proper alt text. Solid. So let's review really quick because there was quite a bit of things
09:40 or a number of things going on in here. So first of all, Kelly built us this image chooser, which is awesome. And the important bits for us was just to, first of all, render the image chooser with the image from the note. So that first image. And if that image exists, then we have an existing image.
09:59 We'll behave differently based on that. So if it does exist, then we have a preview for that image and that's what is displayed right there. If it doesn't, then we will not. So here in this one, we do not have a preview image. And then we have, yeah, we display that preview image here. If we do have an existing image,
10:18 then we need to let the backend know that we're trying to update that existing image. So we will pass that ID. And then, and the reason that's important is because right now we may only have one image per note, but in the future we will have multiple. And so we need to let them know which image we're trying to update.
10:37 And so we need to have that image ID there. And then we have our image chooser. That's our input. That is of type file. We give it the name of file and we tell it to accept only images. And then we set the name of our alt text. So then on the backend piece of this, we can switch from request.formdata
10:56 to this parseMultipartFormData. You can feel free to dive into the implementation of this if you so choose or desire. It'll be right in here. There's a lot of stuff going on with parsing these different chunks and everything. It is, you know, it's kind of interesting. This is using the multipart parser
11:16 coming from the Web3 storage module. So you can take a look at that if you so choose. And then the createMemoryUploadHandler, also not as complicated as you might think. So we dive into that and it's just taking it chunk by chunk. And if we get to a chunk that's too big, we say, oh, no, we can't take that anymore. And we, you know, throw an error.
11:36 And so that's why we're able to just chunk this up into memory safely because we know people can't fill our server's memory with files that are too big. And then it creates a file object out of that. So really not quite as complicated as you might think. And then once we have that, we have our form data. We parse it like normal.
11:56 And so the properties that are there are gonna be parsed and work out as they were before. But then the things that we don't have as part of our schema, we're going to just grab ourselves. And so we'll get the image ID and the file, which now is going to be that file object that the memory upload handler is creating for us
12:15 and the alt text. And so, yeah, we've got a little bit more work to do, but the feature totally works. Let's ship this thing. Good job. And yeah, let's move on to the next step.