Your First Component
The easiest way to learn Ornata is to enhance a small piece of existing HTML with a clear component contract.
The markup
Section titled “The markup”<section data-counter> <p> <span data-count-label>Clicks</span>: <strong data-count-value>0</strong> </p> <button type="button" data-count-button>Increment</button></section>Even without JavaScript, this markup still renders understandable content.
The component
Section titled “The component”import { defineComponent } from "ornata";
const Counter = defineComponent({ name: "Counter", state: { count: { default: 0 }, label: { default: "Clicks" }, }, elements: { label: { query: "[data-count-label]" }, value: { query: "[data-count-value]" }, button: { query: "[data-count-button]" }, }, methods: { increment() { this.state.count += 1; }, }, watch: { count({ newValue, oldValue, isInitial }) { if (isInitial) return;
console.log(`Count changed from ${oldValue} to ${newValue}`); }, }, computed: { total() { return this.state.count; }, }, render: { label() { return { text: this.state.label, }; }, value() { return { text: String(this.computed.total), }; }, button() { return { events: { click: () => this.methods.increment(), }, }; }, },});import { defineComponent } from "ornata";
interface CounterState { count: number; label: string;}
interface CounterElements { label: Element | null; value: Element | null; button: Element | null;}
interface CounterMethods { increment(): void;}
interface CounterComputed { total: number;}
const Counter = defineComponent<{ state: CounterState; elements: CounterElements; methods: CounterMethods; computed: CounterComputed;}>({ name: "Counter", state: { count: { default: 0 }, label: { default: "Clicks" }, }, elements: { label: { query: "[data-count-label]" }, value: { query: "[data-count-value]" }, button: { query: "[data-count-button]" }, }, methods: { increment() { this.state.count += 1; }, }, watch: { count({ newValue, oldValue, isInitial }) { if (isInitial) return;
console.log(`Count changed from ${oldValue} to ${newValue}`); }, }, computed: { total() { return this.state.count; }, }, render: { label() { return { text: this.state.label, }; }, value() { return { text: String(this.computed.total), }; }, button() { return { events: { click: () => this.methods.increment(), }, }; }, },});The mounting code
Section titled “The mounting code”const root = document.querySelector("[data-counter]");
if (root) { Counter.mount(root);}const root = document.querySelector("[data-counter]");
if (root) { Counter.mount(root);}How to read this component
Section titled “How to read this component”Each top-level option has a clear job:
statedefines reactive valueselementsfinds important DOM nodes inside the rootmethodsdefines reusable internal behaviorwatchreacts to state changescomputedderives values from staterenderapplies DOM updates to resolved elements
That separation is one of Ornata’s main strengths. It keeps the contract between markup, behavior, and state visible, which makes the component easier to reuse in larger HTML-first systems.
This example intentionally shows several top-level sections together. Smaller components do not need every section, but the shape stays consistent across the library.
If you want a deeper look at action-oriented component logic, read Methods.
Add stronger typing
Section titled “Add stronger typing”When you want clearer contracts, provide typed component parts:
interface CounterState { count: number; label: string;}
interface CounterMethods { increment(): void;}
const Counter = defineComponent<{ state: CounterState; methods: CounterMethods;}>({ name: "Counter", state: { count: { default: 0 }, label: { default: "Clicks" }, }, methods: { increment() { this.state.count += 1; }, },});Continue to Component Anatomy for a deeper look at every option section.
If you want a fuller walkthrough of inference and explicit typed parts, read TypeScript.