How to code: A responsive search bar in SvelteKit
Published at Feb 12, 2024
See demo
Introduciton
When I was coding my map making website got to an issue of how to create a responsive location search box like one on Google maps:
Important feature to me was that results would be shown as user is typing. I didn’t want the user to click Search
button and wait for the results.
API endpoint
For the location search endpoint I will be using Nominatim API . An Open-source geocoding using OpenStreetMap data.
Data structure
Quering /search
endpoint returns a list of places as JSON object. See docs for more details. For me the interesing bits were:
- Name
- Latitude and Longitude
- Address type
The latter is useful when e.g. a city and state share the same name like “New York”.
The following response I mapped to TypeScript interface in $lib/osm_nominatim.ts
export interface OSMNominatimPlace {
addresstype: string
boundingbox: number[]
class: string
display_name: string
lat: string
lon: string
name: string
type: string
}
Fetching places
Nothing too fancy here, prepared a async function that makes API call and casts responses to OSMNominatimPlace
interface.
export const getLocations = async (query: string): Promise<OSMNominatimPlace[]> => {
const url = `https://nominatim.openstreetmap.org/search?q=${query}&limit=10&format=json`
const response = await (await fetch(url)).json()
const locations = response as OSMNominatimPlace[]
return locations
}
Debouncing
Having prepared above functions I thought of fetching location on every key stroke, but I quickly realised that I need debouncing.
Depending on latency fetch request back and forth can take 1000ms. People typing quickly can type quicker than that.
For example take a user looking for “London”. So they type each letter every ~200ms. When doing requests on each keystroke you would clog UI with subsequent API calls for “L”, “Lo”, “Lon”, “Lond” etc.
It makes sense to “debounce” user input which effectively means wait small delay when user stops typing. This code found at great blog post by Justin Ahinon Check out his posts for more Svelte content.
export const debounce = (callback: Function, wait = 300) => {
let timeout: ReturnType<typeof setTimeout>
return (...args: any[]) => {
clearTimeout(timeout)
timeout = setTimeout(() => callback(...args), wait)
}
}
Note: When your API is not free you can save a lot with debouncing.
Displaying results in UI
Having all building blocks, now it’s just a matter of putting them together in single .svelte
component.
Again see demo to check how it works in practice.
<script lang="ts">import { debounce } from "$lib/index";
import { getLocations } from "$lib/osm_nominatim";
let foundLocations = [];
let query = "";
const searchLocations = async (event) => {
foundLocations = await getLocations(query);
};
</script>
<input
type="search"
placeholder="Search location"
bind:value={query}
//search location on each keystroke with debounce
on:input={debounce(searchLocations)}
/>
{#each foundLocations as location}
<ul>
<li>
{location.display_name}
</li>
</ul>
{/each}