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.