Plan Delete Puppy UI0:00
All right, let's work towards making our app a little bit more cruddy because right now it's just a crew app create read. So we are going to start by adding the ability to delete puppies. So here's what I'm thinking. For each puppy card on the top right, we're going to have a floating uh, delete icon button here. And so it's going to give the ability to users to delete puppies.
And so it's going to give the ability to users to delete puppies. Obviously we only want the owner of a puppy to be able to delete that puppy, but we're going to go in multiple steps and let's focus first on the ui. So let's display a delete button and then a modal that's going to ask to confirm, are you sure you really want to delete that poor puppy? It's pretty sad. Okay, so let me show you some inspiration.
Review Dialog Inspiration0:54
are you sure you really want delete that poor puppy. It's pretty sad. Okay, so let me show you some inspiration that we have already on the existing app. If I go to the dashboard that comes with the starter kit that we've never quite used, so this comes for free with the starter kit, we're not using it in this app, but if I go in this settings page, I will show you what I mean. Here there is this delete account action that is going to toggle a model saying, Hey,
Here there is this delete account action that is going to toggle a model saying, Hey, are you sure you wanna do this? So we are not going to make the User type the password to delete a Puppy, but I think we can take inspiration from this model, uh, as uh, a user experience to provide to delete the Puppy with a confirmation. So in terms of codes, this delete User UI actually has a delete User component.
So in terms of codes, this deleteUser UI actually has a DeleteUser component. And you can see here that it's using mostly the Dialog component coming from Chati and UI. And so it has these different moving parts like the trigger, the contents, title, description. And so we are going to create something very similar to wrap our deleteForm, uh, into a nice ConfirmationModal interface.
Add Delete Button Component1:57
to wrap our delete form, uh, into a nice confirmation model interface. Alright, so I think our first logical step is to try to have some sort of UI inside that PuppyCard and we are going to create a uh, DeletePuppy component that we are going to place up here. So if I go in the PuppysList component, basically we have the PuppyCard and I think that this is where inside the image or on top of the image, we want to place our delete button.
and I think that this is where inside the image or on top of the image, we want to place our delete button. So I can make the ally wrapper, I can give it a position of relative, which means that above the image here, we could have an absolute position element and we could go top maybe 2 and right 2 and inside of here is where we would place our delete button. So I'll just go delete for now and just so you can see it, I'll give you the class
So I'll just go delete for now and just so you can see it, I'll give you the class of text-white and there you go. That kinda works. So we are going to make this a component which is going to be the trigger for the dialogue. Okay, so let's create a new component here and we can call this, well I want to delete a puppy, but we are going to have multiple components for puppy actions.
but we are going to have multiple components for puppy actions. So let's call it PuppyDashDelete instead. So all our puppy actions are gonna be co-located in the file system. And so here we're going to export a function called puppyDelete. And so let's start by just rendering a SHAIconButton. So return and because I'll want multiline, I'll just add a wrapping div.
So return and because I'll want multiline, I'll just add a wrapping D so it doesn't get rid of the parenthesis. So button, which is going to be the UI component. And then I think I can go size equals icon. And the variant, because it's destructive is going to be, well a destructive and inside a button we will have a trash icon. I think that will work with a size of four like that.
trash icon. I think that will work with a size of four like that. I need to go in the puppies list. And instead of this placeholder delete, this is where we want to have our puppy delete component. We will indeed have to pass the puppy to the component for it to make sense. But let's first take a look and there you go. That's arguably a little bit aggressive. Maybe uh, we could go with a subtle white.
That's arguably a little bit aggressive. Maybe uh, we could go with a subtle white with some red outline, but for the purposes of what you're doing, this is going to work. I have a clickable button on top of the image. Alright, so what I'm in here, let's pass the puppy to that component so we are able to determine which puppy we're trying to delete. And so I need to add the type signature for this.
to determine which puppy we're trying to delete. And so I need to add the type signature for this. So this is going to receive a puppy, which is going to be our puppy type from the types. And we're going to need to use this to do the delete action and maybe even display the name of the puppy in the model to try convince the user to not delete this poor puppy. If we use its name, the user is less likely to really pull the trigger. I don't know. Okay, so quick look at the chat
Implement Confirmation Modal5:03
to really pull the trigger. I don't know. Okay, so quick look at the chat and UI components and you can see the dialogue components, but there is also an alert dialogue and this one interrupts the user with important content and expects a response. So when you toggle it, you cannot scroll, you cannot do anything else. Then interface with either cancel or continue. And I think that's what we want here.
Then interface with either cancel or continue. And I think that's what we want here. We wanna make sure that the user is fully committed to making the decision instead of getting distracted and scrolling and liking another puppy and forgetting that they're trying to delete a puppy. So this component is called alertDialog and then you can import the moving parts. I don't know if the alertDialog is actually used in the SHA_CNUI starter kit and I don't think it is.
I don't know if the alert dialogue is actually used in the SHA CNUI starter kit and I don't think it is. So let's install that. So npx sha cn add alert dialogue, I think that's how it's called. Okay, so it's depending on the button components that exists, we do not want to override this. We're gonna keep our file so it skipped the recreation of the button, but it's created this alert dialogue file for us.
of the button, but it's created this alert dialogue file for us. Perfect. So let's maybe grab this basic demo example of how to use the alert dialogue. So I will copy all of that and I will paste it after the button, although the button is going to become the trigger remember, but we are going to have lots of squiggly until we import all of these components. But again, I don't like the relative path.
until we import all of these components. But again, I don't like the relative path. I want at /components and I want the same for our Button. There we go. And I'll move the trash icon to the top because it's an NPM import where these are local imports, right? So I think that out of the box, if we visit the ui, we should be able to see a working dialogue. Uh, but it's going to have a open
we should be able to see a working dialogue. Uh, but it's going to have an open text instead of our button. And yet there it is. I don't know if you see the black text next to the button, but if I click this, I'm going to have my model that is going to ask me if I want to cancel or continue and I cannot stab outside of it. I cannot scroll. This is focusing the attention on the action. So what we wanna do now is instead of this open, use
focusing the attention on the action. So what we wanna do now is instead of this open, use that delete button with the icon as the trigger. And while that should be very easy, I'm going to take my button here and cut it and paste it inside the dialogue trigger. Like so that is not going to work perfectly just yet, although it actually might and yep, it does, but I think it's going to add an extra wrapper around our button.
but I think it's going to add an extra wrapper around our button. And so you see we're going to have a button that wraps another button. This is the alert dialogue trigger and this is our nested button. And to avoid this nesting of buttons, which is probably not a good idea, we can use the asChild prop on the parent on the alert trigger. So all I need to do is here add the asChild prop.
as child prop on the parent on the alert trigger. So all I need to do is here add the as child prop and this element is now going to pass all its props into the button and the button is going to act as the trigger. As a result, we now only have one button. There is no more nesting of buttons to be seen, which is perfect and the dialogue is still going to work. Nice and quick aside, because we do not have any text on this button,
Nice and quick aside, because we do not have any text on this button, I think it's a nice idea to add an area label to this button of delete puppy so screen readers will have a better time explaining what this button is trying to do. Alright, so onto the guild trip. Now, are you absolutely sure? Okay, let's go with something drastic. Who in their right mind would delete such a cute puppy? Seriously, that is perfect.
Who in their right mind would delete such a cute puppy? Seriously, that is perfect. And then we are going to have the cancel and instead of continue, we here again, are going to tap into the emotions by saying delete. And since we pass the puppy, we can go puppy.name. Okay? And let's look at the improved, uh, guild trip experience. So are you absolutely sure who would do this? Seriously, delete princess. All right, so despite the guild,
Create Destroy Backend Route9:03
Seriously, delete princess. All right, so despite the guild, if I do click on delete princess, uh, nothing is going to happen because we haven't wired up anything in the backend. So this is going to be our next step. Let's go and create a controller action to delete a Puppy. So into the PuppyController, we go and we have an index method like store. And after this store, I think we are going
and we have an index method like store. And after this store, I think we are going to have a delete method. So I'll copy this beautiful comment here and call this one delete. All right, so Coot is going to try to ruin my work, but I want a public function destroy, destroy being the convention verb to remove an entry. And so this is going to receive the request and the puppy, whoops,
And so this is going to receive the request and the puppy, whoops, let's spell it correctly. And while we're going to start very basic and perhaps naively and go puppy->delete(). And because Inertia expects a redirect, we can return redirect() and precisely like this, we're going to redirect to the home route, set the user on the first page of the pagination and have a success flash message puppy deleted successfully.
of the pagination and have a success flash message puppy deleted successfully. Although in my heart this should be a warning or an error because you don't want to delete puppies. Come on. All right, so let's accept that. Maybe I'll put it on multiple lines so it reads a bit nicer. And so this is a slightly naive implementation, it's missing some important parts that we're going to add later, but this is going to get the job done. I believe that with that in place,
to add later, but this is going to get the job done. I believe that with that in place, if we click on the delete button and confirm it, the puppy is gonna be gone. The last thing we need to do is create a route that hits this destroy action on the PuppyController. So let's go in web.php and yeah, we'll definitely assume that you want to be logged in to be able to delete a Puppy. That'll be a first point of security.
to be logged in to be able to delete a puppy. That'll be a first point of security. And so we'll do route delete and the endpoint is going to be puppies/{puppy}. And we are going to hit the destroy action on the PuppyController and name this route puppies.destroy. And I keep saying the last thing I need to do, but there's one more thing I need to do and is to actually hit that route.
but there's one more thing I need to do and is to actually hit that route. Uh, when we click the confirm button. So in my PuppyDelete component that we've created, we need to hook up the action. So I imagine the Alert dialogue action has some onclick event handler that we could use, but we can possibly also make this a button type of submit and wrap all of this in form. This could be handy because if the delete action takes a
of submit and wrap all of this in form. This could be handy because if the delete action takes a little bit of time, we could use the processing state from the useForm hook from Inertia and display a loading spinner and then close the model after the deletion has happened. So let's try do that. So I will wrap maybe not the trigger, but the content here. We are going to have a form inside of it, and here we can use the onSubmit event handler to do what we want to do.
and here we can use the onSubmit event handler to do what we want to do. So like I said, we probably want the, uh, processing state. So const processing equals useForm, indeed well done from Inertia. And the other thing we definitely want is the delete action. To delete the entry. And because delete is a reserved keyword, we kind of need to LES it to destroy like so, so we importing the delete function, which takes a URL.
to LES it to destroy like so, so we importing the delete function, which takes a URL and options, but we renaming it to destroy. So let's go handle, uh, form submit. We are going to capture the event and before we do anything, we're going to e.preventDefault() to prevent the form to actually submitting a GET request. And then we're going to want to use our destroy method, remember, which actually is the delete method. So it wants a URL or a route. And then options.
remember, which actually is the delete method. So it wants a URL or a route. And then options. The route we want to hit is called route('puppies.destroy'). And by the way, if you wonder why I'm not using Wayfinder for things like this, I might do an update to this course or a sequel when Wayfinder and Ranger are out with all the goodness that's been announced at Laracon us. So for now, I'm just going to use magic strings. Sorry Joe.
that's been announced at Laracon us. So for now, I'm just going to use magic strings. Sorry Joe. All right, so that's probably all we need, but let's improve the UX a little bit by making the preserveScroll to true. So we stay at the same level where we're at. So we have a trigger, the user clicks the trigger, it opens the dialogue. Are you sure if they say yes, it's going to submit the form, which is going to trigger this form.submit,
Are you sure if they say yes, it's going to submit the form, which is going to trigger this submit, and we are going to hit the puppies.destroy route, which remember is this one which points to the destroy controller. And then here we're going to delete the puppy and redirect to the homepage the same route and say success, you've killed the puppy. So with a bit of luck, we've one shot at it and when we delete a puppy, it's going to, well delete it,
So with a bit of luck, we've one shot at it and when we delete a puppy, it's going to, well delete it, remove it from the UI and give us the toast notification confirmation. Let's give it a try. All right, moment of truth, who do I delete? Oh my god, I got no choice but deleting myself honestly. So are you sure to delete Simon? Whoops, looks like we need some uh, spacing here. I've broken the UI maybe.
Whoops, looks like we need some uh, spacing here. I've broken the UI maybe. Alright, so I guess that's the alert dialogue content has a flex or grid parent with some gap, which handles the spacing between the header and the footer. But actually what I can do is scope down the form to just be literally the action here. I will wrap the alert dialogue action only in the form.
I will wrap the alert dialogue action only in the form. And so we should have reclaimed our nice spacing. So now we have the header, the footer starts the cancel button, we don't care. And the form is really to have this type submit with one button inside of it. And there you go. This is better. We back to the proper layout. So when I delete Simon, I'm hoping to see the notification.
We back to the proper layout. So when I delete Simon, I'm hoping to see the notification and Simon should be gone. Ready, 3, 2, 1, delete and nothing has happened. Okay? Uh, let's open the console and try again. So I'm going to delete Simon. Ah, here we go. Ah, of course we haven't passed the puppy parameter to the destroy route, which expects it. So here the route puppy is destroy, is expecting a puppy id just like this puppy id and now this time it should work.
So here the route puppy is destroy, is expecting a puppy. Id just like this puppy id and now this time it should work. So let's try again. I'm going to delete myself. And so 3, 2, 1, delete Simon, it's gone. And we have the successful notification noise. All right, this is a great start, but I see two, at least two problems with this implementation a little problem and a very, very big problem. So we're going to leave the big problem for the next lesson.
and a very, very big problem. So we're going to leave the big problem for the next lesson. And that problem is that anyone that is logged in can delete puppies. So I could create a new account with, uh, any username and then delete, uh, Simon's and everyone else's puppies like I feel so, and this is not a good idea. So we're going to leave that practice for the next lesson. But the small problem I want to tackle now is, well,
Delete Attached Images16:33
So we're going to leave that practice for the next lesson. But the small problem I want to tackle now is, well, let me show you remember the delete and use images command that we had created. Well, let's try to run it. And oops, you can see that we have deleted a puppy, but we have not deleted the attached image. And so every time we delete a puppy, we should actually also delete the image. Luckily, this should be a pretty easy fix.
we should actually also delete the image. Luckily, this should be a pretty easy fix. So let's do this to wrap up this video. All right, so we want to remove the image from the public storage. And to do that, we need the file path to this image. Let's go imagePath equals and exactly like this. We are going to grab the puppy image, URL fields, but remove the storage segment on the URL. So this is going to give us something like puppies/
but remove the storage segment on the URL. So this is going to give us something like puppies/webp instead of /storage/puppies. So I don't think that we should delete the image before we delete the puppy because imagine a scenario where the puppy deletion doesn't work, but we've removed the image. That's no good. So maybe let's, now that we have the path to the image, delete the puppy first, and then below that go
That's no good. So maybe let's, now that we have the path to the image, delete the puppy first, and then below that go and delete the image from the file storage if we've deleted the puppy. So I will let the puppy delete action happen. And before the redirect, let's first check if this puppy image exists. So if let's be defensive, we do indeed have an image path and well, I'll read this. That file exists in the public storage disk.
and well, I'll read this. That file exists in the public storage disk. In that case, we are going to delete the file from the public storage disk. And of course, it's all disappeared. Please give it back to me. There we go. So that should work. We grab the image path, we delete the puppy, and then we make sure that the image exists in there. And if it does, we delete it. So let's go try to delete yet another puppy.
And if it does, we delete it. So let's go try to delete yet another puppy. Just before I do that, let's actually delete the unused image by saying yes to the prompt we had open. So to be crystal clear, if I run it, we don't have any unused images and now we're going to, sorry, fancy, delete. Fancy. And we should still have zero images that are unused. Let's try to run the command.
zero images that are unused. Let's try to run the command. And indeed, it has successfully deleted the puppy, but also deleted the image that was attached to the puppy. Great stuff.
