Search Feature Overview0:00
In this episode, we're going to be building a dynamic inline search, very useful for documentation and blogs, especially where third-party search tools like Algolia or Typesense would be an overkill. Here's how it works. Let me search for a term in the posts and if it appears in the title, it gets highlighted. If it appears in the summary, it's highlighted or even if it's found somewhere within inside the post, that part of the post is picked up, it's shown and the search term is highlighted. Notice that I can use my mouse or I can navigate using the keyboard as well. And I can escape to close it and I can again focus back into it to open and I can hit enter to actually navigate to the post, but also it gets saved in my recent searches.
And I can escape to close it and I can again focus back into it to open and I can hit enter to actually navigate to the post, but also it gets saved in my recent searches. So if I try to search again, I'll be able to see the recent searches, click on it and trigger it very easily. Also I can clear the recent searches. All right, so let's build this. This is the starting state. Of course, everything looks the same, but if I start searching, I won't be able to see any highlights here and I'm not able to use any of the keyboard navigation and there are no recent searches as well.
Reviewing Base Component1:23
any highlights here and I'm not able to use any of the keyboard navigation and there are no recent searches as well. So let's start from here and add the features that we need. This is the Livewire component so far. It's a single file component with a single public property search and a computed property results. Here, I'm simply searching for the search term in title, excerpt, content and author name. I'm just using where like here, but if you're concerned about the performance, you can use Laravel Scout or something like that.
I'm just using where like here, but if you're concerned about the performance, you can use Laravel Scout or something like that. Here just for the demo purpose, I'm simply searching and I get the latest published articles and limit them to 10. Down here, all this UI is mostly handled by Alpine. That is, we use the open state to open or close the dropdown. And this is the input which has WireModelLive, the search public property. On focus, it opens and on escape, it closes as you saw. And down here, we do have a loading spinner with WireLoading where wire target is search and also we have the clear button.
And down here, we do have a loading spinner with WireLoading where wire target is search and also we have the clear button. If you didn't see it, it's here. So if I start searching for something, I can just click on that and clear the search. Simple stuff. And here comes the dropdown which transitions and shows when XShow is set to open. If the search term is less than two characters, we show this type at least two characters to search. Otherwise, if the search term is greater than two and there are some results, we show the results.
Adding Keyboard Navigation3:06
Otherwise, if the search term is greater than two and there are some results, we show the results. Right now, as you can see, we show the results count and the list of results with just the title, category, author, name, excerpt else. That is, if the search term is greater than two characters but no results found, we just show a sad face emoji and we say try searching for something else. That's it. So from here, let's first add the keyboard navigation. If you watched Episode 4 Tag Input, it's very similar to how we did it there. But if you didn't, you can watch it now.
If you watched Episode 4 Tag Input, it's very similar to how we did it there. But if you didn't, you can watch it now. Firstly in the input, let's go to the input. I need to bind these to the arrow up and arrow down keys. So here let's use addKeyDown.ArrowDown.Prevent, no, it's not this. Let's just say we use this. Let's just create a method MoveDown. It doesn't exist yet, but I'll be creating it. And similarly, KeyDown.ArrowUp.Prevent is MoveUp. Now the reason we need prevent is because typically in an input, if you press the down
And similarly, KeyDown.ArrowUp.Prevent is MoveUp. Now the reason we need prevent is because typically in an input, if you press the down arrow, it just moves to the end. The cursor moves to the end of the text. And if you press up, it moves to the beginning. So we need to prevent that and call this method. So let's create these two methods at the top, right here. Even before we add the methods MoveUp and MoveDown, we need an index to track the current highlighted post. So let's add HighlightedIndex and set it to minus one.
highlighted post. So let's add HighlightedIndex and set it to minus one. Zero will be the first post. And now let's add the MoveUp method. So here we check if HighlightedIndex is greater than zero, then we decrement it. So we are moving up and I'll add MoveDown. If this HighlightedIndex, no, this will not work. I'll have to check if there is a result after this HighlightedIndex. So if there is a next result, then we will move down. Otherwise, we will not move down.
So if there is a next result, then we will move down. Otherwise, we will not move down. For that, let me first scroll down to the drop-down list, which is here, ul, and add an xref to it so I'm able to reference it. xref equals, let's call it ResultsList. So now in this ResultsList, if there is a child after HighlightedIndex, then we can move down to it. So I will say this.refs.ResultsList.Children. And I check this index, which is Highlighted, yeah, this.HighlightedIndex plus one. So if this item exists, that is the next item exists, then I move down.
And I check this index, which is Highlighted, yeah, this.HighlightedIndex plus one. So if this item exists, that is the next item exists, then I move down. But this result itself might not exist. So let me add a question mark there. Okay, now we have the MoveUp and MoveDown methods. We also have these key bindings. But we also need a way to actually show that highlight. So let's go down to each of the list item. And if this is the Highlighted one, let's add a class Binding. So we can say, bind this class with, you know, just check if this is the HighlightedIndex
And if this is the Highlighted one, let's add a class Binding. So we can say, bind this class with, you know, just check if this is the HighlightedIndex with the LoopIndex. If it is, add something like maybe BGBlue50 at this background so it's highlighted, otherwise do nothing. Or maybe we could move this hover here. So if we hover over the Highlighted item, nothing happens. If this is not the Highlighted item, then we show BGGray50. Let's see if this much works. Refresh.
Let's see if this much works. Refresh. Let me just search for something and try to move using my keyboard. It is working. But if I use my mouse, notice that it still stays right here. We have to add the mouse interaction. We didn't do that. Let's go back. And yeah, right here, I will add this mouse enter and set the HighlightedIndex to this LoopIndex.
And yeah, right here, I will add this mouse enter and set the HighlightedIndex to this LoopIndex. So this way I can either use my mouse or my keyboard. So go back again. Search for something again. Design. Yeah, I can use my keyboard and I can use my mouse. The HighlightedIndex changes. But notice something happening here. After I move to the third item, I am moving to the fourth item, but that doesn't show
But notice something happening here. After I move to the third item, I am moving to the fourth item, but that doesn't show up. It has to scroll into view. So let's go back. And along with the MoveUp and MoveDown, let me create another method which is ScrollToHighlighted. So this is what this method does after the DOM has updated once we increase the HighlightedIndex or decrease it, it finds this HighlightedIndex item and brings it into view with this block as nearest, which means if it's already in the view, it does nothing. But if it's not, then it brings it into the view.
as nearest, which means if it's already in the view, it does nothing. But if it's not, then it brings it into the view. So right after we move up or down, we need to call this ScrollToHighlighted. So let's say this.ScrollToHighlighted and add this here also. We don't need semicolons. OK. And let's try this out. Let me search for LiveWire. And now if I scroll down, it's completely visible. And if I scroll back up, that particular item also becomes visible.
And now if I scroll down, it's completely visible. And if I scroll back up, that particular item also becomes visible. And if it's already visible, you notice no scroll. All right. Now that keyboard navigation is completely working, we can add a footer at the drop-down which shows that you can use the arrow keys to navigate. Just a simple UX add-on, scroll down to the drop-down and right after UL, let me add this footer. All right, let's check. So now if you search for something, you will see this nice footer that says you can navigate
Highlighting Search Matches9:41
All right, let's check. So now if you search for something, you will see this nice footer that says you can navigate using your keyboard. Next, let's highlight the search term wherever it appears. This highlighting can be handled either client-side using Alpine or server-side using LiveWire. Since we are anyway re-rendering the HTML every time the search term changes, we can handle it server-side itself. So in LiveWire, let's add a method highlightMatch where we send the text and we anyway have the search term with us. And we call this little helper method that I have.
the search term with us. And we call this little helper method that I have. Let me show that to you. This little helper method highlightMatch takes in the text and the search term. It just returns if it's less than two, but otherwise it matches this particular search term, replaces it with a mark tag and we see a highlight of yellow color. And here it's just escaping the text for safety. In the method, we are simply calling this highlightMatch with the text and the search. So down here in the drop-down, instead of simply showing the title, the excerpt, we can show the highlighted version instead.
So down here in the drop-down, instead of simply showing the title, the excerpt, we can show the highlighted version instead. So instead of saying post title, we'll have to render the HTML because we have the mark tag and the classes. So here we call the highlightMatch method, which we just created along with the post title. So this way, instead of just displaying the post, it displays the highlighted term within the post. Let's do the same thing. Let me just add this in the new line.
Let's do the same thing. Let me just add this in the new line. So it's more readable. Let's do this for the excerpt. I'll just copy this, paste it here, excerpt and we'll do this for the author name as well. So if somebody searches by author, they should be able to find it. AuthorName. Alright. Name. Let me search for design again and great.
Name. Let me search for design again and great. Design is highlighted in the title. Design is highlighted in title as well as the excerpt here. But notice that there's nothing highlighted in this post. That's because it's probably found somewhere deep in the content and we're not showing the content. So what we can do is let's check if it appears in the excerpt. Great. If it doesn't, let's replace it with the post content instead.
Great. If it doesn't, let's replace it with the post content instead. So here, instead of simply showing the excerpt, let's add a GetSnippet method above here in LiveWire. A GetSnippet method, which checks if the search term exists in the excerpt or not. If it exists, great. We anyway show the excerpt only. If it doesn't, what we do is we use this excerpt helper method, which exists in Laravel and we check if the search term exists in the content. If it does, what we do is we get 50 characters before the search term and 50 characters after
we check if the search term exists in the content. If it does, what we do is we get 50 characters before the search term and 50 characters after the search term and this returns automatically along with the ellipsis. So there is ellipsis at the beginning and at the end. You don't have to do this manually. We can simply use this helper method from Laravel. And if it's not found there also, which means it's probably found in the title or the author name, we simply return the excerpt. So down here in the dropdown, instead of passing the excerpt directly, we use the GetSnippet method now.
So down here in the dropdown, instead of passing the excerpt directly, we use the GetSnippet method now. So we either get the excerpt or the post, depending on whatever is matched. And yeah, we pass the post here. Let's try this out. Let me search design again. And now you notice, you notice this ellipsis, which means it was found somewhere in the content and this is not the excerpt. This is part of the content where we're able to see the search term, which has 50 characters before it and 50 characters after it.
Implementing Recent Searches13:57
This is part of the content where we're able to see the search term, which has 50 characters before it and 50 characters after it. Great. Next, let's add recent searches. So in our component, let's add a public property, recent searches and set it to a blank array. And to persist the recent searches during the session for the user, we can simply use the SessionLiveWire attribute. This will automatically persist the recent searches in the current session. But if you want to persist it for a longer duration, like if this is within a user authenticated area, you can probably use database.
But if you want to persist it for a longer duration, like if this is within a user authenticated area, you can probably use database. But for this demo, I'm just going to continue with the session. So to add a search to the recent searches, we need a method. Let's call it public function add to recent searches. Yeah. I'm just going to replace the content here. Only if the search term is greater than two, we continue. We just make sure that this is case insensitive and we limit it to just five recent searches. Also, while we are adding this to recent search, we don't have to render any HTML.
We just make sure that this is case insensitive and we limit it to just five recent searches. Also, while we are adding this to recent search, we don't have to render any HTML. So let's use the renderless attribute here. And while we are at it, let's also add a clear recent searches method because we have the feature to clear it. All right. Clear recent searches. And this is also renderless. So now when we actually click on one of the results or select it using enter, we should be able to add this to recent searches and we don't have any method right now.
So now when we actually click on one of the results or select it using enter, we should be able to add this to recent searches and we don't have any method right now. Let's add a select highlighted method. So yes, if this highlighted index is greater than zero, we actually get the selected item this way and we can do whatever we want with that selected, like get the URL and navigate it to it. But here in our case, we have to also add it to the recent searches. So we just say dollar wire dot add to recent searches. Yeah. Dollar wire dot search.
Yeah. Dollar wire dot search. And also we can close the dropdown now. So we say this dot open equals false. So now let's bind the input with the enter key right here. We say at key down dot enter dot prevent select highlighted. So we call this method. And also we need to call the method when we click on it using the mouse. That has to be added to the dropdown list item. So here we say at click.
That has to be added to the dropdown list item. So here we say at click. Yeah, we could either do this manually or we can just say select highlighted. Okay. So now when we click on it or select it somehow, it gets added to the recent searches array. But we also need to show that dropdown. So here when the search term is either empty or too short, we either show this or we show the recent searches. So if recent searches exist, we show that, otherwise we showed this text. So let me replace this with this, which is just recent searches or empty state.
So if recent searches exist, we show that, otherwise we showed this text. So let me replace this with this, which is just recent searches or empty state. So this template shows up if there is recent searches, let me scroll down. Yeah. Otherwise it shows this type at least two characters. And here within the recent searches, we just have a button to clear the recent search. Otherwise we have the same list which loops through the recent searches. And if I click on any of the recent search, it just updates the recent search with this term. Now we need to add this method.
term. Now we need to add this method. So let's go back up and add it here. Use search term. Here we simply set the Livewire public property search to this term. And we also add it to recent searches once again. So if I searched the fourth item, it has to again go back on top and also close the dropdown. There's a comma missing here. All right. Let's see.
All right. Let's see. This should work. Let me search for design again and let's use the keyboard this time and I hit enter. Okay. It looks like, yeah, I need to accept dollar term here. Here there's an argument term. Let me search for design again and I'm using the keyboard. Let me hit enter. And yes, it looks like something happened.
Let me hit enter. And yes, it looks like something happened. Let me clear the search, click on it again. And I do see the recent search. If I start typing again, the recent search is gone. Let's search for Livewire this time. Let me use my mouse to click on it. Looks like it worked. So yes, Livewire has also gotten added to the recent search. This one last thing, there was a bug.
Fixing Index Reset Bug19:09
So yes, Livewire has also gotten added to the recent search. This one last thing, there was a bug. I don't know if you noticed. So if I type Livewire and yeah, so notice that the highlighted index is at three already. So if I start searching for something new, the highlighted index should change to, you know, it should reset to minus one and always start fresh. So let's go back and here in the Alpine methods, let's add this to the init. Yeah. So it keeps watching the search public property and as and when it changes, it just resets the highlighted index to minus one.
So it keeps watching the search public property and as and when it changes, it just resets the highlighted index to minus one. This way there are no bugs. Let me search for something and I'll use the keyboard. But when I change the search, it gets reset again. This is fantastic. We have a complete dynamic inline search working with keyboard navigation, highlighted text with all the little details and recent search also. I hope you enjoyed this one.
I hope you enjoyed this one.
