Introducing Headless UI dropdowns0:07
Welcome back. In this episode we'll take a look at dropdown menus and Headless UI. So have a look at our site for each Post. We see this context menu here, but of course right now it's not functional. If I click on it, it doesn't do anything at all. Now we could take 10 or 15 minutes and implement our own dropdown menu, but that's wildly outside the scope
and implement our own dropdown menu, but that's wildly outside the scope of what we're trying to cover here. So instead, I'm gonna reach for headlessui, which works great with Vue. Let's go to headlessui.com. And if I look for dropdown menu, yeah, this is what we are working with here, something like that, uh, we're gonna reach for, okay, so to get started, we need to pull in headlessui,
Installing Headless UI0:42
we're gonna reach for, okay, so to get started, we need to pull in headlessui, but actually we're gonna use Vue here of course. So if I scroll down, there is a Vue version. So within the terminal, let's paste that in. And while that's doing its thing, if I switch back, here's our basic example and the way this works is Headless UI provides entirely unstyled components. And in that way you can make it look however you want.
un styled components. And in that way you can make it look however you want. You're not locked to a specific aesthetic or theme. In our case though, I do want something that'll just get me up and running as quickly as possible. So I believe if I scroll to the bottom, um, there is a link. Let's see, there is a link to Tailwind UI, which is a paid tool, but don't worry, I believe they have a free dropdown, uh, component that we can reach for.
Copying Tailwind UI menu1:22
but don't worry, I believe they have a free dropdown component that we can reach for. So no purchase required here. Uh, we're just going to snatch a nice little component here. So I'm gonna look for dropdown And you'll see we want the code, but specifically the code for view. And what you'll see is it does behind the scenes use headless UI.
And what you'll see is it does behind the scenes use headlessUI because the same team makes Tailwind and uh, headlessUI. So this is what we want. We want the dark theme and I'm gonna copy it within my editor. Let's go into post view, let's find the dots. Yes, something right here. And yeah, uh, let's just comp that out and I will throw that whole bit of code within here.
that out and I will throw that whole bit of code within here. Let's grab that and move it up to our script properly. Now we can get rid of that. If I switch back, there should be no template. Let's reformat. And then finally, um, right here, this Chevron down icon, we can pull in those icons if we want. I'm just going to ignore that entirely, so we're not gonna reach for that at all.
I'm just going to ignore that entirely, so we're not gonna reach for that at all. Okay, so now we are referencing these components. If I were to switch back to the browser, now sure enough, I see options and we will swap that out. But if I click on it, it just works and we have a styled example to get up and running, we might even want to tweak some of the colors here. And you can, of course, that's the entire point.
to tweak some of the colors here. And you can, of course, that's the entire point. So if I were to switch back for the menu items, yeah, right here you can see it's locked to BG Gray. I dunno, could we do BG pixel dark? We have that one, right? Let's see. Click on it. And now that's kind of dark or we can make it lighter, of course. Now I don't wanna spend too much time on this at all. So if you want, if you're working along
Now I don't wanna spend too much time on this at all. So if you want, if you're working along style these however you like. Okay, so now I'm gonna clean this up. We don't need all of these, but do notice they have an example of how to handle a form and notice the form wraps the menu item. All right, something to keep in mind. So now we just have a single anchor tag. And you know what? I'm not even gonna do any of this dynamic stuff.
So now we just have a single anchor tag. And you know what? I'm not even gonna do any of this dynamic stuff. We'll just clear this out. And this should be something like delete, ultimately delete post. Okay, come back. And now among other things we would have, oops, yeah, there we go. We have an option to delete it. Okay, finally, we actually want this, right?
We have an option to delete it. Okay, finally, we actually want this, right? Instead of uh, a button that says options, it should be, uh, these dots. So perhaps I can grab those, move them in. It looks like Simon had group flex and then gap and then padding. So let's grab all of that and place it on our menu button right there.
and place it on our menu button right there. So do we have a gap? Yeah, we can get rid of that. Padding is duplicated, uh, and of course we can tweak anything else that we need to. Okay, so now come back. There we go. It's slightly different. It has a border again, it doesn't really matter. I can remove all of that stuff, but I don't wanna get bogged down here.
Adding menu actions4:27
I can remove all of that stuff, but I don't wanna get bogged down here. But nonetheless, yeah, I wanted to get rid of the background color, get rid of the hover styles, stuff like that. If I switch back, we mostly have what we had before, but now we have a functioning dropdown menu. So perhaps we could have two here. Let's have one to simply view the Post and you can click on it anyways,
Let's have one to simply view the Post and you can click on it anyways, but if you want it, uh, in a different way, view Post, that would take you directly to uh, route post.show. And I think we have to give it the post profile and the post itself, right? And this will be an Inertia link. Okay, so now if I were to switch back, I can view the Post and I can go back. And next let's implement the delete functionality.
Wiring up delete route5:11
I can view the post and I can go back. And next let's implement the delete functionality. So let's go side by side. If I return to my routes file, let's figure out how we can delete the posts. I know I'm looking for a destroy action. Okay, so it looks like Jeremy uses, again, a Post. He's not really using a restful system, that's okay. People can structure things however they want. Uh, the endpoint is the profile/status/{post},
People can structure things however they want. Uh, the endpoint is the profile status post, and then a URI of destroy and that will hit PostController at destroy. Okay? So if you think about it, if I switch back, all we would need here is a link that's gonna go to uh, route PostDestroy. We're gonna send through all of the um, relevant URI segments and I know what you're thinking by the
relevant URI segments and I know what you're thinking by the way, but we're gonna make this work. So if I refresh and open the dropdown menu for delete, if I inspect the element, you'll see yep, the href is correct status 41/destroy. But of course that's gonna make a GET request, right? And that's not what we want. We're making a POST request here. So as it turns out, with Inertia's link components,
We're making a POST request here. So as it turns out, with Inertia's link components, we can actually, uh, tell it what method, uh, to use here. And in fact, I'll show you, let's go to inertiajs.com, let's review the docs for links. And if we look for a method, yeah, you can see we can set the method to POST and now it will take care of the process of using router.post behind the scenes. Also notice in this case, we want it to be treated
of using router dot, uh, post behind the scenes. Also notice in this case, we want it to be treated as a button rather than an anchor tag. So we should add that as well. Cool. Come back. Method is post and that should be treated as a button. Alright, so let's see, before we even run that, what happens when you hit that destroy action? And yeah, just come along real quick as I learn this, this is my first time reading it.
And yeah, just come along real quick as I learn this, this is my first time reading it and maybe if you didn't want section two, this is your first time as well. Alright, so it looks like Jeremy is grabbing the current user's profile, okay? And he has this success variable that he's including with the response, Jason, or could we successfully delete this? Alright, we're gonna tweak that though.
or could we successfully delete this? Alright, we're gonna tweak that though. He has, uh, this looks like authorization. So is the current user's profile_id the same as the ID of the profile you're trying to delete? If so, then uh, you are deleting the post entirely and then returning a success, okay? So in our case, I'm just gonna do this post->delete() and then return presumably back or a redirect wherever would be appropriate.
and then return presumably back or a redirect wherever would be appropriate. Uh, next I would ideally like that to be, um, locked to an authorization rule or a policy so that I don't have to duplicate that. Uh, next, let's keep going. So repost post, get any reposts where the profileId again matches the current user. So it's finding if there are reposts for this post for the current user and if we found any,
So it's finding if there are reposts for this post for the current user and if we found any, then delete it and redirect back. So why don't we just say delete directly here, but it's possible, um, that there are none, in which case repost where first will return null and then you can't call delete. So I'm gonna add a question mark here to say, okay, if it's not null, then continue on and delete and then redirect back.
if it's not null, then continue on and delete and then redirect back. And then we don't need the variables, so let's get rid of that. So next, uh, we don't need success anymore, we delete the post, then we try to delete any reposts of that post. But notice here we redirect back and then we do it again. There's no reason to do it twice. We can just do it a single time at the bottom.
There's no reason to do it twice. We can just do it a single time at the bottom. Another thing is current profile will be an instance of Profile. Um, and profile will be an instance of Profile. And on Eloquent models, there is an is method and that allows you to rewrite these things so you don't have to reach for the IDs. You could say, well if the current profile is, or actually I'm gonna change this,
You could say, well if the current profile is, or actually I'm gonna change this, if the profile is the current User's profile and behind the scenes, that's still gonna check the IDs, but it just makes it a little more clear. If the the current profile is the same as the profile we're trying to delete, then you are authorized. And that's an important thing. This is the authorization step.
And that's an important thing. This is the authorization step. So with that in mind, I wouldn't mind rewriting this. For example, I kind of wanna say, what am I checking for here? If the current User can update, uh, this Post, right? That is effectively what we're trying to do here. So why don't we instead say, well, if the authenticated User can update the Post then and only on that condition, uh, do we delete it.
can update the Post then and only on that condition, uh, do we delete it and now I can remove that. And then this section doesn't require the same authorization, right? Correct me if I'm wrong here. So to delete the original Post itself, you have to be the person who made it, but you could also be somebody deleting their own repost. And in that case you don't delete the Post itself,
but you could also be somebody deleting their own repost. And in that case you don't delete the Post itself, but you do delete the Post. And this is already scoped to whoever's currently signed in. So there's no authorization issue there, at least I don't think. Yeah. Okay. So I think this ends up looking a little better, but at the moment this will not work because we don't have any kind of gate or policy.
Creating Post policy10:34
but at the moment this will not work because we don't have any kind of gate or policy. So I'm gonna do that now within the console, I'm gonna make a new policy. It's a policy for a Post, and this is related to the Post model. Now, whenever you run this, you're gonna see a bunch of different things here, all these different rules. Um, and this can be useful if, if the logic for each of these things, like the logic for viewing a Post is going
Um, and this can be useful if, if the logic for each of these things, like the logic for viewing a post is going to be different from editing or updating or deleting. In our case though, I often find I don't need all of these, it's just not necessary. So instead, why don't we just have a single one for, uh, like updates or modify or workWith is one I've used in the past. Let's stick with updates. And this means can you edit it,
Let's stick with updates. And this means can you edit it, can you update it, can you delete? It's just anything. Can you work with this post? And we're just gonna call it updates. Alright, so this accepts the current user and this accepts the post and question. And yeah, again, the the logic is going to be return if the post’s profile is the user’s profile, right?
return if the posts profile is the user's profile, right? Is the current user's profile the same as the profile of the post? I guess that's how Jeremy is, um, confirming that. And that would work. So now because we have a policy, it will automatically be inferred, uh, based on how we use this. So when I call can update post behind the scenes, Laravel knows, okay, we need a gate
So when I call canUpdate Post behind the scenes, Laravel knows, okay, we need a gate or a policy, let's see if we have a policy for the Post model and it's gonna look for something that matches this convention within the policies directory. Yes. And next it's gonna call an update method on PostPolicy. This will return a Boolean that indicates the permission and we're off to the races. It works. Finally this variable, uh,
and we're off to the races. It works. Finally this variable, uh, it's only used in one place now. So I don't think we need that anymore. Why don't we swap that out with offUserProfileId? And then next while I'm here, um, I think it would stand to reason that anytime you have a User instance or the current User instance, you always want the profile associated with it, right? So that's something to think about.
you always want the profile associated with it, right? So that's something to think about. Do you always eager load the profile anytime you fetch the current user or any user for that matter? And it could be, yeah, these two are basically linked. Uh, so if that's the case, be a little sparing with this, but if you have a good argument for it, then you could add width to the model and say no. Always eager load the profile. Um, whenever you fetch the user.
Always eager load the profile. Um, whenever you fetch the User. Yeah, so again, be a little careful. Sometimes people are a little too, I dunno, rambunctious about adding this and then suddenly you're eager loading all of this stuff in so many situations where it's not even necessary. Let's open up, um, php artisan tinker and yeah, if I say app\Models\User, gimme the first one. Notice it doesn't have anything, uh, to do with the profile.
and yeah, if I say app\Models\User, gimme the first one. Notice it doesn't have anything, uh, to do with the profile. Of course, I could eager load this on the fly and yeah, often this is what you wanna do, be explicit about what you need. However, in again, in situations where it's like, no, you always want the profile, it's it's, they're, they're linked. Um, you're always referencing them in one way, shape, or form.
Um, you're always referencing them in one way, shape, or form. Well you can add with profile. And now that means yeah, just assume with is always included there. So php artisan tinker, if I run that again, notice I fetched the first User, but I also get the profile along with it. So yeah, I think, I think maybe there's a good argument for including that in this case, but we'll see.
So yeah, I think, I think maybe there's a good argument for including that in this case, but we'll see. Alright, so I think this looks pretty good. Now if I switch back to our project, all right, so I'm gonna create a Post and yet, now if I click on this, uh, I can choose delete. It hits that endpoint, it deletes the post and we're all set to go. Perfect. Why don't we repost? How about this one here? All right, but if I delete this, it should delete the repost.
Hiding unauthorized delete14:19
Perfect. Why don't we repost? How about this one here? All right, but if I delete this, it should delete the repost and that part works as well. Okay, next, to wrap up very quickly, I see this context menu for every single Post, including ones where I am not the author. That's okay if it has things like view post that anyone can click, but just be careful. I'm now showing a delete link for a Post that I do not have authorization to delete.
I'm now showing a delete link for a Post that I do not have authorization to delete. So here's what we can do in these situations. If I come back and I go to that post component, let's scroll down to our menu and yeah, right here, and by the way, indentation is being a little wonky there, not sure what I'm missing there, but it's not working. Anyways, I wanna say, well, I only want to render this menu item if the current user can update the post.
to render this menu item if the current user can update the Post. So it would be cool if maybe I had authorization rules directly on Post, like post can update, and that would be linked to what the current user can do. Uh, that's, that's an often cool approach you can take. So what would we do? We'd go to PostResource right down here. We would add a new rule can, and what are we checking? If you can update the post
We would add a new rule can, and what are we checking? If you can update the post and notice right here, we're gonna do the exact same thing that we did within the controller check. If the authenticated user, uh, can update the post. So this is a resource for a post. So we can access the associated model, in this case, the post instance by saying this resource behind the scenes. And that's made available through, um, the parent. So now if I switch back to post, this should just work.
And that's made available through, um, the parent. So now if I switch back to Post, this should just work. Let's check refresh this post. We didn't create. So if I open this up, I can view it, but I can't delete it. Uh, let's create a dummy post for myself. I did create this, so I should have access to delete and that works as well. And remember, you have to add the authorization on the front end, uh, but also on the backend.
to add the authorization on the front end, uh, but also on the backend. Clearly I can hide that delete link, but that doesn't prevent the user from attempting to make the request themselves behind the scenes, which is why within PostController, uh, within the destroy action, we also perform the authorization. You can do it at the controller level, you can do it as middleware at the route level.
You can do it at the controller level, you can do it as middleware at the route level. It's whatever you want. But don't forget to do this.
