5.0.0-beta.1 · Published beta · b06750c5. Published on npm. Match your installed version.
ConsumerDOM Interactions#
Marionette View and CollectionView instances manage DOM interactions through
a root DOM element, el. Core uses the browser DOM API by default: view.$()
and bound getUI() values are native NodeList instances, and delegated
handlers receive native DOM events.
View, CollectionView, and Behavior use the public EventDelegator runtime
adapter described below. Core provides a native DOM adapter by default.
DOM Ownership Boundaries#
Use these boundaries when deciding where DOM work belongs:
- The external shell chooses where a root View is mounted. Pass a concrete DOM
element as
el, or append the View's generatedelto the shell's mount. - A View owns its
eland the nodes produced by its template. - A Behavior borrows its host View's DOM boundary. It does not own a separate root; see Behavior host communication.
- The external shell or owning View owns the DOM element used as a Region mount. The Region manages the placement and lifecycle of its current child View at that mount. Use the View Region APIs to show, access, detach, or empty that child.
- A child View owns its own
eland handles interactions inside it.
DOM scoping is structural, not ownership-aware. view.$(), ui, and delegated
selectors are rooted at view.el, so they exclude matching elements outside
that root. They can still match a descendant owned by a child View. Do not use a
parent query such as parentView.$('.child-control') to manipulate child-owned
DOM. Give each owner distinct selectors and communicate across View boundaries
through public View or Region APIs and
explicit child events.
Canonical View Interaction#
The example below defines selectors once in ui, handles a save click through
events, and translates a close click into the form:close View event through
triggers.
import { View } from 'marionette';
export const FormView = View.extend({
template() {
return `
<form>
<button class="save" type="button">Save</button>
<button class="close" type="button">Close</button>
</form>
`;
},
ui: {
save: '.save',
close: '.close'
},
events: {
'click @ui.save': 'onSave'
},
triggers: {
'click @ui.close': 'form:close'
},
onSave(event) {
const [saveButton] = this.getUI('save');
saveButton.disabled = true;
this.triggerMethod('form:save', this, event);
},
onFormClose(view) {
view.el.dataset.closed = 'true';
}
});
Create and render the View before accessing its bound UI elements:
const formView = new FormView();
formView.render();
document.querySelector('#form-host').append(formView.el);
The shell owns #form-host; formView owns the generated formView.el inside
it. Destroy the View when the shell is finished with it so delegated handlers
and other owned resources are cleaned up.
View events#
The events attribute delegates DOM events from the View's el to functions or
methods on the View. A key has this shape:
'<dom event> [CSS selector]': 'methodName'
The CSS selector is optional. Without one, the handler is bound to the View's
root el. Use @ui.<name> in place of a literal selector to reference a
declared ui key, as the canonical example does with @ui.save.
The handler receives the native DOM event as its first argument and runs with
the View as its context. An events value must be a function or a string that
resolves to a callable method. Invalid handlers throw MarionetteError with
code MN0019 before Marionette delegates any handler from
that event map.
Delegation sees matching descendants throughout el. If a child View contains
the same selector, its bubbling DOM event can reach the parent handler. Prefer
owner-specific selectors; use Marionette events for parent-child communication
instead of relying on DOM bubbling across ownership boundaries.
Call view.delegateEvents(events) to refresh delegated DOM handlers after
changing a callable events or triggers definition. UI references use the
View's current selector bindings; a Behavior retains the selector map captured
at construction, as described in Behavior UI resolution. A supplied event
map replaces only the View's configured events for that delegation pass;
View triggers and Behavior events and triggers remain active. The method first
removes existing handlers, so repeated calls do not duplicate them.
view.undelegateEvents() removes the View and Behavior DOM handlers. Both
methods return the View, and both are no-ops after destruction has started.
Construction calls delegateEvents(). A subclass override remains responsible
for delegating to the base method when it wants Marionette's cleanup and redelegation.
EventDelegator Adapter#
An EventDelegator owns how one normalized events or triggers declaration is
registered and removed. Marionette still owns declaration resolution, handler
context, UI normalization, and the timing of registration and cleanup.
Configure every View, CollectionView, and Behavior class with the root setter:
import { setEventDelegator } from 'marionette';
setEventDelegator(MyEventDelegator);
Or configure one class hierarchy through its static setter:
const InstrumentedView = View.extend({});
InstrumentedView.setEventDelegator(MyEventDelegator);
The supplied object is a complete adapter, not a partial overlay. It must provide this method. This example retains native selector and focus behavior; an instrumentation adapter could record around the same registration:
export const CustomEventDelegator = {
delegate({ eventName, selector, handler, rootEl }) {
const capture = eventName === 'focus' || eventName === 'blur';
const listener = selector ? event => {
const target = event.target.nodeType === 1 ?
event.target : event.target.parentElement;
const match = target && target.closest(selector);
if (match && match !== rootEl && rootEl.contains(match)) {
event.delegateTarget = match;
return handler(event);
}
} : handler;
rootEl.addEventListener(eventName, listener, capture);
return () => rootEl.removeEventListener(eventName, listener, capture);
}
};
The arguments are:
eventName: the first token in the declaration key. Begin the key with the event name, without leading whitespace.selector: the remaining selector, or an empty string for a direct handler.handler: Marionette's normalized callback. The adapter must preserve its arguments and return behavior.rootEl: the View or CollectionView's currentel. A Behavior receives its host View's currentel.
delegate must return an idempotent cleanup function that removes exactly the
registration it created, including its original root, listener, namespace, and
capture/options policy. Marionette owns and stores that opaque cleanup. The
adapter must not mutate View internals.
Marionette invokes the returned cleanups during redelegation or destruction, in reverse registration order. Registration and cleanup errors propagate to the caller and stop the operation. Core does not roll back failed registration or attempt remaining cleanup after a callback throws. See the shared synchronous failure boundary.
setEventDelegator requires an adapter with a callable delegate method.
Each registration must return a working cleanup. The TypeScript contract
checks these shapes; core trusts the configured adapter.
Adapter selection occurs at registration time. Changing a global or per-class
adapter does not reinterpret existing registrations; their original opaque
cleanups remain authoritative. The newly configured adapter is used the next
time declarations are delegated, including a new instance, an explicit
delegateEvents() call. A per-class setter creates an own
adapter override for that class hierarchy, so a later root setter does not
replace it.
The native adapter uses addEventListener. Selector declarations walk from a
text or element target to the closest matching descendant of rootEl and set
event.delegateTarget to that match. Native event names are literal:
namespaces such as click.menu are not interpreted, and non-bubbling events
such as mouseenter are not emulated.
Delegated native focus and blur use capture because those events do not
bubble. The delegated handler therefore runs before a target-element listener.
A Marionette trigger stops propagation by default, which prevents the event
from reaching that target listener. Set stopPropagation: false on that
trigger when the target must also observe the focus or blur event; the
Marionette trigger still runs first. Marionette does not silently translate
these declarations to focusin or focusout.
A jQuery adapter can implement the same protocol with paired .on() and
.off() calls. Compatibility tests exercise that protocol, but v5 does not yet
ship a jQuery EventDelegator. A custom adapter is needed only when the
application requires jQuery-specific namespaces, programmatic dispatch, and
delegated focus behavior without adding jQuery to the core production graph.
React and Vue normally own events within the subtree they
render; integrate those subtrees through explicit DOM and lifecycle ownership
boundaries instead of replacing Marionette's EventDelegator with a React or Vue
adapter.
View triggers#
The triggers attribute translates a DOM event into a Marionette View event.
In the canonical example, clicking the close button emits exactly
form:close. Listeners and the matching onFormClose method receive the
triggering View first, followed by the native DOM event.
By default, a trigger calls preventDefault() and stopPropagation() on the
DOM event. Configure either behavior for one trigger with an object:
triggers: {
'click @ui.close': {
event: 'form:close',
preventDefault: true,
stopPropagation: false
}
}
These settings are local to the configured trigger. Selectors remain scoped only
by the View's root el.
For a child owned through a Region, automatic parent handling and forwarding is
opt-in. childViewEvents calls a configured parent handler,
childViewTriggers re-emits a configured parent event, and a non-false
childViewEventPrefix forwards prefixed events. A parent may instead subscribe
directly with public listenTo(childView, ...),
but that is an explicit subscription rather than automatic bubbling. See
Child View Events for the configured contracts.
Organizing a View with ui#
The ui attribute gives frequently used CSS selectors stable names:
ui: {
save: '.save',
close: '.close'
}
When Marionette iterates a UI definition for binding, or a map passed to a UI
normalization helper, it uses own enumerable string keys in standard JavaScript
own-key order. Inherited, symbol, and non-enumerable properties are ignored by
those iterations, and a numeric length is an ordinary key rather than an
array-like signal. Arrays, sparse arrays, and other array-like values are not
supported as UI maps. A literal own __proto__ key remains an own entry in
normalized and bound UI maps without changing either map's prototype. Direct
@ui.<name> lookup follows the own-declaration contract described below and
does not require the declared selector property to be enumerable.
When the View renders, Marionette queries each selector within view.el and
replaces the configured string with the resulting collection. With the default
DOM API, view.getUI('save') and view.ui.save are native NodeList
instances. Marionette rebinds those collections to replacement nodes after
each render.
Use getUI(name) after declaring a ui map and binding its elements when
application code needs a named element. Calling it without a declared map,
before binding, or after unbinding throws
MarionetteError with code MN0023. Once bound, a missing
key preserves the existing undefined result. Use the @ui.<name>
form in events, triggers, Behaviors, and Regions so a selector change has one
source of truth.
Every @ui.<name> reference must contain a non-empty name for an own, declared
key in the applicable ui map. Missing or inherited keys throw
MarionetteError with code MN0018 during normalization.
Selector values must be strings. An own key with undefined is not diagnosed
as missing by core; do not rely on a particular result for that unsupported value.
An explicitly declared empty selector is a known key, though the DOM API may
reject it when the selector is used.
Optional jQuery DOM Adapter#
Applications that explicitly configure
@mnjs/adapters/dom/jquery
before constructing Views receive jQuery collections from query methods. The
application-owned $el setup can add a
wrapper on View, CollectionView, and Behavior subclasses; no base-class helper
is exported. Core examples use native
collections so the default package remains jQuery-free.