Clone on Gitea
Introduction
Explained by ducks
FAQ
Getting Started
Components
Component Basics
Auto Initialization From DOM
Templates
Children and Parents
Roles
Strictness
Behaviors
Overview
PopupBehavior
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

Behaviors

A Behavior is a reusable capability attached to a Component.

Components describe what something is. A SelectComponent, for example, represents a select control with its own state, parts, fields and events. Behaviors describe additional things a Component can do, such as positioning a popup, supporting drag and drop or making something resizable.

A Behavior always belongs to one Component, but it itself is not a Component. It doesn't participate in the Component tree and it doesn't automatically have its own parts, fields, flags, roles or children. Behaviors, therefore, are useful for functionality that needs to be shared by otherwise unrelated Components without introducing another inheritance layer.

Adding a Behavior

Every Component has a behaviors manager. A Behavior class declares its default name using behavior_name

import Behavior from "/src/behaviors/behavior.js";

class ExampleBehavior extends Behavior {
    static behavior_name = "example";
}

You attach it to a Component with behaviors.add()

this.behaviors.add(ExampleBehavior);

The Behavior is constructed with the Component as its owner and is attached immediately. The add() method returns the newly-created Behavior instance as well:

let behavior = this.behaviors.add(ExampleBehavior);

In most cases, however, there is no need to store another reference to it. The Component can retrieve the Behavior by name whenever it needs it with the help of this.behaviors.get("example") and it will raise an exception in case the behavior doesn't exist. You may also use this.behaviors.has("example") to check if a behavior is attached without raising an exception.

Behavior names are unique within a Component. Attempting to register another Behavior under a name that's already in use results in an error.

Behavior names

Behavior classes should declare an explicit, stable behavior_name

class PopupBehavior extends Behavior {
    static behavior_name = "popup";
}

Qite intentionally does not derive this name from the JavaScript class name. The class may be renamed without changing the name used by Components. If a Behavior has no default behavior_name, it must be registered with an explicit name like this:

let behavior = this.behaviors.add("my_behavior", ExampleBehavior);

Options

The last argument to add()(second argument when no custom behavior name is provided and third when it is) is passed to the Behavior as its options:

this.behaviors.add(PopupBehavior, {
    anchor: this.part("control"),
    popup:  this.part("popup")
});

The base Behavior stores both the owner and options:

this.owner
this.options

A Behavior may then expose important options directly as instance properties if that makes its API clearer:

constructor(owner, options={}) {
    super(owner, options);
    this.anchor = options.anchor;
    this.popup  = options.popup;
}

Options belong to the Behavior and component state should continue to live on the Component itself.

Using the owner

A Behavior may use the normal facilities provided by its owning Component:

this.owner.part("control");
this.owner.fields.get("value");
this.owner.flags.get("disabled");
this.owner.children;

It's intentional as a Behavior extends the capabilities of its owner rather than creating another layer of Component state.

Do not turn a Behavior into a miniature Component. If a capability needs its own independent place in the Component tree, roles, children and application state, it is probably a Component rather than a Behavior.

Multiple instances

Sometimes one Component may need multiple instances of the same Behavior.

For example, it could have two independent popups. Since Behavior names must be unique, give those instances explicit names:

this.behaviors.add("menu_popup", PopupBehavior, {
    anchor: this.part("menu_button"),
    popup:  this.part("menu")
});

this.behaviors.add("help_popup", PopupBehavior, {
    anchor: this.part("help_button"),
    popup:  this.part("help")
});

They can then be accessed independently:

this.behaviors.get("menu_popup").open();
this.behaviors.get("help_popup").open();

The explicit instance name belongs to the Component's Behavior registry and doesn't need to become part of the Behavior's normal options.

Lifecycle

Adding a Behavior follows this lifecycle:

behaviors.add(...)
    |
    +-- construct Behavior
    |
    +-- register Behavior
    |
    +-- attach()

Behavior.attach() activates resources owned by the Behavior, including its event handling. Removing it performs the opposite operation:

this.behaviors.remove("example");

so it looks like this:

behaviors.remove(...)
    |
    +-- detach()
    |
    +-- remove from registry

When the owning Component itself is removed, Qite clears its Behavior manager and detaches all attached Behaviors automatically. This is important because a Behavior may own event subscriptions and DOM event listeners that shouldn't survive its Component.

Behavior events

Every Behavior has its own events object.

A Behavior should register events through this object rather than through owner.events

class ClickBehavior extends Behavior {
    constructor(owner, options={}) {
        super(owner, options);

        this.events.addOne("@click", "self", () => {
            this.handleClick();
        });
    }
}

Behavior events use the owning Component's event sources and DOM structure, so the same target notation used by Components can be used by Behaviors. The Behavior subscribes to the owner's publisher when attached — child component publishers and DOM targets are connected once the owner has mounted, when the Component tree is complete. When the Behavior is detached, subscriptions and DOM listeners belonging to its events object are removed with it.

Register Behavior-owned listeners with this.events, not this.owner.events. Otherwise their lifetime belongs to the Component and removing the Behavior would not clean them up.

Strictness

Behaviors may declare static strict in the same way as Components:

class ExampleBehavior extends Behavior {
    static strict = {
        parts:   ["control"],
        flags:   ["enabled"],
        methods: ["refresh"]
    };
}

These requirements are checked against the owning Component after it's mounted. This allows reusable Behaviors to state what they expect from their owner without requiring a particular Component subclass. But Strictness is optional -- use it only when the Behavior genuinely depends on something being present.

Removing and clearing Behaviors

Remove one Behavior by name with:

this.behaviors.remove("example");

To detach and remove every Behavior:

this.behaviors.clear();

Components normally don't need to call clear() themselves because Qite does that automatically during Component removal.

Behaviors and inheritance

Behaviors are not a replacement for Component inheritance. Inheritance is appropriate when two Components share what they fundamentally are. For example, specialized form controls may inherit common behavior from FormFieldComponent. A Behavior, on the other hand, is appropriate when otherwise different Components need the same capability

For example, popup positioning does not make something a particular kind of Component. A Select, menu, date picker and tooltip may all need popup positioning while having little else in common.

Instead of introducing an inheritance hierarchy such as:

BaseComponent
    |
    +-- PopupComponent
            |
            +-- SelectComponent

a Component can attach the capability explicitly:

this.behaviors.add(PopupBehavior, {
    anchor: this.part("control"),
    popup:  this.options_el
});

There, the distinction can be summarized as:

  • Component inheritance describes what a Component is.
  • A Behavior describes something a Component can do.

Keep orchestration explicit

Behaviors should encapsulate reusable mechanics, but the Component should remain responsible for its own application semantics. For example, a Select decides when it is open, but PopupBehavior handles the mechanics of displaying and positioning its popup:

open() {
    this.el.classList.add("opened");
    this.behaviors.get("popup").open();

    // Select-specific logic...
}

This is preferable to exposing internal Behavior steps such as:

popup.measure();
popup.chooseDirection();
popup.position();

The Component should call one meaningful operation and let the Behavior handle the reusable mechanics behind it. At the same time, avoid making Behaviors invisible through method injection or other magic. The fact that functionality belongs to a Behavior should remain clear in the code:

this.behaviors.get("popup").open();

That keeps ownership and control flow explicit, which is the central idea behind Behaviors in Qite.