Elements
One of Ornata’s nicest features is that DOM references are part of the component contract.
Instead of scattering querySelector() calls across event handlers and render code, you define the important elements once in elements, then let Ornata resolve and validate them during mount.
Why this matters
Section titled “Why this matters”This approach gives you a few benefits at once:
- lookups are scoped to the component root
- each element definition communicates intent
- structural mistakes are caught earlier
- duplicate element references can be caught during mount
- render and method code can focus on behavior instead of DOM plumbing
Root-scoped queries
Section titled “Root-scoped queries”Every query and queryAll lookup happens inside the component root.
elements: { button: { query: "[data-disclosure-button]" }, items: { queryAll: "[data-item]" },}interface DisclosureElements { button: Element | null; items: Element[];}
elements: { button: { query: "[data-disclosure-button]" }, items: { queryAll: "[data-item]" },}That keeps component lookups local and reduces the chance of accidentally grabbing matching elements from elsewhere on the page.
Express intent with the right resolver
Section titled “Express intent with the right resolver”Ornata supports four ways to resolve an element property:
queryfor a single elementqueryAllfor a list of elementscreateto create a new elementresolvefor custom logic
elements: { panel: { query: "[data-panel]" }, items: { queryAll: "[data-item]" }, status: { create: "output" }, input: { resolve(root) { return root.querySelector("[data-filter-input]"); }, },}interface ExampleElements { panel: Element | null; items: Element[]; status: HTMLOutputElement; input: HTMLInputElement | null;}
elements: { panel: { query: "[data-panel]" }, items: { queryAll: "[data-item]" }, status: { create: "output" }, input: { resolve(root) { return root.querySelector("[data-filter-input]"); }, },}Validate cardinality with min and max
Section titled “Validate cardinality with min and max”When your markup has structural expectations, make them explicit.
elements: { items: { queryAll: "[data-tab]", min: 2, }, button: { query: "[data-submit]", max: 1, },}interface ExampleElements { items: HTMLElement[]; button: HTMLButtontElement;}
elements: { items: { queryAll: "[data-tab]", min: 2, }, button: { query: "[data-submit]", max: 1, },}This is especially useful for components like tabs, menus, accordions, and lists where the DOM shape matters.
Use resolve() when you need stronger control
Section titled “Use resolve() when you need stronger control”resolve() is the escape hatch for cases where a plain selector is not enough.
It is a good fit when you want to:
- apply custom filtering logic
- implement fallback logic
- return a specific element array shape
- create non-HTML elements such as SVG elements
- store references to elements outside the root
- return a narrower DOM type like
HTMLInputElement | null
elements: { input: { resolve(root) { return root.querySelector("[data-search-input]"); }, },}interface SearchElements { input: HTMLInputElement | null;}
elements: { input: { resolve(root) { return root.querySelector( "[data-search-input]" ) as HTMLInputElement | null; }, },}A good mental model
Section titled “A good mental model”Think of elements as a DOM schema for the component.
It tells future readers:
- what nodes the component cares about
- how those nodes are found
- how many are expected
- whether any custom resolution rules exist
That makes the component easier to trust and easier to change.