Event Handlers
Handlers are registered with
events.add(...). This is the core API you will
use inside your components. The shape is always the same:
[ event_name, event_source, handler ]
You can pass a single entry or an array of entries. In practice, most components register multiple handlers at once.
Basic form
In a real app you usually react to both DOM input and child component events. For example, a Checkout component may listen to changes in the customer profile form, react to a "pay" button click, and also handle events coming from a payment method picker.
class CheckoutComponent extends BaseComponent {
constructor(tree_node) {
super(tree_node);
this.events.add([
// Customer profile updates (DOM fields inside this component)
["@input", ".email", (e) => this.fields.set({ email: e.target.value })],
["@input", ".phone", (e) => this.fields.set({ phone: e.target.value })],
// Cart UI actions (DOM parts)
["@click", "#apply_coupon", () => this.applyCoupon()],
["@click", "#pay_now", () => this.events.publish("checkout_submit")],
// Child components publishing higher-level events
["change", ">shipping", (data) => {
this.fields.set({ shipping_method: data.method });
}],
["select", ">payment_picker", (data) => {
this.fields.set({ payment_method: data.method });
}]
]);
}
}
This example mixes three practical sources:
-
".email"and".phone"are field elements and only emit DOM events. -
"#apply_coupon"and"#pay_now"are parts inside the Checkout template. -
">shipping"and">payment_picker"are child components whose job is to publish domain-level events to be handled by the parent.
Event name
Event names are plain strings.
-
DOM events use
"@"prefix, for example"@click","@input" -
Custom events do not use
"@", for example"submit"or"open"
You may also pass an array of event names:
this.events.add([
[["@mousedown", "@touchstart"], "#handle", (e) => this.startDrag(e)]
]);
This registers the same handler for multiple events.
Event source
The second argument tells Qite where the event is coming from.
The most common forms are:
-
">role"— a child component with a given role -
"role"— same as above (role of a child component) -
"#part"— a DOM element withdata-part="part"inside this component -
".field"— a field element inside this component -
"self"— this component itself
Examples:
["@click", "#close", (e) => this.remove()]
["submit", ">form", (data) => this.handleSubmit(data)]
["open", "self", () => this.animate()]
When you listen to a child component via its role, you are defining your component’s public integration surface. The child publishes; the parent reacts.
Multiple handlers
The third argument may also be an array of functions:
this.events.add([
["submit", ">form", [
(data) => this.validate(data),
(data) => this.persist(data)
]]
]);
Handlers are executed in the order they are defined.
Source objects (advanced form)
In addition to string sources, you may pass an object describing markers. This is useful when you need to match multiple roles or more complex conditions. For example:
this.events.add([
["submit", { roles: ["form", "dialog"] }, (data) => this.handle(data)]
]);
Most components will never need this form, but it exists when you need fine-grained control.
Event payloads
Every handler receives a single argument — the payload.
- For DOM events, payload is the native event object.
-
For custom events, payload is the
datavalue you provided.
If you want to send structured data, pass an object. But when you just want to signal something — publish without data. For example:
class CustomerProfileComponent extends BaseComponent {
constructor(tree_node) {
super(tree_node);
this.events.add([
// DOM event -- payload is native event object
["@input", ".email", (e) => {
console.log("Typed:", e.target.value);
}],
// Custom event -- payload is whatever was passed as data
["save", "self", (data) => {
console.log("Saved profile for:", data.customer_id);
}]
]);
}
saveProfile() {
// Publishing structured data -- we pick it up in this
// component using the "self" source (above), but nothing
// prevents parent of this component to also listen to the
// same event and pick it up as well.
this.events.publish("save", { data: { customer_id: 42 }});
}
}
How matching works (conceptually)
When an event is published, Qite compares the event’s markers with the markers
you declared in
events.add(...)
Matching is exact : a handler only runs when the event satisfies what you declared. If you require a specific role, only events published by children with that role will match.
However,
declaring a role does not require that such a child already exists.
You may register a handler for
">payment_picker"
even if that child has not
been appended yet. When it is added later and begins publishing events, your
handler will run normally.
For DOM parts and fields—
"#part"
and
".field"— listeners are attached to
existing elements at registration time. If those elements do not exist yet,
no listener is attached. If your DOM structure changes dynamically, you may
need to reattach listeners.
In short: role-based matching is dynamic and works as children appear. Part- and field-based matching depends on DOM listeners being attached.
Practical guidance
Keep handlers local and focused. A low-level component will mostly listen to DOM events, because its job is to translate user interaction into meaningful signals. A higher-level component, on the other hand, should mostly listen to events published by its children and react to those domain-level signals.
Do not use events to simulate a state machine. If you find yourself chaining events to control UI flow, that is a sign you should use states instead. Events are signals — they are not your state model.