5.0.0-beta.1 · Published beta · b06750c5. Published on npm. Match your installed version.
ConsumerMarionette.Behavior#
A Behavior shares interaction logic across views. It uses its host view's
DOM and can handle DOM, model, and collection events without giving each
view another copy of the same handlers.
Behavior includes:
Attach a Behavior class to a view through its behaviors
definition. The view constructs the Behavior and manages its lifetime.
Documentation Index#
- Instantiating a Behavior
- Using Behaviors
- Nesting Behaviors
- The Behavior's
view - Host Communication and Event Proxies
- Behavior Lifecycle
- Destroying a Behavior
Instantiating a Behavior#
Unlike other Marionette classes, Behaviors are not meant to
be instantiated except by a view.
Using Behaviors#
The easiest way to see how to use the Behavior class is to take an example
view and factor out common behavior to be shared across other views.
import { View } from 'marionette';
const MyView = View.extend({
template() {
return '<button class="destroy-btn" type="button">Destroy</button>';
},
ui: {
destroy: '.destroy-btn'
},
events: {
'click @ui.destroy': 'warnBeforeDestroy'
},
warnBeforeDestroy() {
alert('This view will be removed.');
this.destroy();
},
onRender() {
this.getUI('destroy')[0].title = 'What a nice mouse you have.';
}
});
Interaction points, such as tooltips and warning messages, are generic concepts.
There is no need to recode them within your Views so they are prime candidates
to be extracted into Behavior classes.
Defining and Attaching Behaviors#
import { Behavior, View } from 'marionette';
const DestroyWarn = Behavior.extend({
// You can set default options
// They will be overridden if you pass in an option with the same key.
options: {
message: 'You are destroying!'
},
ui: {
destroy: '.destroy-btn'
},
// Behaviors have events that are bound to the view's DOM.
events: {
'click @ui.destroy': 'warnBeforeDestroy'
},
warnBeforeDestroy() {
const message = this.getOption('message');
window.alert(message);
// Every Behavior has a hook into the
// view that it is attached to.
this.view.destroy();
}
});
const ToolTip = Behavior.extend({
options: {
text: 'Tooltip text'
},
ui: {
tooltip: '.tooltip'
},
onRender() {
this.getUI('tooltip')[0].title = this.getOption('text');
}
});
export const MyView = View.extend({
template() {
return [
'<button class="destroy-btn" type="button">Destroy</button>',
'<span class="tooltip">More information</span>'
].join('');
},
behaviors: [DestroyWarn, ToolTip]
});
Each behavior will now be able to respond to user interactions as though the event handlers were attached to the view directly. In addition to using array notation, Behaviors can be attached using an object:
const MyView = View.extend({
behaviors: {
destroy: DestroyWarn,
tooltip: ToolTip
}
});
Arrays are the only supported list form for behaviors. Object maps use own
enumerable string keys in standard JavaScript own-key order. Inherited, symbol,
and non-enumerable properties are ignored, and a numeric length property is
an ordinary map entry rather than an array-like signal.
Behavior Options#
When we attach behaviors to views, we can also pass in options to add to the
behavior. This tends to be static information relating to what the behavior
should do. In our above example, we want to override the message to our
DestroyWarn and Tooltip behaviors to match the original message on the View:
const MyView = View.extend({
behaviors: [
{
behaviorClass: DestroyWarn,
message: 'You are about to destroy all your data!'
},
{
behaviorClass: ToolTip,
text: 'What a nice mouse you have.'
}
]
});
There are several properties, if passed, that will be attached directly to the instance:
collectionEvents, events, modelEvents, stateEvents, triggers, ui
Using an object, we must define the behaviorClass attribute to refer to our
behaviors and then add any extra options with keys matching the option we want
to override. Any passed options will override the values from options property.
Behavior options can also provide collaborators that the Behavior needs. These
values are selected during construction and retained by reference. Read an
arbitrary collaborator with getOption() so that a class default and an
attachment override follow the same option precedence; arbitrary option names
are not copied directly onto the Behavior instance. A host can explicitly pass
an injected service through a behaviors() function:
initialize(options, hostView) receives the same host View exposed as
this.view.
import { Behavior, View } from 'marionette';
const SelectionBehavior = Behavior.extend({
initialize() {
this.listenTo(
this.getOption('service'),
'selection:change',
this.onSelectionChange
);
},
onSelectionChange(selection) {
this.view.showSelection(selection);
}
});
export const SelectionView = View.extend({
template() {
return '<output class="selection"></output>';
},
ui: {
selection: '.selection'
},
behaviors() {
return [{
behaviorClass: SelectionBehavior,
service: this.getOption('selectionService')
}];
},
showSelection(selection) {
this.getUI('selection')[0].textContent = selection.label;
}
});
getOption() does not fall back to options on the host. Use this.view for
dependencies owned by the host, such as its model or collection. A nested
Behavior receives its own definition options while sharing the same host View
as the Behavior that declared it.
When a Behavior is removed directly or its host is destroyed, Marionette removes
subscriptions created by that Behavior with listenTo(). It does not destroy or
dispose arbitrary values passed through Behavior options, and unrelated listeners
on those collaborators remain active.
Errors An error will be thrown if the Behavior class is not passed.
Nesting Behaviors#
In addition to extending a View with Behavior, a Behavior can itself use
other Behaviors. The syntax is identical to that used for a View:
import { Behavior } from 'marionette';
const Modal = Behavior.extend({
behaviors: [
{
behaviorClass: DestroyWarn,
message: 'Whoa! You sure about this?'
}
]
});
Nesting groups Behavior declarations; it does not transfer cleanup ownership to the declaring Behavior. Nested Behaviors act as direct Behaviors of the same host view, so destroying the declarer leaves them active until they are removed directly or the host is destroyed.
The Behavior's view#
The view is a reference to the View instance that the Behavior is attached to.
import { Behavior } from 'marionette';
Behavior.extend({
handleDestroyClick() {
this.view.destroy();
}
});
Host Communication and Event Proxies#
A Behavior is an event-capable object attached to one host View. It can handle host events, DOM events, and host entity events while keeping its own events separate from the host.
Host and Behavior Events#
When the host calls triggerMethod(), the host's corresponding onEvent method
runs first. The event is then broadcast with the same arguments to every attached
Behavior, where the corresponding method runs with that Behavior as its context.
Nested Behaviors participate directly in the same host broadcast. Calling the
host's trigger() also broadcasts to Behaviors, but does not call the host's onEvent
method. Do not rely on an ordering among Behavior handlers.
Host and Behavior DOM declarations are delegated independently. If multiple Behaviors or the host declare the same event and selector, every matching declaration runs once. Do not use declaration collisions to establish precedence or suppress another handler.
Host broadcasts include events produced by:
- Calls to
triggerMethod() - DOM
triggers childViewTriggers- Child events forwarded through a non-false
childViewEventPrefix
childViewEvents calls the configured host handler directly. It becomes a host
broadcast only if that handler explicitly calls triggerMethod().
A call to behavior.triggerMethod() stays local to that Behavior. It does not
invoke the host or sibling Behaviors. To request host work, call an appropriate
public host method or explicitly use this.view.triggerMethod(). The latter is a
host broadcast, so every attached Behavior receives it, including the Behavior
that sent it. Do not re-emit the same host event from that Behavior's corresponding
handler, as doing so would recurse.
import { Behavior, View } from 'marionette';
const SaveBehavior = Behavior.extend({
ui: {
save: '.save'
},
events: {
'click @ui.save': 'requestSave'
},
requestSave() {
this.view.requestSave();
}
});
export const FormView = View.extend({
behaviors: [SaveBehavior],
template() {
return '<button class="save" type="button">Save</button>';
},
requestSave() {
this.triggerMethod('save:requested', this);
}
});
Behavior DOM queries and delegation stay scoped to the host View. A matching
element outside the host does not participate. Literal configuration errors fail
eagerly: an undeclared @ui reference throws MN0018, and a
string handler that does not resolve to a callable method throws
MN0019. For example, declaring the event above without
ui.save, or naming requestSave without defining that method, is invalid.
A Behavior's DOM triggers are emitted on
the host automatically. The host method runs first, and all attached Behaviors,
including the Behavior that declared the trigger, receive the broadcast.
For general event naming and handler conversion, see
triggerMethod.
Proxy Handlers#
Behaviors provide proxies to a number of the view event handling attributes including:
import { Behavior } from 'marionette';
Behavior.extend({
events: {
'click .foo-button': 'onClickFooButton'
},
triggers: {
'click .bar-button': 'click:barButton'
},
modelEvents: {
'change': 'onChangeModel'
},
collectionEvents: {
'change': 'onChangeCollection'
},
onClickFooButton(evt) {
// ..
},
onClickBarButton(view, evt) {
// ..
},
onChangeModel(model, opts) {
// ..
},
onChangeCollection(model, opts) {
// ..
}
});
Initialize Order#
The View + Behavior initialize process is as follows:
- View construction begins and the View's
preinitializeruns - Behavior is constructed
- Behavior is initialized with view property set
- Callable Behavior
eventsandtriggersare resolved and delegated - View is initialized
- View triggers an
initializeevent on the behavior.
This means that the behavior can access the view during its own initialize method.
It can also access state established by the View's preinitialize method.
Callable events and triggers may use state established by that method before
the View initializes.
The View's initialize is called later with its original constructor arguments.
It can observe Behavior-driven state only when a Behavior explicitly sets that
state or calls a host method; Marionette does not inject Behavior information.
The initialize event is triggered on the behavior indicating that the view is fully initialized.
Using ui#
As in views, events and triggers can use the ui references in their
listeners. For more details, see the ui documentation.
These can be defined on either the Behavior or the View. The fragment below
assumes a Backbone model with save() and a configured
Backbone DataApi:
import { Behavior } from 'marionette';
const MyBehavior = Behavior.extend({
ui: {
saveForm: '.btn-save'
},
events: {
'click @ui.saveForm': 'saveForm'
},
modelEvents: {
invalid: 'showError'
},
saveForm() {
this.view.model.save();
},
showError() {
alert('You have errors');
}
});
UI resolution and binding#
For a host whose el is empty at construction, the host constructs each Behavior
before the host's initialize and before binding UI elements. During that
construction, the Behavior resolves its own ui declaration and the host's ui
declaration into one selector map. When both declarations contain the same key, the
host's selector wins. This allows a Behavior to provide reusable defaults without
dictating the host's markup. Marionette establishes this merged map before the
Behavior's first DOM event and trigger delegation, so host-only keys and host
overrides are available immediately.
The merged selector map is available to the Behavior's initialize, before either
the Behavior or host has bound UI elements. The map is captured for that Behavior
instance during construction; later changes to values returned by a ui function do
not replace its captured selectors. The host evaluates its own ui again when it
binds. If a stateful host ui function returns a different selector then, the host
binds the later selector while the Behavior continues to bind its construction-time
selector. Keep ui functions deterministic when the host and Behavior share keys.
The Behavior's el is also available during initialize. Behaviors
can initialize their own $el wrapper with $(this.el) at this point. DOM event and trigger declarations are delegated only after initialize
returns, so callable declarations may safely depend on state established there.
Before binding, behavior.ui contains selector strings. A template-rendered View
binds those selectors during render, after which the values are array-like element
collections found only within the host's el. Its rerender replaces the contents and
rebinds the same Behavior to the replacement elements. Code must read the current
behavior.ui or call behavior.getUI(name) after binding instead of retaining an
element collection from an earlier render. Calling getUI() without a declared
ui map, before binding, or after unbinding throws MN0023.
A CollectionView also binds Behavior UI automatically when its render processes a
template. Without a template, CollectionView#render leaves Behavior UI as selector
strings; call collectionView.bindUIElements() after the expected elements exist to
bind them explicitly.
Once the owning View or CollectionView starts destruction, its base
bindUIElements() method and direct bindUIElements() calls on a Behavior owned by
or retained from that host are chainable no-ops. They do not resolve host UI or query
the retained root element. unbindUIElements() remains available for cleanup, and
getUI() continues to throw MN0023 while UI is unbound. Reusing
a Behavior after calling Behavior#destroy() while its host remains live is outside
this terminal-host contract.
A View initialized around pre-rendered content binds its own UI before it
constructs Behaviors. This contract pins only that construction ordering. It
intentionally leaves the mixed Behavior UI representation for that path unresolved;
do not infer the selector-before-binding sequence above or rely on that representation.
import { Behavior, View } from 'marionette';
const SaveBehavior = Behavior.extend({
ui: {
save: '.btn-save'
},
events: {
'click @ui.save': 'requestSave'
},
requestSave() {
this.getUI('save')[0].classList.add('is-saving');
this.view.requestSave();
}
});
export const FormView = View.extend({
behaviors: [SaveBehavior],
template() {
return [
'<button class="btn-save" type="button">Default save</button>',
'<button class="btn-primary" type="button">Save</button>'
].join('');
},
ui: {
save: '.btn-primary'
},
requestSave() {
this.triggerMethod('save:requested', this);
}
});
Host DOM boundary#
The host View or CollectionView owns the DOM boundary for each attached
Behavior. A Behavior's el is the host's current el, and its $() lookup
delegates to the host so that results stay scoped to that element. Native core
does not create $el. With the optional
jQuery adapter, application code can
assign this.$el = $(this.el) once in initialize().
The host and its Behaviors keep the same root for their lifetime. Rendering can
replace its contents, and delegateEvents() refreshes View and Behavior handlers.
Destroying the host removes those handlers. Behaviors do not own or replace the root.
Each Behavior can also reference its host through the view attribute. Read
model values through the host's selected DataApi so the same code works with plain
objects and configured observable providers:
import { Behavior } from 'marionette';
const ViewBehavior = Behavior.extend({
onRender() {
const shouldHighlight = this.view.Data.get(this.view.model, 'selected');
this.el.classList.toggle('highlight', shouldHighlight);
Array.from(this.$('.view-class')).forEach(element => {
element.classList.add('highlighted-icon');
});
}
});
Behavior Lifecycle#
A Behavior has a host-managed lifetime rather than the independent rendered,
attached, and destroyed state exposed by a View. In this table, the host view is
either a View or CollectionView. It constructs its Behaviors, keeps the same
instances through render and attachment transitions, and cleans them up when it
is destroyed. Nested Behaviors participate as Behaviors of the same host view.
| Operation | Host view | Behavior |
|---|---|---|
| Construct the View | Constructs each Behavior before the View's initialize. |
Receives its view and runs its own initialize; after the View initializes, receives the View's initialize notification. |
| Render or re-render the View | Runs each View lifecycle callback first. | The same instance receives the corresponding lifecycle callback after the View. |
| Show, detach, or re-show the host through a Region with lifecycle monitoring enabled | Changes the host's attachment state. | The same instance receives the corresponding attachment lifecycle after the host. |
First direct behavior.destroy() while the View is alive |
Remains alive without the removed Behavior. | Undelegates its events, stops listening, removes itself from the View, and deletes its entity-event handlers. It receives no later host lifecycle notifications. |
| Destroy the View | Runs before:destroy, tears down the View, and runs its destroy callback. Repeated View destruction is a no-op. |
Receives before:destroy while the View is alive, is cleaned up after the View enters destroyed, then receives destroy after the View's callback. Nested Behaviors follow the same ordering. |
Behavior does not expose an independent isDestroyed() state. Repeated direct
behavior.destroy() calls, reuse after direct cleanup, and other post-cleanup
operations are outside this lifecycle contract. Dependency access, invalid
references, and dynamic replacement semantics are separate Behavior contract
decisions; this table does not add an Application or State lifecycle to Behavior.
If a Region's owning view sets monitorViewEvents: false, the shown host does not
receive attachment lifecycle notifications, so its Behaviors do not receive them
either. Separately, setting monitorViewEvents: false on the host itself does not
by itself suppress Region attachment lifecycle. It suppresses the host's
dom:refresh and dom:remove notifications, so its Behaviors do not receive those
notifications.
Destroying a Behavior#
myBehavior.destroy() synchronously returns the Behavior after removing its
DOM and entity subscriptions, releasing its State subscriptions and owned State,
calling stopListening(), and removing it from the host. It does not emit an
independent destroy lifecycle or await Promises. Errors propagate and can leave
cleanup incomplete; the host itself remains alive.