Today, I want to go through how you can implement a search page that reacts to changes to the url with sveltekit. Reacting to URL change rather than another event such as a form submit keeps implementation simple as the load event becomes the main driver for the search behaviour.

Getting Started

First’s things first, follow the Getting started for sveltekit to create a blank app.

When creating an app, remember to pick Skeleton project and use Typescript

Once you have that working, let’s create a dummy endpoint for our search functionality to query.

Simply create a search GET endpoint in /src/routes/search/index.json.ts with the following content:

export function get({ query }) {
  const searchQuery = query.get('q')
  let result = [...Array(125).keys()]

  switch (searchQuery) {
    case 'empty':
      result = []
      break
    case 'unique':
      result = [...Array(51).keys()]
      break
  }

  return {
    status: 200,
    body: {
      items: result
    }
  }
}

When you now run your sveltekit app using npm run dev, you should be able to to simply go to http://localhost:3000/search.json, http://localhost:3000/search.json?query=unique or http://localhost:3000/search.json?q=empty – you will get different result count for each query.

References

Creating Our Search Page

To create a search page, styling being out of scope, style it however you please 🙂

Let’s just change the content of /src/pages/index.ts to a simple search form.

<h1>Search</h1>
<form >
    <input type="search" name="search"/><button type="submit">Search</button>
</form>

Now, let’s give it some state and render the result items if they exist, or say no result if there’s none.

<script lang="ts">
    export let searchResult = {
      items: []
    }
    export let searchQuery = 
</script>

<h1>Search</h1>
<form}>
    <input type="search" name="search" bind:value={searchQuery} /><button type="submit">Search</button>
</form>

{#if searchResult.items.length > 0}
<ul>
    {#each searchResult.items as item}
        <li>{item}</li>
    {/each}
</ul>
{:else}
    No result
{/if}

You can play around with the searchResult variable at this point to see the different ways it would render the results.

References

Adding Search Form Submit Handler

Now, to add some behaviour to the form submit event – for this we’re simply changing the browser URL state by calling goto, note the keepfocus: true – this ensures that we don’t lose the focus state of our search textbox after we submit by hitting the return / enter key.

<script lang="ts">
    import { goto } from '$app/navigation'
    import { browser } from '$app/env'

    let searchResult = {
      items: []
    }
    let searchQuery = ''

    let onSubmit = async () => {
      let currentSearchTerm = ''

      if (browser) {
        const urlParams = new URLSearchParams(window.location.search)
        currentSearchTerm = urlParams.get('q')
      }

      if (searchQuery.trim() == currentSearchTerm?.trim())
        return

      await goto(`/?q=${encodeURIComponent(searchQuery.trim())}`, {
        keepfocus: true
      })
    }
</script>

<h1>Search</h1>
<form on:submit|preventDefault={onSubmit}>
    <input type="search" name="search" bind:value={searchQuery} /><button type="submit">Search</button>
</form>

{#if searchResult.length > 0}
<ul>
    {#each searchResult.items as items}
        <li>{searchResult}</li>
    {/each}
</ul>
{:else}
    No result
{/if}

Now, you should be able to see the query string change whenever the search form is submitted.

References

Reacting to URL change on the same page

Now, for the final piece to complete the search functionality, we need to hook up an API call on load.

The load function within a module is called whenever the url changes (client-side), and it’s also called server side by default during initial page load.

The fetch object passed the the load function is special and it’s definitely what we want to use in this case rather than an external library such as axios. The reason of this is the caching of the initial result, meaning that on page load, even though fetch() is called, the client does not actually call out to the network, but rather uses the already existing result that happened server-side.

Continuing on, for the load function, we simply want to see what’s in q within the querystring, and use that to search for results and simply return our page with the state that we intend for it to render with.

<script context="module" lang="ts">
  const searchUrl = '/search.json'

  export async function load({ page, fetch }) {
    const searchQuery = page.query.get('q')
    const searchResult = await (await fetch(`${searchUrl}?q=${encodeURIComponent(searchQuery)}`)).json()

    return {
      status: 200,
      props: {
        searchQuery: searchQuery,
        searchResult: searchResult
      }
    }
  }
</script>

<script lang="ts">
    import { goto } from '$app/navigation'
    import { browser } from '$app/env'

    export let searchResult = {
      items: []
    }
    export let searchQuery = ''

    let onSubmit = async () => {
      let currentSearchTerm = ''

      if (browser) {
        const urlParams = new URLSearchParams(window.location.search)
        currentSearchTerm = urlParams.get('q')
      }

      if (searchQuery.trim() == currentSearchTerm?.trim())
        return

      await goto(`/?q=${encodeURIComponent(searchQuery.trim())}`, {
        keepfocus: true
      })
    }
</script>

<h1>Search</h1>
<form on:submit|preventDefault={onSubmit}>
    <input type="search" name="search" bind:value={searchQuery} /><button type="submit">Search</button>
</form>

{#if searchResult.items.length > 0}
<ul>
    {#each searchResult.items as item}
        <li>{item}</li>
    {/each}
</ul>
{:else}
    No result
{/if}

References

The source code is available via github.

If you like this post, please do share as I will be doing more things with svelte and sveltekit and will be sharing anything that I find useful in those topics.