I think I came up with something cooler and more relevant to what I actually needed to do - it matches at the beginning of the value string or anywhere in the product_desc string.
The forked jsFiddle is highly commented so those wanting to just do a lookup on two values (where the match is at the beginning of either string), can comment out some lines and achieve that effect.
I also implemented a number of other features:
- Match highlighting
- Display number of matches
- Scrollable results area
- Populate page elements with multiple values from an object
- Templated results
- Multiple instances on one page
These are also heavily commented so they can be removed or modified if required.
I would still appreciate confirmation that this is a relatively clean solution that does not clutter up the dom with two many divs (autocomplete seems to add lots of unused divs to the dom?)
JS
// BEGIN show message prompt requiring 2 characters $(".my_lookup").on("keyup", function() { var product_container = $(this).parent(); var $matches_count = product_container.find(".matches_count"); var $matches_text = product_container.find(".matches_text"); var input_length = $(this).val().length; if (input_length < 2 && input_length !== 0) { $(this).siblings(".matches_prompt").text("search requires min 2 characters").show(); $matches_count.text("").hide(); $matches_text.text("").hide(); } else if (input_length === 0 || input_length >= 2) { $(this).siblings(".matches_prompt").hide(); } }); // END show message prompt requiring 2 characters // BEGIN look up on two values solution // see: https://stackoverflow.com/a/15849692 function lightwell(request, response) { // request.term is what you typed // response is the data to suggest to the user // added the 'match_type' argument to hasMatch() to: // - check for match at beginning of object's 'value' string // - check for match anywhere in object's 'product_desc' string // BEGIN hasMatch() function function hasMatch(s, match_type) { // to match beginning of string only, // use: === 0 // see: https://stackoverflow.com/a/12482182 // originally was !==-1 // to match anywhere in the string, // use !==-1 // when match_type === "start", // below returns true if request.term is at the 0 index // of the object.value or object.product_desc string // when match_type === "anywhere", // returns true if at any index other than no index // original usage (anywhere) below // (remove 'match_type' argument and references to it): // return s.toLowerCase().indexOf(request.term.toLowerCase()) !== -1; // added conditional handling // BEGIN if match_type === "start" if (match_type === "start") { // check if typed in term is at the 0 index of object's 'value' string var is_at_start_of_string = s.toLowerCase().indexOf(request.term.toLowerCase()) === 0; if (is_at_start_of_string === true) { console.log("typed term is at start of value string"); } return is_at_start_of_string; } // END if match_type === "start" // BEGIN if match_type === "anywhere" else if (match_type === "anywhere") { // check if typed in term is at any index of object's 'product_desc' string var exists_in_string = s.toLowerCase().indexOf(request.term.toLowerCase()) !== -1; if (exists_in_string === true) { console.log("typed term exists in product_desc string"); } return exists_in_string; } // END if match_type === "anywhere" } // END hasMatch() function // declare variables, i and obj are undefined, matches is [] var i, obj, matches = []; // BEGIN get the index of the input being used // see: https://stackoverflow.com/a/11278006/1063287 var current_input_index = $(document.activeElement).parent().index() - 1; // END get the index of the input being used // BEGIN get refererences to dproduct relative dom elements var $product_container = $(".product_container").eq(current_input_index); var $matches_count = $product_container.find(".matches_count"); var $matches_text = $product_container.find(".matches_text"); // END get refererences to dproduct relative dom elements // BEGIN if the typed in term is nothing // pass an empty array to response() if (request.term === "") { console.log("this thing is happening"); // hide the matches count display $matches_count.text("").hide(); $matches_text.text("").hide(); response([]); return; } // END if the typed in term is nothing // get length of array_of_objects var array_of_objects_length = array_of_objects.length; // for each object in the array, call the hasMatch() function // and pass it the object's 'value' and 'product_desc' strings for (i = 0; i < array_of_objects_length; i++) { obj = array_of_objects[i]; // if either of the below conditions return true, // push the object to the matches array if (hasMatch(obj.value, "start") || hasMatch(obj.product_desc, "anywhere")) { matches.push(obj); } } // pass the matches array to response() // get the count of matches for display var matches_count = matches.length; console.log("matches length is: " + matches_count) if (matches_count === 0 || matches_count > 1) { var matches_text = " matches" } else if (matches_count === 1) { var matches_text = " match" } // display the count of matches $matches_count.text(matches_count).show(); $matches_text.text(matches_text).show(); response(matches); } // END look up on two values solution // BEGIN autocomplete $(".my_lookup").autocomplete({ // only show 5 results with scrollbar // from: http://anseki.github.io/jquery-ui-autocomplete-scroll maxShowItems: 5, source: lightwell, minLength: 2, // called on input blur if value has changed change: function(event, ui) { var product_container = $(this).closest(".product_container"); var $matches_count = product_container.find(".matches_count"); var $matches_text = product_container.find(".matches_text"); $matches_count.text("").hide(); $matches_text.text("").hide(); }, // called when selecting an option select: function(event, ui) { // get references to product relative selectors var product_container = $(this).closest(".product_container"); var product_desc = product_container.find(".product_desc"); var product_type = product_container.find(".product_type"); var product_attr_01 = product_container.find(".product_attr_01"); var product_attr_02 = product_container.find(".product_attr_02"); var $matches_count = product_container.find(".matches_count"); var $matches_text = product_container.find(".matches_text"); $matches_count.text("").hide(); $matches_text.text("").hide(); // add object's values to relative product container inputs product_desc.val(ui.item.product_desc); product_type.val(ui.item.product_type); product_attr_01.val(ui.item.product_attr_01); product_attr_02.val(ui.item.product_attr_02); // BEGIN animate realtive inputs when selecting an option product_desc.animate({ "backgroundColor": "#d0e4ff" }, { "queue": false, "duration": 800 }); product_desc.animate({ "borderColor": "#7190dd" }, { "queue": false, "duration": 800 }); product_type.animate({ "backgroundColor": "#d0e4ff" }, { "queue": false, "duration": 800 }); product_type.animate({ "borderColor": "#7190dd" }, { "queue": false, "duration": 800 }); product_attr_01.animate({ "backgroundColor": "#d0e4ff" }, { "queue": false, "duration": 800 }); product_attr_01.animate({ "borderColor": "#7190dd" }, { "queue": false, "duration": 800 }); product_attr_02.animate({ "backgroundColor": "#d0e4ff" }, { "queue": false, "duration": 800 }); product_attr_02.animate({ "borderColor": "#7190dd" }, { "queue": false, "duration": 800 }); setTimeout(function() { product_desc.animate({ "backgroundColor": "#ffff" }, { "queue": false, "duration": 800 }); product_desc.animate({ "borderColor": "#cacaca" }, { "queue": false, "duration": 800 }); product_type.animate({ "backgroundColor": "#ffff" }, { "queue": false, "duration": 800 }); product_type.animate({ "borderColor": "#cacaca" }, { "queue": false, "duration": 800 }); product_attr_01.animate({ "backgroundColor": "#ffff" }, { "queue": false, "duration": 800 }); product_attr_01.animate({ "borderColor": "#cacaca" }, { "queue": false, "duration": 800 }); product_attr_02.animate({ "backgroundColor": "#ffff" }, { "queue": false, "duration": 800 }); product_attr_02.animate({ "borderColor": "#cacaca" }, { "queue": false, "duration": 800 }); }, 2000); // END animate realtive inputs when selecting an option }, // show fontawesome loading animation when search starts search: function(event, ui) { console.log("search started"); $(this).closest(".product_container").find(".fa-circle-o-notch").css("visibility", "visible"); }, // hide fontawesome loading animation when search finishes response: function(event, ui) { console.log("search finished"); $(this).closest(".product_container").find(".fa-circle-o-notch").css("visibility", "hidden"); }, // BEGIN add styles to results // from: https://gist.github.com/DBasic/9545690 create: function() { $(this).data('ui-autocomplete')._renderItem = function(ul, item) { // create a new 'value' string for match highlighting // see: https://stackoverflow.com/a/9889056/1063287 var newValueText = String(item.value).replace( // changed "gi" to i so that the match // on instance of search term is limited // to the first match, see: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp new RegExp(this.term, "i"), "<span class='ui-state-highlight'>$&</span>"); // create a new 'description' string for match highlighting var newDescText = String(item.product_desc.toUpperCase()).replace( new RegExp(this.term, "gi"), "<span class='ui-state-highlight'>$&</span>"); return $("<li></li>") .data("ui-autocomplete-item", item) // newValueText below used to be: item.value.toUpperCase() // newDescText below used to be: item.product_desc.toUpperCase() .append("<div class=\"my_result\"><span class=\"helper\"></span><img class=\"result_img\" src=\"https://placeimg.com/30/30/nature\">" + newValueText + " - " + newDescText + "</div>") .appendTo(ul); } } // END add styles to results }); // END autocomplete // new data set generated from https://github.com/Marak/faker.js var array_of_objects = [{ "value": "020101", "product_desc": "Handmade Frozen Pants", "product_type": "Automotive", "product_attr_01": "106.00", "product_attr_02": "171.00" }, { "value": "010101", "product_desc": "Practical Fresh Hat", "product_type": "Movies", "product_attr_01": "589.00", "product_attr_02": "777.00" }, { "value": "7099", "product_desc": "Awesome Concrete Sausages", "product_type": "Grocery", "product_attr_01": "249.00", "product_attr_02": "167.00" }, { "value": "5740", "product_desc": "Sleek Plastic Pizza", "product_type": "Home", "product_attr_01": "924.00", "product_attr_02": "379.00" }, { "value": "7627", "product_desc": "Intelligent Plastic Mouse", "product_type": "Games", "product_attr_01": "71.00", "product_attr_02": "412.00" }, { "value": "6606", "product_desc": "Handmade Steel Pizza", "product_type": "Music", "product_attr_01": "71.00", "product_attr_02": "791.00" }, { "value": "0973", "product_desc": "Intelligent Granite Tuna", "product_type": "Industrial", "product_attr_01": "25.00", "product_attr_02": "441.00" }, { "value": "5453", "product_desc": "Generic Steel Sausages", "product_type": "Sports", "product_attr_01": "887.00", "product_attr_02": "786.00" }, { "value": "3871", "product_desc": "Refined Wooden Keyboard", "product_type": "Sports", "product_attr_01": "897.00", "product_attr_02": "402.00" }, { "value": "9646", "product_desc": "Incredible Soft Chicken", "product_type": "Kids", "product_attr_01": "849.00", "product_attr_02": "438.00" }, { "value": "2948", "product_desc": "Licensed Plastic Gloves", "product_type": "Baby", "product_attr_01": "608.00", "product_attr_02": "748.00" }, { "value": "1561", "product_desc": "Sleek Steel Towels", "product_type": "Music", "product_attr_01": "315.00", "product_attr_02": "332.00" }, { "value": "5012", "product_desc": "Licensed Rubber Computer", "product_type": "Electronics", "product_attr_01": "886.00", "product_attr_02": "738.00" }, { "value": "4827", "product_desc": "Unbranded Wooden Shoes", "product_type": "Shoes", "product_attr_01": "390.00", "product_attr_02": "753.00" }, { "value": "0056", "product_desc": "Handcrafted Fresh Sausages", "product_type": "Shoes", "product_attr_01": "26.00", "product_attr_02": "257.00" }, { "value": "0628", "product_desc": "Fantastic Steel Tuna", "product_type": "Tools", "product_attr_01": "881.00", "product_attr_02": "127.00" }, { "value": "8498", "product_desc": "Gorgeous Soft Fish", "product_type": "Toys", "product_attr_01": "105.00", "product_attr_02": "604.00" }, { "value": "9265", "product_desc": "Gorgeous Wooden Cheese", "product_type": "Clothing", "product_attr_01": "257.00", "product_attr_02": "438.00" }, { "value": "0666", "product_desc": "Small Soft Keyboard", "product_type": "Baby", "product_attr_01": "960.00", "product_attr_02": "852.00" }, { "value": "4628", "product_desc": "Intelligent Plastic Car", "product_type": "Music", "product_attr_01": "598.00", "product_attr_02": "339.00" }, { "value": "3341", "product_desc": "Intelligent Metal Mouse", "product_type": "Garden", "product_attr_01": "92.00", "product_attr_02": "371.00" }, { "value": "6547", "product_desc": "Awesome Concrete Shirt", "product_type": "Health", "product_attr_01": "344.00", "product_attr_02": "145.00" }, { "value": "0803", "product_desc": "Unbranded Metal Chair", "product_type": "Kids", "product_attr_01": "343.00", "product_attr_02": "700.00" }, { "value": "9769", "product_desc": "Awesome Granite Bike", "product_type": "Home", "product_attr_01": "545.00", "product_attr_02": "391.00" }, { "value": "3087", "product_desc": "Refined Wooden Tuna", "product_type": "Industrial", "product_attr_01": "58.00", "product_attr_02": "68.00" }, { "value": "3202", "product_desc": "Small Concrete Gloves", "product_type": "Kids", "product_attr_01": "846.00", "product_attr_02": "60.00" }, { "value": "9638", "product_desc": "Generic Rubber Ball", "product_type": "Garden", "product_attr_01": "160.00", "product_attr_02": "123.00" }, { "value": "4762", "product_desc": "Tasty Frozen Computer", "product_type": "Health", "product_attr_01": "698.00", "product_attr_02": "832.00" }, { "value": "6606", "product_desc": "Rustic Frozen Shirt", "product_type": "Automotive", "product_attr_01": "867.00", "product_attr_02": "92.00" }, { "value": "6853", "product_desc": "Ergonomic Steel Pants", "product_type": "Sports", "product_attr_01": "712.00", "product_attr_02": "378.00" }, { "value": "5418", "product_desc": "Awesome Cotton Cheese", "product_type": "Toys", "product_attr_01": "483.00", "product_attr_02": "918.00" }, { "value": "0230", "product_desc": "Licensed Cotton Towels", "product_type": "Clothing", "product_attr_01": "540.00", "product_attr_02": "415.00" }, { "value": "3975", "product_desc": "Sleek Granite Pants", "product_type": "Outdoors", "product_attr_01": "823.00", "product_attr_02": "331.00" }, { "value": "7581", "product_desc": "Ergonomic Concrete Bacon", "product_type": "Automotive", "product_attr_01": "640.00", "product_attr_02": "718.00" }, { "value": "8550", "product_desc": "Practical Granite Table", "product_type": "Shoes", "product_attr_01": "94.00", "product_attr_02": "487.00" }, { "value": "3358", "product_desc": "Fantastic Plastic Computer", "product_type": "Clothing", "product_attr_01": "448.00", "product_attr_02": "440.00" }, { "value": "4586", "product_desc": "Ergonomic Steel Table", "product_type": "Games", "product_attr_01": "218.00", "product_attr_02": "806.00" }, { "value": "6331", "product_desc": "Intelligent Wooden Gloves", "product_type": "Shoes", "product_attr_01": "236.00", "product_attr_02": "546.00" }, { "value": "2871", "product_desc": "Handcrafted Wooden Salad", "product_type": "Beauty", "product_attr_01": "546.00", "product_attr_02": "259.00" }, { "value": "1648", "product_desc": "Tasty Soft Pants", "product_type": "Kids", "product_attr_01": "641.00", "product_attr_02": "251.00" }, { "value": "8096", "product_desc": "Practical Steel Chair", "product_type": "Toys", "product_attr_01": "609.00", "product_attr_02": "374.00" }, { "value": "5810", "product_desc": "Refined Steel Chicken", "product_type": "Kids", "product_attr_01": "529.00", "product_attr_02": "705.00" }, { "value": "7057", "product_desc": "Tasty Metal Mouse", "product_type": "Garden", "product_attr_01": "911.00", "product_attr_02": "935.00" }, { "value": "2344", "product_desc": "Intelligent Cotton Pizza", "product_type": "Sports", "product_attr_01": "705.00", "product_attr_02": "220.00" }, { "value": "9188", "product_desc": "Awesome Wooden Ball", "product_type": "Movies", "product_attr_01": "896.00", "product_attr_02": "850.00" }, { "value": "1474", "product_desc": "Sleek Plastic Salad", "product_type": "Tools", "product_attr_01": "15.00", "product_attr_02": "668.00" }, { "value": "6513", "product_desc": "Small Soft Chips", "product_type": "Health", "product_attr_01": "433.00", "product_attr_02": "74.00" }, { "value": "4036", "product_desc": "Unbranded Wooden Soap", "product_type": "Grocery", "product_attr_01": "826.00", "product_attr_02": "920.00" }, { "value": "9226", "product_desc": "Licensed Fresh Cheese", "product_type": "Industrial", "product_attr_01": "144.00", "product_attr_02": "102.00" }, { "value": "1944", "product_desc": "Fantastic Rubber Shoes", "product_type": "Electronics", "product_attr_01": "820.00", "product_attr_02": "808.00" }, { "value": "9379", "product_desc": "Small Wooden Tuna", "product_type": "Baby", "product_attr_01": "199.00", "product_attr_02": "160.00" }, { "value": "7888", "product_desc": "Handcrafted Frozen Sausages", "product_type": "Electronics", "product_attr_01": "70.00", "product_attr_02": "419.00" }, { "value": "1941", "product_desc": "Fantastic Granite Car", "product_type": "Grocery", "product_attr_01": "821.00", "product_attr_02": "853.00" }, { "value": "8322", "product_desc": "Licensed Wooden Fish", "product_type": "Games", "product_attr_01": "998.00", "product_attr_02": "703.00" }, { "value": "5586", "product_desc": "Fantastic Cotton Salad", "product_type": "Games", "product_attr_01": "887.00", "product_attr_02": "841.00" }]; /* code to generate the above data set, from: https://github.com/Marak/faker.js var array_of_objects = []; for (i = 0; i < 5; i++) { var obj = {}; obj['value'] = faker.fake("{{finance.mask}}"); obj['product_desc'] = faker.fake("{{commerce.productName}}"); obj['product_type'] = faker.fake("{{commerce.department}}"); obj['product_attr_01'] = faker.fake("{{commerce.price}}"); obj['product_attr_02'] = faker.fake("{{commerce.price}}"); array_of_objects.push(obj); } */