# Marionette 5.0.0-beta.1 documentation bundle
Version: 5.0.0-beta.1
Channel: next
Publication: beta (published on npm)
Source revision: b06750c507494441f0b2298766b70087e45346a2
Local changes: false
Content SHA-256: 3fe8788a771994d9effd124fee94d7444637a27ec974a5b520ccbf43b55bbec9
Read individual documents for focused tasks. Each section below contains the exact published reading Markdown, including its source identity.
---
Document: docs/readme.md
Canonical URL: https://marionettejs.com/docs/
Markdown URL: https://marionettejs.com/docs/index.md
Reading SHA-256: 7f73d9a4ff2f5ec0af5843ea7b954c0afc6160d925850f54e98cc702e060a668
# Build your first piece of UI
A View handles a piece of the interface. A Region puts it on the page and cleans
it up when it is replaced. Start there; add the other pieces when you need them.
## Where do you want to start?
- **[Build something](/docs/installation.md#quick-start)** — set up Marionette and show your first View.
- **[Work with an agent](/docs/agents.md)** — give your agent the right contract and a concrete task.
- **[Look up an API](/docs/public-api.md)** — find the class, method, or integration you need.
## A button that does something
With a [matching v5 build](/docs/installation.md#install) installed, add a place for the
View in your HTML:
```html
```
Then run this module in your application:
```javascript
import { Region, View } from 'marionette';
const Counter = View.extend({
initialize() { this.count = 0; },
template: ({ count }) => ``,
templateContext() { return { count: this.count }; },
events: { 'click button': 'increment' },
increment() {
this.count += 1;
this.el.querySelector('span').textContent = String(this.count);
}
});
export const region = new Region({ el: '#app' });
region.show(new Counter());
```
Click the button: **Count: 0 → Count: 1 → Count: 2**. The View handles the click
and updates the number in place. The button stays the same DOM element.
When that part of the screen is finished, `region.empty()` destroys the View and
removes its event handlers. The `#app` mount remains, ready for the next View.
## Give it a little more to do
| You want to… | Next step |
| --- | --- |
| Show a list that changes | [Render children with CollectionView](/docs/collection-view.md) |
| Open a detail screen | [Show and replace a View](/docs/region.md) |
| Save a form without losing a draft | [Forms and accessibility](/docs/forms-and-accessibility.md) |
| Connect an existing router or data source | [Choose integrations](/docs/choosing-integrations.md) |
| Check that it works | [Test an application](/docs/testing.md) |
You can keep Backbone models, an existing router, or a preferred template system.
Choose each integration for the job it does; the button above needs none of them.
For versions before v5, see the [backbone.marionette repository](https://github.com/marionettejs/backbone.marionette).
[Canonical source](/docs/markdown/docs/readme.md) · [Source identity](/docs/manifest.json)
---
Document: docs/installation.md
Canonical URL: https://marionettejs.com/docs/installation/
Markdown URL: https://marionettejs.com/docs/installation.md
Reading SHA-256: fdd88cef2835ae795727e379c4d6e928eb88292020a0fc57c0ffb5a7e8808b77
# Installing Marionette
Install the core package, show a View, then add the integrations your application
needs. Native DOM APIs, plain objects, and function templates work out of the box.
This guide covers the published Marionette 5.0.0-beta.1 package and its matching companion packages.
## Documentation Index
* [Install](#install)
* [Peer dependencies](#peer-dependencies)
* [Quick start](#quick-start)
* [TypeScript](#typescript)
* [Independent runtimes](#independent-runtimes)
* [Observable data sources](#observable-data-sources)
* [Distribution formats](#distribution-formats)
* [Backbone is optional](#backbone-is-optional)
* [jQuery DOM adapter is optional](#jquery-dom-adapter-is-optional)
* [DOM content adapters are optional](#dom-content-adapters-are-optional)
* [Current v5 documentation](/docs/index.md)
## Install
The v5 package name is `marionette`.
```bash
npm install marionette@5.0.0-beta.1
```
The beta is available on npm. Pin this exact version and use the matching documentation. For unreleased changes, build and pack a known source revision separately.
> The v4 package name has changed. See the [upgrade guide](/docs/upgrade-guide.md)
> for migration guidance from earlier releases.
Core and `@mnjs/data` automatically install the matching `@mnjs/utils`
version. Applications do not need a separate install unless they import helpers
directly. During prereleases, keep Marionette packages on the same version. See the
[shared helpers](/docs/common.md#shared-helpers) for reusable component helpers.
## Peer dependencies
Marionette v5 core has no peer dependencies. The separate
`@mnjs/adapters` package requires the matching Marionette version and
declares the integration-specific peers as optional.
| Peer | Required? | When you need it |
|---|---|---|
| `marionette` `5.0.0-beta.1` | Required | The matching core runtime configured with an adapter. |
| `backbone` `^1.4.0` | Optional | Only if your app imports `@mnjs/adapters/backbone`. See [Backbone is optional](#backbone-is-optional). |
| `@types/backbone` `^1.4.23` | Optional | TypeScript declarations for `@mnjs/adapters/backbone`. JavaScript consumers do not need it. |
| `jquery` `^4.0.0` | Optional | Only if your app uses the `@mnjs/adapters/dom/jquery` adapter. See [jQuery DOM adapter is optional](#jquery-dom-adapter-is-optional). |
| `@types/jquery` `^4.0.1` | Optional | TypeScript declarations for `@mnjs/adapters/dom/jquery`. JavaScript consumers do not need it. |
| `morphdom` `^2.7.8` | Optional | Only if your app imports `@mnjs/adapters/dom/morphdom`. |
| `lit-html` `^3.3.3` | Optional | Only if your app imports `@mnjs/adapters/dom/lit-html`. |
Optional peers are installed only when you opt into them:
```bash
# Only if you use the Backbone integration
npm install @mnjs/adapters@5.0.0-beta.1 backbone
# Only if you use the jQuery DomApi adapter
npm install @mnjs/adapters@5.0.0-beta.1 jquery
# Only if you use XState actors
npm install @mnjs/adapters@5.0.0-beta.1 xstate
```
The XState actor adapter does not import or declare XState as a peer. Install
XState alongside the adapter; the adapter consumes its public actor shape.
Npm does not install missing optional peers. TypeScript consumers of an optional
subpath must install its matching type package explicitly:
```bash
# Only if TypeScript imports @mnjs/adapters/backbone
npm install --save-dev @types/backbone@^1.4.23
# Only if TypeScript imports @mnjs/adapters/dom/jquery
npm install --save-dev @types/jquery@^4.0.1
```
Marionette core does not import or require Underscore. Install it as an
application dependency only when your own code uses it, such as an `_.template`
used by a View.
## Quick start
Marionette v5 exposes its public API through named ESM imports. There is no
default-namespace export; use named imports only. Add a mount element to the page
before running the module:
```html
'
});
const app = new Application({
region: document.getElementById('app'),
onStart() {
this.showView(new RootView());
}
});
await app.start();
```
`View` and `CollectionView` accept a DOM element for `el`. They do not resolve
selector strings — pass `document.querySelector('#root')` at the call site. See
the [upgrade guide](/docs/upgrade-guide.md) for the migration entry. `Region` continues
to accept selector strings.
## TypeScript
Marionette 5.0.0-beta.1 includes declarations for TypeScript 6 and 7, with ESM and
CommonJS entrypoints. Core needs no separate `@types` package. Annotate `initialize`
to describe a View's application options; TypeScript uses that signature to check
construction and `this.options`.
```ts
import { View } from 'marionette';
const MessageView = View.extend({
template: false,
initialize(options: { message: string }) {
this.el.textContent = options.message;
},
message(): string {
return this.options.message;
}
});
const view = new MessageView({ message: 'Hello, Marionette.' });
document.body.append(view.render().el);
```
This View requires a string `message`. Missing options or a numeric message are
compile errors. `template: false` preserves the text set during initialization.
Named imports work with `NodeNext` or bundler module resolution. The
[consumer TypeScript guide](/docs/typescript.md) covers application options,
DOM events, module resolution, and inheritance choices. Optional
integrations may need their own type packages, listed above.
## Independent runtimes
The named root exports form one default runtime. Use `createMarionette()` only when
independent applications in the same process need isolated classes, adapters,
renderer configuration, or Radio channels:
```javascript
import { createMarionette } from 'marionette';
const isolated = createMarionette();
const IsolatedView = isolated.View.extend({ template: () => 'Independent' });
```
See [Runtime isolation](/docs/runtime-isolation.md) for composition and ownership rules.
## Observable data sources
Core's default DataApi supports plain objects and static arrays without a required
dependency. Backbone Models and Collections are observable sources too; retain
them through the [Backbone adapter](/docs/backbone.md) when the application
already uses them. For a new application needing observable Model and ordered
Collection sources, the optional `@mnjs/data` package is the native choice:
```bash
npm install @mnjs/data@5.0.0-beta.1
```
Configure its adapters before constructing owners. See the
[`@mnjs/data` guide](/docs/data-api.md#optional-mnjsdata-sources) for a
complete adapter setup and rendered list example.
Applications using XState actors can select an ordered array of child actor
references through `@mnjs/adapters/xstate`. See
[XState actors](/docs/data-api.md#xstate-actors).
## Distribution formats
ES modules are the canonical path for new applications. Use `import` syntax so
package export conditions select the ESM entry, and use Marionette's named exports.
Marionette also ships compatibility distributions throughout v5:
- CommonJS supports legacy Node and build-tool consumers through
`require('marionette')`.
- Unminified and minified UMD builds support no-bundler, AMD, and
`Marionette`-global consumers.
All four ESM, CommonJS, unminified UMD, and minified UMD outputs remain supported
and distribution-validated for v5. Marionette will not add another format or switch
to unbundled source modules without measured consumer benefit. Six months after
v5.0.0 is published, the distribution review is an evidence checkpoint for a
future major version, not a removal commitment.
## Backbone is optional
Starting with v5, Marionette core does not depend on Backbone at runtime. Plain
objects and arrays use the default DataApi. Applications passing Backbone Models
or Collections to Marionette must configure the Backbone DataApi before
constructing those consumers:
```javascript
import BackboneApi from '@mnjs/adapters/backbone';
import { setDataApi } from 'marionette';
setDataApi(BackboneApi);
```
This configures model and collection use. Select the StateApi role separately
when an owner uses Backbone state; see [Optional Backbone](/docs/backbone.md).
[Data API](/docs/data-api.md) describes the neutral runtime contract.
## jQuery DOM adapter is optional
Marionette v5 core is jQuery-free. The default DOM API uses native browser
methods, and `view.$(selector)` returns a `NodeList`.
Applications that want jQuery-shaped results from Marionette's DOM helpers —
for example, `view.$(selector)` returning a jQuery collection — can opt into
the optional `@mnjs/adapters/dom/jquery` adapter at app boot:
```javascript
import { setDomApi } from 'marionette';
import JQueryDomApi from '@mnjs/adapters/dom/jquery';
setDomApi(JQueryDomApi);
```
The adapter imports `jquery`, so this integration requires `jquery` only when you
select that adapter. If existing code also uses `$el`, assign `this.$el = $(this.el)` in
its View, CollectionView, or Behavior `initialize()` method. See the [upgrade guide](/docs/upgrade-guide.md) for the migration entries on jQuery DOM
compatibility and the `detachContents` policy.
## DOM content adapters are optional
Use the same `@mnjs/adapters` package for incremental rendering. Install
only the DOM library you select:
```bash
npm install @mnjs/adapters@5.0.0-beta.1 morphdom
# or
npm install @mnjs/adapters@5.0.0-beta.1 lit-html
```
Import `MorphdomDomApi` from `@mnjs/adapters/dom/morphdom`, or
`LitDomApi` from `@mnjs/adapters/dom/lit-html`, and pass it to
`ViewClass.setDomApi()` before creating instances. Each adapter preserves unrelated
DOM operations. Lit supplies the attachment hooks its directives need. DataApi and StateApi
configuration remains explicit and separate.
See [Rendering to DOM](/docs/rendering.md#rendering-to-dom)
for examples and lifecycle requirements.
## Getting Started
[Choose a class for the job](/docs/classes.md), or learn the
[shared configuration patterns](/docs/basics.md).
[Canonical source](/docs/markdown/docs/installation.md) · [Source identity](/docs/manifest.json)
---
Document: docs/beta.md
Canonical URL: https://marionettejs.com/docs/beta/
Markdown URL: https://marionettejs.com/docs/beta.md
Reading SHA-256: 136904dde3bd7dd4f0888a6c6fed9fb0297d2dc1eb3e4e7936ec4797d3a6040a
# Try Marionette v5 beta
`5.0.0-beta.1` is published on npm and ready for application trials. Install the exact registry version. A locally built artifact with the same version string may differ from the published release.
## What beta means
The intended architecture is ready for application trials: named core imports,
View and Region ownership, synchronous UI lifecycle, Application asynchronous
coordination, optional data/state providers, and first-party package declarations.
Use those documented public contracts. Beta feedback can still change an API before
stable; record any change in migration guidance and the release notes.
This beta makes no comparative agent-effectiveness claim. The public corpus remains
an unscored prototype. Architecture lint, generated method metadata, development
inspection, and additional test helpers are separate work, not installed features.
Core is `marionette`. The companion packages are `@mnjs/utils`,
`@mnjs/radio`, `@mnjs/data`, and `@mnjs/adapters`. Keep all package
versions aligned; install optional providers only when needed. See
[the migration ledger](/docs/migration-from-v4.md) and [upgrade guide](/docs/upgrade-guide.md).
Older registry alphas are different implementations and do not define this beta's API.
## Start in an empty directory
Install core and the optional native data package explicitly:
```sh
mkdir my-marionette-app
cd my-marionette-app
npm init -y
npm install marionette@5.0.0-beta.1 @mnjs/data@5.0.0-beta.1
cp -R node_modules/marionette/dist/docs/starter ./starter
cd starter
npm install marionette@5.0.0-beta.1 @mnjs/data@5.0.0-beta.1
npm test
npm run build
npm run dev
```
The starter README explains its files and trial steps. It is also available in the
[source tree](https://github.com/marionettejs/marionette/tree/master/test/fixtures/data-package-starter).
Copying uses a new directory and preserves existing application files. The commands
above use a POSIX shell; on Windows, copy the same folder using your file manager.
Before publication, replace each runtime install with one `npm install` invocation
containing all five absolute candidate tarball paths. The required companions are
not assumed to exist on npm. Use artifacts from the same `release-evidence.json`;
keep their SHA-512 checksums and source commit with your trial report. Do not use
`npm link`, a Git dependency, or source imports as proof of the published install path.
The starter has editable rows, asynchronous local selection, deliberate cancellation,
and teardown. It has no backend, persistence, or URL router. Connect its `navigate`
function to the application's chosen router when URLs are needed. See
[routing](/docs/routing.md) for loader failure, navigation away, and stop/restart rules.
Use [TypeScript guidance](/docs/typescript.md) when adding typed application code.
## Check a real feature
1. Edit a row title without opening it. Reverse rows; the draft should survive.
2. Open the slow first note, then immediately open the second. The second should remain.
3. Change a module during `npm run dev`. The old workspace should release its handlers.
4. Run `npm test` and `npm run build`. Add a regression for your application's behavior.
5. Test keyboard focus and selection in a real browser using the actual DOM adapter.
6. Install the [consumer agent skill](/docs/agent-tools.md) if useful, then ask it to locate
the installed docs and identify the component responsible for cancellation.
The installed-consumer fixture checks the starter outside the repository against
candidate tarballs. The browser release matrix checks its draft, focus, selection,
stale-load suppression, and handler cleanup in Chromium, Firefox, and WebKit.
Those checks do not establish accessibility for an entire application or a router's
history/deployment behavior.
## Report feedback
[Open a reproducible issue](https://github.com/marionettejs/marionette/issues/new/choose)
with the exact package versions/source revision, selected providers, browser and
bundler, expected behavior, actual behavior, and a minimal anonymous reproduction.
Prioritize installation problems, incorrect declarations, lost editable state,
late navigation commits, leaked subscriptions, and confusing documentation.
Do not include private application code or customer data.
## Before publication
A beta needs verified scope/publisher access for all five packages, a clean candidate
commit, and the full [exact-artifact validation](https://github.com/marionettejs/marionette/blob/b06750c507494441f0b2298766b70087e45346a2/docs/release-promotion.md#dry-run).
Review the beta notes, migration guidance and installed starter together. Record
known failures instead of claiming the beta is stable. Registry installation must
be checked immediately after publication; local tarball tests cannot prove npm
permission, propagation, or trusted-publisher configuration.
## If the beta fails in your application
Pin your previous working dependency versions and restore the matching application
code and lockfile. The old `marionette@5.0.0-alpha.2` is not an API-compatible rollback
for this candidate; there is currently no previous published matching five-package
release. Existing v4 applications should retain their pre-migration revision and
`backbone.marionette` lockfile until their beta trial succeeds.
Maintainers must not overwrite a published beta version. Withdraw its recommendation,
deprecate a broken version with a specific reason, and publish a corrected beta.
Move `next` only to a verified compatible prior release; if beta.1 is the first one,
there is no earlier beta to select. Preserve exact artifacts and failure evidence.
See [release recovery](https://github.com/marionettejs/marionette/blob/b06750c507494441f0b2298766b70087e45346a2/docs/release-promotion.md#recovery-and-rollback).
[Canonical source](/docs/markdown/docs/beta.md) · [Source identity](/docs/manifest.json)
---
Document: docs/agents.md
Canonical URL: https://marionettejs.com/docs/agents/
Markdown URL: https://marionettejs.com/docs/agents.md
Reading SHA-256: 9911ce2b807e2139c7120e3486c768b60df95e34d85a7fc0b785cae8d33d1ecb
# Build with Marionette
Use this guide when an agent is building or maintaining an application with
Marionette. It links each decision to the same contracts a human reviewer uses.
For changes to Marionette itself, use the [maintainer guide](https://github.com/marionettejs/marionette/blob/b06750c507494441f0b2298766b70087e45346a2/docs/maintainers/readme.md).
## Establish the installed contract
Before choosing an API, inspect the application's package manifest, lockfile,
installed declarations, and existing Marionette configuration. Record:
- the installed `marionette` version and matching optional package versions;
- whether the dependency comes from a published package, Git commit, or local build;
- the source revision for a checkout or custom artifact;
- the selected renderer, data/state sources, DOM integrations, and router.
This documentation matches the published Marionette 5.0.0-beta.1 package. A website example, a copied prompt, or a third-party search result is not proof that another installed version has that API. Check the installed version, exports, and declarations; reproduce uncertain behavior with a small test against that package.
For a fresh application, follow [installation](/docs/installation.md). For a v4
application, use the [migration guide](/docs/migration-from-v4.md) and
[upgrade guide](/docs/upgrade-guide.md) before applying current patterns. Do not
silently upgrade dependencies to make an example fit.
## Read for the task
| Task | Start here | Verify |
| --- | --- | --- |
| Show or update a piece of UI | [View](/docs/view.md), [rendering](/docs/rendering.md) | The intended element and content change; relevant handlers still work after rendering. |
| Replace part of a screen | [Region](/docs/region.md), [View lifecycle](/docs/lifecycle.md) | The outgoing View is cleaned up and the new View owns the intended mount. |
| Render a changing list | [CollectionView](/docs/collection-view.md), [DataApi](/docs/data-api.md) | Stable item identity, correct ordering, removal cleanup, and preservation of surviving edits. |
| Coordinate a feature or navigate | [Application](/docs/application.md), [routing](/docs/routing.md) | Startup success, stale navigation, failure, stop, and destruction. |
| Choose data, state, rendering, or DOM integration | [Choosing integrations](/docs/choosing-integrations.md) | The chosen capability matches the source; configuring one integration does not implicitly configure another. |
| Add local or shared state | [State sources](/docs/state.md) | The correct observer updates; destroying one borrower does not dispose shared state. |
| Handle DOM or component events | [DOM interactions](/docs/dom-interactions.md), [events](/docs/events.md) | One intended response per interaction and no response after teardown. |
| Diagnose a framework error | [Diagnostic catalog](/docs/diagnostics.md) | The invariant associated with the diagnostic code; do not match only error-message text. |
Read the relevant page and its direct references. Load the full documentation only
when the task requires a broader API review.
## Choose the smallest supported pattern
Keep the application's established integrations unless the task requires changing
them. For new code, start with the built-in defaults: native DOM APIs, function
templates, and plain objects or arrays. Plain sources are not observable; update
the UI explicitly or select an observable integration when the task needs one.
Choose data, state, rendering, and DOM capabilities independently. Follow the
[integration decision order](/docs/choosing-integrations.md) before writing a custom
adapter. Record the chosen provider and its registration point once in the
application's own architecture notes so later agents do not choose again.
Use a View for interface ownership, a Region for placement, and a CollectionView
for repeated children. Use an Application when work has an asynchronous feature
lifecycle. A plain function or class is enough when it needs none of these
contracts. The [class guide](/docs/classes.md) explains the boundaries.
Configure the selected runtime before creating its consumers. The default named
exports share a runtime. Use [runtime isolation](/docs/runtime-isolation.md) when
independent configurations must coexist; do not create a runtime per View.
## Make ownership and cancellation explicit
For each resource, name the owner and the operation that releases it. Let the
owning Region or CollectionView manage its child Views through public APIs.
Use [View lifecycle hooks](/docs/lifecycle.md) for external listeners, timers,
and widgets according to their actual render, attachment, and destruction lifetime.
A rerender must not accumulate resources; destroying a View must not leave them
running.
A supplied `state` source is borrowed. A `createState()` result is owned and uses
the configured StateApi's optional disposal hook when its owner is destroyed.
Marionette does not infer ownership from which object first reads a source.
Await Application lifecycle operations when later work depends on their result.
They return `Promise`: `true` means the target state was reached; `false`
means the request was superseded. A current readiness failure rejects. Keep those
outcomes distinct. Constructor hooks run synchronously, and completion hooks are synchronous
notifications; returning a Promise from them does not add readiness.
Pass the readiness hook's signal to cancellable work. After an asynchronous step,
check that it still belongs to the active operation before committing application
side effects. Marionette suppresses stale lifecycle completion; it cannot undo an
arbitrary write made by application code. Follow the complete
[routing pattern](/docs/routing.md) for navigation and feature startup.
## Prove the behavior in the application
Use the application's existing test runner, scripts, and package manager. Library
maintenance commands are not a consumer project's test strategy.
Test the successful interaction and the boundary most likely to break. For an
asynchronous screen, navigate away while work is pending and ensure its stale
result cannot replace the current screen. For a list, edit a surviving row while
inserting, removing, or reordering another row. For a subscription, destroy one
consumer and confirm the remaining consumer still receives updates.
Use a real browser when correctness depends on focus, attachment, DOM event
propagation, or editable state. A build or screenshot alone does not prove those
interactions. Use documented public APIs for assertions rather than private
framework fields.
When reporting a change, name the behavior, the tested package/source, the exact
commands or interactions performed, and any untested boundary. Keep changes
focused and avoid introducing runtime instrumentation merely to help an agent
understand the code.
## Use agent tools as another way to read the same docs
Follow [Set up an agent](/docs/agent-tools.md) to install the consumer skill and read
version-matched packaged docs. Adapt the [application instruction template](/docs/application-agent-template.md)
to preserve this project's actual decisions across tasks.
A Markdown page or versioned documentation index can be read directly. A service
such as Context7 can help locate the relevant passage, but verify its library and
version selection before using the result. When retrieval is unavailable, use the
same source documents in the repository or the matching documentation artifact.
A documentation index does not install instructions into every agent. An
application's own agent instructions should link to this guide and record its
installed version and architecture choices. They should not copy this entire guide
or use this library's maintainer instructions as application policy.
Use playground tools only to examine the example they control. Their results do
not establish behavior in your application. No hosted AI service or MCP server is
required to use Marionette or follow this workflow.
[Canonical source](/docs/markdown/docs/agents.md) · [Source identity](/docs/manifest.json)
---
Document: docs/classes.md
Canonical URL: https://marionettejs.com/docs/classes/
Markdown URL: https://marionettejs.com/docs/classes.md
Reading SHA-256: e55e03ae39732d7b536522e0f38c71ce4179b21b14e8ed05970eb4f84cd02a7b
# Marionette Classes
Each Marionette class has a job: render a piece of interface, manage where it goes,
repeat it, share an interaction, or coordinate a feature. Start with the job you
need, then follow the reference for its options and lifecycle.
The classes share [configuration and inheritance patterns](/docs/basics.md#class-based-inheritance)
and a [common set of methods](/docs/common.md).
## [Marionette.View](/docs/view.md)
A `View` owns a piece of interface through its root element, `el`. It renders a
template, handles DOM interactions, and can divide a screen into Regions for child
Views. Plain objects and function templates work with the default configuration.
`View` includes:
- [The DOM API](/docs/dom-api.md)
- [Class Events](/docs/class-events.md#view-events)
- [DOM Interactions](/docs/dom-interactions.md)
- [Child Event Bubbling](/docs/events.md#event-bubbling)
- [Entity Events](/docs/entity-events.md)
- [View Rendering](/docs/rendering.md)
- [Prerendered Content](/docs/prerendered-dom.md)
- [View Lifecycle](/docs/lifecycle.md)
A `View` can have [`Region`s](#marionetteregion) and [`Behavior`s](#marionettebehavior)
## [Marionette.CollectionView](/docs/collection-view.md)
A `CollectionView` manages an ordered set of child Views inside its root element.
Use it for rows, cards, or other repeated content. A plain array supplies a static
collection; an observable data integration can notify it of changes. You can also
manage child Views directly without supplying a collection.
`CollectionView` includes:
- [The DOM API](/docs/dom-api.md)
- [Class Events](/docs/class-events.md#collectionview-events)
- [DOM Interactions](/docs/dom-interactions.md)
- [Child Event Bubbling](/docs/events.md#event-bubbling)
- [Entity Events](/docs/entity-events.md)
- [View Rendering](/docs/rendering.md)
- [Prerendered Content](/docs/prerendered-dom.md)
- [View Lifecycle](/docs/lifecycle.md)
A `CollectionView` can have [`Behavior`s](#marionettebehavior).
## [Marionette.Region](/docs/region.md)
A `Region` gives a View a place to appear. Showing a new View renders and attaches
it; replacing or emptying the Region destroys its current View by default.
`Region` includes:
- [Class Events](/docs/class-events.md#region-events)
- [The DOM API](/docs/dom-api.md)
## [Marionette.Behavior](/docs/behavior.md)
A `Behavior` shares interaction logic between Views, such as keyboard shortcuts or
a reusable button action. The host View constructs and cleans up its Behaviors.
`Behavior` includes:
- [Class Events](/docs/class-events.md#behavior-events)
- [DOM Interactions](/docs/dom-interactions.md)
- [Entity Events](/docs/entity-events.md)
## [Marionette.Application](/docs/application.md)
An `Application` coordinates a feature's asynchronous start, stop, restart, and
destruction. It can own child Applications and display a View through an optional
Region. Use it for work that should start and stop together.
`Application` includes:
- [Class Events](/docs/class-events.md#application-events)
- [Radio API](/docs/radio.md#marionette-integration)
- [Common Marionette Functionality](/docs/common.md)
- [State API](/docs/state.md)
An `Application` can have a single [region](/docs/application.md#application-region).
## [Marionette.MnObject](/docs/mn-object.md)
`MnObject` gives a nonvisual object initialization, events, options, and cleanup.
Use it when those conventions are useful without an element or an Application's
asynchronous lifecycle.
`MnObject` includes:
- [Class Events](/docs/class-events.md#mnobject-events)
- [Radio API](/docs/radio.md#marionette-integration).
## [State sources and StateApi](/docs/state.md)
Give a feature or View its own state, or pass in a source it should share.
`StateApi` connects that source's notifications and cleanup to its owner.
## Routing in Marionette
Choose a router that fits your application. Route handlers can start an Application
or show a View using ordinary application code.
[Continue Reading](/docs/routing.md) about routing in Marionette.
[Canonical source](/docs/markdown/docs/classes.md) · [Source identity](/docs/manifest.json)
---
Document: docs/basics.md
Canonical URL: https://marionettejs.com/docs/basics/
Markdown URL: https://marionettejs.com/docs/basics.md
Reading SHA-256: 38df7b22ced5ac44884ecb3f86cec653e494afa002ae2ae9592a072f2f32302a
# Common Marionette Concepts
Learn the configuration patterns once, then use them across Marionette's classes.
Each class's reference explains when it reads an option and whether it reads it
again. For checked application options, see the
[TypeScript example](/docs/installation.md#typescript).
## Documentation Index
* [Importing Marionette](#importing-marionette)
* [Class-based Inheritance](#class-based-inheritance)
* [Value Attributes](#value-attributes)
* [Functions Returning Values](#functions-returning-values)
* [Binding Attributes on Instantiation](#binding-attributes-on-instantiation)
* [Common Marionette Functionality](/docs/common.md)
## Importing Marionette
Install the v5 `marionette` package and use named imports:
```javascript
import { Application, View } from 'marionette';
const view = new View();
const app = new Application();
```
V5 has no default namespace export. The separate `@mnjs/adapters` package
provides optional integration subpaths; see [Installing Marionette](/docs/installation.md)
for the entrypoints and their dependencies.
Existing no-bundler applications may serve the published
`dist/marionette.umd.js` artifact. It exposes the named API on the global
`Marionette` object and supports `Marionette.noConflict()`. Package-based named
imports are the canonical path for new applications.
## Class-based Inheritance
Like [Backbone](http://backbonejs.org/#Model-extend), Marionette provides a
pseudo-class `extend` method. [All built-in classes](/docs/classes.md), such as
`View` and `MnObject`, provide this method.
The `protoProps` and `staticProps` hashes passed to `extend` contribute their own
enumerable string and symbol keys. Non-enumerable and inherited input properties
are ignored, except that an own `constructor` selects the child constructor even
when it is non-enumerable. Enumerable string statics from the parent, including
inherited ones, are copied to the child constructor.
In the example below, we create a new pseudo-class called `MyView`:
```javascript
import { View } from 'marionette';
const MyView = View.extend({});
```
You can now create instances of `MyView` with JavaScript's `new` keyword:
```javascript
const view = new MyView();
```
### Value Attributes
When we extend classes, we can provide class attributes with specific values by
defining them in the object we pass as the `extend` parameter:
```javascript
import { View } from 'marionette';
const MyView = View.extend({
className: 'bg-success',
template: () => '',
regions: {
myRegion: '.my-region'
},
modelEvents: {
change: 'removeBackground'
},
removeBackground() {
this.el.classList.remove('bg-success');
}
});
```
When `MyView` creates its element, the element receives the `bg-success` class.
When the View renders, the `myRegion` Region targets `.my-region` within that
element. Entity-event behavior is documented separately because it depends on
an attached entity.
### Functions Returning Values
Many configuration attributes accept either a value or a function returning
that value. Attributes documented as value callbacks call the function with
the Marionette instance as `this`. A `template` function is the renderer itself
and instead receives serialized data as its argument; it does not receive the
View as `this`. Resolution timing is part of each attribute's contract; do not
assume every function runs during construction or that every result is cached
for the object's lifetime.
```javascript
import { View } from 'marionette';
let cancelCalls = 0;
let defaultCalls = 0;
let overrideCalls = 0;
let templateContext;
let templateData;
const MyView = View.extend({
options() {
this.optionsResolutionCount = (this.optionsResolutionCount || 0) + 1;
return {
count: 1,
enabled: true,
label: 'default',
tone: 'quiet'
};
},
className() {
this.classNameResolutionCount = (this.classNameResolutionCount || 0) + 1;
return `notice-${this.getOption('tone')}`;
},
template(data) {
templateContext = this;
templateData = data;
return '';
},
triggers: {
'click .cancel': 'cancel:default',
'click .save': 'save:default'
},
});
const view = new MyView({
count: 0,
enabled: false,
label: null,
tone: 'urgent',
triggers: {
'click .save': 'save:override'
},
});
const classNameBeforeRender = view.el.className;
view.on('cancel:default', () => {
cancelCalls += 1;
});
view.on('save:default', () => {
defaultCalls += 1;
});
view.on('save:override', () => {
overrideCalls += 1;
});
view.render();
view.el.querySelector('.save').click();
view.el.querySelector('.cancel').click();
export {
cancelCalls,
classNameBeforeRender,
defaultCalls,
overrideCalls,
templateContext,
templateData,
view
};
```
Here `options()` supplies class defaults, the constructor's `tone` wins, and
`className()` resolves while the View creates its element. The constructor's
`triggers` map replaces the class map rather than merging with it.
### Function Context
Use a normal method when a configuration callback needs the instance context.
An arrow function retains its surrounding lexical `this`, so it is appropriate
only when the callback does not need the Marionette instance.
### Binding Attributes on Instantiation
The documented constructor options for each class can replace matching values
defined on its prototype. This supports runtime configuration such as a View's
events, triggers, model, collection, and Region definitions:
```javascript
import { View } from 'marionette';
const MyView = View.extend({
template: () => 'Details'
});
const myView = new MyView({
triggers: {
'click a': 'show:link'
}
});
```
This will set a trigger called `show:link` that will be fired whenever the user
clicks an `` inside the view.
Constructor values replace matching class values; map options are not
implicitly deep-merged. For example:
```javascript
import { View } from 'marionette';
const MyView = View.extend({
template: () => 'Details',
triggers: {
'click @ui.save': 'save:form'
}
});
const myView = new MyView({
triggers: {
'click a': 'show:link'
}
});
```
In this example, `show:link` is the only configured trigger. The constructor's
`triggers` object completely replaces the class-defined object.
## Setting Options
Every Marionette class stores its merged class defaults and constructor values
on `this.options`. `getOption(name)` reads a defined value from `this.options`
before falling back to the instance. A constructor value of `false`, `null`, or
`0` therefore remains an intentional override; only `undefined` falls through.
Resolved class defaults and constructor option hashes contribute their own
enumerable string and symbol properties when Marionette builds `options`.
Inherited and non-enumerable properties are ignored. `mergeOptions` copies only
the requested own enumerable string options onto an instance.
```javascript
import { View } from 'marionette';
const MyView = View.extend({
checkOption() {
console.log(this.getOption('foo'));
}
});
const view = new MyView({
foo: 'some text'
});
view.checkOption(); // prints 'some text'
```
Constructor/default option merges use own enumerable string and symbol properties. See
[`getOption` and `mergeOptions`](/docs/common.md#getoption) for the exact lookup and
copying boundaries.
## Common Marionette Functionality
Marionette has a few methods and core functionality that are common to [all classes](/docs/classes.md).
[Continue Reading...](/docs/common.md).
[Canonical source](/docs/markdown/docs/basics.md) · [Source identity](/docs/manifest.json)
---
Document: docs/terminology.md
Canonical URL: https://marionettejs.com/docs/terminology/
Markdown URL: https://marionettejs.com/docs/terminology.md
Reading SHA-256: 7f32fedf7105689e6873026e967d7bc061b117def920c7f3b55c99b877aee44e
# Terms used in these guides
These names describe what a value does, where it belongs, and who cleans it up.
The distinctions matter when connecting data, composing Views, or waiting for an
Application to finish starting.
## Models, template data, and state
A **model** is one value displayed by a View or represented by a CollectionView's
child View. It may be a plain object or a value from your chosen data library.
An **ordered model snapshot** is the current sequence returned by
`DataApi.models(collection)`. Collection change records refer to those original
models through `added`, `removed`, `previous`, and `current`.
**Serialized data** is the value prepared for a template. The default
`serializeCollection()` returns each model's serialized value; it does not return
the raw model snapshot. An override may return another shape. When the View has
no model, its template receives that collection serialization result as `models`.
See [Rendering](/docs/rendering.md).
A **state source** holds state for an Application, MnObject, View, CollectionView,
or Behavior. `getState()` returns the source itself, with its own values and
methods. State is configured separately from the model or collection a View
displays. See [State sources](/docs/state.md).
| API | What it connects |
| --- | --- |
| [`DataApi`](/docs/data-api.md) | Model reads, template serialization, collection order, and data events. |
| [`StateApi`](/docs/state.md#stateapi) | State events and cleanup of owned state. |
| [`DomApi`](/docs/dom-api.md) | Element creation, selection, content, and attachment. |
An **adapter** implements the methods for one or more of these APIs using your
chosen tools. Installing an integration package makes its adapter available;
configure it on the runtime or class that will use it. DataApi, StateApi, and
DomApi support partial overlays: supplied methods replace the corresponding
methods, while omitted methods remain inherited. An EventDelegator is a complete
replacement. See [Choosing integrations](/docs/choosing-integrations.md) before
selecting or implementing an adapter.
## Ownership and cleanup
A Behavior's **host View** is the View it is attached to. A **child View** is shown
by a Region or managed by a CollectionView. These names describe relationships;
a particular child might be a row, a card, or another item in your interface.
A **parent Application** owns its registered child Applications. Parents locate
and control children; children receive the collaborators they need explicitly.
The Application at the top of that hierarchy is its **root Application**.
For state, **borrowed** and **owned** describe who is responsible for disposal:
- A supplied or declared `state` is borrowed. Destroying an owner releases that
owner's subscriptions and leaves the source available to other users.
- A `createState()` result is owned. Destroying the owner releases its
subscriptions, then calls the selected StateApi's optional `disposeOwned()`.
A **cleanup function** releases a subscription or other resource. **Idempotent**
means repeated calls have the same effect as one call. Adapters must return
idempotent subscription cleanup functions; core does not wrap each returned
cleanup to establish that property.
## Default and isolated runtimes
The named exports from `marionette` belong to the **default runtime**.
`createMarionette()` returns an **isolated runtime** with its own classes,
adapter and renderer configuration, and Radio channels. Choose one runtime's
classes and setters when composing that part of the application. See
[Runtime isolation](/docs/runtime-isolation.md).
A state source created for one owner is still an owned state source; it does not
create another runtime. Current package imports use `marionette`; historical
migration guides may refer to the old `backbone.marionette` package name.
## Application lifecycle and readiness
`start()`, `stop()`, `restart()`, and `destroy()` are Application **lifecycle
operations**. A **readiness hook** is one of `onBeforeStart`, `onBeforeStop`, or
`onBeforeDestroy`. Marionette awaits a Promise returned by one of those hooks
before completing that phase.
The corresponding `before:*` event listeners are synchronous notifications;
their return values are not awaited. `onStart`, `onStop`, `onDestroy`, and their
matching events are **completion notifications** and are not awaited either.
See [Application lifecycle](/docs/application.md) for ordering,
cancellation, and the readiness `AbortSignal`.
[Canonical source](/docs/markdown/docs/terminology.md) · [Source identity](/docs/manifest.json)
---
Document: docs/agent-tools.md
Canonical URL: https://marionettejs.com/docs/agent-tools/
Markdown URL: https://marionettejs.com/docs/agent-tools.md
Reading SHA-256: 84ebeff9b9ed3660c3aa94a245ce5cf2ee82a981982ed802b133dcb86e2b1e92
# Set up an agent
Use the installed package's documentation and a small application instruction file
first. The optional Marionette skill helps an agent select those documents and
apply their lifecycle and integration rules. None of these resources requires an
account, network access, hosted model, or shared API key to read.
## Install the consumer skill
Builds containing these resources ship `dist/agent-skill/` and `dist/docs/` inside
the `marionette` package. Check that both exist in your installed package before
following these steps; earlier artifacts do not contain them. Do not upgrade an
application just to install instructions.
Copy the whole `dist/agent-skill/` directory, including `scripts/`, into the skill
location supported by your agent client, naming the copied folder `marionette`.
Use the client's documented installation mechanism; installing an npm dependency
does not automatically activate an agent skill. For a source checkout, the same
skill lives in `skills/marionette/`. Use the checkout matching the package's known
source revision.
For a client configured to read project skills from `.agents/skills`, run this
from your application directory when that destination does not already exist:
```sh
mkdir -p .agents/skills
cp -R node_modules/marionette/dist/agent-skill .agents/skills/marionette
```
Adapt the source path for a hoisted dependency or package manager without
`node_modules`. When updating an existing copy, review its local changes and
replace it deliberately; do not create nested copies. Keep the skill in the
application's repository if the team should share it. Update it alongside the
package, reviewing any project-specific edits. Agent clients differ in discovery
and reload behavior; follow the client's setup instructions and confirm that it
lists `marionette` before relying on automatic selection.
In a client supporting named skill invocation, try:
```text
Use $marionette to inspect this application's installed version and integrations.
Find the matching routing guide and explain which component owns cancellation.
Do not change the application yet.
```
A successful activation identifies the installed package, reports its documentation
revision, reads the relevant page, and distinguishes the router from Marionette's
lifecycle. A response that only repeats the prompt has not demonstrated retrieval.
If the client cannot load skills, give it [Build with Marionette](/docs/agents.md) and
the matching task guide directly; the skill is an optional entry point.
## Read matching docs locally
The skill bundles a read-only helper requiring Node 24 or later. It addresses a
specific retrieval problem: the copied skill must locate the application's
installed docs, including hoisted dependencies, without importing application code.
It does not add a server, registry, or production dependency.
```sh
node .agents/skills/marionette/scripts/docs.mjs --project . --list
node .agents/skills/marionette/scripts/docs.mjs --project . --page docs/routing.md
```
`--list` returns JSON with absolute page paths, version, source revision, local
change status, and content digest. `--page` accepts an exact `source` path from
that list and prints one provenance record followed by the page's Markdown. Run
from the application workspace, not a neighboring package with a different
Marionette dependency. `--project` defaults to the current directory.
For a package manager without a physical `node_modules` tree, find that
application's physical package directory using its package manager and supply
`--package-root /path/to/marionette`. The helper does not execute resolver hooks or
install packages to guess that path. Exit status `1` indicates missing docs,
invalid arguments, a version mismatch, or inconsistent files; it does not silently
switch to a different source.
The helper validates documentation hashes and their package version. This proves
that the files agree with their manifest, not that an arbitrary custom runtime was
built from that revision. Check installed exports and test uncertain behavior. For local builds, the
version alone cannot identify a source commit; `sourceDirty: true` means
local changes are included. Older packages without docs require an exact release
or known source checkout, not an automatic fallback to today's website.
## Record the application decisions
Adapt the [application instruction template](/docs/application-agent-template.md).
Record actual integration choices, initialization points, resource owners, and
working test commands. Keep those decisions in the application. The library's
maintainer `AGENTS.md` describes changing Marionette itself and should not be
copied into a consumer application.
## Choose an optional service only for a specific need
| Resource | Useful for | Boundary |
| --- | --- | --- |
| Packaged Markdown and manifest | Reading the contract shipped with an installed package | Available offline; verify custom runtime provenance separately. |
| Website Markdown and `llms.txt` | Discovering pages and reading a published snapshot | An index is a set of links, not automatic instruction installation. Check version and source metadata. |
| Context7 | Finding relevant excerpts through a supported agent integration | Optional third-party retrieval; results can omit setup or mix versions. Verify against the exact source. |
| Local skill helper | Finding and checking packaged docs from a consumer workspace | Reads files only; no network, project-code execution, or automatic fallback. |
| Website WebMCP tools | Operating the website's interactive example | Controls that example, not the consumer application. It is not a remote documentation server. |
For Context7, use the public `marionettejs/marionette` library and the client's
Context7 setup instructions. Each developer uses their own account and limits.
Do not put a maintainer's API key in a website, repository, or shared public proxy.
If a free quota is exhausted, read the static or installed docs directly; do not
enable paid overages. Check the current [Context7 plans](https://context7.com/plans)
and [documentation](https://context7.com/docs) before configuring an account.
Public indexing does not prove that the latest source configuration is active.
Marionette does not require a custom MCP server, a hosted AI chat, or a WebMCP
connection to build an application. A future local MCP wrapper would need to solve
a demonstrated client integration gap beyond reading these files. Keep tooling
outside the production import graph and avoid duplicating the contract in tool
prompts. The same documentation remains available to human readers.
[Canonical source](/docs/markdown/docs/agent-tools.md) · [Source identity](/docs/manifest.json)
---
Document: docs/application-agent-template.md
Canonical URL: https://marionettejs.com/docs/application-agent-template/
Markdown URL: https://marionettejs.com/docs/application-agent-template.md
Reading SHA-256: 97316b5362ca10cd602e4274854b9db9f080d89854fd758bb5c028f227245021
# Record an application's agent instructions
Use this template to record decisions an agent cannot safely infer from Marionette
alone. It belongs in the application repository's instruction file, usually
`AGENTS.md` when supported by the agent client. Merge it with existing instructions
instead of replacing unrelated project policy.
Fill each field from the installed package, lockfile, configuration, and actual
test scripts. Delete irrelevant fields. A question still being decided should be
marked unresolved, with the constraint that blocks the decision; do not turn a
placeholder into an invented default. Never put credentials or private customer
data in these instructions.
```markdown
# Marionette application context
## Installed contract
- Application workspace: [directory containing this application's manifest].
- Marionette package/version and install source: [lockfile and resolved package].
- Documentation: [installed dist/docs path or exact release/source snapshot].
- Source revision and local changes, when known: [manifest provenance].
- Optional Marionette packages: [actual versions, or none].
Use matching documentation. Check the installed exports before adopting an API
from an external example. Do not change dependency versions to make a snippet fit.
## Integration decisions
- Runtime and registration point: [actual module; shared or isolated and why].
- Renderer/templates: [actual choice and setup module].
- Data sources and DataApi: [actual choice, observability, registration or default].
- State sources and StateApi: [actual choice, ownership, registration or default].
- DomApi and EventDelegator: [actual choices or defaults].
- Router: [actual library or none; URL/history owner].
- Navigation/loading: [controller or feature owner; stale-result policy].
Preserve compatible established choices. Select these capabilities independently;
a router choice does not imply a data, state, renderer, or DOM adapter change.
## Ownership and verification
- Root mount and View/Region owner: [actual entry point].
- Shared resources and disposal owners: [actual subscriptions/state/widgets].
- Unit/component check: [existing command and working directory].
- Browser interaction check: [existing command and working directory].
- Build/type check: [existing command and working directory].
- Relevant existing patterns: [a few actual source or test paths].
For the changed behavior, verify the appropriate interaction and cleanup boundary.
Report the checks actually run and anything left untested. Update this file when
an application decision changes; keep the API reference in the matching docs.
```
Keep the completed file short. Link to substantial project architecture or test
guides rather than copying them. The purpose is to preserve the application's
choices across tasks, not to prescribe a new router, test runner, or framework.
For skill installation and optional services, see [Set up an agent](/docs/agent-tools.md).
[Canonical source](/docs/markdown/docs/application-agent-template.md) · [Source identity](/docs/manifest.json)
---
Document: docs/marionette.view.md
Canonical URL: https://marionettejs.com/docs/view/
Markdown URL: https://marionettejs.com/docs/view.md
Reading SHA-256: fa47943dac17b3812086bb370caef231c6c4ae13552a53583eb1f0d8b3c0b6f8
# Marionette.View
A `View` manages one part of a screen: its content, DOM interactions, and child
views. Give it a template and data, and it renders into a root element, `el`.
Plain objects and native DOM methods work by default.
Use named [Regions](/docs/region.md) to give child views a place within
that element, and [Behaviors](/docs/behavior.md) to share interaction
logic across views.
`View` includes:
- [The DOM API](/docs/dom-api.md)
- [Class Events](/docs/class-events.md#view-events)
- [DOM Interactions](/docs/dom-interactions.md)
- [Child Event Bubbling](/docs/events.md#event-bubbling)
- [Entity Events](/docs/entity-events.md)
- [View Rendering](/docs/rendering.md)
- [Prerendered Content](/docs/prerendered-dom.md)
- [View Lifecycle](/docs/lifecycle.md)
A `View` can have [`Region`s](/docs/region.md) and [`Behavior`s](/docs/behavior.md)
## Documentation Index
* [Instantiating a View](#instantiating-a-view)
* [Method results and side effects](#method-results-and-side-effects)
* [Rendering a View](#rendering-a-view)
* [Using a View Without a Template](#using-a-view-without-a-template)
* [Refreshing Root Attributes](#refreshing-root-attributes)
* [View Lifecycle and Events](#view-lifecycle-and-events)
* [Entity Events](#entity-events)
* [DOM Interactions](#dom-interactions)
* [Behaviors](#behaviors)
* [Managing Children](#managing-children)
* [Laying Out Views - Regions](#laying-out-views---regions)
* [Showing a Child View](#showing-a-child-view)
* [Accessing a Child View](#accessing-a-child-view)
* [Detaching a Child View](#detaching-a-child-view)
* [Destroying a Child View](#destroying-a-child-view)
* [Region Availability](#region-availability)
* [Efficient Nested View Structures](#efficient-nested-view-structures)
* [Listening to Events on Children](#listening-to-events-on-children)
## Instantiating a View
When instantiating a `View` there are several properties, if passed,
that will be attached directly to the instance:
`attributes`, `behaviors`, `childViewEventPrefix`, `childViewEvents`,
`childViewTriggers`, `className`, `collection`, `collectionEvents`, `el`,
`events`, `id`, `model`, `modelEvents`, `regionClass`, `regions`, `stateEvents`,
`tagName`, `template`, `templateContext`, `triggers`, `ui`
```javascript
import { View } from 'marionette';
const myView = new View({ template: () => '
Content
' });
```
These properties are defined by Marionette's standalone `View` constructor.
When Marionette creates the View's element, it copies own enumerable
`attributes` properties, including symbols. The default DomApi applies string
attribute names only; inherited and non-enumerable properties are not copied. When applied, `id` and `className` assignments occur
afterward and override the corresponding `attributes` keys. See the
[`DomApi.setAttributes` contract](/docs/dom-api.md#setattributesel-attrs).
## Method results and side effects
These operations run synchronously. Use lifecycle hooks for additional work;
returning a Promise from a View hook does not delay rendering or destruction.
| Method | Result | Effect |
| --- | --- | --- |
| `render()` | This View | Evaluates the template, updates contents and UI bindings. Rendering again resets its Regions and destroys their current children. `template: false` and a destroyed View make this a no-op. |
| `renderAttributes()` | This View | Refreshes root attributes without rendering contents or recreating children. |
| `destroy(options)` | This View | Removes the root element, destroys owned Regions/children and Behaviors, releases subscriptions and owned State. Repeated destruction is a no-op. |
| `isRendered()`, `isAttached()`, `isDestroyed()` | Boolean | Read lifecycle state without rendering. Attachment is Marionette's tracked state; see [monitoring](/docs/lifecycle.md). |
| `hasRegion(name)`, `getRegion(name)` | Boolean or Region/`undefined` | Read a named registration without rendering the parent. |
| `getRegions()` | New name-to-Region object | Read registrations; changing this object does not change ownership. |
| `showChildView(name, view, options)` | Supplied child View | Renders the parent if needed, then delegates to the named Region. The result alone does not establish adoption when `allowMissingEl` permits a missing mount. |
| `getChildView(name)` | Current child or `undefined` | Renders the parent if needed before reading the named Region. |
| `detachChildView(name)` | Detached child or `undefined` | Renders the parent if needed, then transfers a live child to the caller. |
| `addRegion(name, definition)` | Registered Region | Constructs or registers a Region without rendering the parent. |
| `addRegions(definitions)` | Map of added Regions, or `undefined` for no entries | Registers the batch; see [ownership constraints](/docs/region.md#reading-region-ownership). |
| `removeRegion(name)` | Removed Region | Destroys that Region and its current child. |
| `removeRegions()` | Map of removed Regions | Destroys every registered Region and its current child. |
| `emptyRegions()` | Map of Regions | Renders the parent if needed, destroys current children, and keeps the Regions available. |
`getChildView`, `showChildView`, `detachChildView`, and `removeRegion` require a
registered name and throw [`MN0020`](/errors/MN0020.md) when it is absent.
`getRegion` returns `undefined` for an absent valid name. Region names must be non-empty strings; an empty string throws
[`MN0032`](/errors/MN0032.md).
A supplied `state` is borrowed rather than copied as a normal constructor
option. See [State ownership](/docs/state.md#borrowed-and-owned-sources)
for `getState()`, `createState()`, subscriptions, and disposal.
## Rendering a View
The Marionette View implements a powerful render method which, given a
[`template`](/docs/rendering.md#setting-a-view-template), will build your
HTML from that template, mixing in `model` or `collection` data and any
extra [template context](/docs/rendering.md#adding-context-data).
Marionette `View` defines `render`, and this method should not be overridden.
To add functionality around rendering, use the
[`render` and `before:render` events](/docs/class-events.md#render-and-beforerender-events).
For more detail on how to render templates, see
[View Template Rendering](/docs/rendering.md).
### Using a View Without a Template
With [`template: false`](/docs/rendering.md#using-a-view-without-a-template),
`render()` returns the View without changing its contents or running
`before:render` and `render`. Other View events and DOM interactions remain
available. Use this for [`prerendered content`](/docs/prerendered-dom.md) that the
View should preserve.
### Refreshing Root Attributes
`renderAttributes()` reevaluates a View's declarative `attributes`, `className`,
and `id`, then applies those values to its existing root element. The method is
also available on `CollectionView`.
```javascript
import { View } from 'marionette';
const SelectableRow = View.extend({
tagName: 'tr',
attributes() {
return {
'aria-selected': this.isSelected ? 'true' : 'false'
};
},
className() {
return this.isSelected ? 'danger' : null;
},
template: false,
setSelected(isSelected) {
this.isSelected = isSelected;
return this.renderAttributes();
}
});
const row = new SelectableRow();
const rootElement = row.el;
row.setSelected(true);
export { rootElement, row };
```
With the default DomApi, only an explicit `null` removes an attribute.
An `undefined` value or omitted key leaves the existing attribute untouched;
Marionette does not retain the names returned by an earlier call. Other values,
including `false`, `0`, and an empty string, use the browser's attribute string
conversion. For boolean HTML attributes, declare `disabled: isDisabled ? '' : null`;
`disabled: false` still creates a present attribute and disables the element.
`id` and `className` continue to override matching keys from `attributes` when
they are declared. Live form properties such as `input.value` and `input.checked`
should be updated explicitly, separately from their default-value attributes.
Use `className` as the View-level class declaration, as shown above. The
`attributes` map continues to use raw DOM attribute names for lower-level cases.
Marionette normalizes the View declaration to the `class` attribute before
calling the DomApi, including for a supplied SVG root.
`renderAttributes()` returns the View. It does not call the template, emit the
render lifecycle, replace the root element, rebind `ui` or DOM events, or reset
Regions. It is not called automatically by `render()`. Calls after destruction
begins are no-ops and do not resolve the attribute declarations.
When a View uses a supplied `el`, construction still leaves that element's
attributes unchanged. A later `renderAttributes()` call applies only the keys
in the current declaration, so unrelated host attributes remain caller-owned.
## View Lifecycle and Events
An instantiated `View` is aware of its lifecycle state and will throw events related to when that state changes.
The view states indicate whether the view is rendered, attached to the DOM, or destroyed.
Read More:
- [View Lifecycle](/docs/lifecycle.md)
- [View DOM Change Events](/docs/class-events.md#dom-change-events)
- [View Destroy Events](/docs/class-events.md#destroy-events)
## Entity Events
A `View` subscribes to its `model` and `collection` through the configured
[DataApi](/docs/data-api.md). Event names and callback arguments belong to that data
provider. Plain objects and arrays do not emit changes; declaring entity event
maps for unobservable values throws `MN0037`.
Read More:
- [Entity Events](/docs/entity-events.md)
## DOM Interactions
`View` provides `events`, `triggers`, and `ui` for DOM interactions.
Read More:
- [DOM Interactions](/docs/dom-interactions.md)
## Behaviors
A `Behavior` provides a clean separation of concerns to your view logic,
allowing you to share common user-facing operations between your views.
Read More:
- [Using `Behavior`s](/docs/behavior.md#using-behaviors)
## Managing Children
`View` provides a simple interface for managing child-views with
[`showChildView`](#showing-a-child-view), [`getChildView`](#accessing-a-child-view), and
[`detachChildView`](#detaching-a-child-view).
These methods all access `regions` within the view.
We will cover this here but for more advanced information, see the
[documentation for regions](/docs/region.md).
### Laying Out Views - Regions
The `View` class lets us manage a hierarchy of views using `regions`.
Regions are a hook point that lets us show views inside views, manage the
show/hide lifecycles, and act on events inside the children.
**This Section only covers the basics. For more information on regions, see the
[Regions Documentation.](/docs/region.md)**
Regions are ideal for rendering application layouts by isolating concerns inside
another view. This is especially useful for independently re-rendering chunks
of your application without having to completely re-draw the entire screen every
time some data is updated.
Regions can be added to a View at class definition, with [`regions`](/docs/region.md#defining-regions),
or at runtime using [`addRegion`](/docs/region.md#adding-regions).
When you extend `View`, we use the `regions` attribute to point to the selector
where the new view will be displayed:
```javascript
import _ from 'underscore';
import { View } from 'marionette';
const MyView = View.extend({
template: _.template(`
`),
regions: {
firstRegion: '#first-region',
secondRegion: '#second-region'
}
});
```
When we show views in the region, the contents of `#first-region` and
`#second-region` will be replaced with the root element of the child View we show. The
string values in this example are CSS selectors scoped to the `View`'s `el`.
### Showing a Child View
To show a view inside a region, simply call `showChildView(regionName, view)`. This
will handle rendering the view's HTML and attaching it to the DOM for you:
```javascript
import { View } from 'marionette';
const ChildView = View.extend({
template() {
return '
Content
';
}
});
const ParentView = View.extend({
template() {
return `
`;
},
regions: {
firstRegion: '.first-region',
secondRegion: '.second-region'
}
});
export function runViewChildRegionLifecycle() {
const parentView = new ParentView();
parentView.showChildView('firstRegion', new ChildView());
const childView = parentView.getChildView('firstRegion');
parentView.detachChildView('firstRegion');
parentView.showChildView('secondRegion', childView);
parentView.getRegion('secondRegion').empty();
return parentView;
}
```
Note: If `view.showChildView(region, subView)` is invoked before the `view` has been rendered, it will automatically render the `view` so the Region's `el` exists within the parent root; the root may still be detached.
### Accessing a Child View
To access the child view of a `View` - use the `getChildView(regionName)` method.
This will return the view instance that is currently being displayed at that
region. The example gets the exact `ChildView` shown in `firstRegion` before
moving it.
If the named Region exists but has no current View, `getChildView` returns
`undefined`.
### Detaching a Child View
You can detach a child view from a Region through `detachChildView(regionName)`.
It returns the same live, rendered View so that it can be shown again without
rendering a second time. In the example, the parent detaches its child from
`firstRegion` before showing that same child in `secondRegion`. This is a proxy
for [Region `detachView()`](/docs/region.md#detaching-existing-views).
### Destroying a Child View
To destroy and clear a child owned by a View, empty its owning Region. The
example calls `parentView.getRegion('secondRegion').empty()`, which destroys the
current child and leaves `secondRegion` empty and available for another View.
### Region Availability
Defined regions are registered during `View` construction. `hasRegion(name)`,
`getRegion(name)`, and `getRegions()` query the View's own Region registry
without rendering, including when the View is unrendered or destroyed.
`getRegions()` returns a fresh, safe own-key snapshot. Child View operations
such as `showChildView`, `detachChildView`, and `getChildView` still render a
live, unrendered View before dispatching through any `getRegion` override.
`emptyRegions()` likewise renders before calling the overridable `getRegions()`
and emptying its returned snapshot.
Calling `getRegion(name)` does not render the parent or resolve the Region
element. Calling the returned Region's `show(view)` resolves its element but does
not render the parent. Use `showChildView`, or
render the parent first, when showing a child into a declared selector Region.
`getRegion(name)` and `hasRegion(name)` support optional lookup: an unknown name
returns `undefined` or `false`, respectively. Operations that require a Region —
`showChildView`, `detachChildView`, `getChildView`, and `removeRegion` — throw a
`RegionError` with code [`MN0020`](/errors/MN0020.md) when the named Region does not
exist. Region names must be non-empty strings. The public types require strings;
an empty name throws a `RegionError` with code [`MN0032`](/errors/MN0032.md).
Child View operations reject empty names before rendering the parent.
## Efficient Nested View Structures
Show a parent's Region children in `onRender` when they should be recreated
with that parent's template. During initial display, this builds the nested
View tree before the owning Region attaches the parent. Keep independently
editable content in child Views and update those children without re-rendering
the parent when their state must survive.
```javascript
import { View } from 'marionette';
const ParentView = View.extend({
// ...
onRender() {
this.showChildView('header', new HeaderView());
this.showChildView('footer', new FooterView());
}
});
myRegion.show(new ParentView());
```
Child Views can show their own Region children in `onRender` too. Marionette
coordinates the render and attachment lifecycles; browser layout and paint
counts depend on the DOM, styles, and application callbacks. Measure those costs
in the running application when they matter.
## Listening to Events on Children
Using regions lets you listen to the events that fire on child views - views
attached inside a region. This lets a parent view take action depending on what
events are triggered in views it directly owns.
Read More:
- [Child Event Bubbling](/docs/events.md#event-bubbling)
[Canonical source](/docs/markdown/docs/marionette.view.md) · [Source identity](/docs/manifest.json)
---
Document: docs/marionette.region.md
Canonical URL: https://marionettejs.com/docs/region/
Markdown URL: https://marionettejs.com/docs/region.md
Reading SHA-256: fc4d0668c1d52763debb29380efbdadb42c3f989d351314e3cb3fd4dfc97b3c7
# Marionette.Region
A `Region` gives a changing part of the screen a place to live. Show a view,
replace it with another, or empty the Region when that part of the interface
is no longer needed. By default, replacing or emptying a view destroys it;
the Region remains available for the next view.
`Region` includes:
- [Common Marionette Functionality](/docs/common.md)
- [Class Events](/docs/class-events.md#region-events)
- [The DOM API](/docs/dom-api.md)
See the documentation for [laying out views](/docs/view.md#laying-out-views---regions) for an introduction in
managing regions throughout your application.
Regions maintain the [View's lifecycle](/docs/lifecycle.md) while showing or emptying a view.
## Documentation Index
* [Instantiating a Region](#instantiating-a-region)
* [Reading Region ownership](#reading-region-ownership)
* [Lifecycle transition contract](#lifecycle-transition-contract)
* [Defining the Application Region](#defining-the-application-region)
* [Defining Regions](#defining-regions)
* [String Selector](#string-selector)
* [Additional Options](#additional-options)
* [Specifying `regions` as a Function](#specifying-regions-as-a-function)
* [Using a RegionClass](#using-a-regionclass)
* [Referencing UI in `regions`](#referencing-ui-in-regions)
* [Adding Regions](#adding-regions)
* [Removing Regions](#removing-regions)
* [Using Regions on a view](#using-regions-on-a-view)
* [Showing a View](#showing-a-view)
* [Checking whether a region is showing a view](#checking-whether-a-region-is-showing-a-view)
* [Wrapping a non-Marionette view](#wrapping-a-non-marionette-view)
* [Emptying a Region](#emptying-a-region)
* [Preserving Existing Views](#preserving-existing-views)
* [Detaching Existing Views](#detaching-existing-views)
* [`reset` A Region](#reset-a-region)
* [`destroy` A Region](#destroy-a-region)
* [Check If View Is Being Swapped By Another](#check-if-view-is-being-swapped-by-another)
* [Set How View's `el` Is Attached and Detached](#set-how-views-el-is-attached-and-detached)
* [Configure How To Remove View](#configure-how-to-remove-view)
## Instantiating a Region
A `Region` accepts `el`, `parentEl`, `allowMissingEl`, and `replaceElement`.
`el` is a native element or a selector; selector resolution is deferred until an
operation needs the element. `parentEl` limits selector lookup and may be an
element, document, or function returning one. `allowMissingEl` and
`replaceElement` may also be functions; a boolean supplied to `show(view,
options)` overrides the corresponding Region setting for that call.
```javascript
import { Region } from 'marionette';
const myRegion = new Region({ el: '#content' });
```
While regions may be instantiated and useful on their own, their primary use case is through
the [`Application`](#defining-the-application-region) and [`View`](#defining-regions) classes.
## Reading Region ownership
A Region registered on a View exposes that existing relationship through pure,
read-only queries. `getOwner()` returns the owning View and `getName()` returns
the Region's name within that View. Neither query renders the View, resolves the
Region element, or changes ownership. A standalone Region returns `undefined`
from both methods. Removing a registered Region or completing its destruction
clears both values. A throwing lifecycle hook interrupts teardown without
rolling back ownership or retrying destruction.
A Region has one authoritative registration. Re-adding that same Region instance
under its current owner and name returns it without lifecycle events or ownership changes.
Registering it under a different owner or name, registering a Region whose
destruction has begun or completed, or replacing an occupied Region name through
`addRegion` throws [`MN0030`](/errors/MN0030.md) before committing the conflicting
registration. A conflict found before `addRegions` starts rejects the whole batch.
Lifecycle hooks must not re-register the Region or occupy its registration name
while registration is in progress. Failed batch registration is not rolled back.
Remove an existing named Region before replacing it, and use a fresh Region instance
when another View needs a Region.
```javascript
const contentRegion = myView.getRegion('content');
contentRegion.getOwner() === myView; // true
contentRegion.getName(); // 'content'
```
## Lifecycle transition contract
A Region owns at most one current View. Its public lifecycle
state can be read without changing it:
| State | `hasView()` | `isDestroyed()` | `currentView` |
| --- | --- | --- | --- |
| Empty | `false` | `false` | `undefined` |
| Occupied | `true` | `false` | The View shown by the Region |
| Destroyed | `false` | `true` | `undefined` |
`isSwappingView()` is a temporary operation flag rather than a fourth stable state.
It is `true` while one occupied Region replaces its current View with another,
including the Region's `before:show`, `before:empty`, `empty`, and `show` callbacks.
It returns to `false` when `show` completes. `isReplaced()` independently reports
whether `replaceElement` has temporarily replaced the Region element; it does not
change which lifecycle operations are valid.
| Operation | Empty Region | Occupied Region | Destroyed Region |
| --- | --- | --- | --- |
| `show(view)` when the Region element resolves | Renders the View if needed, shows it, and enters occupied. | Showing the same View is a no-op. Showing a different View destroys the old View and swaps to the new one. | Returns the Region without inspecting or changing the caller-owned View or resolving the element. |
| `detachView()` | Returns `undefined`; state is unchanged. | Detaches and returns the live View, then enters empty. | Returns `undefined` without changing state or DOM or emitting lifecycle events. |
| `empty()` | Returns the Region and, when its element resolves, removes unmanaged contents from that element. | Destroys the current View, clears `currentView`, and enters empty. | Returns the Region without resolving the element or changing lifecycle state or DOM. |
| `reset()` | Empties the Region and resets its element reference. | Destroys the current View, enters empty, and resets the element reference. | Returns the Region without resolving the element or changing lifecycle state, DOM, or element caches. |
| Current View is destroyed externally | No effect. | Runs the Region's empty lifecycle once, clears `currentView`, and enters empty. | No effect. |
| `destroy()` | Runs the destroy lifecycle and enters destroyed. | Emits `before:destroy`, destroys and empties the current View, enters destroyed, then emits `destroy`. | Returns the Region without repeating cleanup or lifecycle events. |
Successful `show`, `empty`, and `destroy` calls return the Region when their
operation completes. With `allowMissingEl: true`, `show` instead returns `undefined`
and leaves the current View unchanged when its element does not resolve. A View returned
by `detachView()` remains the caller's responsibility until the same or another Region shows it
or it is destroyed. After destruction, `show()`, `empty()`, and `reset()` return
the Region without changing it, and `detachView()` returns `undefined`.
As soon as destruction begins, `show()`, `detachView()`, and recursive `destroy()`
calls are no-ops. `empty()` and `reset()` remain available during cleanup.
A View passed to `show()` during or after destruction remains caller-owned and
unchanged. A destroyed Region cannot be reused.
When its current View destroys itself, the Region clears that View's ownership
and releases the owning parent View's subscriptions to it. Later events on the
destroyed child are no longer forwarded to the parent.
The following example preserves a View by detaching it before showing it again.
Calling `empty()` afterward destroys the View and returns the Region to its empty state.
```javascript
import { Region, View } from 'marionette';
export function runRegionLifecycle() {
const region = new Region({ el: '#content' });
const contentView = new View({
template() {
return '
Content
';
}
});
region.show(contentView);
const detachedView = region.detachView();
region.show(detachedView);
region.empty();
return region;
}
```
## Defining the Application Region
The Application defines a single region `el` using the `region` attribute. This
can be accessed through `getRegion()` or have a view displayed directly with
`showView()`. Below is a short example:
```javascript
import { Application } from 'marionette';
import SomeView from './view';
const MyApp = Application.extend({
region: '#main-content',
onStart() {
const mainRegion = this.getRegion(); // Has all the properties of a `Region`
mainRegion.show(new SomeView());
}
});
```
For more information, see the
[Application docs](/docs/application.md#application-region).
## Defining Regions
In Marionette you can define a region with a string selector or an object literal
on your `Application` or `View`. This section will document the two types as applied
to `View`, although they will work for `Application` as well - just replace `regions`
with `region` in your definition.
Region declaration maps, including maps passed to `addRegions`, 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 Region name rather than an array-like signal. Arrays, sparse arrays, and
other array-like values are not supported as Region declaration maps.
Named View Region operations require a non-empty string name. `addRegion`,
`removeRegion`, `hasRegion`, `getRegion`, `showChildView`, `detachChildView`, and
`getChildView` throw [`MN0032`](/errors/MN0032.md) for an empty name. The public
types require strings; unsupported shapes have no guaranteed diagnostic.
Ordinary collision names such as `constructor`,
`toString`, and `__proto__` remain valid when explicitly registered.
### String Selector
You can use a CSS selector string to define regions.
```javascript
import { View } from 'marionette';
const MyView = View.extend({
regions: {
mainRegion: '#main'
}
});
```
`Region#getEl(selector)` resolves the selector within `parentEl`, or within the
document when no parent is defined, and returns the first matching native DOM
element. A custom `getEl` override must preserve that native-element return
contract; do not return a `NodeList` or jQuery collection. To customize selector
lookup through the DOM adapter, implement `findEl(context, selector)` instead.
The v4 `DomApi#getEl` method is not part of the v5 DOM API.
Selector lookup is deferred until a DOM operation such as `show()` needs it. During construction, `initialize` observes the configured
selector string in `this.el`; constructing a Region does not query the document
or dispatch through a `getEl` override.
### Additional Options
You can define regions with an object literal. Object literal definitions expect
an `el` property - the selector string to hook the region into. With this
format is possible to define whether showing the region overwrites the `el` or
just overwrites the content (the default behavior).
Region defaults and object-literal definitions contribute their own enumerable
properties, including symbols, through object spread. Inherited and
non-enumerable properties are ignored when Marionette builds the Region options.
To replace the Region's placeholder with the child View's root element, use
`replaceElement: true`:
```javascript
import { View } from 'marionette';
const ReplacementView = View.extend({
className: 'new-class',
template: () => '
Replacement content
'
});
const Layout = View.extend({
template: () => '',
regions: {
main: {
el: '.overwrite-me',
replaceElement: true
}
}
});
export const view = new Layout().render();
export const placeholder = view.el.querySelector('.overwrite-me');
export const replacement = new ReplacementView();
// Rendering the parent creates the placeholder. Showing the child replaces it.
view.showChildView('main', replacement);
view.$('.overwrite-me').length; // 0
view.$('.new-class').length; // 1
```
`showChildView()` replaces `.overwrite-me` with the child's `el`; rendering the
parent alone does not. The `className` option takes a class name, without the
`.` used in CSS selectors. Emptying the Region destroys its current child and
restores the original placeholder. The parent View's own root remains unchanged.
This is useful when a container requires particular direct children, such as a
`table` body containing rows. Choose a child `tagName` valid for that container.
```js
import { View } from 'marionette';
const MyView = View.extend({
regions: {
regionDefinition: {
el: '.bar',
replaceElement: true
}
}
});
```
**Errors** An operation that needs the element throws `MN0004` when no `el`
is configured, or `MN0005` when a selector finds no element and
`allowMissingEl` is false. Construction alone does not resolve the selector.
### Specifying `regions` as a Function
On a `View` the `regions` attribute can also be a
[function returning an object](/docs/basics.md#functions-returning-values):
```javascript
import { View } from 'marionette';
const MyView = View.extend({
regions(){
return {
firstRegion: '#first-region'
};
}
});
```
### Using a RegionClass
If you've created a custom region class, you can use it to define your region.
```javascript
import { Application, Region, View } from 'marionette';
const MyRegion = Region.extend({
onShow(){
// Scroll to the middle
const viewHeight = this.currentView.el.getBoundingClientRect().height;
const regionHeight = this.el.getBoundingClientRect().height;
this.el.scrollTop = viewHeight / 2 - regionHeight / 2;
}
});
const MyApp = Application.extend({
regionClass: MyRegion,
region: '#first-region'
})
const MyView = View.extend({
regionClass: MyRegion,
regions: {
firstRegion: {
el: '#first-region',
regionClass: Region // Don't scroll this to the top
},
secondRegion: '#second-region'
}
});
```
### Referencing UI in `regions`
The UI attribute can be useful when setting region selectors - simply use
the `@ui.` prefix:
```javascript
import { View } from 'marionette';
const MyView = View.extend({
ui: {
region: '#first-region'
},
regions: {
firstRegion: '@ui.region'
}
});
```
## Adding Regions
To add regions to a view after it has been instantiated, simply use the
`addRegion` method:
```javascript
import MyView from './myview';
const myView = new MyView();
myView.addRegion('thirdRegion', '#third-region');
```
Now we can access `thirdRegion` as we would the others.
You can also add multiple regions using `addRegions`.
```javascript
import MyView from './myview';
const myView = new MyView();
myView.addRegions({
main: {
el: '.overwrite-me',
replaceElement: true
},
sidebar: '.sidebar'
});
```
## Removing Regions
You can remove all of the regions from a view by calling `removeRegions` or you can remove a
region by name using `removeRegion`. When a region is removed the region will be destroyed.
```javascript
import { View } from 'marionette';
const MyView = View.extend({
regions: {
main: '.main',
sidebar: '.sidebar',
header: '.header'
}
});
const myView = new MyView();
// remove only the main region
const mainRegion = myView.removeRegion('main');
mainRegion.isDestroyed(); // -> true
// remove all regions
myView.removeRegions();
```
## Using Regions on a view
In addition to adding and removing regions there are a few methods to help
utilize regions. `hasRegion` and `getRegion` are pure own-registry queries, and
`getRegions` returns a pure snapshot; none renders. Child View operations and
`emptyRegions` first render a live, unrendered View before resolving or mutating
Regions.
- `getRegion(name)` - Request an own registered Region without rendering.
- `getRegions()` - Return a fresh own-key snapshot of registered Regions without rendering.
- `hasRegion(name)` - Check if a View has an own registered Region without rendering.
- `emptyRegions()` - Render when needed, then empty all Regions returned by `getRegions()`.
## Showing a View
Once a region is defined, you can call its `show` method to display the view:
```javascript
const myView = new MyView();
const childView = new MyChildView();
myView.render();
const mainRegion = myView.getRegion('main');
// render and display the child View
mainRegion.show(childView, { fooOption: 'bar' });
```
The parent View must already be rendered before calling a selector Region's
`show` directly. Use `showChildView('main', childView)` to render the parent when
needed before showing the child.
This is equivalent to a view's `showChildView` which can be used as:
```javascript
const myView = new MyView();
const childView = new MyChildView();
// render and display the view
myView.showChildView('main', childView, { fooOption: 'bar' });
```
Both forms require a Marionette View instance. Construct a `View` explicitly
when displaying a template or static content; Regions do not allocate hidden Views
from View classes, functions, strings, or option objects. The
[wrapper pattern](#wrapping-a-non-marionette-view) provides explicit ownership for legacy integrations.
```javascript
import { View } from 'marionette';
myView.showChildView('header', new View({
template: () => 'Welcome to the site'
}));
```
The argument after the View instance in `Region#show(view, options)` and
`View#showChildView(name, view, options)` is a separate show-options object passed
to the [events fired during `show`](/docs/class-events.md#show-and-beforeshow-events).
For more information on `showChildView` and `getChildView`, see the
[Documentation for Views](/docs/view.md#managing-children)
**Errors**
- A destroyed View throws `MN0007`. Other input shapes are unsupported; core
does not guarantee a Marionette diagnostic for an invalid value.
- An error will be thrown if the view is already managed by a Region or CollectionView,
including a filtered or deferred CollectionView child. Detach it from that owner first.
### Checking whether a region is showing a view
If you wish to check whether a region has a view, you can use the `hasView`
function. This will return a boolean value depending whether or not the region
is showing a view.
```javascript
const myView = new MyView();
myView.render();
const mainRegion = myView.getRegion('main');
mainRegion.hasView() // false
mainRegion.show(new OtherView());
mainRegion.hasView() // true
```
If you show a view in a region with an existing view, Marionette will
[remove the existing View](#emptying-a-region) before showing the new one.
### Wrapping a non-Marionette view
Regions and CollectionViews manage Marionette Views. They do not synthesize
render or destroy events for Backbone Views or fall back to a `remove()` method.
Keep a legacy integration inside a Marionette owner:
```javascript
import { View } from 'marionette';
import LegacyView from './legacy-view.js';
const LegacyWrapper = View.extend({
template: () => '',
onRender() {
this.legacy?.remove();
this.legacy = new LegacyView({ el: this.$('.legacy')[0] });
this.legacy.render();
},
onDestroy() {
this.legacy?.remove();
}
});
```
Show `new LegacyWrapper()` in the Region. The wrapper owns the legacy instance
and translates its actual rendering and cleanup API. No global prototype mixin
or compatibility flags are needed.
## Emptying a Region
You can remove a view from a region (effectively "unshowing" it) with
`region.empty()` on a region:
```javascript
const myView = new MyView();
myView.showChildView('main', new OtherView());
const mainRegion = myView.getRegion('main');
mainRegion.empty();
```
This will destroy the view, clean up any event handlers and remove it from
the DOM. When a region is emptied [empty events are triggered](/docs/class-events.md#empty-and-beforeempty-events).
Calling `empty()` after Region destruction completes returns the Region without
resolving its element, changing the DOM, or emitting empty lifecycle events.
**NOTE** If the region does _not_ currently contain a View it will detach
any HTML inside the region when emptying. If the region _does_ contain a
View, any HTML that doesn't belong to the View will remain.
### Preserving Existing Views
If you replace the current view with a new view by calling `show`, it will
automatically destroy the previous view. You can prevent this behavior by
[detaching the view](#detaching-existing-views) before showing another one.
### Detaching Existing Views
If you want to detach an existing view from a region, use `detachView`.
```javascript
const myView = new MyView();
const myOtherView = new MyView();
const childView = new MyChildView();
// render and display the view
myView.showChildView('main', childView);
// ... somewhere down the line
myOtherView.showChildView('main', myView.getRegion('main').detachView());
```
**Note** Detaching transfers responsibility for the live View to the caller.
Show it again in the same emptied Region or another Region when needed, or call
`destroy()` when finished with it.
## `reset` A Region
Resetting a live Region destroys its current View and restores its original
`el` reference. An original selector is queried again by the next operation that
needs it; an original DOM element is reused without a selector query.
```javascript
const myView = new MyView();
myView.showChildView('main', new OtherView());
const myRegion = myView.getRegion('main');
myRegion.reset();
```
This can be useful in unit testing your views.
Calling `reset()` after Region destruction completes returns the Region without
changing its element reference or cache.
## `destroy` A Region
A region can be destroyed which will `reset` the region, destroy its current View,
remove it from any parent View's Region lookups, and stop any internal Region listeners.
Reentrant Region destruction from `before:destroy` or `destroy`, repeated calls,
and later destruction of the parent View do not repeat the child or Region teardown.
A throwing lifecycle hook stops destruction. Later `destroy()` calls do not
retry hooks or resume partial teardown. Discard the Region after a cleanup error;
its remaining state is not a reusable lifecycle state.
`isDestroyed()` becomes `true` after `reset()` finishes, before the `destroy`
event. It remains `false` in `before:destroy`, `before:empty`, and `empty` handlers
called during teardown.
`destroy()` calls the overridable `reset()` method, which calls `empty()`.
Overrides can use this ordinary synchronous chain while cleanup is in progress.
An override that does not delegate to the base method owns the corresponding
cleanup; for example, a custom `reset()` can call `this.empty()` and reset its own
element reference. Nested `empty()` or `reset()` calls from lifecycle handlers
are ordinary calls, so handlers must avoid recursive loops.
After destruction completes, `empty()` and `reset()` return the Region without
changing its element or DOM. `show()` and `detachView()` already stop accepting
Views or transferring ownership as soon as destruction begins.
```javascript
import { View } from 'marionette';
const MyView = View.extend({
regions: {
mainRegion: '#main'
}
});
const myView = new MyView();
myView.render();
const myRegion = myView.getRegion('mainRegion');
myRegion.show(new ChildView());
myRegion.destroy();
myRegion.isDestroyed(); // true
myRegion.hasView(); // false
myView.hasRegion('mainRegion'); // false
```
## Check If View Is Being Swapped By Another
The `isSwappingView` method returns if a view is being swapped by another one. It's useful
inside region lifecycle events / methods.
The example will show an message when the region is empty:
```javascript
import { Region } from 'marionette';
const EmptyMsgRegion = Region.extend({
onEmpty() {
if (!this.isSwappingView()) {
this.el.append('Empty Region');
}
}
});
```
## Set How View's `el` Is Attached and Detached
Override the region's `attachHtml` method to change how the view is attached
to the DOM (when not using `replaceElement: true`). This method receives one
parameter - the view to show.
The default implementation of `attachHtml` is essentially:
```javascript
import { Region } from 'marionette';
Region.prototype.attachHtml = function(view){
this.el.appendChild(view.el);
}
```
Similar to `attachHtml`, override `detachHtml` to determine how the region detaches
the contents from its `el`. This method receives no parameters.
For most cases you will want to use the [DOM API](/docs/dom-api.md) to determine how
a region html is attached, but in some cases you may want to override a single Region
class for situations like animation where you want to control both attaching and
[view removal](#configure-how-to-remove-view).
This example will make a view slide down from the top of the screen instead of just
appearing in place:
```javascript
import $ from 'jquery';
import { Region, View } from 'marionette';
const ModalRegion = Region.extend({
attachHtml(view){
// Some effect to show the view:
const $el = $(this.el);
$el.empty().append(view.el);
$el.hide().slideDown('fast');
}
});
const MyView = View.extend({
regions: {
mainRegion: '#main-region',
modalRegion: {
regionClass: ModalRegion,
el: '#modal-region'
}
}
});
```
## Configure How To Remove View
Override the region's `removeView` method to change how and when the view is destroyed / removed
from the DOM. This method receives one parameter - the view to remove.
The default implementation of `removeView` is:
```javascript
import { Region } from 'marionette';
Region.prototype.removeView = function(view){
this.destroyView(view);
}
```
`destroyView(view)` destroys a Marionette View and returns it. It forwards the
Region owner's lifecycle-monitoring policy; it does not adapt a Backbone View
or fall back to `remove()`. Keep this helper when overriding `removeView`.
Region operations are synchronous. A `removeView` override must complete cleanup
before returning if callers should observe the normal empty/destroy contract.
Returning a Promise does not delay Region lifecycle completion. For an exit
animation, finish the animation in the application before calling `empty()` or
showing the replacement, and let the Region perform its normal synchronous
teardown. The application owns cancellation when navigation or destruction
interrupts that animation.
[Canonical source](/docs/markdown/docs/marionette.region.md) · [Source identity](/docs/manifest.json)
---
Document: docs/marionette.collectionview.md
Canonical URL: https://marionettejs.com/docs/collection-view/
Markdown URL: https://marionettejs.com/docs/collection-view.md
Reading SHA-256: cf11a246ff891a207873733884de682c188895c5d1ca138168760c61d4ea4cd1
# Marionette.CollectionView
A `CollectionView` manages repeated parts of a screen: rows, cards, or any
ordered set of child views within a root element, `el`. It creates children
from a `collection`, or lets you add and remove child views yourself.
Plain arrays work with the default [Data API](/docs/data-api.md). Use an adapter
when your collection needs to notify the view about changes; mutating a plain
array does not send those notifications.
`CollectionView` includes:
- [The DOM API](/docs/dom-api.md)
- [Class Events](/docs/class-events.md#collectionview-events)
- [DOM Interactions](/docs/dom-interactions.md)
- [Child Event Bubbling](/docs/events.md#event-bubbling)
- [Entity Events](/docs/entity-events.md)
- [View Rendering](/docs/rendering.md)
- [Prerendered Content](/docs/prerendered-dom.md)
- [View Lifecycle](/docs/lifecycle.md)
A `CollectionView` can have [`Behavior`s](/docs/behavior.md).
## Documentation Index
* [Instantiating a CollectionView](#instantiating-a-collectionview)
* [Rendering a CollectionView](#rendering-a-collectionview)
* [Rendering a Template](#rendering-a-template)
* [Defining the `childViewContainer`](#defining-the-childviewcontainer)
* [Re-rendering the CollectionView](#re-rendering-the-collectionview)
* [View Lifecycle and Events](#view-lifecycle-and-events)
* [Entity Events](#entity-events)
* [DOM Interactions](#dom-interactions)
* [Behaviors](#behaviors)
* [Managing Children](#managing-children)
* [Attaching `children` within the `el`](#attaching-children-within-the-el)
* [Destroying All `children`](#destroying-all-children)
* [CollectionView's `childView`](#collectionviews-childview)
* [Building the `children`](#building-the-children)
* [Passing Data to the `childView`](#passing-data-to-the-childview)
* [CollectionView's `emptyView`](#collectionviews-emptyview)
* [CollectionView's `getEmptyRegion`](#collectionviews-getemptyregion)
* [Passing Data to the `emptyView`](#passing-data-to-the-emptyview)
* [Defining When an `emptyView` shows](#defining-when-an-emptyview-shows)
* [Accessing a Child View](#accessing-a-child-view)
* [CollectionView `children` Iterators And Collection Functions](#collectionview-children-iterators-and-collection-functions)
* [Listening to Events on the `children`](#listening-to-events-on-the-children)
* [Self Managed `children`](#self-managed-children)
* [Adding a Child View](#adding-a-child-view)
* [Removing a Child View](#removing-a-child-view)
* [Detaching a Child View](#detaching-a-child-view)
* [Swapping Child Views](#swapping-child-views)
* [Sorting the `children`](#sorting-the-children)
* [Defining the `viewComparator`](#defining-the-viewcomparator)
* [Maintaining the `collection`'s sort](#maintaining-the-collections-sort)
* [Filtering the `children`](#filtering-the-children)
* [Defining the `viewFilter`](#defining-the-viewfilter)
## Instantiating a CollectionView
When instantiating a `CollectionView` there are several properties, if passed,
that will be attached directly to the instance:
`attributes`, `behaviors`, `childView`, `childViewContainer`, `childViewEventPrefix`,
`childViewEvents`, `childViewOptions`, `childViewTriggers`, `className`, `collection`,
`collectionEvents`, `el`, `emptyView`, `emptyViewOptions`, `events`, `id`, `model`,
`modelEvents`, `sortWithCollection`, `stateEvents`, `tagName`, `template`, `templateContext`,
`triggers`, `ui`, `viewComparator`, `viewFilter`
```javascript
import { CollectionView } from 'marionette';
const myCollectionView = new CollectionView();
```
`CollectionView` composes the same visual, event, and State contracts as `View`,
but does not inherit View's named-Region methods. Use `getEmptyRegion()` for its
empty View; put a CollectionView inside a parent View when a layout needs
additional named Regions. A supplied `state` follows the
[State ownership contract](/docs/state.md#borrowed-and-owned-sources).
## Rendering a CollectionView
The `render` method of the `CollectionView` is primarily responsible
for rendering the entire collection. It loops through each of the
children in the collection and renders them individually as a
`childView`.
```javascript
import { CollectionView } from 'marionette';
const MyCollectionView = CollectionView.extend({});
// all of the children views will now be rendered.
new MyCollectionView().render();
```
### Rendering a Template
In addition to rendering children, the `CollectionView` may have a
`template`. The child views can be rendered within a DOM element of
this template. The `CollectionView` will serialize either the `model`
or `collection` along with context for the `template` to render.
For more detail on how to render templates, see
[View Template Rendering](/docs/rendering.md).
### Defining the `childViewContainer`
By default the `CollectionView` will render the children into the `el`
of the `CollectionView`. If you are rendering a template you will want
to set the `childViewContainer` to be a selector for an element within
the template for child view attachment.
```javascript
import { CollectionView } from 'marionette';
const MyCollectionView = CollectionView.extend({
childViewContainer: '.js-widgets',
template: () => '
Widgets
'
});
```
**Errors** An error will throw if the childViewContainer can not be found.
### Re-rendering the CollectionView
If you need to re-render the entire collection or the template, you can call the
`collectionView.render` method. This method will destroy all of
the child views that may have previously been added.
## View Lifecycle and Events
Like `View`, a `CollectionView` exposes its lifecycle as the independent
`isRendered()`, `isAttached()`, and `isDestroyed()` state values. Its managed
children have their own View lifecycle state. Existing contents in the
`CollectionView` element do not make the `CollectionView` rendered; rendering
means its child set has been built and inserted into its element.
The table describes the default rendered and monitored path. Passing
`{ preventRender: true }` to `addChildView` still renders the parent when
needed, but manages the supplied child without rendering it; detaching that
child returns it in its current lifecycle state. Setting
`monitorViewEvents: false` on the `CollectionView` intentionally disables child
attachment events and automatic child `isAttached()` updates.
Disabling monitoring does not make child destruction clear surrounding template
content. Bulk removal is used only when the child container contains those Views'
root elements and optional formatting whitespace.
| Operation | CollectionView state | Managed child state |
| --- | --- | --- |
| Construct | Starts not rendered and not destroyed. It is attached only when its element is already in the document. | No children have been built. |
| `render()` | Enters rendered and preserves its attached state. Repeated render stays rendered. | Builds and renders the current children. Repeated render destroys the previous children before building replacements. |
| A rendered collection resets | Remains rendered and preserves its attached state. | Destroys the previous children and builds replacements for the reset collection. |
| `addChildView(view)` | Renders first when needed, then remains rendered. | Renders and manages the added View. |
| `detachChildView(view)` | State is unchanged. | Removes and returns the live View in a detached state. The caller becomes responsible for it. |
| `removeChildView(view)` or external child destruction | State is unchanged. | Removes the child from the managed set. `removeChildView` destroys it; an externally destroyed child is removed once. |
| The owning Region detaches and re-shows the CollectionView | Remains rendered while attached changes to `false`, then back to `true`. | Live children follow the parent's detached and attached state. |
| `destroy()` | Detaches, becomes not rendered, and enters destroyed. Repeated destroy returns the CollectionView without repeating lifecycle events. | Detaches and destroys every still-managed child after the parent element is removed. |
| `render()` after destruction | Returns the same CollectionView and remains not rendered and destroyed. Repeated calls are no-ops. | Does not recreate or render children. |
| `addChildView(view)` once destruction begins | Returns the supplied View without inspecting it, the index, or options or changing events, ownership, DOM, or lifecycle state. Calls during `before:destroy` and repeated calls after destruction are the same no-op. | The supplied View remains unchanged and can be added to a live owner. |
Collection `sort`, `reset`, and `update` events raised reentrantly during destruction
do not rebuild, add, remove, sort, render, or destroy additional child Views.
A View returned by `detachChildView()` is no longer managed by the
`CollectionView`; another owner may show it, or the caller must destroy it.
Other operations on an already destroyed `CollectionView` remain outside this
lifecycle contract until their invalid-transition behavior is made consistent.
Read More:
- [View Lifecycle](/docs/lifecycle.md)
- [View DOM Change Events](/docs/class-events.md#dom-change-events)
- [View Destroy Events](/docs/class-events.md#destroy-events)
## Entity Events
A `CollectionView` subscribes to its `model` and `collection` through the
configured [DataApi](/docs/data-api.md). Event names and callback arguments belong
to that data provider. Plain objects and arrays do not emit changes; declaring
entity event maps for unobservable values throws `MN0037`.
Read More:
- [Entity Events](/docs/entity-events.md)
## DOM Interactions
`CollectionView` uses the same native [`events`, `triggers`, and `ui`
contracts](/docs/dom-interactions.md) as `View`. Keep parent selectors and handlers
specific to DOM that the `CollectionView` itself owns. Delegation is rooted at
the parent `el`, so a broad selector can also match child-owned descendants; do
not rebind the parent's `ui` to reach into child View DOM.
After application code places parent-owned DOM inside a template-less
`CollectionView`, call `bindUIElements()` before reading it with `getUI()`. Use
that method only to bind the CollectionView's own DOM, not child View DOM.
Calling `getUI()` without a declared `ui` map or while UI elements are unbound throws
[`MN0023`](/errors/MN0023.md).
When parent code needs a child, [retrieve the child View through the public
`children` lookup APIs](#accessing-a-child-view) and call an intentional public
method on that View. For communication initiated by a child, use
[`childViewEvents` or `childViewTriggers`](/docs/events.md#child-view-events), or an
explicit public [`listenTo`](/docs/events.md#listening-to-events) subscription,
instead of querying or mutating the child's DOM from the parent.
Read More:
- [DOM Interactions](/docs/dom-interactions.md)
- [Listening to Events on Children](#listening-to-events-on-the-children)
## Behaviors
A `Behavior` provides a clean separation of concerns to your view logic,
allowing you to share common user-facing operations between your views.
Read More:
- [Using `Behavior`s](/docs/behavior.md#using-behaviors)
## Managing Children
Children are automatically managed once the `CollectionView` is
[rendered](#rendering-a-collectionview). For each model within the
`collection` the `CollectionView` will build and store a `childView`
within its `children` object. This allows you to easily access
the views within the collection view, iterate them, find them by
a given indexer such as the view's model or id and more.
During its first render, the `CollectionView` subscribes through
`DataApi.observeCollection()` to normalized update, reset, and reorder
notifications. The configured provider owns the source event vocabulary;
[Backbone](/docs/backbone.md) is one supported observable integration.
When the `collection` for the view is `reset`, the view will destroy all
children and re-render the entire collection.
When the adapter reports a model addition, the `CollectionView` constructs its
child and renders it if it passes the presentation filter.
When a model is removed from the `collection` (or destroyed / deleted), the `CollectionView`
will destroy and remove that model's child view.
Collection updates, `sort()`, and `filter()` use the same child-rendering path.
Surviving visible children keep their elements mounted, including when a
`viewFilter` or custom `viewComparator` is active. New or newly visible children
are attached through `attachHtml`; existing elements move only when their order
needs to change. Removal alone does not move or rerender surviving children. See
[DOM movement](/docs/dom-api.md#moveelel-parent-before) for focus and text-selection
preservation and the browser fallback behavior.
The `before:render:children` and `render:children` events receive all visible
children. This describes the render pass, not a list of children whose templates
were rerendered. Already-rendered children reuse their contents unless the data
adapter reports them as updated.
Overriding `sort()` or `filter()` replaces that part of the flow. Call the parent
method to retain its behavior; CollectionView does not force a render after an
override that deliberately skips it.
When the `collection` for the view is sorted, the view by default reconciles its child
views to the collection's source order unless the `sortWithCollection` attribute on the
`CollectionView` is set to `false`. Setting `viewComparator: false` disables a separate
presentation sort; it does not disable keyed source-order reconciliation.
```javascript
import BackboneApi from '@mnjs/adapters/backbone';
import Backbone from 'backbone';
import { CollectionView, setDataApi, View } from 'marionette';
setDataApi(BackboneApi);
const collection = new Backbone.Collection();
const MyChildView = View.extend({
template: false
});
const MyCollectionView = CollectionView.extend({
childView: MyChildView,
collection,
});
const myCollectionView = new MyCollectionView();
// Collection view will not re-render as it has not been rendered
collection.reset([{foo: 'foo'}]);
myCollectionView.render();
// Collection view will effectively re-render displaying the new model
collection.reset([{foo: 'bar'}]);
```
When the children are rendered the
[`render:children` and `before:render:children` events](/docs/class-events.md#renderchildren-and-beforerenderchildren-events)
will trigger.
When a childview is added to the children
[`add:child` and `before:add:child` events](/docs/class-events.md#addchild-and-beforeaddchild-events)
will trigger
When a childview is removed from the children
[`remove:child` and `before:remove:child` events](/docs/class-events.md#removechild-and-beforeremovechild-events)
will trigger.
### Attaching `children` within the `el`
The `CollectionView` places new or newly visible child root elements into a
`DocumentFragment`, then calls `attachHtml(fragment, container)` to insert that
batch. Already mounted children remain in place or move only as needed to match
the presentation order; they are not all removed and appended on each pass.
You can override this by specifying an `attachHtml` method in your
view definition. This method takes two parameters and has no return value.
```javascript
import { CollectionView } from 'marionette';
CollectionView.extend({
// The default implementation:
attachHtml(els, container) {
// Unless childViewContainer is set, container === this.el
this.Dom.appendContents(container, els);
}
});
```
The first parameter is the DOM fragment containing child root elements, and the second parameter
is the native DOM container for the children which by default equates
to the view's `el` unless a [`childViewContainer`](#defining-the-childviewcontainer)
is set.
### Destroying All `children`
`CollectionView` implements a `destroy` method which automatically
destroys its children and cleans up listeners.
When a nonempty owned child set is destroyed, the
[`destroy:children` and `before:destroy:children` events](/docs/class-events.md#destroychildren-and-beforedestroychildren-events)
will trigger.
Read More:
- [View Destroy Events](/docs/class-events.md#destroy-events)
## CollectionView's `childView`
When using a `collection` to manage the children of `CollectionView`,
specify a Marionette `View` or `CollectionView` class as `childView`, rather
than an instance. A plain Backbone View is not a supported child;
[wrap it in a Marionette View](/docs/region.md#wrapping-a-non-marionette-view)
when integrating a legacy component.
```javascript
import { View, CollectionView } from 'marionette';
const MyChildView = View.extend({});
const MyCollectionView = CollectionView.extend({
childView: MyChildView
});
```
**Errors** When Marionette needs to construct a collection-backed child and
`childView` is missing, it throws `MN0011`. An empty CollectionView or a
CollectionView with only manually added children does not require `childView`.
You can also define `childView` as a function. In this form, the value
returned by this method is the `ChildView` class that will be instantiated
when a `Model` needs to be initially rendered. This method also gives you
the ability to customize per `Model` `ChildViews`.
```javascript
import _ from 'underscore';
import BackboneApi from '@mnjs/adapters/backbone';
import Backbone from 'backbone';
import { CollectionView, setDataApi, View } from 'marionette';
setDataApi(BackboneApi);
const FooView = View.extend({
template: _.template('foo')
});
const BarView = View.extend({
template: _.template('bar')
});
const MyCollectionView = CollectionView.extend({
collection: new Backbone.Collection(),
childView(model) {
// Choose which view class to render,
// depending on the properties of the model
if (model.get('isFoo')) {
return FooView;
}
else {
return BarView;
}
}
});
const collectionView = new MyCollectionView().render();
const foo = new Backbone.Model({
isFoo: true
});
const bar = new Backbone.Model({
isFoo: false
});
// Renders a FooView
collectionView.collection.add(foo);
// Renders a BarView
collectionView.collection.add(bar);
```
A resolver must return a Marionette View class. Core trusts that result;
unsupported returns can fail later during construction or child setup.
### Building the `children`
The `buildChildView` method is responsible for taking the ChildView class and
instantiating it with the appropriate data. This method takes three
parameters and returns a view instance to be used as the child view.
```javascript
buildChildView(child, ChildViewClass, childViewOptions){
// build the final list of options for the childView class
const options = { model: child, ...childViewOptions };
// create the child view instance
const view = new ChildViewClass(options);
// return it
return view;
},
```
Override this method when you need a more complicated build, but use [`childView`](#collectionviews-childview)
if you need to determine _which_ View class to instantiate.
```javascript
import _ from 'underscore';
import BackboneApi from '@mnjs/adapters/backbone';
import Backbone from 'backbone';
import { CollectionView, setDataApi } from 'marionette';
import MyListView from './my-list-view';
import MyView from './my-view';
setDataApi(BackboneApi);
const MyCollectionView = CollectionView.extend({
childView(child) {
if (child.get('type') === 'list') {
return MyListView;
}
return MyView;
},
buildChildView(child, ChildViewClass, childViewOptions) {
let options;
if (child.get('type') === 'list') {
const childList = new Backbone.Collection(child.get('list'));
options = _.extend({collection: childList}, childViewOptions);
} else {
options = _.extend({model: child}, childViewOptions);
}
// create the child view instance
const view = new ChildViewClass(options);
// return it
return view;
}
});
```
### Passing Data to the `childView`
There may be scenarios where you need to pass data from your parent
collection view in to each of the childView instances. To do this, provide
a `childViewOptions` definition on your collection view as an object
literal. This will be passed to the constructor of your childView as part
of the `options`.
```javascript
import { View, CollectionView } from 'marionette';
const ChildView = View.extend({
initialize(options) {
console.log(options.foo); // => "bar"
}
});
const MyCollectionView = CollectionView.extend({
childView: ChildView,
childViewOptions: {
foo: 'bar'
}
});
```
You can also specify the `childViewOptions` as a function, if you need to
calculate the values to return at runtime. The model will be passed into
the function should you need access to it when calculating
`childViewOptions`. The function may return an object, `null`, or `undefined`. The attributes
of a returned object will be copied to the `childView` instance's options. Whether
provided directly or returned by a function, the object's own enumerable
properties, including symbols, are copied by object spread. `null` or `undefined`
adds no extra options. A supplied `model` option overrides the source model;
use that only when the child deliberately represents different data.
```javascript
import { CollectionView } from 'marionette';
const MyCollectionView = CollectionView.extend({
childViewOptions(model) {
// do some calculations based on the model
return {
foo: 'bar'
};
}
});
```
## CollectionView's `emptyView`
When a collection has no children, and you need to render a view other than
the list of childViews, you can specify an `emptyView` attribute on your
collection view. The `emptyView`, like the
[`childView`](#collectionviews-childview), can be passed as an option on
instantiation. It must be a `View` class or a resolver that returns a `View`
class. Marionette calls resolvers with the `CollectionView` as `this`; arrow and
bound functions retain their normal JavaScript `this` semantics.
If the resolved `emptyView` property is `undefined`, `null`, or `false`, no
empty view is rendered. Because an `undefined` constructor option does not
replace an inherited value, use `null` or `false` to disable an inherited
definition. A resolver may return a `View` class or `undefined`, `null`, or
`false` to disable the empty view. The public types describe these alternatives;
Marionette trusts the result when the collection is empty. Errors thrown by a
resolver propagate unchanged.
When the empty collection is rendered or filtered again, a disabled result also
removes any empty View already shown.
```javascript
import _ from 'underscore';
import { View, CollectionView } from 'marionette';
const MyEmptyView = View.extend({
template: _.template('Nothing to display.')
});
const MyCollectionView = CollectionView.extend({
// ...
emptyView: MyEmptyView
});
```
### CollectionView's `getEmptyRegion`
When a `CollectionView` is instantiated it creates a region for showing the [`emptyView`](#collectionviews-emptyview).
This region can be requested using the `getEmptyRegion` method. It uses the
resolved `childViewContainer` when present, otherwise the CollectionView's `el`,
and is shown with [`replaceElement: false`](/docs/region.md#additional-options).
**Note** The `CollectionView` expects to be the only entity managing the region.
Showing things in this region directly is not advised.
```javascript
const isEmptyShowing = myCollectionView.getEmptyRegion().hasView();
```
This region can be useful for handling the
[EmptyView Region Events](/docs/class-events.md#collectionview-emptyview-region-events).
### Passing Data to the `emptyView`
Similar to [`childView`](#collectionviews-childview) and [`childViewOptions`](#passing-data-to-the-childview),
there is an `emptyViewOptions` property that will be passed to the `emptyView` constructor.
It can be provided as an object literal or as a function.
If `emptyViewOptions` aren't provided, the `CollectionView` falls back to
`childViewOptions`. A callable definition receives no model argument and runs
with the CollectionView as `this`; it must support that empty-view call.
```javascript
import { View, CollectionView } from 'marionette';
const EmptyView = View.extend({
initialize(options){
console.log(options.foo); // => "bar"
}
});
const MyCollectionView = CollectionView.extend({
emptyView: EmptyView,
emptyViewOptions: {
foo: 'bar'
}
});
```
### Defining When an `emptyView` shows
If you want to control when the empty view is rendered, you can override
`isEmpty`:
```javascript
import { CollectionView } from 'marionette';
const MyCollectionView = CollectionView.extend({
isEmpty() {
// some logic to calculate if the view should be rendered as empty
return this.collection.length < 2;
}
});
```
The default implementation of `isEmpty` returns `!this.children.length`.
Use `getEmptyRegion().hasView()` to determine whether an empty View is actually
shown. `isEmpty()` alone does not establish that an `emptyView` was configured:
```javascript
import { CollectionView } from 'marionette';
const MyCollectionView = CollectionView.extend({
// ...
onRenderChildren() {
if (this.getEmptyRegion().hasView()) { console.log('Empty View Shown'); }
}
});
```
## Accessing a Child View
You can retrieve a view by a number of methods. If the findBy* method cannot find the view,
it will return `undefined`.
**Note** `children` is the current presentation container. It can include
unrendered children added with `preventRender` until the next render/filter
pass; filtered-out children remain owned but are absent from this container.
### CollectionView `children`'s: `findByCid`
Find a view by its cid.
```javascript
const bView = myCollectionView.children.findByCid(buttonView.cid);
```
### CollectionView `children`'s: `findByModel`
Find a view by `DataApi.key(model)`. With the default DataApi this is the
model object identity. An adapter may use a stable key so that a new model
object representing the same item resolves the currently indexed child. This
lookup does not promise child retention when a collection observation replaces
the model object; see [collection observations](/docs/data-api.md#collection-observations).
```javascript
const bView = myCollectionView.children.findByModel(buttonView.model);
```
### CollectionView `children`'s: `findByKey`
`children.findByKey(key)` returns the View indexed by the exact key produced by
its DataApi, or `undefined` when absent. Do not assume this key is the model's
`id`: native Marionette and Backbone models use their provider's identity
contract, while snapshot adapters can use an application-selected key.
`children.hasView(view)` checks that the exact View instance is present under
its `cid`; `children.contains(view)` checks instance membership as well.
These lookups refer to the public presentation container. A filtered-out child
can remain owned by the CollectionView without appearing in `children`. Keep
an explicit reference when an application needs to detach such a child; do not
reach into private containers.
### CollectionView `children`'s: `findByIndex`
Find by numeric index (unstable)
```javascript
const bView = myCollectionView.children.findByIndex(0);
```
### CollectionView `children`'s: `findIndexByView`
Find the index of the exact View inside `children`, or `-1` when absent.
```javascript
const index = myCollectionView.children.findIndexByView(bView);
```
### CollectionView `children` Iterators And Collection Functions
The container is iterable: `for (const child of list.children)` visits the
current presentation order. Use `children.toArray()` when you need a separate
array before changing membership.
The container owns the following iteration and collection functions:
* `each`
* `map`
* `reduce`
* `find`
* `filter`
* `reject`
* `every`
* `some`
* `contains`
* `invoke`
* `toArray`
* `first`
* `initial`
* `rest`
* `last`
* `without`
* `isEmpty`
* `pluck`
* `partition`
These methods can be called directly on the container, to iterate and process
the views held by the container.
`each`, `map`, `reduce`, `find`, `filter`, `reject`, `every`, `some`, and
`partition` require callback functions. The public types enforce that contract;
unsupported JavaScript callback shapes have no guaranteed Marionette diagnostic.
String, object, and null iteratee shorthand is not supported. Structurally adding, removing, or
reordering children while a callback runs is unsupported, and these methods do
not promise call-start snapshot semantics. Mutating ordinary properties on a
child View remains valid.
`each(callback, context)` visits every child View in order, calls `callback` as
`(view, index)`, binds `this` to `context` when provided, and returns the
`children` container. An empty container returns itself without calling the
callback.
`map(callback, context)` calls `(view, index)` for every child View and returns a
new ordered array of callback results. An empty container returns a new `[]`.
Use `map(view => view.id)` or `pluck('id')` instead of property-name shorthand.
`reduce(callback, initialValue, context)` calls
`(accumulator, view, index)` in container order and binds optional `context`.
When `initialValue` is supplied, every child View is visited; an empty container
returns that exact value without calling the callback. When it is omitted, the
first child View becomes the accumulator and traversal starts at index `1`. An
empty container without an initial value throws [`MN0024`](/errors/MN0024.md).
`pluck(key)` reads `key` directly from each child View. For example,
`children.pluck('model')` returns the child Views' model objects, and a child
without a model contributes `undefined`. It does not read model attributes; use
an explicit callback such as `children.map(view => view.model?.get('status'))`
for those values. Array-form deep paths are not traversed; replace
`children.pluck(['model', 'cid'])` with
`children.map(view => view.model?.cid)`. An empty container returns `[]`.
`contains(value)` checks for the exact child View instance. A child View's model
or another object with the same properties is not considered contained. An empty
container returns `false`.
`find`, `filter`, `reject`, `every`, `some`, and `partition` call their predicate
with `(view, index)` and set `this` to optional `context`.
`find(predicate, context)` returns the first child View for which the predicate
is truthy, preserving View identity, and stops iterating at that match. It
returns `undefined` when no View matches or the container is empty.
`filter(predicate, context)` and `reject(predicate, context)` visit every child
View and return new ordered arrays containing the Views for which the predicate
is truthy or falsey, respectively. Changing a returned array does not change the
container. An empty container returns `[]` without calling the predicate.
`every(predicate, context)` returns `false` and stops at the first falsey result;
otherwise it returns `true`. `some(predicate, context)` returns `true` and stops
at the first truthy result; otherwise it returns `false`. For an empty container,
`every` returns `true` and `some` returns `false`, without calling the predicate.
`partition(predicate, context)` visits every child View and returns
`[matchingViews, rejectedViews]`. Both members are new arrays that preserve the
container order and contain the exact child View instances. An empty container
returns `[[], []]` without calling the predicate.
`invoke(methodName, ...args)` requires a direct string method name, invokes that
method with each child View as `this`, forwards `args`, and returns a new ordered
array of results. TypeScript restricts the name to callable child methods and
checks their arguments and result types. Function-form and deep-path method
names are not supported. An empty container returns `[]`.
`toArray()` returns a new array containing the current child Views in container
order. Changing the returned array's membership or order does not change the
container. An empty container returns `[]`.
Without a count, `first()` and `last()` return the first or last child View. With
a nonnegative integer count, they return a new ordered array containing up to
that many Views from the corresponding end of the container. A count of `0`
returns `[]`. For an empty container, the no-count forms return `undefined` and
the count forms return `[]`.
`initial(count = 1)` and `rest(count = 1)` return new ordered arrays after
excluding `count` Views from the end or start of the container, respectively.
The count is a nonnegative integer: `0` returns a new array of every child View,
and a count greater than or equal to the container length returns `[]`. An empty
container also returns `[]`. `first`, `initial`, `rest`, and `last` throw
[`MN0024`](/errors/MN0024.md) when a supplied count is not a nonnegative integer.
`without(...views)` returns a new ordered array excluding the exact child View
instances supplied. Models and lookalike objects do not exclude their associated
Views. With no arguments it returns a new array of every child View. Changing the
returned array's membership or order does not change the container. An empty
container returns `[]`.
`children.isEmpty()` reports whether the child container currently has zero
Views. It is distinct from the overridable `CollectionView#isEmpty()` method,
which controls whether a CollectionView renders its `emptyView`.
The child container is iterable. `for...of`, spread, destructuring, and
`Array.from(children)` yield the exact child View instances in container order.
The iterator is defined once on the prototype rather than allocated as an own
property on every container.
The former undocumented Underscore aliases `forEach`, `detect`, `select`, `all`,
`any`, and `include` are not part of the v5 container. Use `each`, `find`,
`filter`, `every`, `some`, and `contains`, respectively.
```javascript
import BackboneApi from '@mnjs/adapters/backbone';
import Backbone from 'backbone';
import { CollectionView, setDataApi } from 'marionette';
setDataApi(BackboneApi);
const collectionView = new CollectionView({
collection: new Backbone.Collection()
});
collectionView.render();
// iterate over all of the views and process them
collectionView.children.each(function(childView) {
// process the `childView` here
});
```
## Listening to Events on the `children`
The `CollectionView` can take action depending on what
events are triggered in its `children`.
Read More:
- [Child Event Bubbling](/docs/events.md#event-bubbling)
## Self-Managed `children`
In addition to children added by Marionette matching the model of a `collection`,
the `children` of the `CollectionView` can be manually managed.
### Adding a Child View
The `addChildView` method can be used to add a view that is independent of your
collection source. This method takes three parameters, the child view instance,
optionally the index for where it should be placed within the
[CollectionView's `children`](#managing-children), and an options hash.
It returns the added view.
```javascript
import { CollectionView, View } from 'marionette';
const ChildView = View.extend({
tagName: 'li',
template() {
return 'Model';
}
});
export function runChildOwnershipLifecycle() {
const collectionView = new CollectionView({ tagName: 'ul' });
const reusableChild = new ChildView();
const remainingChild = new ChildView();
collectionView.render();
collectionView.addChildView(reusableChild);
const detachedChild = collectionView.detachChildView(reusableChild);
collectionView.addChildView(detachedChild);
collectionView.removeChildView(detachedChild);
collectionView.addChildView(remainingChild);
collectionView.destroy();
}
```
`detachChildView()` returns the same live View and transfers responsibility to
the caller. That View may be added again without rendering it a second time.
`removeChildView()` destroys the removed View, while destroying the
`CollectionView` destroys every child that it still manages.
An omitted or `null` index appends the child before sorting and filtering.
The options-only form follows the same rule; use a numeric `index` to choose
an insertion position.
A numeric index bypasses sorting and filtering for that addition only. A later
`sort()` or `filter()` processes the child normally. The numeric `index` in an
options object takes precedence over the separate positional argument.
**Errors** Adding a View that is still managed by a Region or
`CollectionView` throws [`MN0003`](/errors/MN0003.md). Detach the View from its
current owner before transferring it.
Filtering a child out or adding it with `preventRender` still leaves it managed
by that CollectionView. Use `detachChildView()` to transfer it to another owner.
#### `preventRender` option
If you wish to add a child view to the children without the collectionview rendering
the children use the `preventRender` option.
```javascript
import { CollectionView } from 'marionette';
import ButtonView from './button-view';
const myCollectionView = new CollectionView();
const insertIndex = 0; // Add to the top
myCollectionView.addChildView(new ButtonView(), { preventRender: true, index: insertIndex });
myCollectionView.addChildView(new ButtonView(), insertIndex, { preventRender: true });
myCollectionView.addChildView(new ButtonView()); // renders all three children
```
### Removing a Child View
The `removeChildView` method is useful if you need to remove and destroy a view from
the `CollectionView` without affecting the view's collection. In most cases it is
better to use the data to determine what the `CollectionView` should display.
This method accepts the child view instance to remove as its parameter. It returns
the removed view.
Later updates to the retained model do not recreate its removed View. Rendering
the CollectionView again or resetting its collection rebuilds its children from
the current collection.
```javascript
import { CollectionView } from 'marionette';
// Fragment for a collection using the Backbone DataApi.
const MyCollectionView = CollectionView.extend({
childViewEvents: { 'foo:event': 'onChildViewFooEvent' },
onChildViewFooEvent(childView, model) {
// NOTE: we must wait for the server to confirm
// the destroy PRIOR to removing it from the collection
model.destroy({wait: true});
// but go ahead and remove it visually
this.removeChildView(childView);
}
});
```
### Detaching a Child View
The `detachChildView` method is the same as [`removeChildView`](#removing-a-child-view)
with the exception that the removed view is not destroyed.
### Swapping Child Views
Swap the location of two views in the `CollectionView` `children` and in the `el`.
This can be useful when sorting is arbitrary or is not performant.
**Errors** If either of the two views aren't part of the `CollectionView` an error will be thrown.
If only one of the two children is in the presentation `children` container,
[filter](#filtering-the-children) is called after swapping their owned order.
```javascript
import BackboneApi from '@mnjs/adapters/backbone';
import Backbone from 'backbone';
import { CollectionView, setDataApi } from 'marionette';
import MyChildView from './my-child-view';
setDataApi(BackboneApi);
const collection = new Backbone.Collection([
{ name: 'first' },
{ name: 'middle' },
{ name: 'last' }
]);
const myColView = new CollectionView({
collection: collection,
childView: MyChildView
});
myColView.render();
myColView.swapChildViews(myColView.children.first(), myColView.children.last());
myColView.children.first().model.get('name'); // "last"
myColView.children.last().model.get('name'); // "first"
```
## Sorting the `children`
The `sort` method will loop through the `CollectionView` `children` prior to filtering
and sort them with the [`viewComparator`](#defining-the-viewcomparator).
By default, if a `viewComparator` is not set, the `CollectionView` will sort
the views by the order of the models in the `collection`. If set to `false`,
presentation sorting is disabled. Normalized collection observations still reconcile
the keyed children to source order when `sortWithCollection` is enabled.
This method is called internally when rendering.
[`sort` and `before:sort` events](/docs/class-events.md#sort-and-beforesort-events)
fire when owned children exist and a comparator is active.
By default the `CollectionView` will maintain a sorted collection's order
in the DOM. This behavior can be disabled by specifying `{sortWithCollection: false}`
on initialize.
Default source ordering uses each notification's captured snapshot. A nested
notification waits for the current sort, filter, and render pass to finish.
Calling `sort()` outside a collection notification reads the current source
after `before:sort`. With the default comparator, manually added children whose
models are absent from the source stay before the source children.
Custom comparators still determine their own order and data reads. With
`sortWithCollection` enabled, source order breaks ties and manually added
children follow source children on ties. With it disabled, ties retain the
existing child order.
### Defining the `viewComparator`
`CollectionView` allows for a custom `viewComparator` option if you want your
`CollectionView`'s children to be rendered with a different sort order than the
underlying collection uses.
```javascript
import { CollectionView, View } from 'marionette';
const RowView = View.extend({ template: ({ rank }) => String(rank) });
const myCollectionView = new CollectionView({
collection: [{ rank: 2 }, { rank: 1 }],
childView: RowView,
viewComparator: 'rank'
});
```
```javascript
import BackboneApi from '@mnjs/adapters/backbone';
import Backbone from 'backbone';
import { CollectionView, setDataApi, View } from 'marionette';
setDataApi(BackboneApi);
const RowView = View.extend({ template: ({ id }) => String(id) });
const myCollection = new Backbone.Collection([
{ id: 1 },
{ id: 4 },
{ id: 3 },
{ id: 2 }
]);
myCollection.comparator = 'id';
const myDescendingView = new CollectionView({
childView: RowView,
collection: myCollection,
viewComparator: childView => -childView.model.id
});
const mySourceOrderView = new CollectionView({
childView: RowView,
collection: myCollection,
viewComparator: false
});
myDescendingView.render(); // 4 3 2 1
mySourceOrderView.render(); // 1 4 3 2
myCollection.sort();
// myDescendingView remains 4 3 2 1
// mySourceOrderView reconciles to source order: 1 2 3 4
```
A `viewComparator` can be a one-argument criterion function, a two-argument
comparison function, or a string naming a model attribute read through DataApi.
Functions receive child Views, not models, and run with the CollectionView as
`this`. These forms do not require Backbone.
A string or single-argument comparator evaluates one criterion per child View and
sorts stably. Equal, `NaN`, or otherwise incomparable criteria retain their existing
order, while `undefined` criteria sort last. A string comparator therefore places a
child without a model last. Two-argument comparators retain native `Array#sort`
semantics. Sorting keeps the same `children` container in use. If evaluating or
comparing a single-argument criterion throws, the error propagates without changing
the child order.
#### `getComparator`
Override this method to determine which `viewComparator` to use.
```javascript
import BackboneApi from '@mnjs/adapters/backbone';
import { CollectionView, setDataApi } from 'marionette';
setDataApi(BackboneApi);
const MyCollectionView = CollectionView.extend({
sortAsc(view) {
return view.model.get('order');
},
sortDesc(view) {
return -view.model.get('order');
},
getComparator() {
// The collectionView's model
if (this.model.get('sorted') === 'ASC') {
return this.sortAsc;
}
return this.sortDesc;
}
});
```
#### `setComparator`
The `setComparator` method updates `viewComparator` and calls `sort()` when the
value changes. `{ preventRender: true }` defers that sort/filter/child-render
pass. It returns the CollectionView and does not run the parent
`before:render`/`render` lifecycle. Call it after initial rendering, or defer the
pass until the initial `render()`.
```javascript
import { CollectionView, View } from 'marionette';
const RowView = View.extend({ template: ({ orderBy }) => String(orderBy) });
const cv = new CollectionView({
collection: [{ orderBy: 2 }, { orderBy: 1 }],
childView: RowView
});
cv.render();
// Note: the setComparator is preventing the automatic re-render
cv.setComparator('orderBy', { preventRender: true });
// Apply the order without rebuilding the children or parent template
cv.sort();
```
#### `removeComparator`
This function is actually an alias of `setComparator(null, options)`. It is useful
for removing the comparator. `removeComparator` also accepts `preventRender` as a option.
```javascript
import { CollectionView, View } from 'marionette';
const RowView = View.extend({ template: ({ orderBy }) => String(orderBy) });
const cv = new CollectionView({
collection: [{ orderBy: 2 }, { orderBy: 1 }],
childView: RowView
});
cv.render();
cv.setComparator('orderBy');
//Remove the current comparator without rendering again.
cv.removeComparator({ preventRender: true });
```
### Maintaining the `collection`'s sort
By default the `CollectionView` will maintain a sorted collection's order
in the DOM. This behavior can be disabled by specifying `{sortWithCollection: false}`
on initialize or on the view definiton.
```javascript
import BackboneApi from '@mnjs/adapters/backbone';
import Backbone from 'backbone';
import { CollectionView, setDataApi, View } from 'marionette';
setDataApi(BackboneApi);
const RowView = View.extend({ template: ({ id }) => String(id) });
const myCollection = new Backbone.Collection([
{ id: 1 },
{ id: 4 },
{ id: 3 },
{ id: 2 }
]);
myCollection.comparator = 'id';
const mySortedColView = new CollectionView({
childView: RowView,
collection: myCollection
});
const myUnsortedColView = new CollectionView({
childView: RowView,
collection: myCollection,
sortWithCollection: false
});
mySortedColView.render(); // 1 4 3 2
myUnsortedColView.render(); // 1 4 3 2
myCollection.sort();
// mySortedColView auto-renders 1 2 3 4
// myUnsortedColView has no change
```
## Filtering the `children`
The `filter` method will loop through the `CollectionView`'s sorted `children`
and test them against the [`viewFilter`](#defining-the-viewfilter).
The views that pass the `viewFilter` are rendered if necessary and attached
to the CollectionView and the views that are filtered out will be detached.
After filtering the `children` will only contain the views to be attached.
If owned children exist and an active `viewFilter` is applied, the
[`filter` and `before:filter` events](/docs/class-events.md#filter-and-beforefilter-events)
will trigger.
The CollectionView refilters during normalized collection updates and sorting.
An arbitrary child property change does not itself trigger filtering; call
`filter()` when application-owned presentation criteria change.
**Note** This is a presentation functionality used to easily filter in and out
constructed children. All children of a `collection` will be instantiated once
regardless of their filtered status. If you would prefer to manage child view
instantiation, you should filter the `collection` itself.
### Defining the `viewFilter`
`CollectionView` allows for a custom `viewFilter` option if you want to prevent
some of the underlying `children` from being attached to the DOM.
A `viewFilter` can be a function, predicate object, or string. Use `null` or
`false` to disable it. Other shapes are unsupported; core does not guarantee
a diagnostic for an invalid filter.
#### `viewFilter` as a function
The `viewFilter` function takes a view from the `children` and returns a truthy
value if the child should be attached, and a falsey value if it should not.
It runs with the `CollectionView` as `this` and receives the child View, index,
and the live backing child array. A filter pass captures the array's initial
length, visits every index densely, and does not visit entries appended during
that pass.
```javascript
import BackboneApi from '@mnjs/adapters/backbone';
import Backbone from 'backbone';
import { CollectionView, setDataApi, View } from 'marionette';
setDataApi(BackboneApi);
const SomeChildView = View.extend({ template: ({ value }) => String(value) });
const SomeEmptyView = View.extend({ template: () => 'No matches' });
const cv = new CollectionView({
childView: SomeChildView,
emptyView: SomeEmptyView,
collection: new Backbone.Collection([
{ value: 1 },
{ value: 2 },
{ value: 3 },
{ value: 4 }
]),
// Only show views with even values
viewFilter(view, index, children) {
return view.model.get('value') % 2 === 0;
}
});
// renders the views with values '2' and '4'
cv.render();
```
#### `viewFilter` as a predicate object
The `viewFilter` predicate object will filter against the view's model attributes.
Each filter pass snapshots the predicate's own enumerable string keys and values
in standard JavaScript own-key order. Inherited, symbol, and non-enumerable keys
are ignored. Every predicate key must exist in the model attributes and its value
must compare strictly equal; nested objects therefore match by identity. Arrays
are not predicate objects.
```javascript
import BackboneApi from '@mnjs/adapters/backbone';
import Backbone from 'backbone';
import { CollectionView, setDataApi, View } from 'marionette';
setDataApi(BackboneApi);
const SomeChildView = View.extend({ template: ({ value }) => String(value) });
const SomeEmptyView = View.extend({ template: () => 'No matches' });
const cv = new CollectionView({
childView: SomeChildView,
emptyView: SomeEmptyView,
collection: new Backbone.Collection([
{ value: 1 },
{ value: 2 },
{ value: 3 },
{ value: 4 }
]),
// Only show views with value 2
viewFilter: { value: 2 }
});
// renders the view with values '2'
cv.render();
```
#### `viewFilter` as a string
The `viewFilter` string represents the view's model attribute and will filter
truthy values.
```javascript
import BackboneApi from '@mnjs/adapters/backbone';
import Backbone from 'backbone';
import { CollectionView, setDataApi, View } from 'marionette';
setDataApi(BackboneApi);
const SomeChildView = View.extend({ template: ({ value }) => String(value) });
const SomeEmptyView = View.extend({ template: () => 'No matches' });
const cv = new CollectionView({
childView: SomeChildView,
emptyView: SomeEmptyView,
collection: new Backbone.Collection([
{ value: 0 },
{ value: 1 },
{ value: 2 },
{ value: null },
{ value: 4 }
]),
// Only show views 1,2, and 4
viewFilter: 'value'
});
// renders the view with values '1', '2', and '4'
cv.render();
```
#### `getFilter`
Override this function to programatically decide which
`viewFilter` to use when `filter` is called.
```javascript
import BackboneApi from '@mnjs/adapters/backbone';
import { CollectionView, setDataApi } from 'marionette';
setDataApi(BackboneApi);
const MyCollectionView = CollectionView.extend({
summaryFilter(view) {
return view.model.get('type') === 'summary';
},
getFilter() {
if (this.collection.length > 100) {
return this.summaryFilter;
}
return this.viewFilter;
}
});
```
#### `setFilter`
The `setFilter` method updates `viewFilter` and calls `filter()` when the value
changes. `{ preventRender: true }` defers that filter/child-render pass. It
returns the CollectionView without running the parent render lifecycle. Call
it after initial rendering, or defer the pass until the initial `render()`.
```javascript
import BackboneApi from '@mnjs/adapters/backbone';
import Backbone from 'backbone';
import { CollectionView, setDataApi, View } from 'marionette';
setDataApi(BackboneApi);
const RowView = View.extend({ template: ({ value }) => String(value) });
const cv = new CollectionView({
collection: new Backbone.Collection([{ value: 1 }, { value: 2 }]),
childView: RowView
});
cv.render();
const newFilter = function(view, index, children) {
return view.model.get('value') % 2 === 0;
};
// Note: the setFilter is preventing the automatic re-render
cv.setFilter(newFilter, { preventRender: true });
// Apply the new filter while retaining surviving child instances.
cv.filter();
```
#### `removeFilter`
This function is actually an alias of `setFilter(null, options)`. It is useful
for removing filters. `removeFilter` also accepts `preventRender` as a option.
```javascript
import BackboneApi from '@mnjs/adapters/backbone';
import Backbone from 'backbone';
import { CollectionView, setDataApi, View } from 'marionette';
setDataApi(BackboneApi);
const RowView = View.extend({ template: ({ value }) => String(value) });
const cv = new CollectionView({
collection: new Backbone.Collection([{ value: 1 }, { value: 2 }]),
childView: RowView
});
cv.render();
cv.setFilter(function(view, index, children) {
return view.model.get('value') % 2 === 0;
});
// Remove the current filter without rendering again.
cv.removeFilter({ preventRender: true });
```
[Canonical source](/docs/markdown/docs/marionette.collectionview.md) · [Source identity](/docs/manifest.json)
---
Document: docs/marionette.application.md
Canonical URL: https://marionettejs.com/docs/application/
Markdown URL: https://marionettejs.com/docs/application.md
Reading SHA-256: 11b56bbb20e7d69fd623a56103d865d4e28299d34e0cda6cc778e8d14ffea8a8
# Marionette.Application
An `Application` gives a feature a place to start, stop, restart, and clean up.
It coordinates asynchronous work and child Applications, with an optional
Region for the feature's view tree.
`Application` includes:
- [Common Marionette Functionality](/docs/common.md)
- [Class Events](/docs/class-events.md#application-events)
- [Radio API](/docs/radio.md#marionette-integration)
- [State API](/docs/state.md#borrowed-and-owned-sources)
`Application` is an independent class. It does not inherit from `MnObject` and
does not add an element or render method.
The `Application` `cidPrefix` is `mna`.
## Documentation Index
* [Instantiating An Application](#instantiating-an-application)
* [Application Lifecycle](#application-lifecycle)
* [Application Ownership](#application-ownership)
* [Application and root View communication](#application-and-root-view-communication)
* [Application State](#application-state)
* [Application Region](#application-region)
* [Application Region Methods](#application-region-methods)
## Instantiating an Application
When instantiating an `Application` there are several properties, if passed,
that will be attached directly to the instance:
`channelName`, `radioEvents`, `radioRequests`, `region`, `regionClass`,
`stateEvents`
```javascript
import { Application } from 'marionette';
const myApplication = new Application();
```
### Initialization hooks
`preinitialize(options)` runs after `options` and `cid` are assigned, before
Marionette sets up the Region, Radio, and State. Use it to prepare instance
configuration those steps depend on. `initialize(options)` runs after that
setup, before State event subscriptions are connected. Owned State is still
created lazily when `getState()` is first called.
```javascript
const FeatureApplication = Application.extend({
preinitialize(options) {
this.channelName = options.featureName;
this.region = { el: options.element };
},
initialize() {
// The configured Region and Radio channel are now available.
}
});
```
Both hooks receive the original constructor arguments and run synchronously;
returned Promises are not awaited. Use `onBeforeStart` for asynchronous startup
readiness.
Constructor errors propagate to the caller. Marionette does not undo partially
completed initialization or automatically release resources from a constructor
that throws. See the shared [synchronous failure boundary](/docs/lifecycle.md#synchronous-failures).
Application's asynchronous lifecycle has its own cancellation and failure contract,
described below.
## Application Lifecycle
`start`, `stop`, `restart`, and `destroy` return a `Promise`. The
Promise resolves `true` when the requested target state is reached, including
an idempotent call when that state is already current. It resolves `false` when
a later incompatible operation supersedes the request. `false` is cancellation,
not failure. A current lifecycle hook failure rejects its operation Promise.
Compatible repeated calls share the in-flight Promise. Before destruction
begins, the latest incompatible operation wins: for example, `stop()` during
startup resolves the earlier `start()` as `false`, completes the stop lifecycle,
and prevents a stale `start` event. A `start()` that supersedes an in-flight
stop waits for the already-running `onBeforeStop` readiness hook before beginning startup;
it does not emit the invalidated `stop` completion. Once destruction begins it is terminal;
`start()` and `restart()` resolve `false`, while `stop()` follows the active
teardown until it has reached a stopped or destroyed state. Completion of an
invalidated asynchronous hook cannot change the Application's running or
destroyed state or emit the invalidated success event.
`isRunning()` is `true` only after startup readiness completes and while the
Application is running. It is `false` before the first start, during lifecycle
transitions, after stop, and after destroy.
### Lifecycle operations
| Current condition | Operation | Lifecycle | Result |
| --- | --- | --- | --- |
| Not running | `start(options)` | `before:start`, await readiness, `start` | `true` when running |
| Running | `start(options)` | No-op | `true` |
| Running or starting | `stop(options)` | Invalidates startup when needed, then `before:stop`, `stop` | `true` when stopped; the invalidated start resolves `false` |
| Stopped | `stop(options)` | Empty a root View shown outside startup; otherwise no-op | `true` |
| Any live, non-destroying state | `restart(options)` | Stop when needed, then start | `true` when running |
| Running or starting | `destroy(options)` | Stop when needed, then `before:destroy`, `destroy` | `true` when destroyed |
| Stopped | `destroy(options)` | `before:destroy`, `destroy` | `true` when destroyed |
| Destroying | repeated `destroy()` | Shares the active destroy lifecycle | Same in-flight Promise |
| Destroying | `start()` or `restart()` | Terminal no-op | `false` |
| Destroying | `stop()` | Follows active teardown without interrupting it | `true` once stopped or destroyed; rejects if teardown fails before stopping |
| Destroyed | `start()` or `restart()` | Terminal no-op | `false` |
| Destroyed | `stop()` or `destroy()` | Terminal no-op | `true` |
The `onBeforeStart`, `onBeforeStop`, and `onBeforeDestroy` methods may return a
Promise. Their corresponding `before:*` events still fire synchronously, but
event-listener return values are not readiness inputs. `onStart`, `onStop`,
`onDestroy`, and their matching events are completion notifications and are not
awaited. A `before:*` method must not await the same operation whose readiness it
is defining. `restart` composes the stop and start lifecycles; it does not add a
parallel restart hook path.
Each readiness hook and `before:*` event receives the Application, the
operation options, and a context object with an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal):
`(application, options, { signal })`. When a later operation invalidates
readiness, Marionette aborts its signal before starting replacement readiness.
The signal makes cancellation cooperative; the invalidated operation still
resolves `false` even when a handler ignores it. When a start, restart, or
destroy operation adopts an in-flight stop phase, it also adopts that phase's
original options and context, and does not abort its signal.
If a replacement start has already canceled the remaining child stops, that
stop phase is no longer adopted. A later `stop()`, `restart()`, or `destroy()`
begins a fresh stop phase with its own options and context.
The context belongs to the readiness phase rather than to one caller's Promise.
Completion methods and events receive only `(application, options)`.
Owned child Applications participate in the same operation. After the owner's
`before:start` readiness, children start sequentially in registration order
before the owner reaches running and emits `start`. After `before:stop`
readiness, children stop in that order before the owner reaches stopped and
emits `stop`. Restart and destroy compose those same phases.
If a direct child operation supersedes an owner-requested child start or stop,
the owner operation resolves `false`, retains its prior stable state, and does
not emit its completion event. Children that already reached the requested
state remain there. `isRunning()` describes that Application, not an aggregate
of every descendant state; callers receiving `false` can inspect child state
through the public hierarchy. Once owner destruction begins, descendant `start`
and `restart` calls resolve `false` so they cannot interrupt terminal teardown.
### Starting an Application
Once configured, await `start(options)` before dispatching work that requires a
running Application. The optional argument is passed to the lifecycle methods
and events.
The application below loads a session before showing its root View. The supplied
`loadSession({ signal })` function returns a Promise for an object with a
`name` string. It can use `fetch`, a cache, or the project's existing data layer.
```javascript
import { Application, View } from 'marionette';
const SessionView = View.extend({
template: () => '',
onRender() {
this.el.querySelector('h1').textContent = this.model.name;
}
});
export function createSessionApplication({ el, loadSession }) {
const SessionApplication = Application.extend({
async onBeforeStart(app, options, { signal }) {
const session = await loadSession({ signal });
if (signal.aborted) return;
this.session = session;
},
onStart() {
this.showView(new SessionView({ model: this.session }));
}
});
return new SessionApplication({ region: { el } });
}
```
Create and start it at the application entry point:
Serve this application and its API over HTTPS in production; relative requests
use the application origin.
```javascript
const app = createSessionApplication({
el: document.querySelector('#root-element'),
async loadSession({ signal }) {
const response = await fetch('/api/bootstrap', { signal });
if (!response.ok) throw new Error(`Session request failed: ${response.status}`);
return response.json();
}
});
const started = await app.start();
if (started) {
// Dispatch work that requires the running feature.
}
```
Check the readiness signal after asynchronous work and before mutating
application state. Marionette prevents a canceled operation from emitting its
success event, but cannot undo a stale assignment inside application code.
A current loader failure rejects `start()`; handle it at the application entry
point. Route registration and browser-history startup belong to the router's
owner, outside a feature's restartable `onStart` hook. See
[router integration](/docs/routing.md) for per-navigation loading and cancellation.
## Application Ownership
An Application may own named child Applications. Ownership is one-way: an
Application locates and controls its children, while children receive required
collaborators explicitly. Internal parent references exist only to enforce
lifecycle and unlink children safely; upward lookup is not public API.
`addChildApp(name, application)` registers an existing live,
parentless Application instance under a non-empty string name and returns that
instance. Registration does not construct or implicitly start the child. Use
`hasChildApp(name)` before constructing a dynamic child when duplicate
allocation matters. Registering the same instance again under its existing
owner and name is an idempotent no-op. A conflicting owner, name, runtime, or cyclic ownership relationship throws
[`MN0031`](/errors/MN0031.md).
Calls to `addChildApp` after the owner's destruction begins return the supplied
value without inspecting or adopting it. A child from the same runtime whose
destruction has begun is also returned without registration. Live registrations
require the owner and child to belong to the same Marionette runtime.
```javascript
const root = new Application();
if (!root.hasChildApp('search')) {
root.addChildApp('search', new SearchApplication());
}
const search = root.getChildApp('search');
search.getName(); // 'search'
root.getChildApps(); // { search }
```
`getChildApps()` returns a fresh snapshot. Changing the snapshot does
not change ownership. Child lookup methods are reads; they do not start, render,
or otherwise mutate an Application.
Owner lifecycle options are forwarded to each child. A child failure rejects
the owner operation and leaves the owner in its last committed stable state.
Children that already reached the requested state remain there; retry visits
the same registration order, where completed child operations are idempotent.
An owner transition completes only after every child remains in the requested
stable state. A direct opposing child operation cancels the owner transition,
and superseding the owner from `before:start` or `before:stop` prevents the
stale transition from changing any further children.
`removeChildApp(name, options)` destroys the named child and resolves
with it after destruction. An unknown name resolves with `undefined`. A child
also removes itself from its parent's child hierarchy when destroyed directly. A
running parent stops its children before `before:destroy`, then destroys owned
children in registration order and finally emits the parent's `destroy`
completion. A parent's `onBeforeDestroy` readiness hook can therefore inspect its
stopped, live children. A stopped parent also stops any child that was
started directly before entering destroy readiness. A concurrent direct child
destroy joins terminal teardown and may remove that child before parent
readiness. If child stop or destroy readiness fails, the parent returns to its
last committed stable state and retains that child so destruction can be retried.
The canonical child-Application pattern is explicit construction followed by
ownership registration. Registration means lifecycle ownership; it is not a
dormant service registry and it has no per-child lifecycle flags. Put a service
that must outlive an Application under a longer-lived owner and pass it to the
shorter-lived child as a dependency.
```javascript
import { Application } from 'marionette';
export const lifecycle = [];
const SearchApplication = Application.extend({
onBeforeStart(app, options) {
lifecycle.push(`search:before:start:${ options.source }`);
},
onStart(app, options) {
lifecycle.push(`search:start:${ options.source }`);
},
onBeforeStop(app, options) {
lifecycle.push(`search:before:stop:${ options.source }`);
},
onStop(app, options) {
lifecycle.push(`search:stop:${ options.source }`);
},
onDestroy() {
lifecycle.push('search:destroy');
}
});
const RootApplication = Application.extend({
onBeforeStart(app, options) {
lifecycle.push(`root:before:start:${ options.source }`);
},
onStart(app, options) {
lifecycle.push(`root:start:${ options.source }`);
},
onBeforeStop(app, options) {
lifecycle.push(`root:before:stop:${ options.source }`);
},
onStop(app, options) {
lifecycle.push(`root:stop:${ options.source }`);
},
onDestroy() {
lifecycle.push('root:destroy');
}
});
export const root = new RootApplication();
export const search = root.addChildApp('search', new SearchApplication());
export const started = await root.start({ source: 'owner' });
export const stopped = await root.stop({ source: 'owner' });
```
## Application and root View communication
Keep the ownership direction visible. The Application constructs the root View,
passes dependencies and initial values down through its options or public methods,
and listens to semantic View events for messages back up. The View should not find
its Application through DOM ancestry or private ownership fields. Use Radio only
when the sender and receiver do not share this direct ownership boundary.
```javascript
import { Application, View } from 'marionette';
export const refreshes = [];
const DashboardView = View.extend({
initialize(options) {
this.initialStatus = options.initialStatus;
},
template() {
return '';
},
events: {
'click .refresh': 'requestRefresh'
},
onRender() {
this.showStatus(this.initialStatus);
},
requestRefresh() {
this.trigger('refresh:requested', this, { source: 'button' });
},
showStatus(status) {
this.el.querySelector('.status').textContent = status;
}
});
const DashboardApplication = Application.extend({
region: '#dashboard',
onStart() {
const view = new DashboardView({ initialStatus: 'Idle' });
this.listenTo(view, 'refresh:requested', this.refreshDashboard);
this.showView(view);
},
refreshDashboard(view, request) {
refreshes.push(request);
view.showStatus('Updated');
}
});
export const dashboard = new DashboardApplication();
await dashboard.start();
export const dashboardView = dashboard.getView();
```
## Application state
An Application may compose one [state source](/docs/state.md). A supplied
`state` is borrowed; a `createState(options)` result is owned. `getState()`
returns the exact source, and `stateEvents` are installed through the selected
StateApi after `initialize`.
Application state persists across stop and restart. Destruction releases its
subscriptions, then disposes its owned state source through StateApi.
Stateless Applications allocate no source or subscription. Asynchronous startup
work must use the readiness context's abort signal before committing values so
invalidated startup cannot apply stale changes.
## Application Region
An `Application` coordinates one root View through a single
[region](/docs/region.md). The `region` property can be
[defined in multiple ways](/docs/region.md#defining-regions).
```javascript
import { Application } from 'marionette';
import RootView from './views/root';
const MyApp = Application.extend({
region: '#root-element',
onStart() {
this.showView(new RootView());
}
});
const myApp = new MyApp();
await myApp.start();
```
The `onStart` callback synchronously renders and shows `RootView`.
`before:render` and `render` run for its template; `before:attach` and `attach`
also run when the Region is attached to a document and lifecycle monitoring is
enabled. `start()` itself remains asynchronous.
`region` can also be passed as an option during instantiation.
The Application owns a Region that it constructs from a selector, Region class,
or definition object. Passing an existing Region instance instead borrows that
host. Stopping the Application empties the Region's current View, including one
shown directly through the Region. Destroying the Application also destroys a Region it
constructed, but never destroys a borrowed Region.
The Application's View is whatever its Region currently shows. Showing a View
through either `app.showView(view)` or `app.getRegion().show(view)` updates what
`app.getView()` returns. Emptying or detaching the Region leaves no current View
without stopping the Application. Restart removes the current View before
`onStart` may show a new View. If the Region has no View, stopping the Application
leaves any unmanaged HTML alone.
### `regionClass`
By default the [`Region`](/docs/region.md) is used to instantiate the `Application`'s region.
An extended Region can be provided to the `Application` definition to override the default.
```javascript
import { Application, Region } from 'marionette';
const MyRegion = Region.extend({
isSpecial: true
});
const MyApp = Application.extend({
regionClass: MyRegion
});
const myApp = new MyApp({ region: '#foo' });
myApp.getRegion().isSpecial; // true
```
`regionClass` can also be passed as an option during instantiation.
## Application Region Methods
The Marionette Application provides helper methods for managing its attached region.
### `getRegion()`
Return the current host [region object](/docs/region.md) for the
Application, or `undefined` if none was configured. This synchronous query does
not resolve its element or render a View. The host reference is released when
the Application is destroyed.
### `showView(view, options)`
Display a `View` instance in the Region attached to the Application. This runs the
[`View lifecycle`](/docs/lifecycle.md). The Application itself is never passed
to `Region#show` and does not become renderable.
This method is synchronous and returns the supplied View, forwarding `options`
to `Region#show`. Configure a Region before calling it. It does not call
`start()` or wait for Application readiness. Once destruction begins it returns
the supplied View without displaying or adopting it. A missing element allowed
by `allowMissingEl` also leaves the View caller-owned; use `getView() === view`
to check that it was shown.
### `getView()`
Return the Region's `currentView`, including a View shown directly through the
Region or before Application startup. Returns `undefined` when the Region has no
current View or the Application has no Region.
[Canonical source](/docs/markdown/docs/marionette.application.md) · [Source identity](/docs/manifest.json)
---
Document: docs/marionette.behavior.md
Canonical URL: https://marionettejs.com/docs/behavior/
Markdown URL: https://marionettejs.com/docs/behavior.md
Reading SHA-256: 7bdf6ecba5120701c4500a6d104817365230b455c183b811bbc1aae34fce7457
# Marionette.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:
- [Common Marionette Functionality](/docs/common.md)
- [Class Events](/docs/class-events.md#behavior-events)
- [DOM Interactions](/docs/dom-interactions.md)
- [Entity Events](/docs/entity-events.md)
[Attach a Behavior class to a view](#using-behaviors) through its `behaviors`
definition. The view constructs the Behavior and manages its lifetime.
## Documentation Index
* [Instantiating a Behavior](#instantiating-a-behavior)
* [Using Behaviors](#using-behaviors)
* [Defining and Attaching Behaviors](#defining-and-attaching-behaviors)
* [Behavior Options](#behavior-options)
* [Nesting Behaviors](#nesting-behaviors)
* [The Behavior's `view`](#the-behaviors-view)
* [Host Communication and Event Proxies](#host-communication-and-event-proxies)
* [Host and Behavior Events](#host-and-behavior-events)
* [Proxy Handlers](#proxy-handlers)
* [Initialize Order](#initialize-order)
* [Using `ui`](#using-ui)
* [Host DOM Boundary](#host-dom-boundary)
* [Behavior Lifecycle](#behavior-lifecycle)
* [Destroying a Behavior](#destroying-a-behavior)
## Instantiating a Behavior
Unlike other [Marionette classes](/docs/classes.md), `Behavior`s 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.
```javascript
import { View } from 'marionette';
const MyView = View.extend({
template() {
return '';
},
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
```javascript
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 [
'',
'More information'
].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:
```javascript
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:
```javascript
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`.
```javascript
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 '';
},
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`:
```javascript
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.
```javascript
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.
```javascript
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 '';
},
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](/docs/diagnostics.md#look-up-a-code), and a
string handler that does not resolve to a callable method throws
[MN0019](/docs/diagnostics.md#look-up-a-code). For example, declaring the event above without
`ui.save`, or naming `requestSave` without defining that method, is invalid.
A Behavior's DOM [`triggers`](/docs/dom-interactions.md#view-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`](/docs/events.md#triggermethod).
### Proxy Handlers
Behaviors provide proxies to a number of the view event handling attributes
including:
* [`events`](/docs/dom-interactions.md#view-events)
* [`triggers`](/docs/dom-interactions.md#view-triggers)
* [`modelEvents`](/docs/entity-events.md)
* [`collectionEvents`](/docs/entity-events.md)
```javascript
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:
1. View construction begins and the View's `preinitialize` runs
2. Behavior is constructed
3. Behavior is initialized with view property set
4. Callable Behavior `events` and `triggers` are resolved and delegated
5. View is initialized
6. View triggers an `initialize` event 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](/docs/dom-interactions.md#organizing-a-view-with-ui).
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](/docs/backbone.md):
```javascript
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`](/errors/MN0023.md).
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`](/errors/MN0023.md) 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.
```javascript
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 [
'',
''
].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](/docs/dom-api.md#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:
```javascript
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.
[Canonical source](/docs/markdown/docs/marionette.behavior.md) · [Source identity](/docs/manifest.json)
---
Document: docs/view.lifecycle.md
Canonical URL: https://marionettejs.com/docs/lifecycle/
Markdown URL: https://marionettejs.com/docs/lifecycle.md
Reading SHA-256: 978b05fb72fbffe2d124f1e257cb7d9b06cb66a28f1d339f61ef074888120080
# View Lifecycle
Both [`View` and `CollectionView`](/docs/classes.md) are aware of their lifecycle state
which indicates whether the View is rendered, attached, or destroyed.
## Documentation Index
* [View Lifecycle](#view-lifecycle)
* [Lifecycle State Methods](#lifecycle-state-methods)
* [`isRendered()`](#isrendered)
* [`isAttached()`](#isattached)
* [`isDestroyed()`](#isdestroyed)
* [Instantiating a View](#instantiating-a-view)
* [A fixed root element](#a-fixed-root-element)
* [Rendering a View](#rendering-a-view)
* [`View` Rendering](#view-rendering)
* [`CollectionView` Rendering](#collectionview-rendering)
* [Rendering Children](#rendering-children)
* [Attaching a View](#attaching-a-view)
* [Detaching a View](#detaching-a-view)
* [Destroying a View](#destroying-a-view)
* [Synchronous failures](#synchronous-failures)
* [Destroying Children](#destroying-children)
## Lifecycle State Methods
Both `View` and `CollectionView` share methods for checking lifecycle state.
### `isRendered()`
Returns a boolean value reflecting if the view is considered rendered.
### `isAttached()`
Returns a boolean value reflecting if the view is considered attached to the DOM.
### `isDestroyed()`
Returns a boolean value reflecting if the view has been destroyed.
### State vectors
The three lifecycle methods are independent observations, not one linear state enum.
`View` construction can therefore produce any of the four alive render/attachment vectors:
| Initial `el` | `isRendered()` | `isAttached()` | `isDestroyed()` |
| --- | --- | --- | --- |
| Empty and detached | `false` | `false` | `false` |
| Empty and in the document | `false` | `true` | `false` |
| Populated and detached | `true` | `false` | `false` |
| Populated and in the document | `true` | `true` | `false` |
`CollectionView` starts unrendered regardless of its initial contents and has its own
[lifecycle transition table](/docs/collection-view.md#view-lifecycle-and-events).
With lifecycle monitoring enabled, Marionette-managed operations preserve the
following observable transitions:
| Operation | Result | Repeated call |
| --- | --- | --- |
| `View#render()` with a template function while alive | Runs `before:render` and `render`; rendered becomes `true`; attachment is unchanged | Renders again and runs the render lifecycle again |
| `View#render()` with `template: false` while alive | Returns the View without running the render lifecycle or changing contents or state | Repeated calls are the same no-op |
| `CollectionView#render()` while alive | Runs `before:render` and `render`, rebuilds its children, and becomes rendered; attachment is unchanged | Rebuilds the children and runs the render lifecycle again |
| `view.renderAttributes()` while alive | Applies the current root attribute declarations without changing contents, children, lifecycle events, or state | Reevaluates and applies the declarations again |
| `region.show(view)` | Ensures the view is rendered; attached becomes `true` only when the Region is in the document | Showing the current view is a no-op |
| `region.detachView()` | Rendered is preserved; attached becomes `false`; destroyed stays `false` | Returns `undefined` with no transition |
| Re-show a detached view | Rendered stays `true`; attachment reflects the Region | Does not render the view again |
| `region.empty()` or `view.destroy()` | Rendered and attached become `false`; destroyed becomes `true` | Repeated destroy is a no-op |
| `view.render()` after destruction | Returns the same View with rendered and attached `false` and destroyed `true` | Repeated calls are no-ops |
| `view.renderAttributes()` once destruction begins | Returns the same View before resolving declarations or changing the root element or lifecycle state | Repeated calls are no-ops |
| `CollectionView#addChildView(view, ...)` once destruction begins | Returns the supplied child before inspecting or taking ownership of it; the caller remains responsible for that child | Repeated calls are no-ops for the destroyed CollectionView |
| `view.delegateEvents()` or `view.undelegateEvents()` once destruction begins | Returns the same View without changing View or Behavior DOM delegation | Repeated calls are no-ops |
| `view.bindUIElements()` once destruction begins | Returns the same View without resolving host UI, querying DOM, or binding View or Behavior UI | Repeated calls are no-ops |
Setting `monitorViewEvents: false` on a Region's owning view intentionally disables
attachment events and automatic `isAttached()` updates for the shown view.
This table specifies the managed and terminal operations listed above. Do not
infer behavior for other calls on a destroyed View; custom overrides also own
their behavior unless they delegate to a guarded base method.
## Instantiating a View
Every Marionette `View` and `CollectionView` has a native DOM element in `el`.
Pass an existing element with `el: document.querySelector('.foo-selector')`, or
create one first with `document.createElement()`. Selector strings and jQuery
collections are not valid View `el` values.
When `el` is omitted, Marionette creates the root element from `tagName` (a
`div` by default) and applies the resolved `id`, `className`, and `attributes`.
The element remains the View's root for its entire lifetime. Native core does not create `$el`;
applications can initialize their own wrapper when using the
[jQuery adapter](/docs/dom-api.md#optional-jquery-adapter).
Marionette determines whether the initial root is already
[rendered](#rendering-a-view) or [attached](#attaching-a-view). If a View starts
rendered or attached, its [state](#lifecycle-state-methods) reflects that status, but the
[related events](/docs/class-events.md#dom-change-events) will not have fired.
An element owned by template content is detached while that owner document has no
document element. Showing its View later through an attached Region runs the managed
attachment lifecycle once for the View and its existing children.
For more information on instantiating a view with pre-rendered DOM, see
[Pre-rendered Content](/docs/prerendered-dom.md).
### A fixed root element
Choose the root with the constructor's `el` option, or let Marionette create it.
A View and its Behaviors keep that element for their lifetime. `el` is readonly
in the public instance types; assigning another element directly is unsupported.
There is no public `setElement()` method.
Rendering changes the root's contents. Moving or detaching a View through a
Region preserves its root and its child ownership. If another system replaces
the root, destroy the old View and construct a new View with the new element.
Keep state that must survive that replacement outside the View.
## Rendering a View
In Marionette [rendering a view](/docs/rendering.md) is changing a view's `el`'s contents.
What rendering indicates varies slightly between the two Marionette views.
**Note** A completed render leaves the View rendered until destruction. During
a normalized collection update, CollectionView may mark an updated child
unrendered before rendering it again; a filtered child can remain unrendered
until it becomes visible.
### `View` Rendering
For [`View`](/docs/view.md), rendering with a template function runs the
`before:render` lifecycle, serializes the View's data, passes it to the template,
places the result in `el`, binds UI, marks the View rendered, and then runs the
`render` lifecycle. A newly constructed `View` is already considered rendered if
its initial `el` contains content. A later template may produce empty content;
the completed render still leaves the View rendered.
`template: false` is different from a template that returns an empty value.
Calling `View#render()` with `template: false` returns the View without running
the render lifecycle, changing the DOM, or changing its rendered state.
### `CollectionView` Rendering
For [`CollectionView`](/docs/collection-view.md), every live `render()` is
bracketed by `before:render` and `render`. After it completes, collection-backed
children have been rebuilt, the optional template and visible children have
been rendered, and the CollectionView is rendered. Any children the
CollectionView owned before that render have been destroyed.
Inserting a child element into the CollectionView is not itself an attachment
transition. When the CollectionView is monitored as attached, rendering marks
and notifies the inserted children as attached; when the parent is detached or
child lifecycle monitoring is disabled, their monitored attachment state remains
detached even though their elements are inside the parent element.
A CollectionView with no children is still rendered, with or without an
[`emptyView`](/docs/collection-view.md#collectionviews-emptyview). Its own
template controls the container markup but does not determine rendered state.
## Rendering Children
Rendering child views is often best accomplished after the View renders, as the first render typically happens before
the View enters the DOM. This helps to prevent unnecessary repaints and reflows by making the DOM insertion at the
highest practical View in the view tree.
The exception is Views with [pre-rendered content](/docs/prerendered-dom.md). When a View is instantiated
rendered, child Views are best managed in the View's [`initialize`](/docs/common.md#initialize).
### `View` Children
In general the best method for adding a child view to a `View` is to use [`showChildView`](/docs/view.md#showing-a-child-view)
in the [`render` event](/docs/class-events.md#render-and-beforerender-events).
View Regions are emptied on each render, so Views shown outside of the `render` event still need to be shown again
on subsequent renders.
### `CollectionView` Children
The primary use case for a `CollectionView` is maintaining collection-backed
child Views. Marionette creates and removes those children as the collection
changes.
`addChildView()` can also add a child that is independent of the collection,
but that child is not unmanaged. The CollectionView owns it, includes it in its
child containers, and may sort or filter it. Rendering, collection reset, or
CollectionView destruction destroys every child that is still owned, including
manually added children. `detachChildView()` is the explicit operation that
removes a child from ownership without destroying it and transfers cleanup
responsibility to the caller.
See [Self-Managed `children`](/docs/collection-view.md#self-managed-children)
for the supported add, remove, detach, sorting, and filtering contracts.
## Attaching a View
`isAttached()` is Marionette's monitored lifecycle state, not a live query of
the physical DOM on every call. Construction initializes it
from the current root element, and Marionette-managed Region and CollectionView
operations update it while attachment monitoring is enabled.
The [`attach` event](/docs/class-events.md#attach-and-beforeattach-events) is the
appropriate place to add listeners to the root `el`. Render can replace the
contents while that root remains attached; use
[`dom:refresh`](/docs/class-events.md#domrefresh-event) for listeners tied to those
rendered descendants.
Moving `view.el` directly with native DOM APIs, such as
`document.body.append(view.el)`, changes its physical location without running
Marionette attachment lifecycles or updating `isAttached()`. The same caveat
applies when application code directly removes or moves an attached root.
Prefer a Region or CollectionView for managed transitions; if application code
moves the element directly, it owns the resulting lifecycle mismatch.
A child shown in a rendered but detached parent View's Region is rendered and remains
detached. When the parent is later shown in an attached Region, attachment propagates
to its existing children. A child shown during the parent's `onAttach` is attached
immediately. Showing the same attached parent again is a no-op for both parent and child
attachment lifecycles.
## Detaching a View
A managed View becomes detached when Marionette removes its `el` from the DOM
and updates its monitored attachment state.
Use the [`before:detach` event](/docs/class-events.md#detach-and-beforedetach-events)
to clean up listeners added to the root `el`. Render can replace descendants
while the root remains attached; use
[`dom:remove`](/docs/class-events.md#domremove-event) to clean up listeners tied to
those rendered descendants.
Detaching a parent View propagates detachment to its managed Region children while
preserving their rendered state and ownership. Re-showing that parent attaches the same
children again. Emptying the parent-owning Region then detaches and destroys the parent
and its still-managed children once.
## Destroying a View
Destroying a View (for example, `myView.destroy()`) removes Marionette-owned
resources: delegated View and Behavior DOM handlers, bound UI, outgoing
`listenTo()` subscriptions, entity-event bookkeeping, Behaviors, Regions and
their current Views, and CollectionView children that remain owned. It detaches
the root element and leaves the View rendered `false`, attached `false`, and
destroyed `true` after successful teardown.
Destroy does not remove callbacks registered directly on the View with `on()`,
destroy its model, collection, or arbitrary option collaborators, or clean up
application resources Marionette does not own. Release those resources in the
appropriate lifecycle callback.
The [`before:destroy` event](/docs/class-events.md#destroy-and-beforedestroy-events) is the best place to clean
up any added listeners not related to the view's DOM.
Once destruction begins, reentrant `destroy()` calls from `before:destroy` or
`destroy`, and later repeated calls, return the same View without restarting
teardown. During a normal successful teardown, an attached parent and its owned
children complete their detach and destroy lifecycles once.
Base `View#bindUIElements()` and `CollectionView#bindUIElements()` calls are
also terminal no-ops once destruction begins. They do not resolve callable UI,
query the retained root element, or bind attached Behaviors. A direct
`Behavior#bindUIElements()` call through a Behavior owned by or retained from
that host returns the Behavior without binding. `unbindUIElements()` remains
available for cleanup, and `getUI()` continues to throw `MN0023` when UI is
unbound.
Errors from lifecycle handlers propagate and stop the operation, as described
under [Synchronous failures](#synchronous-failures). A throwing `before:destroy`
or later cleanup handler does not clear the destruction guard or make a later
`destroy()` call resume teardown.
Successful destruction retains the root `el` object but detaches it. Do not
infer that all of its contents are retained: owned child Views are removed as
they are destroyed, and Region or CollectionView cleanup can detach contents
from managed containers. Marionette makes no general cleanup promise for
unowned DOM outside those managed boundaries.
## Synchronous failures
Marionette expects valid adapters and working registration and cleanup callbacks.
An exception during synchronous registration, construction, rendering, or teardown
propagates to the caller and aborts that operation. Completed work is not rolled
back. Marionette does not promise to release every resource after a callback throws,
restore a partially initialized or rendered instance, or recover on the next call or
source notification. Fix the failing callback or adapter; do not rely on partial
instance state after a failure.
Successful cleanup and the documented ownership and repeated-destruction rules still
apply. A callback that destroys or mutates an owner during an in-progress render does
not acquire additional recovery guarantees merely because it calls a public method;
use the documented lifecycle boundaries for that workflow.
Application's [asynchronous lifecycle](/docs/application.md#application-lifecycle)
has its own readiness, cancellation, rejection, and restart semantics. This synchronous
failure boundary does not replace those contracts or change ordinary supersession
into an error.
## Destroying Children
Children still owned by a View's Region or a CollectionView are automatically
destroyed when their owner completes a re-render or is destroyed. A CollectionView also
destroys its currently owned children when its collection is reset before
building the replacement collection-backed children. A child returned by
`detachView()` or `detachChildView()` is no longer owned and is not included in
later owner cleanup.
During owner destruction, children are removed after the parent root is detached
to avoid repeated reflows or repaints.
[Canonical source](/docs/markdown/docs/view.lifecycle.md) · [Source identity](/docs/manifest.json)
---
Document: docs/view.rendering.md
Canonical URL: https://marionettejs.com/docs/rendering/
Markdown URL: https://marionettejs.com/docs/rendering.md
Reading SHA-256: 8214bfbec96f34c818764430b042fe18b18094ed6ba0d4e7c488de020b6a3877
# View Template Rendering
Give a view a template function, then call `render()` to put its result in the
view's element. A plain function is enough to get started; template engines
and custom renderers can fit the same workflow.
The renderer evaluates the template; DomApi applies the result to the element.
Projects can configure template evaluation with `setRenderer()` directly. Lit
and Morphdom are DOM adapters configured with `setDomApi()`.
```javascript
import { View } from 'marionette';
const MyView = View.extend({
tagName: 'h1',
template: () => 'Contents'
});
const myView = new MyView();
myView.render();
```
This renders `
Contents
`, available at `myView.el`.
## Documentation Index
* [What is a template](#what-is-a-template)
* [Setting a View Template](#setting-a-view-template)
* [Using a View Without a Template](#using-a-view-without-a-template)
* [Rendering the Template](#rendering-the-template)
* [Using a Custom Renderer](#using-a-custom-renderer)
* [Rendering to HTML](#rendering-to-html)
* [Rendering to DOM](#rendering-to-dom)
* [Serializing Data](#serializing-data)
* [Serializing a Model](#serializing-a-model)
* [Serializing a Collection](#serializing-a-collection)
* [Serializing with a `CollectionView`](#serializing-with-a-collectionview)
* [Adding Context Data](#adding-context-data)
* [What is Context Data?](#what-is-context-data)
## What is a template?
A template is a function that given data returns either an HTML string or DOM.
[The default renderer](#rendering-the-template) in Marionette expects the template to
return an HTML string. If your application uses Underscore, its
[template compiler](http://underscorejs.org/#template) can create that function.
Install Underscore as an application dependency to use the following example;
Marionette does not include it.
```javascript
import _ from 'underscore';
import { View } from 'marionette';
const MyView = View.extend({
template: _.template('
Hello, world
')
});
```
This doesn't have to be an underscore template, you can pass your own rendering
function:
```javascript
import Handlebars from 'handlebars';
import { View } from 'marionette';
const MyView = View.extend({
template: Handlebars.compile('
Hello, {{ name }}
')
});
```
## Setting a View Template
Marionette views use the `getTemplate` method to determine which template to use for
rendering into its `el`. By default `getTemplate` is predefined on the view as simply:
```javascript
getTemplate() {
return this.template
}
```
In most cases by using the default `getTemplate` you can simply set the `template` on the
view to define the view's template, but in some circumstances you may want to set the template
conditionally.
```javascript
import _ from 'underscore';
import { View } from 'marionette';
const MyView = View.extend({
template: _.template('Hello World!'),
getTemplate() {
if (this.Data.has(this.model, 'user')) {
return _.template('Hello User!');
}
return this.template;
}
});
```
### Using a View Without a Template
By default `CollectionView` has no defined `template` and will only attempt to render the `template`
if one is defined. For `View` there may be some situations where you do not intend to use a `template`.
Perhaps you only need the view's `el` or you are using [prerendered content](/docs/prerendered-dom.md).
In this case setting `template` to `false` will prevent the template render. In the case of `View`
it will also prevent the [`render` events](/docs/class-events.md#render-and-beforerender-events).
```javascript
import { View } from 'marionette';
const MyIconButtonView = View.extend({
template: false,
tagName: 'button',
className: 'icon-button',
triggers: {
'click': 'click'
},
onRender() {
console.log('You will never see me!');
}
});
```
## Rendering the Template
Each view class has a renderer which by default passes the [view data](#serializing-data)
to the template function and returns the html string it generates.
The current default renderer is essentially the following:
```javascript
import { View, CollectionView } from 'marionette';
function renderer(template, data) {
return template(data);
}
View.setRenderer(renderer);
CollectionView.setRenderer(renderer);
```
The default expects a function template; it does not look up script elements
by selector.
### Using a Custom Renderer
You can set the renderer for a view class by using the class method `setRenderer`.
The renderer accepts two arguments. The first is the template passed to the view,
and the second argument is the data to be rendered into the template. Marionette
invokes the renderer with the View as `this`, so use a regular function when the
renderer needs access to the View instance.
Rendering is synchronous. A renderer must return content supported by the
chosen DomApi immediately; returning a Promise does not make `render()` await
it. Complete asynchronous loading before rendering, or update the View when the
result becomes available under its owner's cancellation rules.
Marionette passes the renderer's return value to
[`attachElContent`](#customizing-attachelcontent), which calls `Dom.setContents`.
The renderer evaluates the template; the DOM adapter applies its result. The
native, jQuery, and Morphdom adapters treat `null` and `undefined` as empty
contents. Lit accepts these values as empty content too. Returning `undefined`
does not bypass content attachment.
Here's an example that allows for the `template` of a view to be an underscore template string.
```javascript
import BackboneApi from '@mnjs/adapters/backbone';
import Backbone from 'backbone';
import _ from 'underscore';
import { setDataApi, View } from 'marionette';
setDataApi(BackboneApi);
View.setRenderer(function(template, data) {
return _.template(template)(data);
});
const myView = new View({
template: 'Hello <%- name %>!',
model: new Backbone.Model({ name: 'World' })
});
myView.render();
// myView.el is
Hello World!
```
The renderer can also be customized separately on any extended View. This
standalone example uses the default plain-object DataApi and requires the
application to install Handlebars.
```javascript
import Handlebars from 'handlebars';
import { View } from 'marionette';
const MyHBSView = View.extend();
// Similar example as above but for handlebars
MyHBSView.setRenderer(function(template, data) {
return Handlebars.compile(template)(data);
});
const myHBSView = new MyHBSView({
template: 'Hello {{ name }}!',
model: { name: 'World' }
});
myHBSView.render();
// myHBSView.el is
Hello World!
```
**Note** These examples while functional may not be ideal. If possible it is recommended to
precompile your templates which can be done for a number of templating engines using various plugins
for bundling tools such as [Browserify or Webpack](/docs/installation.md).
### Rendering to HTML
The default Marionette renderer returns the HTML as a string. This string is passed to the view's
`attachElContent` method which in turn uses the DOM API's [`setContents`](/docs/dom-api.md#setcontentsel-html)
to set the contents of the view's `el` with DOM from the string.
#### Customizing `attachElContent`
You can modify the way any particular view attaches a compiled template to the `el` by overriding `attachElContent`.
This method always receives the result of the view's renderer, including `undefined`.
For instance, perhaps for one particular view you need to bypass the [DOM API](/docs/dom-api.md) and set the html directly:
```javascript
attachElContent(html) {
this.Dom.setContents(this.el, html);
}
```
### Rendering to DOM
A DOM adapter can update existing content incrementally. The optional
`@mnjs/adapters` package includes Morphdom and Lit HTML integrations.
Install only the DOM adapter peer your application uses and configure a View subclass
before creating its instances. `setDomApi` overlays the supplied methods and
preserves unrelated operations, including jQuery queries.
For HTML string templates:
```javascript
import { View } from 'marionette';
import MorphdomDomApi from '@mnjs/adapters/dom/morphdom';
const MessageView = View.extend({
template: () => '
Hello again.
'
});
MessageView.setDomApi(MorphdomDomApi);
```
Morphdom updates the View's contents using its normal matching rules, including
element IDs. Empty roots take the direct HTML insertion path. For Lit templates,
select the Lit DOM adapter:
```javascript
import { View } from 'marionette';
import { html } from 'lit-html';
import LitDomApi from '@mnjs/adapters/dom/lit-html';
const MessageView = View.extend({
template: ({ message }) => html`
${message}
`,
templateContext: { message: 'Hello again.' }
});
MessageView.setDomApi(LitDomApi);
```
Both adapters apply template output through `Dom.setContents`. The root remains
owned by the View; refresh its dynamic `className`, `id`, or `attributes` with
[`renderAttributes()`](/docs/view.md#refreshing-root-attributes).
A parent render still destroys its Region children. Keep Region placeholders
empty so the renderer and Region do not manage the same contents.
Lit replaces preexisting contents on its first explicit render. Keep
`monitorViewEvents` enabled and manage attachment through Regions so directives
receive connection changes through `Dom.notifyAttach(el)` and `Dom.notifyDetach(el)`.
The View keeps the same root throughout its lifetime. Automatic directive
connection management requires monitoring on the View and its ancestors. Lifecycle overrides must call their parent methods;
avoid independently replacing Lit's contents or switching DOM adapters after rendering.
See the [render adapter guide](/docs/adapters-package.md#dom-contents)
for installation, directive cleanup, and root ownership.
Rendering configuration is separate from data and state integration. Configure
[`DataApi`](/docs/data-api.md) and [`StateApi`](/docs/state.md) explicitly when
your sources need them.
## Serializing Data
Marionette will automatically serialize the data from its `model` or `collection` through the configured
[`DataApi`](/docs/data-api.md) for the template to use
at [rendering](#rendering-the-template). You can override this logic and provide serialization of other
data with the `serializeData` method. The method is called with no arguments, but has the context of the
view and should return a javascript object for the template to consume. If `serializeData` does not return
data the template may still receive [added context](#adding-context-data) or an empty object for rendering.
```javascript
import _ from 'underscore';
import { View } from 'marionette';
const MyView = View.extend({
template: _.template(`
<%- user.name %>
<% _.each(groups, function(group) { %>
<%- group.name %>
<% }) %>
`),
serializeData() {
// For this view I need both the
// model and collection serialized
return {
user: this.serializeModel(),
groups: this.serializeCollection(),
};
}
});
```
**Note** You should not use this method to add arbitrary extra data to your template.
Instead use `templateContext` to [add context data to your template](#adding-context-data).
### Serializing a Model
If the view has a `model`, it passes `DataApi.serialize(model)` to the template.
The default adapter returns the original plain object.
```javascript
import _ from 'underscore';
import { View } from 'marionette';
const MyView = View.extend({
template: _.template('
Hello, <%- name %>
')
});
const myView = new MyView({ model: { name: 'world' } });
```
How the `model` is serialized can also be customized per view.
```javascript
import BackboneApi from '@mnjs/adapters/backbone';
import _ from 'underscore';
import { setDataApi, View } from 'marionette';
setDataApi(BackboneApi);
const MyView = View.extend({
serializeModel() {
const data = _.clone(this.Data.serialize(this.model));
// serialize a nested Backbone model through the configured adapter
data.subModel = this.Data.serialize(data.subModel);
return data;
}
});
```
### Serializing a Collection
If the view does not have a `model` but has a `collection`, DataApi supplies its
ordered models and serializes each one into an array provided as a `models`
attribute to the template. These are the results of calling `DataApi.serialize()`
for each model, not the raw model instances returned by `DataApi.models()`.
```javascript
import _ from 'underscore';
import { View } from 'marionette';
const MyView = View.extend({
template: _.template(`
<% _.each(models, function(data) { %>
<%- data.name %>
<% }) %>
`)
});
const collection = [
{name: 'Steve'}, {name: 'Helen'}
];
const myView = new MyView({ collection });
```
How the `collection` is serialized can also be customized per view.
```javascript
import BackboneApi from '@mnjs/adapters/backbone';
import _ from 'underscore';
import { setDataApi, View } from 'marionette';
setDataApi(BackboneApi);
const MyView = View.extend({
serializeCollection() {
return _.map(this.Data.models(this.collection), model => {
const data = _.clone(this.Data.serialize(model));
// serialize a nested Backbone model through the configured adapter
data.subModel = this.Data.serialize(data.subModel);
return data;
});
}
});
```
### Serializing with a `CollectionView`
If you are using a `template` with a `CollectionView` that is not also given a `model`, your `CollectionView`
will [serialize the collection](#serializing-a-collection) for the template. This could be costly and unnecessary.
If your `CollectionView` has a `template` it is advised to either use an empty `model` or override the
[`serializeData`](#serializing-data) method.
## Adding Context Data
Marionette views provide a `templateContext` attribute that is used to add
extra information to your templates. This can be either an object, or a function
returning an object. The keys on the returned object will be mixed into the
model or collection keys and made available to the template.
When serialized data and template context are combined, each contributes its
own enumerable properties, including symbols, through object spread. Inherited
and non-enumerable properties are ignored. If only one object exists, Marionette passes that
original object through unchanged.
```javascript
import _ from 'underscore';
import { View } from 'marionette';
const MyView = View.extend({
template: _.template('
Hello, <%- name %>
'),
templateContext: {
name: 'World'
}
});
```
Additionally context data overwrites the serialized data
```javascript
import BackboneApi from '@mnjs/adapters/backbone';
import _ from 'underscore';
import { setDataApi, View } from 'marionette';
setDataApi(BackboneApi);
const MyView = View.extend({
template: _.template('
Hello, <%- name %>
'),
templateContext() {
return {
name: this.Data.get(this.model, 'name').toUpperCase()
};
}
});
```
You can also define a template context value as a method. How this method is called is determined
by your templating solution. For instance with handlebars a method is called with the context of
the data passed to the template.
```javascript
import BackboneApi from '@mnjs/adapters/backbone';
import Handlebars from 'handlebars';
import Backbone from 'backbone';
import { setDataApi, View } from 'marionette';
setDataApi(BackboneApi);
const MyView = View.extend({
template: Handlebars.compile(`
Hello {{ fullName }}
,
`),
templateContext: {
isDr() {
return (this.degree) === 'phd';
},
fullName() {
// Because of Handlebars `this` here is the data object
// passed to the template which is the result of the
// templateContext mixed with the serialized data of the view
return this.isDr() ? `Dr. ${this.name}` : this.name;
}
}
});
const myView = new MyView({
model: new Backbone.Model({ degree: 'masters', name: 'Joe' })
});
```
**Note** the data object passed to the template is not deeply cloned and in some cases is not cloned at all.
Take caution when modifying the data passed to the template, that you are not also modifying your model's
data indirectly.
### What is Context Data?
While [serializing data](#serializing-data) deals more with getting the data belonging to the view
into the template, template context mixes in other needed data, or in some cases, might do extra
computations that go beyond simply "serializing" the view's `model` or `collection`.
This fragment assumes an application-specific Backbone model with
`getOrganization()` and `getFullName()` methods, and a Backbone collection of
groups; these helpers are not Marionette APIs.
```javascript
import BackboneApi from '@mnjs/adapters/backbone';
import _ from 'underscore';
import { CollectionView, setDataApi } from 'marionette';
import GroupView from './group-view';
setDataApi(BackboneApi);
const MyCollectionView = CollectionView.extend({
tagName: 'div',
childViewContainer: 'ul',
childView: GroupView,
template: _.template(`
Hello <%- name %> of <%- orgName %>
You have <%- stats.public ?? 0 %> group(s).
You have <%- stats.private ?? 0 %> group(s).
Groups:
`),
templateContext() {
const user = this.model;
const organization = user.getOrganization();
const groups = this.collection;
return {
orgName: organization.get('name'),
name: user.getFullName(),
stats: groups.countBy('type')
};
}
})
```
[Canonical source](/docs/markdown/docs/view.rendering.md) · [Source identity](/docs/manifest.json)
---
Document: docs/dom.interactions.md
Canonical URL: https://marionettejs.com/docs/dom-interactions/
Markdown URL: https://marionettejs.com/docs/dom-interactions.md
Reading SHA-256: 88731d52fd2f5ab76a53e14ee260c90a6a7a46b52d1d37d3297fddde44062095
# DOM 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 generated `el` to the shell's mount.
* A View owns its `el` and 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](/docs/behavior.md#host-communication-and-event-proxies).
* 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](/docs/view.md#laying-out-views---regions)
to show, access, detach, or empty that child.
* A child View owns its own `el` and 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](/docs/events.md#child-view-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`.
```javascript
import { View } from 'marionette';
export const FormView = View.extend({
template() {
return `
`;
},
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:
```javascript
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:
```javascript
' [CSS selector]': 'methodName'
```
The CSS selector is optional. Without one, the handler is bound to the View's
root `el`. Use `@ui.` 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`](/errors/MN0019.md) 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](/docs/behavior.md#ui-resolution-and-binding). 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:
```javascript
import { setEventDelegator } from 'marionette';
setEventDelegator(MyEventDelegator);
```
Or configure one class hierarchy through its static setter:
```javascript
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:
```javascript
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 current `el`. A Behavior receives its
host View's current `el`.
`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](/docs/lifecycle.md#synchronous-failures).
`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:
```javascript
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, ...)`](/docs/events.md#listening-to-events),
but that is an explicit subscription rather than automatic bubbling. See
[Child View Events](/docs/events.md#child-view-events) for the configured contracts.
## Organizing a View with `ui`
The `ui` attribute gives frequently used CSS selectors stable names:
```javascript
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.` 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`](/errors/MN0023.md). Once bound, a missing
key preserves the existing `undefined` result. Use the `@ui.`
form in `events`, `triggers`, Behaviors, and Regions so a selector change has one
source of truth.
Every `@ui.` 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`](/errors/MN0018.md) 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`](/docs/installation.md#jquery-dom-adapter-is-optional)
before constructing Views receive jQuery collections from query methods. The
[application-owned `$el` setup](/docs/dom-api.md#optional-jquery-adapter) 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.
[Canonical source](/docs/markdown/docs/dom.interactions.md) · [Source identity](/docs/manifest.json)
---
Document: docs/routing.md
Canonical URL: https://marionettejs.com/docs/routing/
Markdown URL: https://marionettejs.com/docs/routing.md
Reading SHA-256: c174373b58f8d7a8125dd471219d93e4ee4454969cfb385fff40f61bbad4953b
# Connect routing to a feature
Keep the project's existing router. A route handler can call an application
function that loads data and shows a View. Marionette does not export a router
or require a routing adapter.
## Choose the boundary
| Responsibility | Owner |
| --- | --- |
| Match URLs, parse parameters, update browser history | Your router |
| Validate route input, load data, handle errors, cancel superseded navigation | Application code |
| Display and replace the feature's View tree | A Marionette Region |
| Start, stop, and destroy the feature | A Marionette Application |
Use a Region directly when navigation only replaces Views. Add an Application
when the feature also needs a start/stop boundary or owns other Applications.
A route change does not inherently require a new Application instance.
If the project has no router, first determine whether it needs URLs at all.
Local selection can be ordinary application state. For URL navigation, choose a
router against the required URL, history, and deployment behavior. That decision
is independent of the [data, state, and DOM integrations](/docs/choosing-integrations.md).
## Load the latest page and discard stale work
This example keeps one Application alive while routes replace its root View.
It retains the previous page during loading and on a current request failure.
A later navigation aborts the previous request. Stopping or destroying the
Application also aborts pending work and removes its displayed View.
Save this module as `page-navigation.js`. `loadPage(id, { signal })` is an
application dependency: it returns a Promise for an object with `title` and
`body` strings. The element must already exist. No data adapter is needed for
these plain objects.
```javascript
import { Application, View } from 'marionette';
const PageView = View.extend({
template: () => '',
onRender() {
this.el.querySelector('h1').textContent = this.model.title;
this.el.querySelector('p').textContent = this.model.body;
}
});
export async function createPageNavigation({ el, loadPage }) {
let pending;
function cancelPending() {
pending?.abort();
pending = undefined;
}
const Pages = Application.extend({
onBeforeStop: cancelPending,
onBeforeDestroy: cancelPending
});
const application = new Pages({ region: { el } });
await application.start();
async function navigate(id) {
if (!application.isRunning()) return false;
cancelPending();
const request = new AbortController();
pending = request;
try {
const page = await loadPage(id, { signal: request.signal });
if (request.signal.aborted || !application.isRunning()) return false;
application.showView(new PageView({ model: page }));
return true;
} catch (error) {
if (request.signal.aborted || !application.isRunning()) return false;
throw error;
} finally {
if (pending === request) pending = undefined;
}
}
return { application, navigate };
}
```
`navigate()` resolves `true` after displaying the requested page and `false`
when navigation was canceled or the Application was not running. A current
load or render failure rejects. Catch that rejection at the route boundary and
show an error appropriate to the application. Render failures do not promise
that the previous View survives; Region replacement is not transactional.
The check after `await` is required even when the loader accepts an
`AbortSignal`: a cache or another provider may finish work after cancellation.
It also prevents a stale rejection from becoming the current page's error.
The identity check in `finally` keeps an older request from clearing the newer
request's cancellation handle.
This controller owns cancellation for page requests. It does not make every
View lifecycle asynchronous. Use Application readiness hooks for work that
must finish before the *feature* can start; see
[Application lifecycle](/docs/application.md#application-lifecycle).
Repeated in-flight `start()` or `restart()` calls share their operation Promise,
so changing their options is not a substitute for navigation cancellation.
## Connect an existing router
Create the feature once, then call `navigate(id)` from the router's existing
matched-route handler. For an application already using `Backbone.Router`,
that can look like this:
Serve this application and its API over HTTPS in production; relative requests
use the application origin.
```javascript
import Backbone from 'backbone';
import { createPageNavigation } from './page-navigation.js';
const status = document.querySelector('#route-status');
const { application, navigate } = await createPageNavigation({
el: document.querySelector('#page'),
async loadPage(id, { signal }) {
const response = await fetch(`/api/pages/${encodeURIComponent(id)}`, { signal });
if (!response.ok) throw new Error(`Page request failed: ${response.status}`);
return response.json();
}
});
const Router = Backbone.Router.extend({
routes: { 'pages/:id': 'page' },
page(id) {
status.textContent = '';
void navigate(id).catch(() => {
status.textContent = 'Could not load this page. Try again.';
});
}
});
const router = new Router();
Backbone.history.start();
// When the owning application leaves this feature:
// await application.stop();
// When that owner permanently releases it:
// await application.destroy();
```
The page supplies `` and
``. The server supplies the page endpoint.
Register this route within the project's existing router when one is already
present; start browser history once at the application entry point. Route
registration and history teardown remain the router owner's responsibility.
Stop the feature on routes that leave it, and restart it with `start()` before
sending it more navigation requests.
Using Backbone for routing alone does not require `BackboneApi`, `setDataApi`,
or `setStateApi`. Configure those only when Marionette owners consume Backbone
data or state. Backbone's URL matching and history behavior remain
[Backbone contracts](https://backbonejs.org/#Router).
## Verify the integration
Check the behavior at the route boundary:
- Navigate from a slow request to a fast one. The fast page must remain visible
when the slow request later resolves or rejects.
- Navigate away or destroy the feature during loading. No late View may appear.
- Fail the current load. Surface the error and allow a later navigation to succeed.
- Replace a displayed page. Its old View must be destroyed through its Region.
- Follow a direct URL and use browser back/forward. Those checks exercise the
router and hosting configuration, beyond the Marionette example.
The executable example fixture tests replacement, cancellation, load failure,
stop/restart, and destruction using deferred loaders, including loaders that
ignore abort. It does not test a particular router or server deployment.
[Canonical source](/docs/markdown/docs/routing.md) · [Source identity](/docs/manifest.json)
---
Document: docs/task-recipes.md
Canonical URL: https://marionettejs.com/docs/task-recipes/
Markdown URL: https://marionettejs.com/docs/task-recipes.md
Reading SHA-256: 89bc04b44c3d8247b325c143fe00f9ddb8d55a4738327a70a8ca7a6a3bb920f1
# Task recipes
Start with the resource that must survive or be cleaned up. These recipes use
Marionette ownership to keep application behavior predictable. Preserve an
existing compatible integration; each task identifies when another one is needed.
| Task | Start here | Owner and decision |
| --- | --- | --- |
| Save a draft without losing focus | [Forms](/docs/forms-and-accessibility.md) | The form View owns input DOM and its pending save; update status without rerendering. |
| Change pages while requests overlap | [Routing](/docs/routing.md) | The application owns URL handling and cancellation; the Region owns the active page. |
| Refresh a root class or ARIA state | [Root attributes](/docs/view.md#refreshing-root-attributes) | Call `renderAttributes()` when only declared root attributes changed. |
| Keep surviving list rows editable | [Collection reconciliation](/docs/collection-view.md#managing-children) | Keep the observable collection and surviving source objects; do not rebuild the entire CollectionView on every change. |
| Reuse server-provided markup | [Prerendered content](/docs/prerendered-dom.md) | Give an existing element to its View; establish child ownership explicitly. |
| Observe a shared model | [DataApi](/docs/data-api.md) | Use the existing provider, or native observable data for a new application; plain objects do not emit changes. |
| React to local owner state | [State](/docs/state.md) | Choose StateApi separately from DataApi; use owner cleanup for subscriptions. |
| Wrap a widget that owns DOM | [The example below](#wrap-a-dom-owning-widget) | The View owns the widget handle and tears it down before DOM removal. |
## Wrap a DOM-owning widget
Use this seam for a chart, editor, map, or other widget that renders inside a
Marionette-owned host. The widget factory receives a DOM element and returns a
synchronous `destroy()` handle. Its own library decides rendering and data updates.
Do not let Marionette and the widget both own the same descendants.
```javascript
import { View } from 'marionette';
export const WidgetView = View.extend({
template: () => '',
initialize({ createWidget }) {
this.createWidget = createWidget;
this.widget = null;
},
onDomRefresh() {
if (!this.widget) {
this.widget = this.createWidget(this.el.querySelector('[data-widget-host]'));
}
},
releaseWidget() {
const widget = this.widget;
this.widget = null;
widget?.destroy();
},
onDomRemove() {
this.releaseWidget();
},
onBeforeDestroy() {
this.releaseWidget();
}
});
```
Here is a complete factory for trying the ownership contract without installing
another library. A real widget adapter supplies the same handle.
```javascript
import { Region } from 'marionette';
import { WidgetView } from './widget-view.js';
const mount = document.createElement('main');
document.body.append(mount);
const region = new Region({ el: mount });
region.show(new WidgetView({
createWidget(host) {
const button = document.createElement('button');
button.type = 'button';
let count = 0;
button.textContent = 'Count: 0';
const increment = () => { button.textContent = `Count: ${++count}`; };
button.addEventListener('click', increment);
host.append(button);
return {
destroy() {
button.removeEventListener('click', increment);
button.remove();
}
};
}
}));
// When leaving: region.destroy(); mount.remove();
```
With default lifecycle monitoring, `dom:refresh` runs after attached rendering
and attachment of rendered content. `dom:remove` runs before that content is
rerendered or detached. Thus a rerender destroys the previous widget before a new
host appears. Detaching destroys the widget but retains the View; showing that
View again creates a fresh widget. Destruction releases any remaining handle.
Keep `monitorViewEvents` enabled for this pattern and use Marionette-managed
attachment. Direct `append()`/`remove()` calls outside the lifecycle do not become
Marionette attachment events. If the widget must retain expensive state across
navigation, persist that state outside its disposable DOM handle or deliberately
choose a different attachment policy.
The factory must clean up partially acquired resources if initialization throws.
An asynchronous widget loader also needs a cancellation/generation check before
it attaches; follow the [navigation cancellation pattern](/docs/routing.md). A View
lifecycle callback does not automatically await arbitrary third-party promises.
The [executable fixture](/docs/source/test/fixtures/docs-application-guides/validate.mjs)
checks one widget per attachment, teardown before rerender, detach/reshow, and
final destruction. See [lifecycle](/docs/lifecycle.md) for event ordering.
## Preserve an edited row during collection changes
A stable model object and a stable child View are different from matching IDs in a
new array. For an observable collection, perform the provider's supported
incremental operations. Then verify the unaffected child View and its input node
are the same objects. Avoid calling `collectionView.render()` after every provider
notification: that explicitly rebuilds children.
If data arrives as an immutable replacement, use a provider/reconciliation policy
that defines how source identity changes are handled. Do not assume `trackBy` or
ID matching preserves the existing View's `model` object under every adapter.
The [integration guide](/docs/choosing-integrations.md) identifies supported contracts;
[testing](/docs/testing.md) explains the input identity and stale-subscription assertions
that catch this failure.
[Canonical source](/docs/markdown/docs/task-recipes.md) · [Source identity](/docs/manifest.json)
---
Document: docs/typescript.md
Canonical URL: https://marionettejs.com/docs/typescript/
Markdown URL: https://marionettejs.com/docs/typescript.md
Reading SHA-256: e473c57281ceee2796572fec6d928ae91120e61bb20479202ca298e24b396e42
# TypeScript in an application
Use the declarations shipped by the installed `marionette` package. Core does
not need `@types/backbone` or an additional Marionette type package. Install type
packages for an optional integration only when your application imports it; see
[installation](/docs/installation.md#peer-dependencies).
## Match the compiler to the runtime
For a browser application whose existing bundler emits JavaScript, a minimal
starting configuration is:
```json
{
"compilerOptions": {
"target": "ES2024",
"lib": ["ES2024", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"noEmit": true,
"skipLibCheck": false
},
"include": ["src"]
}
```
Run the application's installed compiler with `npx tsc --noEmit`, then run its
normal bundler. This example assumes a toolchain supporting that target; it does
not supply browser polyfills. Preserve the application's existing target when it
is constrained by its supported browsers.
For modules executed directly by Node, use `module: "NodeNext"` and
`moduleResolution: "NodeNext"`. Mark ESM using `"type": "module"` in package.json
or `.mts` files. Use `.cts` for explicit CommonJS. Select resolution according to
the program that loads the emitted modules, as described in the
[TypeScript compiler guide](https://www.typescriptlang.org/docs/handbook/modules/guides/choosing-compiler-options).
Marionette's declarations are checked with TypeScript 6 and 7 in the repository.
The [installed consumer fixture](https://github.com/marionettejs/marionette/blob/b06750c507494441f0b2298766b70087e45346a2/test/fixtures/core-types/consumer.mts) covers
strict ESM, CommonJS, and bundler resolution. A successful source-only compiler run
is not a substitute for checking the package your application actually installs.
## Give application options a type
Annotate `initialize` when using `.extend`. The constructor and `this.options`
then share that application option contract. Use public methods to expose
application values rather than writing ad hoc properties through a cast.
```ts
import { Region, View } from 'marionette';
const MessageView = View.extend({
template: () => '',
initialize(options: { message: string }) {
// The annotation defines required application options.
void options;
},
onRender() {
const paragraph = this.el.querySelector('p');
if (!paragraph) throw new Error('Message template requires a paragraph');
paragraph.textContent = this.options.message;
},
message(): string {
return this.options.message;
}
});
const mount = document.createElement('main');
document.body.append(mount);
const region = new Region({ el: mount });
region.show(new MessageView({ message: 'Ready' }));
// On feature removal: region.destroy(); mount.remove();
```
`new MessageView()` and `new MessageView({ message: 42 })` are compile errors.
Return-type annotations are useful on application methods that reference other
inferred methods. Prefer one inheritance style within a View family. `.extend`
uses a callable parent by default; blindly calling inherited `.extend()` on a
native JavaScript class is not equivalent to ordinary `class extends`.
The [implementation notes](https://github.com/marionettejs/marionette/blob/b06750c507494441f0b2298766b70087e45346a2/docs/maintainers/types.md) document advanced constructor
and mixed-inheritance boundaries for library authors.
## Narrow the DOM at its use site
A selector does not prove that a template contains a particular element type.
Check nullable query results. Native DOM event `target` can be a nested element;
Marionette's `delegateTarget` is the matched delegated element.
This complete View narrows the matched element at the event boundary:
```ts
import { View } from 'marionette';
import type { DelegatedEvent } from 'marionette';
export const SearchView = View.extend({
template: () => '',
events: { 'input input': 'showQuery' },
showQuery(event: DelegatedEvent) {
const input = event.delegateTarget;
const output = this.el.querySelector('p');
if (!(input instanceof HTMLInputElement) || !output) {
throw new Error('Search template is incomplete');
}
output.textContent = input.value;
}
});
```
The example checks the matched control rather than asserting that an arbitrary
event target is an input. For elements from another window, use that element's
owner-document constructors or a suitable structural check. Do not use a broad
`any` cast to hide a package-version mismatch.
## Keep lifecycle result types distinct
`View#destroy()` and `Region#destroy()` are synchronous. Application lifecycle
operations return promises; await `app.start()`, `app.stop()`, and `app.destroy()`
when later work depends on their completion. A `true` result means the requested state was reached, including an already-running
`start()` or repeated `destroy()`. A superseded transition resolves `false`;
starting a destroyed application also resolves `false`. Rejection reports a
failed transition. See [Application](/docs/application.md) for exact states.
Types do not establish data validity at a network boundary, protect against stale
asynchronous results, or demonstrate focus retention. Validate external data in
the application and test runtime behavior alongside the compiler.
[Canonical source](/docs/markdown/docs/typescript.md) · [Source identity](/docs/manifest.json)
---
Document: docs/testing.md
Canonical URL: https://marionettejs.com/docs/testing/
Markdown URL: https://marionettejs.com/docs/testing.md
Reading SHA-256: c27414d49c5ff018afb7dc7e0e4c748c1d8d311f04a1e4c4cba720a9503893f9
# Testing a Marionette application
Test observable application behavior through the same package and integrations
used in production. Keep fast View tests for local contracts, then use a real
browser for focus, layout, navigation, and third-party DOM behavior. Marionette
does not require a particular test runner or supply a browser environment.
## A small View test
This complete example uses Node's test runner and a DOM supplied by `jsdom`.
Install `jsdom` as a development dependency and run `node --test counter.test.mjs`.
Keep DOM-dependent modules inside the configured environment. The View uses no
Backbone or jQuery adapter.
```javascript
// counter.test.mjs
import assert from 'node:assert/strict';
import test from 'node:test';
import { JSDOM } from 'jsdom';
test('a delegated button updates the existing screen and stops after destruction', async () => {
const dom = new JSDOM('');
globalThis.window = dom.window;
globalThis.document = dom.window.document;
let region;
try {
const { Region, View } = await import('marionette');
const Counter = View.extend({
template: () => '',
events: { 'click button': 'increment' },
initialize() { this.count = 0; },
increment(event) {
assert.equal(event.delegateTarget.tagName, 'BUTTON');
this.count += 1;
this.el.querySelector('output').textContent = String(this.count);
}
});
region = new Region({ el: document.querySelector('main') });
const view = new Counter();
region.show(view);
const button = view.el.querySelector('button');
button.querySelector('span').click();
assert.equal(view.el.querySelector('output').textContent, '1');
assert.equal(view.el.querySelector('button'), button);
region.empty();
assert.equal(view.isDestroyed(), true);
button.click();
assert.equal(view.count, 1);
assert.equal(document.querySelector('main').children.length, 0);
} finally {
region?.destroy();
dom.window.close();
delete globalThis.window;
delete globalThis.document;
}
});
```
Run tests that mutate global DOM objects in isolation, or use your runner's DOM
environment and cleanup hooks. Configure adapters before constructing owners.
Use `createMarionette()` for independent runtimes with different global defaults;
it is not necessary for every test. Avoid test order dependence from shared Radio
channels or runtime configuration.
## Assert resource ownership
| Change under test | Assertions that establish behavior |
| --- | --- |
| Region replacement | The new View is current; the old one is destroyed exactly once; its subscriptions no longer fire. |
| Deliberate detach | The View is alive and reusable; another owner eventually shows or destroys it. |
| Collection update | Unaffected child View and input identities survive; draft, focus, and selection remain; removed children are destroyed. |
| Provider/source replacement | Updates from the new source reach the owner; old source updates no longer do. |
| Async navigation | Resolve the second request first; a late first success or failure cannot replace it. Test a client that ignores abort. |
| Application shutdown | Await stop/destroy; pending work is canceled; no late DOM write occurs. |
| Widget rendering | Acquire once per host; release before replacement and on final removal; no duplicate global listeners. |
Do not prove teardown only by asserting that `destroy()` was called. Trigger the
old source, click a retained detached node, or resolve the late promise and verify
that nothing commits. The [routing fixture](/docs/source/test/fixtures/docs-routing/validate.mjs)
and [form/widget fixture](/docs/source/test/fixtures/docs-application-guides/validate.mjs)
show these assertions against the exact documented examples.
## Use a real browser where it changes the conclusion
A simulated DOM can establish event wiring and object identity. It cannot prove
layout, paint, native constraint-validation presentation, or announcements by
assistive technology. In the browser, test keyboard submission, focus and selection
through provider updates, direct navigation to a deep URL, and cleanup after
leaving and returning to a feature. Exercise the actual selected DomApi and widget,
not a mock that always preserves nodes.
Observe failures through the rendered UI and application API boundary. A green
compiler, coverage percentage, or matching screenshot alone does not establish
that the intended operation succeeded. Keep test data anonymous and deterministic.
## Keep examples and evidence together
For repository contributions, an `executable-example` marker connects a canonical
JavaScript fence to a fixture that extracts and executes it. The marker checker
checks the connection, not behavior. `npm run test:fixtures` builds and tests
installed package artifacts; `npm run docs:check` verifies example markers and
document links. Application projects should use their own package lock and CI
commands rather than copying Marionette's maintainer workflow wholesale.
[Canonical source](/docs/markdown/docs/testing.md) · [Source identity](/docs/manifest.json)
---
Document: docs/forms-and-accessibility.md
Canonical URL: https://marionettejs.com/docs/forms-and-accessibility/
Markdown URL: https://marionettejs.com/docs/forms-and-accessibility.md
Reading SHA-256: 6befedf3629ad5bc6e1d0b571fe2fbb4b18cd70b1d43f5d66dd9898a0e9ce60c
# Forms and accessible interactions
Use native form controls and keep an unfinished draft in the existing input DOM.
A Marionette View owns the form and its pending save; the application supplies the
persistence operation. A DataApi or StateApi is not required for this local draft.
Choose a shared observable source only when other owners need to observe it.
## Save without replacing the user's input
This complete module uses the default DOM and event implementations. The template
contains only trusted, fixed markup. User data is assigned through `value` or
`textContent`. Each instance gets its own label and message IDs.
```javascript
import { View } from 'marionette';
export const ProfileForm = View.extend({
tagName: 'form',
attributes: { 'aria-label': 'Profile' },
templateContext() { return { id: this.cid }; },
template({ id }) {
return `
`;
},
events: { submit: 'onSubmit' },
initialize({ displayName, save }) {
this.initialName = displayName;
this.save = save;
this.pendingSave = null;
},
onRender() {
this.el.elements.namedItem('displayName').value = this.initialName;
},
onBeforeRender() {
this.cancelSave();
},
onSubmit(event) {
event.preventDefault();
return this.submit();
},
async submit() {
if (this.isDestroyed() || this.pendingSave) return false;
if (!this.el.reportValidity()) return false;
const input = this.el.elements.namedItem('displayName');
const button = this.el.querySelector('button');
const status = this.el.querySelector('[role="status"]');
const request = new AbortController();
this.pendingSave = request;
input.readOnly = true;
button.disabled = true;
this.el.setAttribute('aria-busy', 'true');
status.textContent = 'Saving…';
const displayName = input.value;
try {
await this.save({ displayName }, { signal: request.signal });
if (request.signal.aborted || this.isDestroyed()) return false;
this.initialName = displayName;
status.textContent = 'Saved.';
return true;
} catch {
if (request.signal.aborted || this.isDestroyed()) return false;
status.textContent = 'Could not save. Your changes are still here. Try again.';
return false;
} finally {
if (this.pendingSave === request) {
this.pendingSave = null;
input.readOnly = false;
button.disabled = false;
this.el.removeAttribute('aria-busy');
}
}
},
cancelSave() {
this.pendingSave?.abort();
this.pendingSave = null;
this.el.removeAttribute('aria-busy');
},
onBeforeDestroy() {
this.cancelSave();
}
});
```
Mount it through a Region. This example's persistence is deliberately in memory;
replace `save` with the application's API client for durable storage.
```javascript
import { Region } from 'marionette';
import { ProfileForm } from './profile-form.js';
const mount = document.createElement('main');
document.body.append(mount);
const region = new Region({ el: mount });
let savedProfile = { displayName: 'Taylor' };
region.show(new ProfileForm({
...savedProfile,
async save(profile, { signal }) {
signal.throwIfAborted();
savedProfile = profile;
}
}));
// When the feature is removed: region.destroy(); mount.remove();
```
The submit event handles the button and keyboard submission. Native `required`
validation prevents an empty save. While saving, the input is read-only and the
button is disabled; duplicate programmatic submissions return `false`. A failure
keeps the same input, its value, and its selection. The live status announces the
outcome without replacing the form or forcing focus elsewhere.
Do not call `render()` for a status change. An explicit rerender is a reset to the
last saved value: it cancels a pending request before replacing the controls.
Destruction also aborts the request. The signal check matters even if a client
ignores cancellation. Aborting does **not** prove a server rolled back a write;
reconcile ambiguous writes through the application's API contract.
For server field validation, map known field errors to visible messages, set
`aria-invalid="true"`, and connect each message with `aria-describedby`. Clear
those errors when corrected. Keep an error summary focusable when the user needs
to move among several invalid fields. Avoid displaying raw server errors.
[WAI's form guidance](https://www.w3.org/WAI/tutorials/forms/) explains labels and
structure; its [notification guidance](https://www.w3.org/WAI/tutorials/forms/notifications/)
explains associating errors and communicating results.
## Focus when a screen changes
A Region owns destruction and insertion; it does not decide the application's
navigation focus policy. After a user-initiated route change has successfully
shown the new screen, update `document.title` and focus a meaningful heading with
`tabindex="-1"`. Keep that operation after the current-navigation check in the
[routing guide](/docs/routing.md). A stale response must neither replace the page nor
move focus. Background refreshes should normally leave focus where the user put it.
Prefer `