Designing Topic-Based Filtering0:00
Let's get in our head exactly what it is we're after here. In an ideal world, I would click on one of these pills, in an ideal world there'd be a menu with all of the available topics at the top, but to start, I would click on one of these pills, and I would be taken to a filtered index that only showed Posts with the selected topic. How do we actually handle that in the root? We could do it with a query parameter, so ?topic=general would show us general Posts. However, I think that topics are so integral to how Posts are going to work, it's mandatory that a Post has a topic, for example, that we could actually put it as part of the root.
Writing the Filtering Test0:36
However, I think that topics are so integral to how Posts are going to work, it's mandatory that a Post has a topic, for example, that we could actually put it as part of the root itself. So, /posts shows all Posts, but /posts/general would only show Posts with a topic of general. Of course, if I go to that root at the moment, I get a 404. Let's make it work. I want to start with the test. Let's go to the IndexTest, and we could copy this test here and make a few tweaks. It passes posts to the view, but we don't want to pass posts to the view.
Let's go to the index test, and we could copy this test here and make a few tweaks. It passes posts to the view, but we don't want to pass posts to the view. We want to say it can filter to a topic. We'll need to create a topic. We'll name the variable general. I like to do that, by the way. Actually be very specific when working with tests, naming variables. Make it real. If you have a User, name the User Tom or Jerry or Susan or whatever makes sense so that as you read your test, it reads almost like a little story.
If you have a User, name the User Tom or Jerry or Susan or whatever makes sense so that as you read your test, it reads almost like a little story. In this case, we're creating the general topic, so I'll say Topic::factory()->create(), and when I create these three Post models, why don't I say that they're for this particular Topic? And maybe I could create two Post models instead of three because underneath, I'm going to create another three Post models, but these assign their own Topic. I could make that clear by giving them another variable, so perhaps we could say $otherPosts = Post::factory()->count(3)->create(). We still need to load the User and the Topic, and then we get the root. I need to update this root because I'll be passing a Topic into the root, so we'll say.
We still need to load the User and the Topic, and then we get the root. I need to update this root because I'll be passing a topic into the root, so we'll say topic is equal to general, and then we want to assert that it has a paginated resource and the Post has been updated to only include these two posts here rather than all five posts that are actually available. Does that make sense? Let's run the test. It's going to fail. It fails asserting that two arrays are equal. Okay, we are off to the races.
Updating Routes and Controller2:36
It fails asserting that two arrays are equal. Okay, we are off to the races. Let's make this work. We'll need to jump into web.php first because I need to make an alteration to what we have here. I know we refactored this not too long ago to use root resource, and we're now going to have to turn it back, but I have nothing to apologize for because we work to current specifications. Don't try and second guess what's going to come up in the future. Just make the code as pretty and as concise as can be for current requirements, and then
Don't try and second guess what's going to come up in the future. Just make the code as pretty and as concise as can be for current requirements, and then alter them as you come to them, adapt as you meet new challenges. In this case, we need to go back from being a resource to a GET endpoint. Of course, that means we'll need to update this to point to the index method on the PostController, and we'll get rid of the only call here and give it a name of posts.index like so. All right, now we can introduce our optional topic parameter. So forward slash, and then we'll introduce the parameter with our curly braces. It's a topic, and we'll use the question mark to say you don't have to pass it.
So forward slash, and then we'll introduce the parameter with our curly braces. It's a topic, and we'll use the question mark to say you don't have to pass it. So you could just be going to forward slash posts, or you could be going to forward slash posts, forward slash general. Let's jump into the index method, and we can now accept that topic as a parameter. So topic topic equals null, and it's important we include this equals null because again, it's an optional root parameter. To make sure that everything works up to this point, why don't we just dump and die the topic out and then rerun our test? Hmm, root not found exception, post.index not defined.
topic out and then rerun our test? Hmm, RootNotFoundException, post.index not defined. Okay, I've made a mistake here. Ah, I've put post.index. So let's update the root name, rerun the test, and yeah, here's the topic from our test. It is being correctly passed to the controller. So with that in mind, we should be able to update the query that we add to the posts here. I'm going to extract this to its own variable at the top of the controller. We'll say $posts equals, and let's drop this onto its own line so that it's nice and clean for us to extend, and then we'll pass that $post variable into PostResource collection.
We'll say $posts equals, and let's drop this onto its own line so that it's nice and clean for us to extend, and then we'll pass that $post variable into PostResource collection like so. So thinking about this, when we have a topic, we want to update the query to only load posts that belong to the given topic. Well, if you use Eloquent correctly, you can almost get an English sentence out of this because I can use the when method to say, oh, when there's a topic, and then I'll introduce an instance of the Eloquent builder. Let's call this parameter $query. When there's a topic, then I want to filter to posts that belong to,
Let's call this parameter query. When there's a topic, then I want to filter to posts that belong to, and there's a where belongsTo method you can call, and I can pass the topic in. How cool is that? So when I have a topic, I want to run this closure that will filter to posts that belong to the given topic. Let's rerun our test, and it passes. Everything works. However, if I go to the browser and refresh, I still get a 404 here, and that's because it's still searching for a topic on its ID rather than the slug,
Switching to Slug Binding5:40
However, if I go to the browser and refresh, I still get a 404 here, and that's because it's still searching for a topic on its ID rather than the slug, which is what our intention is. So if we go to the IDE and jump into the Topic model, then we can override a method called getRootKeyName, which allows us to change the default key, the column in the database that will be used to search for this model. So instead of using the ID, I'll return slug, and let's update the method signature with a type here as well. Hopefully now, if we come back to the browser and refresh, yeah, there we go.
and let's update the method signature with a type here as well. Hopefully now, if we come back to the browser and refresh, yeah, there we go. It's working. You can see all of the Post here are general, whereas if I click the posts link again to get rid of the topic, they're now all mixed as we might expect. I imagine as well we could say fanfic, and now only fanfiction Posts are going to be displayed. However, we definitely don't want to manually enter the topic in the route. That's no good for the users.
Linking Topic Pills6:35
However, we definitely don't want to manually enter the topic in the root. That's no good for the users. So why don't we update our pills over here so that as we click them, it automatically goes to the correct filter. We'll go to the posts/index.view component, and here is the pill. So let's update the href to be root. We're looking for posts.index, and we pass an object as the second parameter, setting the topic to post.topic, and rather than using the ID,
and we pass an object as the second parameter, setting the topic to post.topic, and rather than using the ID, of course, we want to pass the slug. Okay, let's come and refresh. I'll click maybe questions, and yes, we have correctly gone to the post/questions endpoint, and I only see posts that have a topic of question. Let's check that pagination works. Yep, that's working very well. If I click posts again, I see all my posts.
Yep, that's working very well. If I click posts again, I see all my posts. Let's click the conspiracies. Yep, here we go. Conspiracies. I only see conspiracy theories. Awesome. That's working really well. It would be good to have some form of indication about the currently selected topic.
Showing Selected Topic UI7:40
It would be good to have some form of indication about the currently selected topic. In order to do that, we're going to have to pass a topic down into the view, so let's write a test to ensure that that functionality works. We'll jump back into IndexTest, and yeah, I can probably copy and paste this and just alter it slightly. Rather than it can filter to a topic, we want to say it passes the topic to the view. Let's say the selected topic to the view. I don't actually need any posts to be created.
Let's say the selected topic to the view. I don't actually need any posts to be created because we're not checking the posts, so I can get rid of all of that. Let's rename this variable to topic, and of course, we still want to load the topic when we call the get function. However, rather than saying assertHasPaginatedResource, I can say assertHasResource, and we'll check for a property called selectedTopic. selectedTopic should be equal to topicResource, make passing in the topic, and that should be the whole test.
Selected topic should be equal to topic resource, make passing in the topic, and that should be the whole test. Let's run it. It fails. Let's fix it. PostController, and down here we have posts, but I'll also introduce selectedTopic. Let's wrap this in a closure so that we can only load posts when moving through pagination rather than having to also load the selectedTopic data as we discussed a number of episodes.
when moving through pagination rather than having to also load the selected topic data as we discussed a number of episodes ago with the inertia only parameter. So selectedTopic, remembering that topic can be null, why don't we use a ternary check? So if there is a topic, we'll wrap a TopicResource in that topic. Otherwise, we're just going to return null. Rerun the test. It passes, meaning that we can update our front end. So we'll jump back into the PostIndex page,
It passes, meaning that we can update our front end. So we'll jump back into the post index page, and we'll come down here. We accept posts, but we're now also going to accept a selectedTopic, and maybe we could show this inside a div at the top as a page heading. I think we actually have, yeah, we're starting to build up a number of H1s across our application, so let's extract this into its own view component. Come up to the components directory.
so let's extract this into its own view component. Come up to the components directory. We'll create a new file called pageheading.view. We need a template tag. Drop the H1 in there, and then let's use a slot here. Okay, that should be everything we need for this component, so we'll go back to create, and we'll update this one. PageHeading inside, create a Post, and we can get rid of that. We also have this one here inside post show, so again, PageHeading, and I want to pass in post.title,
We also have this one here inside Post show, so again, pageHeading, and I want to pass in post.title, and then finally we go back to our index, and here I can make use of pageHeading. I need to set this based on whether there is a selected topic or not, so I'll use vText. vText equals, well, is there a selected topic? If so, selectedTopic.name. Otherwise, why don't we output all posts like that? Okay, what does that look like?
Otherwise, why don't we output all posts like that? Okay, what does that look like? Conspiracies, all posts, general, fan fiction. That's wonderful. Of course, these topics have descriptions, so we could output that as well. Let's use a <p> tag, and we can say selectedTopic.description, but we only want to show this if there is a selectedTopic, so v-if, selectedTopic. How does that look?
so vif, selected topic. How does that look? Very nice. Let's say class, maybe we could do mt1, text-gray-600, and text-small. Yeah, that's nice, and we need a little bit of spacing, I think, as well, so let's add mt4 here. Okay, yeah, that's looking great. Let's go back to all posts, make sure we've not broken anything there. Very nice, that's working wonderfully, and I think rather than having to intuit that you have to click this post link,
Very nice, that's working wonderfully, and I think rather than having to intuit that you have to click this post link, we should just have a link at the top when you're on a topic that goes back to all posts. So again, that should be pretty straightforward. We'll introduce a link. The href can be the root of post.index. We'll say back to all posts. Yep, and then we'll just style it up a little. Maybe we could say text-indigo-500,
Yep, and then we'll just style it up a little. Maybe we could say text-indigo-500, whereas when you hover, we'll set it to text-indigo-700. All right, and then a little bit of spacing, so we'll make it a block level element, and then we'll say margin-bottom-2. Nice, so I'm on questions. If I click this, I go straight back to all posts. That's wonderful.
That's wonderful.
