Element Safeguards
This example demonstrates how Ornata’s elements option can encode DOM expectations directly into a reusable component contract. This is useful for ensuring that the required HTML structure is present when a component first mounts.
If expected elements are missing, or if their required counts are not met, Ornata throws an error and reveals a mismatch between the HTML and the component’s contract. In this example, elements are resolved with more precise selectors and custom resolution logic, and they must satisfy explicit min and max requirements.
Live Demo
Section titled “Live Demo”<section data-tabs> <div> <button data-tab>Tab 1</button> <button data-tab>Tab</button> </div> <section data-panel>Tab 1 content</section> <section data-panel hidden>Tab 2 content</section></section>Component
Section titled “Component”import { defineComponent } from 'ornata';
export const Tabs = defineComponent({ name: 'Tabs', state: { activeIndex: { default: 0, type: Number }, }, elements: { // 👇 Find the one direct div child expected to contain the tab buttons tablist: { query: ':scope > div:has(> [data-tab])', min: 1, max: 1, }, // 👇 Find 2+ tab buttons that are direct children of a direct div child tabs: { queryAll: ':scope > div > [data-tab]', min: 2, }, // 👇 Find 2+ tab panels that are direct children panels: { queryAll: ':scope > [data-panel]', min: 2, }, }, render: { tablist() { return { attributes: { role: 'tablist', }, }; }, tabs({ index }) { const currentIndex = index ?? 0; const isActive = currentIndex === this.state.activeIndex;
return { attributes: { type: 'button', 'aria-selected': String(isActive), }, classes: { 'is-active': isActive, }, events: { click: () => { this.state.activeIndex = currentIndex; }, }, }; }, panels({ index }) { return { attributes: { hidden: (index ?? 0) !== this.state.activeIndex, }, }; }, },});import { defineComponent } from 'ornata';
interface TabsState { activeIndex: number;}
export const Tabs = defineComponent<{ state: TabsState; elements: { tablist: HTMLDivElement; tabs: HTMLButtonElement[]; panels: HTMLElement[]; };}>({ name: 'Tabs', state: { activeIndex: { default: 0, type: Number }, }, elements: { // 👇 Find the one direct div child expected to contain the tab buttons tablist: { resolve(root) { return ( Array.from(root.children).find((child) => { if (!(child instanceof HTMLDivElement)) return false;
return Array.from(child.children).some((grandchild) => { return grandchild.hasAttribute('data-tab'); }); }) ?? null ); }, min: 1, max: 1, }, // 👇 Find 2+ tab buttons that are direct children of a direct div child tabs: { queryAll: ':scope > div > [data-tab]', min: 2, }, // 👇 Find 2+ tab panels that are direct children panels: { queryAll: ':scope > [data-panel]', min: 2, }, }, render: { tablist() { return { attributes: { role: 'tablist', }, }; }, tabs({ index }) { const currentIndex = index ?? 0; const isActive = currentIndex === this.state.activeIndex;
return { attributes: { type: 'button', 'aria-selected': String(isActive), }, classes: { 'is-active': isActive, }, events: { click: () => { this.state.activeIndex = currentIndex; }, }, }; }, panels({ index }) { return { attributes: { hidden: (index ?? 0) !== this.state.activeIndex, }, }; }, },});What this shows
Section titled “What this shows”resolve()gives you more precise control over how elements are retrieved from the DOMminandmaxdocument a structural requirement directly in the component- the component’s DOM dependencies are visible in one place
- render logic stays focused because the lookup work is already done
This is a good pattern for components like tabs, accordions, menus, and list-driven interactions.
For a render-focused version of this pattern, see List Rendering.