Skip to content

Commit 43109f9

Browse files
feat: search-as-you-type with debounce + race-free responses (fixes empty-query 400) (#24)
* fix: don't fire search request when input is empty Pressing Enter on an empty search bar was firing GET /v1/search?query= which the API rejects with 400 Bad Request. Now guards against empty and whitespace-only values before making the request, and hides any previous results. * feat: debounce search + min query length + race-free responses On top of the empty-query guard, add: - Debounce 300ms: a user typing "load-test-34" fires ONE request instead of 12 (one per character) - Minimum query length of 2: single-character inputs don't fire - Race-free responses: only the latest query's response updates the dropdown; older in-flight responses are ignored (prevents stale results when typing quickly) Input event triggers the debounced search (search-as-you-type), Enter flushes any pending timer and searches immediately.
1 parent 0fb22ab commit 43109f9

1 file changed

Lines changed: 39 additions & 14 deletions

File tree

app/templates/account/components/search.html

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<div class="dropdown w-100">
44
<input id="search-input" class="form-control w-100" type="search" placeholder="Search by participant ID, conference ID, etc." aria-label="Search" aria-haspopup="true">
55
<div id="search-results" class="dropdown-menu w-100">
6-
6+
77
</div>
88
</div>
99
</form>
@@ -13,44 +13,69 @@
1313
var searchInput = $('#search-input')
1414
var searchResults = $('#search-results')
1515

16+
var MIN_QUERY_LENGTH = 2
17+
var DEBOUNCE_MS = 300
18+
19+
var debounceTimer = null
20+
var latestQuery = null
21+
1622
var query = function (val) {
17-
searchInput.attr('disabled', true).removeClass('is-invalid')
18-
console.log(val)
23+
latestQuery = val
24+
searchInput.removeClass('is-invalid')
1925

2026
peermetrics.get(peermetrics.urls['search'], {
2127
query: val
2228
}).then(function (res) {
23-
console.log(res)
29+
if (latestQuery !== val) return
30+
2431
res = res.matches
2532
if (res && res.length) {
2633
searchResults.html(res.map(function (result) {
2734
return `<a class="dropdown-item" href="${result.url}">
28-
${result.name ? result.name + ',' : ''} ${result.id},
35+
${result.name ? result.name + ',' : ''} ${result.id},
2936
<span class="text-muted">${result.type}</span>
3037
</a>`
3138
}))
3239
} else {
3340
searchResults.html('<p class="dropdown-header">Could not find any results for this query</p>')
3441
}
3542
searchResults.addClass('show')
36-
searchInput.one('blur', function (ev) {
43+
searchInput.one('blur', function () {
3744
searchResults.removeClass('show')
3845
})
39-
40-
}).catch(function (res) {
46+
}).catch(function () {
47+
if (latestQuery !== val) return
4148
searchInput.addClass('is-invalid')
42-
}).finally(function () {
43-
searchInput.attr('disabled', false)
4449
})
4550
}
4651

47-
searchInput.on('keydown', function (ev) {
52+
var fire = function () {
53+
clearTimeout(debounceTimer)
54+
debounceTimer = null
55+
var val = searchInput.val().trim()
56+
if (val.length < MIN_QUERY_LENGTH) {
57+
searchResults.removeClass('show')
58+
return
59+
}
60+
query(val)
61+
}
62+
63+
searchInput.on('input', function () {
64+
clearTimeout(debounceTimer)
65+
var val = searchInput.val().trim()
66+
if (val.length < MIN_QUERY_LENGTH) {
67+
latestQuery = null
68+
searchResults.removeClass('show')
69+
return
70+
}
71+
debounceTimer = setTimeout(fire, DEBOUNCE_MS)
72+
})
4873

49-
// the enter key
74+
searchInput.on('keydown', function (ev) {
5075
if (ev.which === 13) {
5176
ev.preventDefault()
52-
query(searchInput.val())
77+
fire()
5378
}
5479
})
5580
})
56-
</script>
81+
</script>

0 commit comments

Comments
 (0)