Skip to content

Commit b7b0964

Browse files
committed
Add package search suggestions using datalist
1 parent 3fe88f1 commit b7b0964

File tree

3 files changed

+67
-0
lines changed

3 files changed

+67
-0
lines changed

internal/ui/packagelist/templates.templ

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,14 @@ templ PackagesContent(list *packages.PackagePopularityList, query string, curren
5757
name="query"
5858
placeholder="Search packages"
5959
value={ query }
60+
list="package-suggestions"
6061
/>
6162
if compare != "" {
6263
<input type="hidden" name="compare" value={ compare }/>
6364
}
6465
<button class="btn btn-primary d-none d-sm-block" type="submit">Search</button>
6566
</div>
67+
<datalist id="package-suggestions"></datalist>
6668
</form>
6769
if len(list.PackagePopularities) > 0 {
6870
@PackageTable(list.PackagePopularities, query, currentOffset, limit, compare)

src/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ import "./components/country-map.ts";
55
import "./compare-packages-redirect";
66
import "./packages-redirect";
77
import "./tab-nav";
8+
import "./package-search-suggest";

src/package-search-suggest.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
const DEBOUNCE_MS = 200;
2+
const MIN_QUERY_LENGTH = 2;
3+
const SUGGESTION_LIMIT = 10;
4+
5+
const input = document.querySelector<HTMLInputElement>("#package-search");
6+
const datalist = document.querySelector<HTMLDataListElement>(
7+
"#package-suggestions",
8+
);
9+
10+
if (input && datalist) {
11+
let controller: AbortController | null = null;
12+
let timer: ReturnType<typeof setTimeout>;
13+
14+
const isSuggestion = (query: string): boolean =>
15+
datalist.querySelector(`option[value="${CSS.escape(query)}"]`) !== null;
16+
17+
const fetchSuggestions = async (
18+
query: string,
19+
signal: AbortSignal,
20+
): Promise<string[]> => {
21+
const res = await fetch(
22+
`/api/packages?query=${encodeURIComponent(query)}&limit=${SUGGESTION_LIMIT}`,
23+
{ signal },
24+
);
25+
const data = await res.json();
26+
return data.packagePopularities.map((p: { name: string }) => p.name);
27+
};
28+
29+
const updateDatalist = (names: string[]): void => {
30+
datalist.innerHTML = names
31+
.map((name) => `<option value="${name}"></option>`)
32+
.join("");
33+
};
34+
35+
input.addEventListener("input", () => {
36+
clearTimeout(timer);
37+
const query = input.value.trim();
38+
39+
if (isSuggestion(query)) {
40+
input.form?.submit();
41+
return;
42+
}
43+
44+
if (query.length < MIN_QUERY_LENGTH) {
45+
updateDatalist([]);
46+
return;
47+
}
48+
49+
timer = setTimeout(async () => {
50+
controller?.abort();
51+
controller = new AbortController();
52+
53+
try {
54+
const names = await fetchSuggestions(query, controller.signal);
55+
updateDatalist(names);
56+
} catch (e) {
57+
if (e instanceof DOMException && e.name === "AbortError") {
58+
return;
59+
}
60+
throw e;
61+
}
62+
}, DEBOUNCE_MS);
63+
});
64+
}

0 commit comments

Comments
 (0)