Clone on Gitea
Introduction
Explained by ducks
FAQ
Getting Started
Components
Component Basics
Auto Initialization From DOM
Templates
Children and Parents
Roles
Strictness
Fields
What is a Field
Binding Fields to DOM
Reading and Writing Fields
Value Casting
Flags
Events
Overview
Event Handlers
DOM Events
Custom Events
Propagation and Flow
Advanced Event Control
Beyond Components
States
Overview
StateEngine
DisplayStateEngine
Field Matchers
Ordering and priority
Debugging
Advanced usage
Ajax
ComponentUI
VirtualForm
Unit Testing
Helpers
Overview
Cookies
Arrays

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 with data-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 data value 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.