# Search

Source: https://signy.dev/examples/search/

The input answers at once, the results catch up.

The input writes the query signal on every key. The result list reads a deferred copy of it, which updates once typing pauses, so a slow list never blocks the input. Arrow keys move a highlight that touches only two rows.

## Preview

[Open the page on its own](https://signy.dev/examples/search/demo/index.html.md)

## Code

Save it as an `.html` file and open it in a browser. It loads Signy and Iris CSS from the CDN, so there is nothing to install or build.

**Show the full page**

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="color-scheme" content="light dark">
    <title>Search - Signy example</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/iriscss@1/dist/iris.min.css">
    <style>
      .results li[data-active="true"] {
        border-radius: var(--i-border-radius);
        background-color: var(--i-color-subtle-border);
      }
    </style>
    <script src="https://cdn.jsdelivr.net/npm/signy@1/dist/signy.global.min.js"></script>
  </head>
  <body>
    <main class="container" id="app"></main>

    <script>
      const {
        Component, Signal, Computed, application, html, For, Show,
        createDeferred, createSelector, useTransition,
      } = Signy;

      const ADJECTIVES = ["Amber", "Brisk", "Calm", "Daring", "Eager", "Fuzzy", "Gentle", "Hidden", "Ivory", "Jolly"];
      const NOUNS = ["Falcon", "River", "Lantern", "Meadow", "Comet", "Harbor", "Willow", "Summit", "Ember", "Pebble"];
      const CATEGORIES = ["Books", "Music", "Games", "Tools"];

      // 5,000 made up products
      const ITEMS = Array.from({ length: 5000 }, (_, i) => ({
        id: i + 1,
        name: `${ADJECTIVES[i % 10]} ${NOUNS[Math.floor(i / 10) % 10]} ${i + 1}`,
        category: CATEGORIES[(i * 7) % 4],
      }));

      const LIMIT = 100;

      class Search extends Component {
        query = new Signal("");
        category = new Signal("All");
        // The list follows the query once typing pauses for 150ms
        deferredQuery = createDeferred(() => this.query.value, { timeoutMs: 150 });
        transition = useTransition();
        active = new Signal(null);
        isActive = createSelector(() => this.active.value);
        chosen = new Signal(null);

        matches = new Computed(() => {
          const query = this.deferredQuery().toLowerCase();
          const category = this.category.value;
          return ITEMS.filter(
            (item) => (category === "All" || item.category === category) && item.name.toLowerCase().includes(query),
          );
        });
        shown = new Computed(() => this.matches.value.slice(0, LIMIT));
        stale = new Computed(() => this.query.value !== this.deferredQuery() || this.transition[0].value);

        pickCategory(category) {
          const [, start] = this.transition;
          start(() => (this.category.value = category));
        }

        // Arrow keys move the highlight. Only the row that loses it and the row that gets it update.
        onKey(event) {
          const shown = this.shown.value;
          const index = shown.findIndex((item) => item.id === this.active.value);
          if (event.key === "ArrowDown" || event.key === "ArrowUp") {
            event.preventDefault();
            const next = event.key === "ArrowDown" ? Math.min(index + 1, shown.length - 1) : Math.max(index - 1, 0);
            this.active.value = shown[next]?.id ?? null;
          } else if (event.key === "Enter" && index !== -1) {
            event.preventDefault();
            this.chosen.value = shown[index];
          }
        }

        render() {
          return html`
            <h1>Search</h1>
            <input type="search" aria-label="Search 5,000 products" placeholder="Try amber or comet 12"
              onInput=${(e) => (this.query.value = e.currentTarget.value)}
              onKeyDown=${(e) => this.onKey(e)} />
            <div class="cluster" role="group" aria-label="Category">
              ${["All", ...CATEGORIES].map((category) => html`
                <button class="small" aria-pressed=${() => String(this.category.value === category)}
                  onClick=${() => this.pickCategory(category)}>${category}</button>
              `)}
            </div>

            ${Show({
              when: () => this.chosen.value,
              children: (item) => html`<p class="alert success" role="status">You picked ${item.name}.</p>`,
            })}

            <p aria-live="polite" aria-busy=${() => String(this.stale.value)}>
              ${() => this.matches.value.length} matches${() => (this.matches.value.length > LIMIT ? `, showing ${LIMIT}` : "")}
            </p>
            <ul class="results">
              ${For({
                each: () => this.shown.value,
                keyBy: "id",
                fallback: html`<li>Nothing matches.</li>`,
                children: (item) => html`
                  <li data-active=${() => String(this.isActive(item.id).value)}>
                    ${item.name} <small>${item.category}</small>
                  </li>
                `,
              })}
            </ul>
          `;
        }
      }

      application(Search).mount("#app");
    </script>
  </body>
</html>

```

## What it uses

- [createDeferred() and useTransition()](https://signy.dev/documentation/1/reactivity/transitions/index.html.md)
- [createSelector()](https://signy.dev/documentation/1/reactivity/selectors/index.html.md) for the highlighted row
- [For](https://signy.dev/documentation/1/templates/for-and-index/index.html.md) with `keyBy`
