Overview & Philosophy
ln-search is applied using a decoupled two-host bridge: an input control (<input type="search" data-ln-search-for="targetId">) drives a target container (data-ln-search id="targetId"). Keystrokes on the input write directly to the target's DOM attribute (target.setAttribute('data-ln-search', value)), which acts as the Single Source of Truth. A MutationObserver on the target detects changes, performs order-independent tokenized AND matching against child items, and applies data-ln-search-hide and .hidden to non-matching elements, while clear triggers reset the query instantaneously.
In ln-ashlar, this Two-Host / Attribute Bridge Architecture splits search into two cleanly decoupled roles:
- The Control (
data-ln-search-for="targetId"): Sits on the input element or wrapper. It manages keystroke capture, clear-button interactions, and writes the search term into the target's attribute. - The State Host (
data-ln-search="term"): Sits on the target container (list, table, section). It owns the search term as the Single Source of Truth, detects attribute changes viaMutationObserver, performs reverse synchronization to controls, dispatchesln-search:change, and runs tokenized AND matching.
The Attribute Bridge Contract
Controls never hold direct JS memory references to targets. Writing or editing data-ln-search="term" on a target element (including live edits in DevTools Inspector) immediately updates the target state, reflects the query back into all matching [data-ln-search-for="targetId"] inputs, and dispatches the cancelable ln-search:change event.
Orthogonal Integration Lifecycles
-
The Passive Path (Zero-JS DOM Filtering): When no custom event listener calls
preventDefault(), the target runs an order-independent tokenized AND search over child elements (or deep items viadata-ln-search-items), tagging non-matching items withdata-ln-search-hide="true". Items withdata-ln-search-excludeare exempt. -
The Active Path (Table & Coordinator Integration): Active components (such as
ln-tableor custom coordinators) interceptln-search:change, calle.preventDefault()to deactivate default DOM hiding, and execute their own in-memory or remote query pipelines. -
The Boot Seeding: Authored or deep-linked
data-ln-search="value"on targets, as well as browser form restoration on inputs, are seeded safely throughqueueBootso consumer listeners finish initialization first.
For the full guide see the ln-search README and the architecture reference.
Authoring Rule — Required Chrome
Every search/filter input MUST use the full .search chrome (icon + clear button). The minimal-markup examples further down on this page isolate specific JS behaviors and are NOT authoring templates.
Anatomy
The canonical (required) form places data-ln-search-for="targetId" on the <input> and defines data-ln-search="" on the target container:
<search aria-label="Search countries">
<svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-search"></use></svg>
<input type="search" placeholder="Search..." data-ln-search-for="my-list">
<button type="button" data-ln-search-clear aria-label="Clear search">
<svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-x"></use></svg>
</button>
</search>
<ul id="my-list" data-ln-search="">
<li>Argentina</li>
<li>Brazil</li>
<li data-ln-search-exclude>Exempt Item (Always Visible)</li>
</ul>
The chrome (border, focus ring, icon spacing, clear-button visibility) comes from @mixin search in scss/components/_form.scss.
Default vs. deep targeting
The default behavior iterates direct children of the target. For nested structures (like table rows or grouped icon cells), add data-ln-search-items="selector" on the target element; the component then runs target.querySelectorAll(selector) on every search.
Exemptions and Subtree Exclusion
Use data-ln-search-exclude to control search boundaries: on an item root, the item is completely exempt from filtering and remains visible; on a descendant element (e.g. action buttons or badges), that subtree is omitted from text matching.
Attributes
| Attribute | Element | Description |
|---|---|---|
data-ln-search-for="targetId" | <input> (canonical) or wrapper | Control pointer. Identifies the target element ID to drive search state on. |
data-ln-search="term" | Target container (State Host) | Search term state. Single Source of Truth on the target. |
data-ln-search-items="selector" | Target container | CSS selector for deep item matching instead of direct children. |
data-ln-search-fields="a,b" | Target container | Comma-separated field names forwarded in detail.fields. |
data-ln-search-exclude | Item root or descendant | On item root: exempt from filtering. On descendant: excludes subtree text. |
data-ln-search-clear | <button> (in control or target empty-state) | Universal clear button. Clears input, resets target search state, and refocuses input. |
data-ln-search-clear-for="targetId" | <button> (anywhere) | Remote clear button targeting a specific element ID. Clears linked input and resets target state. |
data-ln-search-hide="true" | Non-matching items (auto) | Set by default filter on items that do not match (styled with display: none !important). |
Events
| Event | On | Bubbles | Cancelable | detail |
|---|---|---|---|---|
ln-search:change | Target element | yes | yes | { term, tokens, targetId, fields } |
detail.term is lowercased and trimmed. detail.tokens is an array of whitespace-separated tokens. The event is dispatched on the target, so consumers (tables, data stores, lists) listen directly on their own host.
Calling preventDefault() disables default DOM walking, allowing external components (like ln-table) to filter their own records.
Example — Filter a list (canonical input-host with tokenized AND matching)
The canonical form with data-ln-search-for="demo-list". Type multiple space-separated words (e.g. "u k" or "unit stat") to test tokenized AND matching:
- Argentina
- Australia
- Brazil
- Canada
- Denmark
- Egypt
- France
- Germany
- India
- Japan
- Kenya
- Mexico
- Netherlands
- Norway
- Poland
- Singapore
- Spain
- Sweden
- United Kingdom
- United States
- 🌍 World Directory (Pinned / Exempt)
Note: The examples below use intentionally minimal markup to isolate specific JS behaviors. They are NOT authoring templates — always author the full .search chrome in real pages.
Example — Directly on the input (no wrapper)
data-ln-search-for directly on an <input> driving a list target:
- Engineering — builds the product
- Design — creates user interfaces
- Marketing — promotes the product
- Sales — closes deals
- Support — helps customers
- Finance — manages budgets
- Legal — handles compliance
- HR — manages people
Example — Custom event handler with preventDefault()
Listen for ln-search:change on the target, call preventDefault(), and own the visual response. This example shows a match counter instead of hiding elements:
Type to search...
- Alpha framework
- Beta library
- Gamma toolkit
- Delta engine
- Epsilon runtime
Example — Deep targeting with data-ln-search-items and Subtree Exclusion
Items are nested under category sections. The target has data-ln-search-items=".icon-cell". Action labels with data-ln-search-exclude are ignored during search text extraction:
Navigation
- home — go home (ignored text)
- arrow-left — back
- arrow-right — forward
- menu — open navigation
Actions
- plus — add new
- trash — delete
- edit — modify
- copy — duplicate
Status
- check — success
- x — error
- alert-triangle — warning
- info-circle — information
Example — Server-side search simulation
The handler calls preventDefault() and logs dispatched events:
Example — Pre-filled input (browser form-restore simulation)
A pre-filled input simulates back-button restore. The control constructor detects the restored value and updates the target via queueBoot:
- Argentina
- Brazil
- Canada
- France
- Germany
- Japan
Composition — ln-table integration
When data-ln-search-for points to an ln-table element with data-ln-search="", ln-table intercepts ln-search:change, calls preventDefault(), and filters records in-memory:
<input type="search" placeholder="Search..." data-ln-search-for="my-table">
<section id="my-table" data-ln-table data-ln-search="">
<table>...</table>
</section>
See the Table and Table Filter demos for live integrations.
Composition — independence from ln-filter
ln-search and ln-filter operate on the same target without coordinating. Each manages its own hide attribute; an item is visible iff both are absent:
[data-ln-search-hide],
[data-ln-filter-hide] {
display: none !important;
}
The CSS rules ship in the library defaults. Neither component knows about the other.
API
// Control instance on the input
const controlEl = document.querySelector('[data-ln-search-for]');
controlEl.lnSearchControl.targetId; // string
controlEl.lnSearchControl.input; // HTMLInputElement
controlEl.lnSearchControl.destroy();
// State instance on the target
const targetEl = document.querySelector('[data-ln-search]');
targetEl.lnSearch.term; // string (current raw term)
targetEl.lnSearch.destroy();
To programmatically set search term, simply set the attribute on the target (Single Source of Truth) or write to the input:
// Recommended: Attribute Bridge (Inspector or Script)
document.getElementById('demo-list').setAttribute('data-ln-search', 'argentina');
Common mistakes
1. Putting data-ln-search="targetId" on the input
data-ln-search is the state attribute on the target. The input control must use data-ln-search-for="targetId".
2. Listening for input directly instead of ln-search:change
Always listen for ln-search:change on the target element; you get normalized terms and token arrays for free.
URL Hash Synchronization (data-ln-hash)
Adding data-ln-hash to the search input or target element synchronizes the search query into the URL hash fragment (e.g. #demo-hash-search:react). This enables deep-linking, shareable bookmarks, and browser Back/Forward navigation:
- JavaScript (ES2026)
- TypeScript
- React & Next.js
- Vue & Nuxt
- Svelte & SvelteKit
<search aria-label="Search technologies">
<svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-search"></use></svg>
<input type="search" placeholder="Search..." data-ln-search-for="demo-hash-list" data-ln-hash>
<button type="button" data-ln-search-clear aria-label="Clear search">
<svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-x"></use></svg>
</button>
</search>
<ul id="demo-hash-list" data-ln-search="">
<li>JavaScript (ES2026)</li>
<li>TypeScript</li>
...
</ul>