bhylu.com

Advertisements

jQuery Book-swap Board Project

Combine scoped selection, safe text, an empty state and a local choice without inventing a reservation service.

Your brief

Build a board with three fictional titles, a search field and a choose button on each visible row. Keep the selected record ID separate from the currently filtered row position. Searching should not silently change the choice. The empty state must explain that no title matches. Before viewing the solution, list which values are retained state and which values can be derived. The worked version redraws rows safely and delegates selection from the stable list.

Run in a separate local browser lab

Save this complete HTML source as index.html in an empty folder. Put the pinned full jQuery 4.0.0 file beside it and preserve the MIT licence notice. The document includes its own CSS and JavaScript. Bhylu’s shared website theme does not load jQuery globally.

Open index.html directly in a normal browser. You may also use the optional loopback fixture server with Node 22 if you want the same HTTP setup as the network lessons.

jQuery examples do not run in Bhylu’s current playground: it blocks external libraries and network access. No Apache, MySQL or website configuration change is needed. Review the full-build setup before using Ajax, effects or plugins.

Worked solution

Show the worked solution
index.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>jQuery Build a Searchable Book-swap Board</title>
    <style>
      :root {
        font:
          18px/1.6 system-ui,
          sans-serif;
        color: #16304e;
        background: #fff;
        color-scheme: light;
      }
      * {
        box-sizing: border-box;
      }
      body {
        margin: 0;
      }
      main {
        max-width: 50rem;
        margin: auto;
        padding: 1.25rem;
      }
      h1 {
        font-size: 1.8rem;
        line-height: 1.2;
      }
      h2 {
        font-size: 1.3rem;
      }
      button,
      input,
      select,
      textarea {
        font: inherit;
        max-width: 100%;
      }
      button {
        margin: 0.2rem;
        padding: 0.4rem 0.7rem;
        background: #034fa0;
        color: white;
        border: 1px solid #034fa0;
        border-radius: 0.3rem;
      }
      button:disabled {
        opacity: 0.6;
      }
      label {
        display: block;
        margin: 0.5rem 0;
      }
      input:not([type='checkbox']),
      select,
      textarea {
        display: block;
        padding: 0.3rem;
      }
      a {
        color: #034fa0;
      }
      output {
        display: block;
      }
      article,
      section,
      fieldset {
        padding: 0.75rem;
        margin-block: 0.75rem;
        border: 1px solid #a9bcd1;
      }
      ul {
        padding-left: 1.5rem;
      }
      :focus-visible {
        outline: 3px solid #0078d4;
        outline-offset: 3px;
      }
      .chosen {
        border: 3px solid #0078d4;
      }
      .highlight {
        font-weight: 700;
      }
      .muted {
        opacity: 0.7;
      }
      pre {
        white-space: pre-wrap;
        overflow-wrap: anywhere;
      }
      :root[data-theme='dark'] {
        color: #d6e7fb;
        background: #061d30;
        color-scheme: dark;
      }
      :root[data-theme='dark'] a {
        color: #8ecbff;
      }
    </style>
  </head>
  <body>
    <main>
      <h1>Neighbourhood book swap</h1>
      <label>Find a title<input id="query" /></label>
      <ul id="books"></ul>
      <p id="empty" hidden>No matching local books.</p>
      <output>No local choice</output>
    </main>
    <script src="jquery.min.js"></script>
    <script>
      jQuery(function ($) {
        const books = [
          { id: 'river', title: 'River Paths' },
          { id: 'garden', title: 'Small Gardens' },
          { id: 'evening', title: 'Evening Stories' },
        ];
        let selected = null;
        function render() {
          const query = String($('#query').val()).trim().toLowerCase();
          const visible = books.filter((book) =>
            book.title.toLowerCase().includes(query),
          );
          $('#books').empty();
          visible.forEach((book) =>
            $('<li>')
              .append(
                $('<button>')
                  .attr({
                    'data-id': book.id,
                    'aria-pressed': String(book.id === selected),
                  })
                  .text(book.title),
              )
              .appendTo('#books'),
          );
          $('#empty').prop('hidden', visible.length !== 0);
        }
        $('#query').on('input.board', render);
        $('#books').on('click.board', 'button', function () {
          selected = $(this).attr('data-id');
          const book = books.find((row) => row.id === selected);
          $('output').text('Local choice: ' + book.title + '. Not reserved.');
          render();
          $('#books button')
            .filter(function () {
              return $(this).attr('data-id') === selected;
            })
            .get(0)
            ?.focus();
        });
        render();
      });
    </script>
  </body>
</html>

Expected result

Choosing River Paths and filtering to Gardens retains the River Paths summary. An unmatched query shows a specific empty message.

The stable ID keeps identity separate from filtered positions. The query is read from the current field; visible rows are derived. Title text never becomes HTML. A real service would recheck stock and permission before accepting a request. Local highlighting and aria-pressed communicate a planning choice, not durable acceptance. Rebuilding rows can affect focus, so review the focused control after a real selection workflow.

Extend and test

Add a clear-choice action with an explicit focus policy. Test the chosen button after redraw and explain what happens when a filter hides it.

Use jQuery’s official API documentation for exact method and return contracts.

Advertisements