PAD + Alpine

Live search

x-model.debounce feeds a fetch of the same page with padFormat=json; the PHP that rendered the first results answers the next ones as JSON - $padExpose names what may leave.

$padExposepadFormat=json x-model.debouncex-forfetch
Live

24 found - the first eight shown

Nothing in the catalogue matches.

Requests to PAD
    Nothing yet - use the example above, and every request a component makes to PAD shows here.

    How it is built

    PAD, on the server Alpine, in the browser

    <div x-data="search({^result})">
    
      <div class="toolbar">
        <div class="search">
          <input class="input" type="search" x-model.debounce.250ms="q" @input.debounce.250ms="ask()" placeholder="Search - try lamp, audio or usb" aria-label="Search the products">
        </div>
        <span class="spinner" x-show="busy" x-cloak aria-hidden="true"></span>
      </div>
    
      <p class="muted small"><span x-text="count">{$result.count}</span> found - the first eight shown</p>
    
      <div class="grid results">
        <template x-for="item in items" :key="item.id">
          <div class="card result">
            <span class="result-emoji" x-text="item.emoji"></span>
            <div>
              <strong x-text="item.name"></strong>
              <div class="muted small" x-text="item.category + ' ยท โ‚ฌ ' + item.price.toFixed(2)"></div>
            </div>
          </div>
        </template>
      </div>
    
      <p class="empty-state" x-show="count === 0" x-cloak>Nothing in the catalogue matches.</p>
    </div>
    
    <script src="{$padRoot}alpine/examples/search.js"></script>
    <?php
    
      // The page renders the first results into the component's argument; every later query
      // asks the same page with padFormat=json, and the same PHP answers - the templates never
      // run for that request, and only $result leaves the server ($padExpose).
    
      $q = trim ( alpineText ( 'q' ) );
    
      $found = array_values ( array_filter ( alpineData ( 'products' ), fn ( $product ) =>
                 $q === '' or stripos ( $product ['name'] . ' ' . $product ['category'] . ' ' . $product ['blurb'], $q ) !== FALSE ) );
    
      $result = [ 'q' => $q, 'count' => count ( $found ), 'items' => array_slice ( $found, 0, 8 ) ];
    
      $padExpose = [ 'result' ];
    
    ?>
    // The search: its first result is the argument PAD wrote - search({^result}) - and every
    // later query asks the same page for JSON. pad.get is www/alpine/alpine-app.js: it adds
    // padFormat=json and logs the request under the example.
    
    document.addEventListener('alpine:init', () => {
    
      Alpine.data('search', (first) => ({
        q: first.q,
        count: first.count,
        items: first.items,
        busy: false,
        asked: 0,
    
        async ask() {
          const mine = ++this.asked;
          this.busy = true;
          try {
            const answer = await pad.get('examples/search', { q: this.q });
            if (mine !== this.asked) return;        // an older answer that came in late
            this.count = answer.result.count;
            this.items = answer.result.items;
          } finally {
            if (mine === this.asked) this.busy = false;
          }
        }
      }));
    
    });