I implemented an autoplete search box on my asp mvc4 site. I am currently able to have the box return results that update as i type in the search box. I also am dynamically generating "category" buttons based on the result "type IDs" and inserting them in a header that appears when the autoplete produces results.
I want to introduce functionality that goes like this: when the user clicks the category button, the existing autoplete results get filtered further so only results of that "type ID" are shown. After that, if the user wants to see all of the results matching the search string again, they can click the "All" button.
To see a working version of this, please check out the search box on Discogs. I have also pasted a screenshot of this widget below, for reference.
How can I implement this? I can't find any stackoverflow posts about this because I don't know how to phrase my question.
My code is below. In it, I already have a functioning autoplete, and I have the portion that dynamically generates the category buttons. Now what I need help with is finding a design pattern to further filter the autoplete results when I click the category buttons that were dynamically generated.
@model myproject.Models.Search_Term
@Scripts.Render("~/bundles/jquery")
<script type="text/javascript">
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// autopopulate input boxes
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//detect the browser resize and close the Autoplete box when that event is triggered
$(window).resize(function() {
$("#searchBox").autoplete("close");
});
//helper method for autopopulate.
//
//this helps in creating a autoplete menu with custom HTML formatting
function monkeyPatchAutoplete() {
$.ui.autoplete.prototype._renderItem = function( ul, item) {
var inner_html = '<img src="' + item.imgPathSmall + '">';
return $("<li>")
.data("ui-autoplete-item", item)
.append(inner_html)
.appendTo(ul);
};
}
// look up search term
$(document).ready(function () {
//call this to enable the autoplete menu with custom HTML formatting
monkeyPatchAutoplete();
//trigger autoplete
$("#searchBox").autoplete({
source: function (request, response) {
$.ajax({
url: "/Explore/SearchAutoplete",
type: "POST",
dataType: "json",
data: { search: request.term },
success: function (data) {
response($.map(data, function (item) {
return {
objectName: item.ObjectName,
detail1: item.Detail1,
detail2: item.Detail2,
detail3: item.Detail3,
imgPathSmall: item.Image_Data_SmallPad_string,
objectType: item.ObjectType,
objectID: item.ObjectID,
image_Data_SmallPad: item.Image_Data_SmallPad,
image_MimeType_SmallPad: item.Image_MimeType_SmallPad
};
}))
}
})
},
select: function (event, ui) {
event.preventDefault();
//redirect to result page
var url;
switch (ui.item.objectType) {
case 1:
url = '@Url.Action("Category1", "Explore")?i=' + ui.item.objectID;
break;
case 2:
url = '@Url.Action("Category2", "Explore")?i=' + ui.item.objectID;
break;
case 3:
url = '@Url.Action("Category3", "Explore")?i=' + ui.item.objectID;
break;
case 4:
url = '@Url.Action("Category4", "Explore")?i=' + ui.item.objectID;
break;
case 5:
url = '@Url.Action("Category5", "Explore")?i=' + ui.item.objectID;
break;
case 6:
url = '@Url.Action("Category6", "Explore")?i=' + ui.item.objectID;
break;
case 7:
url = '@Url.Action("Category7", "Explore")?i=' + ui.item.objectID;
}
window.location.href = url;
}
}).data("ui-autoplete")._renderMenu = function (ul, items) {
//------------------------------------------------------------------------------------
//Append the header
//------------------------------------------------------------------------------------
var header = `
<li>
<div class='acmenu_header'>
<div class="btn-group special" role="group" aria-label="...">
<button type="button" class="btn btn-default btn-xs">All</button>
`;
//helps determine the category buttons to generate
var categories = [];
$.each(items, function (index, item) {
if (item.objectType) {
switch (item.objectType) {
case 1:
categories.push(1);
break;
case 2:
categories.push(2);
break;
case 3:
categories.push(3);
break;
case 4:
categories.push(4);
break;
case 5:
categories.push(5);
break;
case 6:
categories.push(6);
break;
case 7:
categories.push(7);
}
}
});
//helps determine the category buttons to generate
var uniqueCategories = [...new Set(categories)];
var arrayLength = uniqueCategories.length;
//generate the category buttons within the header
for (var i = 0; i < arrayLength; i++) {
switch (uniqueCategories[i]) {
case 1:
header = header + '<button type="button" class="btn btn-default btn-xs">Category1</button>'
break;
case 2:
header = header + '<button type="button" class="btn btn-default btn-xs">Category2</button>'
break;
case 3:
header = header + '<button type="button" class="btn btn-default btn-xs">Category3</button>'
break;
case 4:
header = header + '<button type="button" class="btn btn-default btn-xs">Category4</button>'
break;
case 5:
header = header + '<button type="button" class="btn btn-default btn-xs">Category5</button>'
break;
case 6:
header = header + '<button type="button" class="btn btn-default btn-xs">Category6</button>'
break;
case 7:
header = header + '<button type="button" class="btn btn-default btn-xs">Category7</button>'
}
}
header = header + `
</div>
</div>
</li>
`;
$(ul).append(header);
//------------------------------------------------------------------------------------
//append the autoplete results
var that = this;
var currentCategory = "";
var currentCategoryLabel = "";
$.each(items, function (index, item) {
if (item.objectType != currentCategory) {
if (item.objectType) {
switch (item.objectType) {
case 1:
currentCategoryLabel = "Category1";
break;
case 2:
currentCategoryLabel = "Category2";
break;
case 3:
currentCategoryLabel = "Category3";
break;
case 4:
currentCategoryLabel = "Category4";
break;
case 5:
currentCategoryLabel = "Category5";
break;
case 6:
currentCategoryLabel = "Category6";
break;
case 7:
currentCategoryLabel = "Category7";
}
ul.append("<li class='ui-autoplete-category'>" + currentCategoryLabel + "</li>");
}
currentCategory = item.objectType;
}
that._renderItem(ul, item);
});
//append the footer
var footer = `
<li>
<mark><span class="glyphicon glyphicon-cog" aria-hidden="true"></span> Advanced search</mark>
</li>
`;
$(ul).append(footer);
};
})
</script>
@using (Html.BeginForm("Search", "Explore", FormMethod.Post, new { id = "searchFormNavbar", @class = "nav navbar-form navbar-left", enctype = "multipart/form-data" }))
{
@Html.AntiForgeryToken()
<div class="input-group" id="searchDiv">
@Html.EditorFor(m => Model.SearchTerm, new { htmlAttributes = new { @class = "form-control", @id = "searchBox", placeholder = "Search x, y, z, and more...", style = "width:100%; min-width: 380px;" } })
<div class="input-group-btn">
<button id="searchBtn" class="btn btn-default" type="submit" style="color:steelblue">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
</button>
</div>
</div>
}
I implemented an autoplete search box on my asp mvc4 site. I am currently able to have the box return results that update as i type in the search box. I also am dynamically generating "category" buttons based on the result "type IDs" and inserting them in a header that appears when the autoplete produces results.
I want to introduce functionality that goes like this: when the user clicks the category button, the existing autoplete results get filtered further so only results of that "type ID" are shown. After that, if the user wants to see all of the results matching the search string again, they can click the "All" button.
To see a working version of this, please check out the search box on Discogs.. I have also pasted a screenshot of this widget below, for reference.
How can I implement this? I can't find any stackoverflow posts about this because I don't know how to phrase my question.
My code is below. In it, I already have a functioning autoplete, and I have the portion that dynamically generates the category buttons. Now what I need help with is finding a design pattern to further filter the autoplete results when I click the category buttons that were dynamically generated.
@model myproject.Models.Search_Term
@Scripts.Render("~/bundles/jquery")
<script type="text/javascript">
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// autopopulate input boxes
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//detect the browser resize and close the Autoplete box when that event is triggered
$(window).resize(function() {
$("#searchBox").autoplete("close");
});
//helper method for autopopulate.
//https://stackoverflow./questions/2435964/how-can-i-custom-format-the-autoplete-plug-in-results
//this helps in creating a autoplete menu with custom HTML formatting
function monkeyPatchAutoplete() {
$.ui.autoplete.prototype._renderItem = function( ul, item) {
var inner_html = '<img src="' + item.imgPathSmall + '">';
return $("<li>")
.data("ui-autoplete-item", item)
.append(inner_html)
.appendTo(ul);
};
}
// look up search term
$(document).ready(function () {
//call this to enable the autoplete menu with custom HTML formatting
monkeyPatchAutoplete();
//trigger autoplete
$("#searchBox").autoplete({
source: function (request, response) {
$.ajax({
url: "/Explore/SearchAutoplete",
type: "POST",
dataType: "json",
data: { search: request.term },
success: function (data) {
response($.map(data, function (item) {
return {
objectName: item.ObjectName,
detail1: item.Detail1,
detail2: item.Detail2,
detail3: item.Detail3,
imgPathSmall: item.Image_Data_SmallPad_string,
objectType: item.ObjectType,
objectID: item.ObjectID,
image_Data_SmallPad: item.Image_Data_SmallPad,
image_MimeType_SmallPad: item.Image_MimeType_SmallPad
};
}))
}
})
},
select: function (event, ui) {
event.preventDefault();
//redirect to result page
var url;
switch (ui.item.objectType) {
case 1:
url = '@Url.Action("Category1", "Explore")?i=' + ui.item.objectID;
break;
case 2:
url = '@Url.Action("Category2", "Explore")?i=' + ui.item.objectID;
break;
case 3:
url = '@Url.Action("Category3", "Explore")?i=' + ui.item.objectID;
break;
case 4:
url = '@Url.Action("Category4", "Explore")?i=' + ui.item.objectID;
break;
case 5:
url = '@Url.Action("Category5", "Explore")?i=' + ui.item.objectID;
break;
case 6:
url = '@Url.Action("Category6", "Explore")?i=' + ui.item.objectID;
break;
case 7:
url = '@Url.Action("Category7", "Explore")?i=' + ui.item.objectID;
}
window.location.href = url;
}
}).data("ui-autoplete")._renderMenu = function (ul, items) {
//------------------------------------------------------------------------------------
//Append the header
//------------------------------------------------------------------------------------
var header = `
<li>
<div class='acmenu_header'>
<div class="btn-group special" role="group" aria-label="...">
<button type="button" class="btn btn-default btn-xs">All</button>
`;
//helps determine the category buttons to generate
var categories = [];
$.each(items, function (index, item) {
if (item.objectType) {
switch (item.objectType) {
case 1:
categories.push(1);
break;
case 2:
categories.push(2);
break;
case 3:
categories.push(3);
break;
case 4:
categories.push(4);
break;
case 5:
categories.push(5);
break;
case 6:
categories.push(6);
break;
case 7:
categories.push(7);
}
}
});
//helps determine the category buttons to generate
var uniqueCategories = [...new Set(categories)];
var arrayLength = uniqueCategories.length;
//generate the category buttons within the header
for (var i = 0; i < arrayLength; i++) {
switch (uniqueCategories[i]) {
case 1:
header = header + '<button type="button" class="btn btn-default btn-xs">Category1</button>'
break;
case 2:
header = header + '<button type="button" class="btn btn-default btn-xs">Category2</button>'
break;
case 3:
header = header + '<button type="button" class="btn btn-default btn-xs">Category3</button>'
break;
case 4:
header = header + '<button type="button" class="btn btn-default btn-xs">Category4</button>'
break;
case 5:
header = header + '<button type="button" class="btn btn-default btn-xs">Category5</button>'
break;
case 6:
header = header + '<button type="button" class="btn btn-default btn-xs">Category6</button>'
break;
case 7:
header = header + '<button type="button" class="btn btn-default btn-xs">Category7</button>'
}
}
header = header + `
</div>
</div>
</li>
`;
$(ul).append(header);
//------------------------------------------------------------------------------------
//append the autoplete results
var that = this;
var currentCategory = "";
var currentCategoryLabel = "";
$.each(items, function (index, item) {
if (item.objectType != currentCategory) {
if (item.objectType) {
switch (item.objectType) {
case 1:
currentCategoryLabel = "Category1";
break;
case 2:
currentCategoryLabel = "Category2";
break;
case 3:
currentCategoryLabel = "Category3";
break;
case 4:
currentCategoryLabel = "Category4";
break;
case 5:
currentCategoryLabel = "Category5";
break;
case 6:
currentCategoryLabel = "Category6";
break;
case 7:
currentCategoryLabel = "Category7";
}
ul.append("<li class='ui-autoplete-category'>" + currentCategoryLabel + "</li>");
}
currentCategory = item.objectType;
}
that._renderItem(ul, item);
});
//append the footer
var footer = `
<li>
<mark><span class="glyphicon glyphicon-cog" aria-hidden="true"></span> Advanced search</mark>
</li>
`;
$(ul).append(footer);
};
})
</script>
@using (Html.BeginForm("Search", "Explore", FormMethod.Post, new { id = "searchFormNavbar", @class = "nav navbar-form navbar-left", enctype = "multipart/form-data" }))
{
@Html.AntiForgeryToken()
<div class="input-group" id="searchDiv">
@Html.EditorFor(m => Model.SearchTerm, new { htmlAttributes = new { @class = "form-control", @id = "searchBox", placeholder = "Search x, y, z, and more...", style = "width:100%; min-width: 380px;" } })
<div class="input-group-btn">
<button id="searchBtn" class="btn btn-default" type="submit" style="color:steelblue">
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
</button>
</div>
</div>
}
Share
Improve this question
asked Jan 17, 2020 at 14:45
sion_cornsion_corn
3,1518 gold badges42 silver badges68 bronze badges
8
- I'm checking your site (discogs.) and already have that functionallity. Can you elaborate? – M. Ruiz Commented Jan 21, 2020 at 19:32
- you did not prehend what I wrote above. I am saying that I want to implement the same type of autoplete as on Discogs.. Discogs is not my site. I want to create an autoplete widget on my site that functions the same way as the Discogs autoplete. – sion_corn Commented Jan 21, 2020 at 21:42
- I'm not much of a java coder, I found your question because of the C# tag. If I understand correctly, you already have the two functionalities you need functioning, you just seem to be having trouble connecting them, is that right? – Matheus Rocha Commented Jan 21, 2020 at 22:57
-
@MatheusRocha - Kind of, yes. I have the autoplete functionality working, and I also have the dynamic creation of the filter buttons within the autoplete working. What I need to figure out now is how to hide/remove the autoplete results that do not match the
ui.item.objectType
associated with the corresponding filter button. by the way, this question has nothing to do with java. the script you see above is javascript/jquery/ajax – sion_corn Commented Jan 21, 2020 at 23:14 - I'll post ment because I think this hardly qualifies as a answer as I have no experience with ajax, but, conceptually speaking, can't you trigger a new autoplete search with the additional category filter when the user clicks one of the buttons? You'd basically add a new value to your POST request's data and have it handled on your "SearchAutoplete" script. Tell me if I'm way off, I'm just trying to help. – Matheus Rocha Commented Jan 21, 2020 at 23:58
3 Answers
Reset to default 3 +50You are asking for a design pattern, so here is one:
store the category as custom data-attribute inside the header buttons:
//generate the category buttons within the header for (var i = 0; i < arrayLength; i++) { header += '<button type="button" data-category="' + uniqueCategories[i] + '" '; header += 'class="btn btn-default btn-xs">Category' + uniqueCategories[i] + '</button>'
For the showAll button just set an empty attribute:
<button type="button" class="btn btn-default btn-xs" data-category>All</button>
inside the _renderItem function store the category for each list item:
return $('<li data-category="' + item.objectType + '">')
After that, You can use event delegation to filter the list items:
- set display: block for all list items
- set display: none for the non-matched list items
If You set the styles in a strict loop, there will be just only one redraw* in the browser:
// this function can be further optimized
function applyFilter(c) {
$("li[data-category]").show().each(function() {
if(c && c != $(this).data("category")) {
$(this).hide();
}
});
}
$(document).on("click", ".btn[data-category]", function(e){
// category shall be undefined to show all items, otherwise 1,2, and so on
applyFilter($(this).data("category"));
})
Feel free to ask for more details, but I believe, You will get the idea.
(*) Is DOM rendering GUARANTEED to block during a single (synchronous) function's execution?
Call a backend api, get autoplete results and put them in an array. I assume, that functionality doesn't need to be written out here (if you need help with that, please let me know). Once you have an array with your results you can use https://www.w3schools./jsref/jsref_filter.asp
Basically you can filter your array with a logic you can implement and make custom to your needs. As a simple example:
var ages = [32, 33, 12, 40];
function checkAdult(age) {
return age >= document.getElementById("ageToCheck").value;
}
function myFunction() {
document.getElementById("demo").innerHTML = ages.filter(checkAdult);
}
.filter uses the function checkAdult to only return elements in the array that are bigger than the ageToCheck. Here you can easily implement logic to filter your results locally, to prevent repetitive calls to the backend.
Hope this helps! If you need further details, let me know. :)
Considering your ment:
Kind of, yes. I have the autoplete functionality working, and I also have the dynamic creation of the filter buttons within the autoplete working. What I need to figure out now is how to hide/remove the autoplete results that do not match the ui.item.objectType associated with the corresponding filter button. by the way, this question has nothing to do with java. the script you see above is javascript/jquery/ajax
this is exactly that.
As you suggested yourself, the best method is, in fact, to keep the autoplete search results cached and do further filtering locally. I have no experience with AJAX or Javascript, but I can exemplify some simple concepts in C# that might help you with it.
For starters, I see in your code you have a method to do the autoplete search. You seem to use lambda expressions a lot, however you might want to extract named methods from them. What I would do is have the autoplete search method return an array of objects containing your matching results. Like I said, unfamiliar with AXAJ, but in ASP.NET there are somethings you need to note when saving local temp data. One of them is the page cycle, the other is variable persistency. I'm assuming you're familiar with your environment, and know how to properly store data so that it properly persists through the page's lifetime. I'd use a session variable for this.
You could have a filtering function which would be something like this:
// For the sake of prehensability, let's assume your result object type is called ACResult, and ACResult.ObjectType is an integer as a type ID.
internal ACResult[] FilterResults(int objType)
{
return cachedResults.Where((result) => { return result.ObjectType == objType; }).ToArray();
}
In the example above, cachedResults is assumed to be an array with you search result items. It uses LINQ's extension method Where<T>(Func<T, bool> predicate)
The ToArray<T>()
method is there because the Where
method will return an IEnumerable
which needs to be turned back into an array to conform with the methods return type. LINQ or some equivalent might not be available. The simpler alternative is to iterate over them and select the ones that match the desired type:
internal ACResult[] FilterResults(object objType)
{
List<ACResult> ret = new List<ACResult>();
foreach (ACResult result in cachedResults)
{
if (result.ObjectType == objType)
ret.Add(result);
}
return ret.ToArray();
}
Each category button would call the same eventhandler with the type ID as parameter, which will call this method to filter the cached results and re-render the autoplete suggestion list with the returned results. Something like:
void CategoryButton_OnClicked(int objType)
{
ACResult[] matchingCategory = FilterResults(objType);
if (objType != -1) // Assuming -1 means it's your "All" button.
{
//Pseudo function encapsulating your suggestion list rendering:
ReRenderSuggestionList(matchingCategory);
}
else
{
//Your "All" button clears the cat filtering without reloading the results, which is why the FilterResults method doesn't alter the original "cachedResults" array.
ReRenderSuggestionList(cachedResults);
}
}
It may not be much of a help, but I'm really trying to help you here. It intrigues me how something that might be trivial in some environments can turn into a real problem in others, specially in web apps, which is probably why I stay away from them. My focus is WinForms, Class Libraries (.NET (Framework | Core | Standard)) and UWP apps.