Skip to content

Live Filter

This example filters an existing list as the user types, which is a good fit for HTML-first interfaces where the list already exists in the markup.

Open this demo in a new tab

<section data-filter>
<label>
Search
<input type="search" data-filter-input />
</label>
<ul data-filter-list>
<li data-filter-item>Alpha</li>
<li data-filter-item>Beta</li>
<li data-filter-item>Gamma</li>
</ul>
</section>
import { defineComponent } from "ornata";
export const LiveFilter = defineComponent({
name: "LiveFilter",
state: {
query: { default: "" },
},
elements: {
input: {
resolve(root) {
return root.querySelector("[data-filter-input]");
},
},
items: { queryAll: "[data-filter-item]" },
},
render: {
input() {
return {
events: {
input: (event) => {
const target = event.currentTarget;
if (!(target instanceof HTMLInputElement)) return;
this.state.query = target.value;
},
},
};
},
items({ index }) {
const item = this.elements.items[index ?? 0];
const text = item?.textContent?.toLowerCase() || "";
const matches = text.includes(this.state.query.toLowerCase());
return {
attributes: {
hidden: !matches,
},
};
},
},
});

This example highlights a nice Ornata pattern:

  • resolve() gives the component one explicit place to describe its DOM contract
  • queryAll handles the list collection cleanly
  • the DOM lookup contract is defined once, then reused by render code
LiveFilter.mount("[data-filter]");