How to code: A responsive search bar in SvelteKit

Published at Feb 12, 2024

#sveltekit#guides#osm

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:

alt text

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}

Olek site © 2024

Your Icon Your Icon