Adding Image Preview0:00
Let's finish our app by adding a little image upload preview. Now we're going to do something pretty basic. If you want a more immersive and advanced experience, I recommend using a JavaScript library. A FilePond is actually pretty good if you use Filament. That's what they're using to, you have the little progress bar with the green progressing happening.
you have the little progress bar with the green progressing happening and really nice polished experience here. We just want to show the user what image is currently there and when, when they select a file, what image is about to go and replace it. Let's do this. All right, so our first point of attack is when we start editing a Puppy and it has the default image.
Show Current Image0:51
of attack is when we start editing a Puppy and it has the default image. We want to show that default image and by default I mean the current existing image. So remember, we've decided to not pre-populate the image field with the existing image value, uh, because if the user doesn't upload an image, we just wanna do nothing with images. And this is reflected in the code here
we just wanna do nothing with images. And this is reflected in the code here where we have an image set to null. Uh, and that can be a file as well. However, while we do not have an image at the start, we want to display the current image, which is puppy.image_url. So let's go to the image field. There it is. And we have the errors and everything. And I think under here, within the fieldset, we are going to check, do we have data that image?
And I think under here, within the field set, we are going to check, do we have data that image? And if we do not have data that image, then we want to have an img tag. And here the src is going to be exactly pappyImageUrl, except it's camel case. And let's give some classes to that img. So maybe a height of 24, a rounded-lg and let's add a margin top of 2 as well. So that should display a little image preview.
and let's add a margin-top of 2 as well. So that should display a little image preview of the current image until an image is selected. Alright, let's try edit the puppy or the person again. And yeah, there we go. Pretty nice. So I wanna increase the margin-top and reduce the corner-radius. So let's go rounded-md and empty-4. And yeah, I think that looks nice. So that's kind of cool and that works.
And yeah, I think that looks nice. So that's kind of cool and that works. But once you choose an image, you are going to lose all the goodness. So if I upload this image now it's gone and uh, we need to try to find a way to preview the next image that's coming up. The problem with that is that image at this stage is not uploaded yet, so we can't just have an image source and points to a file.
Preview Selected File2:44
that image at this stage is not uploaded yet, so we can't just have an image source and points to a file because the file is not there yet. So we need to do a little bit more work. Okay, so let's modify a code because we wanted to always show an img tag and not only if there is no dataImage. So let's go to just the img tag and then inside of here we are going to have for now a little gnarly ternary operator
and then inside of here we are going to have for now a little gnarly turny operator and check, do we have data that image question mark? If so, we are going to have something and otherwise we are going to have the current puppy image. If there is an image, we need to sort of pass that data. And the way you can do this and copilot is not going to let me reveal slowly is to create an object URL, which you can do with URL.createObjectURL.
to create an object URL, which you can do with URL.createObjectURL. And then you can pass this data to image. So F-Y-I, URL.createObjectURL is a static method and basically it takes a Blob like a file upload, which is exactly what we have. And then it gives you a URL that you can point to that image. So that works on all the browsers. So let's try it one more time.
So that works on all the browsers. So let's try it one more time. We have the current image, but if I choose a file like this one and I select it, we are going to update to the new image. Very cool. Let's try this one more time and I'm going to select this time, this image and yep, it seems to be working quite nice. And when I save, this is the image that should go to the file just like so from a UX standpoint,
Fix Object URL Leak4:24
And when I save, this is the image that should go to the file just like so from a UX standpoint, that's actually pretty good. Uh, there's a little problem though with our implementation and it is that every single time we select an image we create a new object URL and we never destroy it. So it's sort of a memory leak. Every single time the user selects a new image, we create a new instance and we never really discard them. So to be a good citizen,
we create a new instance and we never really discard them. So to be a good citizen, what we should do is when the component unmounts or when the file changes, we remove that image and we revoke that objectURL. Conveniently there is a revokeObjectURL method to release an existing objectURL that was previously created by createObjectURL, which is exactly what we've done. That looks like exactly what we need.
Build Preview Component5:08
which is exactly what we've done. That looks like exactly what we need. So what we're gonna do is create a little standalone image upload preview component that's going to take care of rendering that image preview, but also creating and revoking object URLs. All right, so let's go in components and we are going to call this ImageUploadPreview, that's .tsx and it's going to export a function called image
preview, that's TSX and it's going to export a function called imageUploadPreview. And so this preview needs to receive a source. We would like it to be either a file, if it's an upload, let's also support null. So uh, preview works before we upload an image. Alright, so we're going to wrap our create and revoke uh, actions of the object URL in a useEffect. So useEffect from React takes a function.
and revoke uh, actions of the URL object in a useEffect. So useEffect from React takes a function and something you might or might not know is that if you return a function part of it, this is where you can do your tear down logic. So effectively we are going to create the URL object here and then we are going to return the revokeObjectURL. So whenever the component on mounts is going to call this return function and tear it down. So we want to do this create ObjectURL only.
to call this return function and tear it down. So we want to do this createObjectURL only whenever the source is a file. So if source is an instance of file, if the source is an instance of the file, then we are going to have our const objectURL equals URL.createObjectURL(source). And actually still inside the if check because we only want to tear down the objectURL if we've created it,
because we only want to tear down the objectURL if we've created it, we are going to here call URL::revokeObjectURL with this objectURL. So once again, if the source that we pass to the component is a file, then we create an objectURL based on that file and we have a return callback function to revoke that objectURL whenever it's not used anymore. And by the way, I need to pass the array of dependencies.
that object URL whenever it's not used anymore. And by the way, I need to pass the array of dependencies before I forget. And this is for when to rerun the effect. And here whenever the source changes we should rerun the effect like so. So now basically I want to return an img tag, return image and for the source we are going to work that out in a second. But let's add the same class name that we had
to work that out in a second. But let's add the same class name that we had before which was mt-4, h-24 and rounded-md. So after all our image upload preview is always going to return an image with these classes just like it did before. The big difference is what is going to go in there. And this is going to change based on what happens in the useEffect.
And this is going to change based on what happens in the useEffect. So we need to use some state as well, a little bit of state use state. And inside the component we're going to have a const src for the source and the set src, which is going to be useState defaulting to null. But as Copilot suggests, we want to be a bit more specific that it can be either a string or null and it's null by default.
that it can be either a string or null and it's null by default. Then when the component mounts the effect runs, if it's an instance of a file, we are going to create that object URL and indeed set the source to this object URL like so. So if we've deemed that it's a file, we get that URL and then we set that so we can set our image to the SRC state. Now we're going to have a little problem
to the src state. Now we're going to have a little problem because the image source cannot be null, it can only be string or undefined. But either way we don't want to have an image tag with a string of undefined or no. So basically, uh, before returning the image outside the useEffect, we can say hey, if there is no image, if there's no image source, just abort short circuits,
we can say hey, if there is no image, if there's no image source, just abort short circuits, get out of there return null. So basically the whole imageUploadsPreview component, if uh, it doesn't have a source, it'll just return null and not render anything. If it does have a source, it'll be an img tag with that source. So now we've taken care of the file here or the null situation,
So now we've taken care of the file here or the null situation, but if I look at the implementation we had in the previous uh, lesson, we had this file or we had puppyImageUrl, which is a string. And so what I'm saying here is we supporting the file or the null condition, but we also want to use our imagePreview with a string, a path to an image. So we need to go and update it
a path to an image. So we need to go and update it and also support the string type. Uh, we are going to add the string condition here in our type. And so if the source is a file, we are doing the object URL buildup and tear down. But if it's a string we should uh, we could have probably the LS statement here because the source can be string
we could have probably the ls statement here because the source can be string or null, which is the two other possibilities here. So let's set that source to either a string or null. If, if it's not an image file, so here we can go set src source like so I think everything should work and yeah, we have no squiggles. So basically there's three possibilities in terms of types. It can be a file, a string or null, if it's a file we go through that code path.
It can be a file, a string or no, if it's a file we go through that code path and if it's either of the other, we go through that. Either way we set the source so the image is always going to render this source, but if there is no source, if it's undefined or null, uh it is going to show up here and be short circuited. So it's definitely a little bit more complex. But if instead of this we now try to render our
So it's definitely a little bit more complex. But if instead of this we now try to render our image upload preview component and we are going to pass a source to it. We need to either pass the dataImage if there is one or the puppyImageURL, so we can go dataImage and then pass the puppyImageURL in case that is null and copilot figures out that now we can replace that because this is the equivalent basically. And so let's go see the result.
because this is the equivalent basically. And so let's go see the result of using our image upload preview file. I'm going to update this puppy once again image preview and we are going to select this image perfect and see what happens. We can see there it's working and we can update a component and it works nice, nice, nice, nice. I already feel a little bit better about not leaving a bunch of create object URLs that stack up in memory.
I already feel a little bit better about not leaving a bunch of create object URLs that stack up in memory. And the other good thing is we can without any change go in the createPuppy function, uh, the front end components and we should be able to use this ImageUploadsPreview component. Just drop it in and it should work. Let's go try it. And so to refresh your memory right now, if you choose an image, we will have nothing to sort of preview that image.
if you choose an image, we will have nothing to sort of preview that image. So you are just going to have a bit of text. But if we go in that new PuppyForm component and we go in the imageUpload field which is just there and we drop in our imageUploadPreview, it should work. Now dataImage is I believe also going to be null at the start, but our component supports null. It's just going to not render any preview, which I think is fine or we could have a placeholder maybe,
It's just going to not render any preview, which I think is fine or we could have a placeholder maybe, but let's go and try to upload a file now again. And I think that it's going to show a beautiful, well it is showing an image. I don't think beautiful applies here, but the preview seems to be working. Let's fix it quickly. You know what, let's support custom classes real quick to make a nice finish class name
let's support custom classes real quick to make a nice finish class name equals self start I believe. Although we want just justify self start here and let's go empty four as well. And as you can see it's telling me, hey, class name does not exist on your thing. So let's go in the image upload preview and we're going to add a class name and for good measure let's also have the rest props
and we're going to add a class name and for good measure let's also have the rest props that we can pass to the Image component. class name is going to be an optional string and we can handle the rest props maybe like this, but instead of any we'll use unknown. So we still got some red squiggles because we are not using these two. But in the component here I can spread the rest props here.
But in the component here I can spread the rest props here. It doesn't matter if it's after the class name, it's not gonna override it because rest props does not contain class name. And here we want to merge the classes passed with these base classes. So here I can use the cn function from chatcn/ui, which is going to intelligently merge the base classes with our className attribute using tailwind-merge.
which is going to intelligently merge the base classes with our class name attribute using tailwind-merge. So now our image preview should have mt-4 h-24 rounded-md but also whatever classes we pass to it. So for example, if I passed rounded-full and empty-8 there, it should keep the height of h-24 but override the conflicting values which is the power of tailwind-merge. So let's try upload an image one more time. Let's go with this one.
So let's try upload an image one more time. Let's go with this one. Okay, so we are definitely having mt-8 and rounded-full and the h-24 what we want is not justify-self-start which is justify-self-flex-start. We want self-start which is align-self-flex-start, which is on the secondary axis, which is horizontal here. And this time uh, our image is going to work properly. So let me remove the overrides here as a demo,
And this time uh, our image is going to work properly. So let me remove the overrides here as a demo, which were empty eight and round full. But now we have a nice way to pass custom classes to our image uploads preview component and I've just realized that uh, yeah instead of passing key unknown here, which is really anything, we sort of know that it's an image component. So what is probably a better way to pass that is to say and anything that is an image attribute on the image
So what is probably a better way to pass that is to say and anything that is an image attribute on the img HTML element as well. So that's going to only allow uh, other props, the rest props that are valid attributes on the img element. So if I was to try pass fu equals bar, fu is not a valid attribute so it's gonna tell us, Hey, what are you doing? But if I was trying to pass height equals 96 for example,
End-to-End CRUD Demo15:40
so it's gonna tell us, Hey, what are you doing? But if I was trying to pass height equals 96 for example, this would be accepted because it is one of these attributes on the image element. Alright, and to finish up with this whole series, let's go through a whole process of a CRUD operation. I'm going to create a new User and then create a Puppy, update the Puppy, and sadly delete the Puppy. So I'm going to log out
and sadly delete the puppy. So I'm going to log out and I'm going to create a new account. This is going to be puppyLover. We don't really care about the email address and the password. This is throwaway stuff. And so puppyLover is arrived and is very happy on its way to the creation form. puppyLover is going to love a few puppies.
and is very happy on its way to the creation form. Puppy lover is going to love a few puppies and so we are going to create a new my puppy the best and we need to choose a fabulous image for this last puppy. Let's scroll down and see what we've got. Uh, this is a little bit of a gambling to do this on camera, but can we see a puppy? What's that? All right, we're gonna use that. It's not a puppy but it's a picture of me announced
What's that? All right, we're gonna use that. It's not a puppy but it's a picture of me announced to speak at laracon AU this year in November. Hopefully I'll see you there. Uh, let me select this and I am seeing the preview. I'm going to add this puppy. There I am. I'm going to like that puppy. There it is. I am going to edit that puppy. So let's call it myCoolPuppy loves Brisbane, which is where the laracon AU will be.
So let's call it my cool puppy loves Brisbane, which is where the racon au will be and we will change the photo. Again, I don't want to spend too much time finding one, so I'll go back to this image. Uh, the upload update preview image thingy is working. The update creation is working. You can see that I'm only allowed or seeing the update and delete buttons for the puppies that I own, which is just this one.
and delete buttons for the puppies that I own, which is just this one. And so sadly we're going to go and finish the loop by deleting my cool puppy and after two seconds it's been deleted successfully. There is one very last thing I want to do and it's remove the sleep functions in the update, create maybe and delete uh, methods. So let me go in the PuppyController and search for sleep. We are going to remove the 200 milliseconds.
So let me go in the PuppyController and search for sleep. We are going to remove the 200 milliseconds of sleep in the like that I wasn't even thinking of. We are going to remove the store one and the delete one we have any other we do or the update function. So four sleeps. And with that we have crossed the finish line for this course. I hope you learned a thing or two and enjoyed the simplicity of using React.
I hope you learned a thing or two and enjoyed the simplicity of using React with Laravel and Inertia. I am a reactive first myself, but I found myself enjoying React a lot more when paired with Laravel. In Inertia you can move a lot of the state and complicated stuff you do on the front end to the backend, uh, and really just use React as a templating language mostly,
to the backend, uh, and really just use React as a templating language mostly, which I think JSX is exceptional to do. This I really enjoyed. I'm going to continue using Laravel React and Inertia. I hope you like this stack as well. Uh, if you don't that's fine. Use Vue, use Livewire, use whatever you want. But I do like Laravel React Inertia and I hope you like this series.
But I do like Laravel React Inertia and I hope you like this series. I will see you in the next one. Bye.
